Correcting normals after vertex displacement with Shader Graph
Approximate new vertex normals for materials that displace a mesh’s vertices.
Overview
Vertex displacement shaders can create a myriad of dynamic effects, such as water, terrain, and undulating surfaces. However, displacing vertex positions without correcting the vertex normals can result in incorrect lighting, as shown in the examples below:
Assuming the vertices of your mesh have correct normals predisplacement, and the displacement is a continuous function of the vertex positions in model space, you can correct the normals of your vertex displacement shader by sampling the displacement of neighboring points along the surface of the mesh and approximating a new normal direction with the cross product.
For each vertex in the mesh, find two neighboring points on the surface of the mesh that are in orthogonal directions to one another. Approximate the new normal direction by displacing these points with the same function that displaces the original vertex, and then take the cross product between the new vectors pointing toward them. See Loading entities with ShaderGraph materials to download a sample project containing this shader along with many others.
Create a displacement node graph
Start by creating a node graph that takes a position input in object or model space and outputs a model position offset vector. See Creating a vertex displacement material with Shader Graph for one such example, replicated below:
[Image]
In this example, the Displacement node graph is as follows:
[Image]
Set up the offset and normal node graph
Create the outline for a node graph that calculates the model position offset and the new normal direction in tandem:
Select the Displacement node graph and all of its inputs.
Control-click on one of the selected nodes and choose Compose Node Graph from the contextual menu.
Name the new NodeGraph node “OffsetAndNormal”.
Add a new output of type
Vector3 (Float)to the OffsetAndNormal node graph and name it “Normal”.Connect the
Normaloutput of the OffsetAndNormal node graph to theNormalinput of theGeometry Modifiernode.Open the OffsetAndNormal node graph and optionally rename its inputs and outputs.
The main body of the shader is shown here:
[Image]
See the interior of the OffsetAndNormal node graph below:
[Image]
Set up the neighbors node graph
Prepare a new node graph to calculate two orthogonal neighboring positions at a given distance away from a given vertex position:
Inside the OffsetAndNormal node graph, add a new
NodeGraphnode and name it “Neighbors”.Add a new input of type
Floatto the Neighbors node and name it “Distance”.Add a new input of type
Vector3 (Float)to the Neighbors node and name it “Position”.Add a new output of type
Vector3 (Float)to the Neighbors node graph and name it “Neighbor1Position”.Add a new output of type
Vector3 (Float)to the Neighbors node graph and name it “Neighbor2Position”.Connect the Position node to the
Positioninput of the Neighbors node.Set the
Distanceinput value of the Neighbors node to a small value, such as 0.01.
The interior of the OffsetAndNormal node graph is as follows:
[Image]
Find neighboring positions along the surface
Calculate two orthogonal neighbor positions inside the Neighbors node graph:
Add a Normal node and set its
Spacetoobject.Calculate a vector orthogonal to the normal (tangent to the surface) by taking the cross product of the
Normalnode with an arbitrary unit vector, such as (1, 0, 0), using a Cross Product node. Name it “Tangent”.Compute the cross product of the Tangent
Cross Productnode with the Normal node and name it “Bitangent”. This produces a second vector orthogonal to the normal, but also tangent to the surface of the mesh.Normalize the output of both the Tangent and the Bitangent
Cross Productnodes with Normalize nodes.Multiply the output of both the
Normalizenodes by theDistanceinput with Multiply nodes.Compute the two neighbor positions by adding the
Positioninput to the result of both theMultiplynodes with Add nodes.Connect the output of the
Addnodes to theNeighbor1PositionandNeighbor2Positionoutputs.
The interior of the Neighbors node graph is as follows:
[Image]
While this node graph outputs orthogonal neighboring positions under most circumstances, it fails when the normal vector is similar or equal to the arbitrary unit vector you take the cross product with to get the tangent. One approach to remedying this issue is to compute the similarity between the normal and the arbitrary unit vector with a dot product, and then linearly interpolate toward a different arbitrary unit vector by the similarity value before computing the first tangent, as shown here:
[Image]
Sample the displacement at the neighboring positions
Calculate the model position offset at both of the neighboring positions by making instances of your Displacement node graph:
Create two instances of your Displacement node graph (in the Inspector, Control-click > Create Instance), naming one “DisplacementNeighbor1” and the other “DisplacementNeighbor2”.
Connect the
Neighbor1Positionoutput of the Neighbors node graph to thePositioninput of the DisplacementNeighbor1 node graph instance.Connect the
Neighbor2Positionoutput of the Neighbors node graph to thePositioninput of the DisplacementNeighbor2 node graph instance.
Perform these steps inside the OffsetAndNormal node graph, connecting the Amplitude, Rate, and Scale input nodes to the corresponding inputs of each Displacement node, as shown in the image below:
[Image]
Compute the new normal direction
Calculate the new normal direction by taking the cross product between the directions from the displaced vertex position to the displaced neighbor positions:
For each of the three displacement nodes, get the displaced position by adding the same value connected to the
Positioninput of the node to theModel Position Offsetoutput of the node with anAddnode.Calculate the vectors pointing from the displaced vertex position to the displaced neighbor positions by subtracting the original displaced vertex position from the displaced neighbor positions with Subtract nodes.
Take the cross product between the vectors with a
Cross Productnode to get the new normal direction. Name it “Normal”.Normalize the normal direction with a
Normalizenode.Connect the normalized normal direction to the
Normaloutput.
The resulting node graph is as follows:
[Image]
The following videos show the result of correcting the normals for a shader displacing the vertices of a plane mesh: