World space tiling texture with normal map

theres any way to make a shader that make a tileable texture with normal map applied on it too? without using triplanar… i have the albedo already working, but i need the normal map working in the same way too:

Shader "Custom/SurfaceShaderWithNormalTexture" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _NormalMap ("Normal Map", 2D) = "bump" {}
    }
   
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 200
       
        CGPROGRAM
        #pragma surface surf Lambert
       
        sampler2D _MainTex;
        sampler2D _NormalMap;
       
        struct Input {
            float2 uv_MainTex;
            float3 worldNormal;
            float3 worldPos;
        };
       
        void surf (Input IN, inout SurfaceOutput o) {
            // Sample the textures
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
           
            // Use normal map to modify the normal
            float3 normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            normal = normalize(normal * 2.0 - 1.0); // Transform from [0,1] to [-1,1]
            normal = normalize(mul(normal, (float3x3)unity_WorldToObject)); // Convert to object space
           
            // Calculate projected UV
            float3 worldNormal = normalize(IN.worldNormal);
            float3 worldPos = IN.worldPos;
            float3 projectedPos = worldPos + worldNormal * 0.1; // Adjust projection factor as needed
           
            float2 projectedUV;
            if (abs(worldNormal.x) > abs(worldNormal.y) && abs(worldNormal.x) > abs(worldNormal.z)) {
                // If the normal is primarily in the X axis
                projectedUV = float2(projectedPos.z, projectedPos.y);
            } else if (abs(worldNormal.y) > abs(worldNormal.x) && abs(worldNormal.y) > abs(worldNormal.z)) {
                // If the normal is primarily in the Y axis
                projectedUV = float2(projectedPos.x, projectedPos.z);
            } else {
                // If the normal is primarily in the Z axis
                projectedUV = float2(projectedPos.x, projectedPos.y);
            }
           
            // Apply the final color
            c = tex2D (_MainTex, projectedUV);
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
   
    FallBack "Diffuse"
}

This is a triplanar shader, just one without any smooth blending between the planes. The way to fix this is the same as if it was a blended triplanar shader, because, again, it is a triplanar shader.

See my article on triplanar normal mapping here:

The main thing is when using shader created UVs is you have to transform the normal map from whatever space that UV is in back to world space. Or, in the case of Surface Shaders, back into the mesh’s existing tangent space.

I have an example surface shader here:

The key parts unique to Surface Shaders are the WorldToTangentNormalVector() function and needing to have the INTERNAL_DATA macro in the Input struct.

Beyond that, much of the rest of the code in that example shader is going to be needed, only with the code for each plane done inside the matching if instead of all 3 at once. If you have a hard time understanding the shader, I would recommend reading the article itself to understand what’s happening.

Lastly, there’s a fairly large error in the above shader already.

float3 normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
normal = normalize(normal * 2.0 - 1.0); // Transform from [0,1] to [-1,1]

The UnpackNormal() function already outputs a normal in a -1 to 1 range. Doing the * 2.0 - 1.0 again means it’s now in a -3 to 1 range! You only need to do that part if you’re not using the UnpackNormal() function, which you could choose to do if you manually packed the normal map in a different way than how Unity does.

damn bro you are a beast! the second link you send on your github is what i was looking for youre a really beasts bro, thanks so much!