Hello everyone! My name is Taras, and I am a Technical Artist. I’ve created some quick tips about normals on Artstation, and I’d like to share them here as well.
It often happened when I’ve been working on displacement effects, wind effects, water waves, or something else involving vertex offset in the shader that I had issues with normals and, consequently, problems with lighting.
What are normals in a shader?
Well, in general, a normal is a vector perpendicular to a surface. In a shader, our surface is made up of triangles constructed from vertices. Normals can be represented in different spaces, such as world space, object space, view space, or tangent space.
So, our goal is to find a perpendicular vector to a surface in the required space. In all spaces except tangent space, the perpendicular vector is calculated via the cross product of two vectors that are already perpendicular to each other. To find these two vectors, we need to take the derivative of a surface function.
I know derivatives may sound difficult, and I struggled with them in university and high school too. But in truth, it’s easier than I thought. There are two general ways to find derivatives:
- Analytical: This is the most straightforward method but can be computationally inexpensive at runtime. To simplify your life, you can use any derivative calculator.
- Finite difference: This method is much easier to implement in a shader but can be computationally heavier at runtime.
So, I have implemented 5 methods in the Unreal Material Editor for finding normals:
- Using DDX and DDY functions
- Finite difference of vertex offsets
- Finite difference of a function
- Analytical derivative of a height function
- Finite difference of a height map
See below for examples of implementations
1. DDX and DDY functions:
This is the fastest way to obtain normals. I use it when I just want to test something. Its disadvantages are “pixelation” of normals, and if you use it for vertex positions, it creates hard edges. To make it smoother, there is a quite complex solution.
2. Finite difference of vertex offsets:
It is a universal method to calculate normals from any vertex offset function. However, there are some limitations to this approach:
You need a uniform wireframe of a mesh.
If you use an expensive vertex offset function (such as Voronoi noise), it may impact GPU performance.
3. Finite difference of a function
Similar to the previous method, this is also a universal approach. However, sometimes it can be challenging to implement in a shader. If a function takes Vector3 or Vector4 as input, it may be somewhat unclear. Additionally, it can affect GPU performance when complex procedural functions are used.
4. Analytical derivative of a height function
This approach is the most cost-effective among the methods mentioned above. However, it can be quite tricky to implement, especially when using complex functions. It is also impossible to implement this approach for textures.