Okay so like, i need to share this again because as of now no one has indicated to me that they understand just how absurd this is.
https://youtu.be/J7IKKzzApg8?si=04tTU-EeDF-A09mL
This is a zoom of a 41 iteration dragon curve. That's right 41 iterations. Why? Because i can :D Fun fact usually generating a dragon curve the memory requirement doubles each iteration, because you are copying and rotating the current curve. Double the segments, at best without any clever optimization its O(2^x)
So how hard is it to make say, 20 iterations? It was a long, long time ago, like 10 years, i followed an article that said you kind of have to write it in c so its more performant because 20 iterations on their machine took 20 minutes. 2^20 is going to be in the low one-millions range.
Now lets be more optimistic. Lets say computation time on a single cpu core has sped up 20 times, so you can generate a 20 iteration curve in 1 minute.
So 21 would take af least 2 minutes.
Now our scaling looks something more like 1m*2^(x-20). If we plug 21 more iterations to bring us up to 41, well we already know 2^20 is about one million, so this would be 2 million minutes or a bit under 2 years.
But guess what. This is rendering in real time. It would be incorrect to say that it generates the curve over 60 times per second, but it generates the visible part of the curve.
So what clever optimization trick gets me over a 120 million times speed boost at 41 iterations? Parallelization. See, this isnt just any dragon curve you see in c or java or python.
Nah girlie's running on the unity engines CG/HLSL shader language. The trick is to instead of generating the curve, and placing the segments with that info, we repeat the process that forms the dragon curve backwards. A dragon curve starts with a single segment, and it has 1 mid point. When folded, it turns into 2, and has 2 midpoints.
So what we do is we test every pixel, find its closest hypothetical midpoint, and then perform the transformation backwards. Since they originate from one segment, and this method ends up halvening the midpoints each time by merging them on top each other. These means after your desired number of iterations, we can simply check to see if the pixel we were on collapsed back into a specific midpoint.
Making midpoints integers an example would be the line segment from (0,0) to (0,2). Well then im that case the midpoint would be (0,1).
This scaling even allows us to keep track of everything via modulo operations. The vector math ends up working out so that most the time you're rotating and scaling, but the net displacement from the two is a fixed size vector along the cardinal direction, which when converted to integer math, is the same as either adding xor subtracting, just 1 from your current x xor y coordinate, based on the direction the rotation would take it
We can add a color gradient by keeping track of the turns in the curve. I dont remember exact details but its literally as simple as left? 0: right? 1. And then converting that binary number to decimal it gives you what number along the curve that pixels nearest midpoint belonged to.
Now there are some tricky edge cases like 0 iterations, or the first and last segment only being one line segment instead of a curve. Additionally if we try to smooth the color a bit by sampling all 4 directions, we'd get color bleeding between two very different parts of the curve that have been folded into proximity. You could probably weight this based on the proximity between segments but i dont recall if i ever bothered.
But yeah its some really fascinating stuff. Dont fully understand why the binary counting works but uh dont think i need to.
I've been working on a raymarcher in my free time to practice making my own custom sdfs. And now a friend of mine asked me to make þem a volumetric tornado for Unity HDRP.
...some of you may know my distaste of Unity HDRP >:[
However, þis gave me an excuse to overwrite þe render πpeline wiþ my own handiwork, so I begrudgingly agreed — I didn't really have a choice eiþer way to begin wiþ þough 😅
Here's a couple photos of my current implementation, which will hopefully improve in coming weeks:
....currently one of þe rotation matrices aren't *quite* right, so I'll have to go þrough þose again, but for now I want to rework þe sdf to be more 'tornado-y'
p.s. I may have downloaded ahk for þe sole purpose of replacing th wiþ 'þ'. Do let me know if it's too much, I can turn it off to improve legibility of my posts :D
So you want to be a shader coder? But you still need to learn the basics? Or maybe you're already somewhat familiar with shader coding but could use some expert guidance?
Another piece of information we can easily get our hands on thats very useful for postprocessing is the normals of the scene. They show in which direction the surface at any given pixel is pointing.
To understand how to get and use the normals of the scene it’s best to know how to access the scene depth first, I made a tutorial on how to do that here: https://ronja-tutorials.tumblr.com/post/175440605672/postprocessing-with-the-depth-buffer
We start this tutorials with the files from the depth postprocessing tutorial and expand them as we need.
The first change is to remove all of the code from the c# script which we used to drive the wave in the previous tutorial.
Then, we don‘t tell the camera to render the depth of objects anymore - instead we tell it to render a texture which includes the depth as well as the normals.
And that’s already all of the setup we need to access the normals. Next we edit our shader.
We also remove the all of the code used for the wave function here. Then we rename the _CameraDepthTexture to _CameraDepthNormalsTexture, so it’s written in by unity.
With this setup we can now read from the depthnormals texture in our fragment shader. If we just do that and just draw the texture to the screen, we can already see something interresting.
But what we can see isn’t what we really want, we only see red and green values and some blue in the distance. That’s because as it’s name suggests, this texture holds the normals as well as the depth texture, so we have to decode it first. Luckily unity provides us a method that does exactly that. We have to give it the depthnormal value as well as two other values the function will write the depth and the normals in.
Unlike the depth texture, the depth value we have now is already linear between the camera and the far plane, so we can easily adapt the code from the previous tutorial to get the distance from the camera again.
But let’s go back to using the normals. When we just print the normals as colors to the screen we already get a pretty good result.
But if we rotate the camera, we can can see that one point on a surface doesn’t always have the same normal, that’s because the normals are stored relative to the camera. So if we want the normal in the world we have to go additional steps.
We can easily convert our viewspace normals to world space, but sadly unity doesn’t provide us a function for that so we have to pass it to our shader ourselves. So we go back to our C# script and implement that.
First we get a reference to our camera, we already get the camera in our start method, so we can directly save it to a class variable right there. Then in the OnRenderImage method we get the viewspace to worldspace matrix from the camera and then pass it to our shader. The reason we can’t pass the matrix to our shader once in the start method is that we want to move and rotate our camera after starting the effect and the matrix changes when we do that.
Next we can use that matrix in our shader. we add a new variable for it and then multiply it with the normal before using it. We cast it to a 3x3 matrix before the multiplication so the position change doesn’t get applied only the rotation, that’s all we need for normals.
Now that we have the worldspace normals, we can do a simple effect to get comfortable with them. We can color the top of all objects in the scene in a color.
To do this, we simply compare the normal to the up vector. We do this via a dot product which returns 1 when both normalized vectors point in the same direction(when the surface is flat), 0 when they’re orthogonal (in our case on walls) and -1 when they’re opposite to each other(in our case that would mean a roof over the camera).
To make it more obvious what’s on top and what doesn’t count as on top, we can now take this smooth value and do a step to differentiate between top and not on top. If the second value is smaller, it will return 0 and we will see black, if it’s bigger, we will see white.
The next step is to bring back the original colors where we don’t define the surface to be on top. For that we just read from the main texture and then do a linear interpolation between that color and the color we define to be on top (white at the moment).
And as a last step we’re going to add some customizability. So we add a property and a global variable for the up cutoff value and the top color.
Then we replace the fixed 0.5 we used previously for our cutoff value with the new cutoff variable and linearly interpolate to the top color instead of the fix white color. We can then also multiply the up color with the alpha value of the top color, that way when we lower the alpha value the top will let some of the original color through.
This effect was mainly made to show how the depthnormals texture works. If you want a snow effect it’s probably better to just do it in the shader for the object the snow is on instead of a postprocessing effect. I’m sorry I didn’t come up with a better example.
You can also find the source here:
https://github.com/axoila/ShaderTutorials/blob/master/Assets/17_NormalPostprocessing/NormalPostprocessing.cs
https://github.com/axoila/ShaderTutorials/blob/master/Assets/17_NormalPostprocessing/17_NormalPostprocessing.shader
I hope that I was able to convey how to access normal textures and that this will be a solid foundation for future effects.
If you have any questions feel free to contact me here on tumblr or on twitter @axoila.
This project was set up using the Universal Windows Program using DirectX 11 mainly to understand the workings of the graphics API and to add this to my portfolio.
The above screenshots try to show off certain features of the project some of which I will list below:
1. .obj and .fbx model loader that loads in vertex data, index data, vertex colors uv information [ material data extraction will be patched in].
2. Screenshot 1 demonstrates the .obj file loaded in, Terrain[explained below], and the infinity skybox.
3. In the 2nd screenshot above, I have marked out the additive effects of three different types of lighting[ RED: Directional, GREEN: Spot, BLUE: Point] on the model.
4. Screenshot 3 shows off my proudest feature, the terrain. I created a terrain generator that uses height maps. I load in a flat .obj with approximately 10000 triangles. I use the height map as a Texture2d resource for the vertex shader and modify the Y values of the vertices.
5. Screenshot 4 demonstrates an addition to the previous terrain loader of applying multitexture to the terrain I loaded in based on the height of the vertex. For this, I loaded in two Texture2d resources for the pixel shader and passed over the Y value from the vertex to the pixel shader. Interpolating on the Y value in the pixel shader and appropriately texturing the terrain.
6. An incomplete feature, for now, is the 2nd viewport on the top. I was going for a racing game style rear view mirror but ended up with an opposing camera to look around corners.
I have worked more on making an engine but that is currently in a win32 app which I hope to transfer onto here or vice-versa. A debug system, collision, and an animation system can all be added to this project.
I hope to redo these features in Vulkan or OpenGL just to get a feel for the other API options.