Skip to content

Terrain shader: Fix incorrect tangent normals sampled using seamless UV method#66

Closed
wheatleymf wants to merge 2 commits intoFacepunch:masterfrom
wheatleymf:master
Closed

Terrain shader: Fix incorrect tangent normals sampled using seamless UV method#66
wheatleymf wants to merge 2 commits intoFacepunch:masterfrom
wheatleymf:master

Conversation

@wheatleymf
Copy link
Copy Markdown
Contributor

Issue

Currently, terrain shader samples tangent normal maps using seamless UVs 'as is', which means that normals are not transformed to be inline with the new UV rotation angle. This results in some issues with NdotL shading. Here's a comparison between current, and corrected normals:

image image

Fix

The solution for this is to multiply tangent normal with 2x2 rotation matrix that we can access from Terrain_SampleSeamless2D in TerrainCommon.hlsl. This is probably very stupid, but I had to add an output out float2x2 uvAngle so I can access this matrix from other shaders:

float2 Terrain_SampleSeamlessUV( float2 uv, out float2x2 uvAngle )

To avoid breaking any shaders that rely on a previous implementation of this function, I've added an overload that has an old set of arguments:

float2 Terrain_SampleSeamlessUV( float2 uv ) 
{
    float2x2 dummy;
    return Terrain_SampleSeamlessUV( uv, dummy ); 
}

In terrain.shader, inside the Terrain_Splat4 function, tangent normal is transformed like this:

<...>
float3 normal = ComputeNormalFromRGTexture( nho.rg );
normal.xz *= g_TerrainMaterials[ i ].normalstrength;
            
// Rotate normal map to match seamless UV angle and avoid shading errors 
normal.xy = mul( uvRotation, normal.xy );

normal = normalize( normal );

albedos[i] = SrgbGammaToLinear( bcr.rgb );
<...>

I am not entirely sure if that's a correct way to approach this, and I absolutely don't know if it was a good idea to change seamless UV function like I did, so please let me know if my solution is stupid.

@lolleko lolleko requested a review from sampavlovic December 1, 2025 11:55
@sampavlovic
Copy link
Copy Markdown
Contributor

sexy hunk

@sampavlovic sampavlovic added the triaged triaged pull-requests are replicated on the internal sbox repo label Dec 3, 2025
@lolleko lolleko removed the triaged triaged pull-requests are replicated on the internal sbox repo label Dec 3, 2025
@lolleko
Copy link
Copy Markdown
Contributor

lolleko commented Dec 3, 2025

Could update your branch, please 🙏

Terrain_SampleSeamlessUV now additionally returns a 2x2 rotation matrix which can be used to transform vectors and other data if necessary.

This also adds a function overload `Terrain_SampleSeamlessUV( float2 uv )` for cases when you don't need a rotation matrix. This will avoid breaking any shaders that rely on old implementation of this function
Previous implementation of seamless UV was sampling tangent normal map 'as is', without transforming vectors to be inline with the UV rotation angle - which resulted into NdotL shading errors. This is now corrected and tangent vector accounts for UV rotation.
@wheatleymf
Copy link
Copy Markdown
Contributor Author

Could update your branch, please 🙏

Should be done now

@handsomematt handsomematt added the triaged triaged pull-requests are replicated on the internal sbox repo label Dec 4, 2025
@sboxbot sboxbot added the accepted this pull request was accepted, hurrah! label Dec 5, 2025
@sboxbot
Copy link
Copy Markdown
Contributor

sboxbot commented Dec 5, 2025

This PR has been merged upstream.

@sboxbot sboxbot closed this Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accepted this pull request was accepted, hurrah! triaged triaged pull-requests are replicated on the internal sbox repo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants