The Return of the Obra Dinn is an indie game
classic with a unique art-style that (unlike some video games)
isn't patented so you can replicate and change it all you want. Isn't the free exchange of ideas and concepts
fun? Today I'm using the Built-in Render Pipeline
to implement this effect. It's not hard to port over to the new Scriptable
Render Pipeline but it is so much simpler to add effects this
way. Just write a script with the OnRenderImage
method and call Graphics.Blit(). The material parameter here contains the shader
for the effect, found almost entirely within the Frag function. We write a shader that takes the MainTex parameter
as our screen input and a Noise texture as our filter. You see, this cross-hatching effect where
we use two colours to represent a gradient is called "Dithering"
and it's been around in Image Processing for a long time. It works by applying a noise texture across
a greyscale image, then comparing each pixel in the image with
its respective pixel in the noise. If the image pixel has a large value than
the noise value, that's a 1, otherwise it's a 0. Hence the name "1-bit graphics". These are represented by whichever two colours
you like. Converting a colour into greyscale is simple,
you just multiply out its components using the dot product like so. Finding the respective pixel in our noise
filter, is also simple, we just divide the screen coordinates by the
size of the noise texture and multiply the result by the size of the screen. Yes, this is a division, don't question it. Check against the image pixel like so and
boom we've got a Dither Effect. But... ew... This is a problem. As we move our camera around, the dither texture
and the environment around us don't match up, as they're being checked in Screen Space. We need a better way to sample our noise. Luckily, Lucas Pope, the creator of Obra Dinn
wrote extensive blog posts about his development of the game,
and one such blog post documents his solution for this exact problem. He got some okay results offsetting the noise
by the amount the Camera has rotated and this is the method most recreations use
but his best results were by projecting the texture onto a sphere around the Player's
camera. Unfortunately projecting a 2D texture onto
a 3D sphere, or vice versa, is an incredibly difficult task... (Just ask anyone whose tried to successfully
map the Earth) And Lucas Pope doesn't divulge which projection
method he uses (though he refers to it as "rings", whatever
that means) He also suggests that you could change the
noise texture into a shape that can tile around a sphere like a hexagon
but everyone knows that you can't actually tile a sphere with hexagons
as there will always be 12 pentagons at the corners of the icosahedron which
EULER PROVED IN THE 1700S AND WE ARE STILL GETTING WR-
My producers have informed me that I have to cut the 5-minute rant about tiling hexagons
around a sphere but long story short we're going to have to
find a projection ourselves. No more hand-holding. The first step is to figure out what point
of the sphere each pixel is on. This took some time to get right but the gist
is to calculate the camera's frustrum (that's the 4 corners of the camera's view)
and lerp between them based on the screen coordinates. Now we've got our point on the sphere, we
need to map it to our 2D texture. The key measure of quality here is how evenly
spaced the pixels of the texture are. We want it nice and uniform. I tried a cylinder mapping but this was too
small at the corners. *coding interlude* I used a UV sphere mapping but it was the same problem at the poles. *coding interlude* Finally, I settled on a cubic mapping, the kind you might find on a Skybox. This can be a little warped at the corners
but for the most part performs very well. *coding intensifies* For the best results, we use this mapping
at 2x the resolution we want to end up with and then downsample the image with bilinear
filtering to smooth out those rough edges The small problem is that we no longer have
a 1-bit output now, and as we downsample the image we're inviting
a few dozen shades of grey. Some *coughs* other tutorials will say this
is a necessary compromise and Lucas Pope himself doesn't stick to the
1-bit aesthetic. But simply looking a screenshot of the game
in photoshop will disprove that theory. Luckily, the solution also lies within Pope's
blog posts. We just need a simple thresholding shader
for the final Blit. Now, the dither sticks perfectly as you rotate
around and any distortion when moving is negligible. The final pieces of Pope's effect are an edge
detection filter and how he changes between 2 different noise
textures depending on the surface. An early blog post explains how he does this,
using a custom surface shader to convert all lighting to the blue channel,
the type of dithering to the alpha channel and
reserving the red and green channels for vertex colours so that each face can be differentiated. But I don't need show an example of that ri- *BIG CODING INTERLUDE* *sighs*
Okay, its sometime later and I've implemented it. Looks pretty neat. Uses this fairly simple surface shader and
"minor" changes to the code. I just manually set the vertex colours in
this scene, but Pope used a tool to randomly set and then adjust them. Check my project files if you need more information. The effect is now complete. The package I'm using for my First Person
controller is called The First Person by the wonderful and amazing Breogán Hackett. Unity's standard asset controllers are long
past their prime and this is a fantastic alternative that is
easy to modify and even has support for Third Person cameras! Finally, this effect wouldn't be possible
if Lucas Pope hadn't been so open in the development of his game. While it's cool to recreate it, I do recommend
iterating and improving upon the style, helping make it your own. Here are some my experiments with retaining
colour values in the dither or using different methods to calculate the
threshold in the first place. Mess around with it and have fun! Like and subscribe and thanks for stopping
by! This video wouldn't be possible if it wasn't
for my patrons. Of course, I don't have any so I've hooked
up a credits sequence to a random name generator. I'm taking this opportunity to announce that
I got a job as a Junior Game Programmer! From what I can gather, the custom is to keep
the studio secret until your first day but the bad news is that I likely won't have
time to make tutorials after I start. My plan is to finish off Pixelsmith and make
a few more videos before that happens, but after that, the only videos that go on
this channel will most likely be of the video essay variety. This has been a fantastic experience and massively
helped me put my game programming skills to use over
the pandemic. All the comments I've received have been wonderful
and feel free to email me or comment down below if you need any help
with any of the concepts I talk about in these videos. I look forward to seeing what you all come
up with! See you next time!