For my last iteration of experimentation for this module, I wanted to combine all the elements I'd experimented with prior - programming, maths formulas, shader languages and Unreal. It's all good and well understanding the theory I covered in the 'Shadertoy' blog post, however the mission I set out for myself was to utilise technical art knowledge in the context of games, so where better to execute this knowledge than Unreal engine?
I followed along with renderBucket's video "UE5 Tutorial - HLSL - Distortion, Animation & Raymarching" (renderBucket, 2023) . These topics sounded interesting to me, and I had recently watched SimonDev's video on raymarching (SimonDev, 2022) so I was intrigued to see this applied in an Unreal setting.
Here is the node setup for the material:
And here is the HLSL code that I inputted to the custom node. The variables TexCoord, Time
float3 rayStep = viewDir * -1;
float4 color;
struct texDistort
{
float2 texScale(float2 uv, float2 scale)
{
float2 texScale = (uv - 0.5) * scale + 0.5;
return texScale;
}
float2 texRotate(float2 uv, float angle)
{
float2x2 rotationMatrix = float2x2(cos(angle), sin(angle),
-sin(angle), cos(angle));
return mul(uv - 0.5, rotationMatrix) + 0.5;
}
float2 texDistortion(float2 uv, float time)
{
float angle = atan2(uv.y - 0.5, uv.x - 0.5);
float radius = length(uv - 0.5);
float distortion = 4 * sin(3 * radius + 2 * time);
float primDist = sin(4.0 * angle) * distortion;
return texRotate(uv, primDist);
}
};
texDistort txd;
for (int i = 0; i < 3; i++)
{
color = Texture2DSample(texObject, texObjectSampler, txd.texDistortion(uv, time));
if (color.r > 0.1 && color.g > 0.1 && color.b > 0.1)
{
return color * float3(0.49, 0.15, 0.79);
}
else if (color.r > 0.01 && color.g > 0.01 && color.b > 0.01)
{
return color * float3(0.93, 0.47, 0.74);
}
uv += rayStep * 1.5;
}
return(color);
I'm going to try my best to break down the code and what it does. The variable rayStep is set up for raymarching further on in the code. It multiplies the cameraVector (viewDir) by -1. This is used to give the shader fake depth. color is defined here for use later.
float3 rayStep = viewDir * -1;
float4 color;
Next, a struct is created to contain the functions we will be calling to manipulate visuals.
struct texDistort
{
The function texScale is a float2 value that inputs a uv value and scale value. This normalises and scales the texture so any calculations applied to it are applied from the centre.
float2 texScale(float2 uv, float2 scale)
{
float2 texScale = (uv - 0.5) * scale + 0.5;
return texScale;
}
texRotate is a float2 function that has an input of a uv value and an angle. The rotationMatrix allows our uv to be rotated. This combination of values outputs a clockwise rotation. The return value applies our rotation to the uv space by multiplying the normalised uv by the rotationMatrix. Adding 0.5 keeps the rotation at the centre of the uvs rather than the corners.
float2 texRotate(float2 uv, float angle)
{
float2x2 rotationMatrix = float2x2(cos(angle), sin(angle),
-sin(angle), cos(angle));
return mul(uv - 0.5, rotationMatrix) + 0.5;
}
texDistortion is a float2 function that has an input of a uv value and time, which will be used to animate the shader. An angle float is defined by passing normalised uv values through an atan2 function.
Atan2 is something I was researching over Christmas due to it being mentioned in the book I was reading, Real Time Rendering (Akenine-Moller, 2018). I spent a while trying to figure out what it was and why it differs from arctan but I didn't fully understand it. The closest I got to understanding was thanks to this video by Khan Academy, outlining inverse trig functions (Khan Academy, 2009). It seems that arctan restricts range in output to the 1st and 4th quadrant of the unit circle, however atan2 allows outputs from all 4 quadrants. I'm still not quite sure how this manifests but I attempted to learn how it works.
The float radius holds the distance from the centre of the UV to the edge of the UV.
The distortion float then starts calculating wave patterns with time to output funky patterns on the shader. Since the distortion formula is hard to visualise, I remembered a resource that was mentioned in the Shadertoy tutorial (kishimisu, 2023) from my previous blog article that helps visualise mathematical graphs, aptly called Graphtoy (Quilez, 2023). Below is a visualisation of 4 * sin(3 * radius + 2 * time).
primDist then creates another sine wave using the atan2 angle created earlier multiplied by a value that controls how many times this pattern is duplicated. This is then multiplied by the distortion to output a funky pattern which can be project onto our mesh.
This funky value is then passed through the texRotate function along with the uv pattern to make it rotate and distort.
float2 texDistortion(float2 uv, float time)
{
float angle = atan2(uv.y - 0.5, uv.x - 0.5);
float radius = length(uv - 0.5);
float distortion = 4 * sin(3 * radius + 2 * time);
float primDist = sin(4.0 * angle) * distortion;
return texRotate(uv, primDist);
}
After this point the texDistort struct comes to an end and is redefined as txd which allows us to access the functions within it.
};
texDistort txd;
Then a for loop is defined which will be looped through 3 times.
for (int i = 0; i < 3; i++)
{
color is a float 4 that outputs a Texture2DSample to the screen. It uses the texture you provide it with (texObject) and applies the txd.texDistortion values to it when displayed on the screen.
color = Texture2DSample(texObject, texObjectSampler, txd.texDistortion(uv, time));
This section of the code defines colour threshholds which outputs different colours dependent on the formula above. The if statement will output purple and the else if statement will output pink.
if (color.r > 0.1 && color.g > 0.1 && color.b > 0.1)
{
return color * float3(0.49, 0.15, 0.79);
}
else if (color.r > 0.01 && color.g > 0.01 && color.b > 0.01)
{
return color * float3(0.93, 0.47, 0.74);
}
The uv is then manipulated by the rayStep value we defined earlier, which adds a layer of fake depth to each iteration of the loop.
uv += rayStep * 1.5;
And then finally, outside of the for loop we return the colour value.
return(color);
And this is how it looks in world! I plugged it into the emissive colour to give it a cool glow.
I'm so happy with the final output and I learnt a lot about HLSL and vector maths in the process. In the future, I'd love to create a more complex version of this with my own calculations and formulas using Graphtoy to visualise it.
Akenine-Moller, T. et al. (2018) Real-time rendering, Fourth edition. Boca Raton: CRC Press.
Khan Academy (2009) Inverse trig functions: Arctan | trigonometry | khan academy, YouTube. Available at: https://www.youtube.com/watch?v=Idxeo49szW0 (Accessed: 19 December 2023).
kishimisu (2023) An introduction to shader art coding, YouTube. Available at: https://www.youtube.com/watch?v=f4s1h2YETNY&list=PL42gZMHWYcvjs5SfnoV4X6ocuSnFQsLwS&index=2&t=1s (Accessed: 3 November 2023).
Quilez, I. (2023) Graphtoy. Available at: https://graphtoy.com/ (Accessed: 3 January 2024).
renderBucket (2023) UE5 tutorial - HLSL - distortion, animation & raymarching, YouTube. Available at: https://www.youtube.com/watch?v=LS9Uc4LCncY&list=PL42gZMHWYcvjs5SfnoV4X6ocuSnFQsLwS&index=62&t=20s (Accessed: 3 January 2024).
SimonDev (2022) Ray Marching, and making 3D worlds with math, YouTube. Available at: https://www.youtube.com/watch?v=BNZtUB7yhX4&list=PL42gZMHWYcvjs5SfnoV4X6ocuSnFQsLwS&index=41&t=1s (Accessed: 19 December 2023).
留言