Search Unity

World Space shader Stretching when object is rotated on y axis

Discussion in 'Shaders' started by Nishtana, Oct 16, 2019.

  1. Nishtana

    Nishtana

    Joined:
    Jan 5, 2017
    Posts:
    6
    So I'm pretty new to shaders however I'm beginning to understand enough to make something basic. I've been trying to create a World Space shader that allows for a normal texture to be projected in world space as well as Diffuse, and Spec map. I've got it mostly working, however I'm running into a snag where when I rotate the object either 90 or 270 degrees the textures on the z and x axis faces stretch almost as if the shader is remembering which faces originally faced z and x. I've taken a couple images to show what I mean, and I'm hoping some one here can show me what I'm missing.
    - Before Rotating

    - After Rotating

    Code (CSharp):
    1.  
    2. Shader "Diffuse - Worldspace" {
    3. Properties {
    4.     _Color ("Main Color", Color) = (1,1,1,1)
    5.     _MainTex ("Base (RGB)", 2D) = "white" {}
    6.     _Emission ("Emission (RGB)", Color) = (0,0,0,1)
    7.     _Bump ("Normal (RGB)", 2D) = "blue" {}
    8.     _SpecMap ("Specular (G)", 2D) = "grey" {}
    9.     _Rotation ("Rotation", Range(0,1)) = 0
    10.     _Scale ("Texture Scale", Float) = 1.0
    11. }
    12. SubShader {
    13.     Tags { "RenderType"="Opaque" }
    14.     LOD 200
    15.  
    16. CGPROGRAM
    17. #pragma surface surf StandardSpecular vertex:vert
    18.  
    19. sampler2D _MainTex;
    20. sampler2D _Bump;
    21. sampler2D _SpecMap;
    22. fixed4 _Emission;
    23. fixed4 _Color;
    24. float _Scale;
    25. float _Rotation;
    26.  
    27. struct Input
    28. {
    29.     float3 sworldNormal;
    30.     float3 worldPos;
    31.     float2 uv_Bump;
    32.  
    33. };
    34.  
    35. void vert (inout appdata_full v, out Input o)
    36.         {
    37.                UNITY_INITIALIZE_OUTPUT(Input,o);
    38.      
    39.             o.sworldNormal = abs(v.normal);
    40.         }
    41.  
    42. void surf (Input IN, inout SurfaceOutputStandardSpecular o)
    43. {
    44.  
    45.     float2 UV;
    46.     fixed4 c;
    47.     float r = _Rotation;
    48.     float3 uv = IN.sworldNormal.xyz;
    49.     half4 x;
    50.     half4 y;
    51.     half4 z;
    52.     half4 n = float4(1,1,1,1);
    53.  
    54.  
    55.     if(r<0.5)
    56.     {
    57.             if(abs(IN.sworldNormal.x)>0.5)
    58.         {
    59.                 UV = IN.worldPos.zy; // side
    60.                 c = tex2D(_MainTex, UV* _Scale);
    61.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    62.                 half4 x = tex2D (_MainTex, uv.zy);
    63.                 x = tex2D(_Bump, UV* _Scale);
    64.                 n = lerp(n,x,IN.sworldNormal.r);
    65.         }
    66.            else if(abs(IN.sworldNormal.z)>0.5)
    67.         {
    68.                 UV = IN.worldPos.xy; // front
    69.                 c = tex2D(_MainTex, UV* _Scale);
    70.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    71.                 half4 z = tex2D (_MainTex, uv.xy);
    72.                 z = tex2D(_Bump, UV* _Scale);
    73.                 n = lerp(n,z,IN.sworldNormal.b);
    74.         }
    75.             else
    76.         {
    77.                 UV = IN.worldPos.zx; // top
    78.                 c = tex2D(_MainTex, UV* _Scale);
    79.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    80.                 half4 y = tex2D (_MainTex, uv.zx);
    81.                 y = tex2D(_Bump, UV* _Scale);
    82.                 n = lerp(n,y,IN.sworldNormal.g);
    83.         }
    84.     }
    85.     else
    86.     {
    87.                 if(abs(IN.sworldNormal.x)>0.5)
    88.         {
    89.                 UV = IN.worldPos.zy; // side
    90.                 c = tex2D(_MainTex, UV* _Scale);
    91.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    92.                 half4 x = tex2D (_MainTex, uv.zy);
    93.                 x = tex2D(_Bump, UV* _Scale);
    94.                 n = lerp(n,x,IN.sworldNormal.r);
    95.         }
    96.            else if(abs(IN.sworldNormal.z)>0.5)
    97.         {
    98.                 UV = IN.worldPos.xy; // front
    99.                 c = tex2D(_MainTex, UV* _Scale);
    100.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    101.                 half4 z = tex2D (_MainTex, uv.xy);
    102.                 z = tex2D(_Bump, UV* _Scale);
    103.                 n = lerp(n,z,IN.sworldNormal.b);
    104.         }
    105.             else
    106.         {
    107.                 UV = IN.worldPos.xz; // top
    108.                 c = tex2D(_MainTex, UV* _Scale);
    109.                 o.Specular = tex2D(_SpecMap, UV* _Scale).a;
    110.                 half4 y = tex2D (_MainTex, uv.xz);
    111.                 y = tex2D(_Bump, UV* _Scale);
    112.                 n = lerp(n,y,IN.sworldNormal.g);
    113.         }
    114.     }
    115.  
    116.  
    117.  
    118.  
    119.     o.Normal = UnpackNormal(n);
    120.     o.Albedo = c.rgb * _Color;
    121.     o.Emission = _Emission;
    122.  
    123. }
    124. ENDCG
    125. }
    126.  
    127. Fallback "VertexLit"
    128. }
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Your “sworldNormal” isn’t the world normal, it’s the object space normal, but the worldPos is the world position. When you rotate the object, the object space "sworldNormal" is rotating with it, thus exposing the stretched textures as your code is now choosing the wrong "face" for the UVs. You also should be doing the abs() in the vert function, nor do you even need to be using the custom vertex function since you get the world normal without it.

    You can see how to get the world normal in a Surface shader that writes to o.Normal here:
    https://github.com/bgolus/Normal-Ma...der/blob/master/TriplanarSurfaceShader.shader

    Also you can see how to actually do proper normal mapping in a triplanar shader, because just lerping between them isn't going to do it correctly. See this related article on the topic:
    https://medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a
     
  3. Nishtana

    Nishtana

    Joined:
    Jan 5, 2017
    Posts:
    6
    I really appreciate the help on this! I've played with triplanar shaders before trying to create what I want however it creates an odd blend on my angled faces, which I assume is from lerping?
    Is there a way to limit at which point the planes project? meaning the sides and diagonals remain the same texture direction, when the top face is separate?
     
    Last edited: Oct 17, 2019
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Ideally you shouldn't be using a lerp at all. If you've properly calculated the blend weights then you just need to multiply each face by the blend amount and add them all together (see the part of my article on this particular topic). If you're having to resort to using a lerp it usually means something is going wrong elsewhere.

    However lerp or not, the above issue is an artifact of that face being exactly in the middle of all 3 blend directions. Each axis's texture is contributing to 1/3rd of the overall texture. Cranking up the sharpness of the blend won't help either since it's still exactly in the middle of the blend. Using an if statement for a fully hard edge won't necessarily help either since due to floating point precision and interpolation the shader may not be consistent with which face it picks.

    If all your geometry is going to be this blocky, with little all faces either being on the main axis, 45 degree angles, or these perfect corner diagonals, then you could modify the blend weights to give the top a greater chance of getting chosen. See the asymmetrical blend in the article. Alternatively using height map based blending would work over a greater range of surfaces as it lets you increase the sharpness of the blend between textures and a bit of randomness across the blend that can help hide the corner angle degenerate cases.
     
  5. Nishtana

    Nishtana

    Joined:
    Jan 5, 2017
    Posts:
    6
    That asymmetric blending works really well for what I'm doing, I'm just trying to switch it so that the diagonal sides that split the axis have the x and z textures projected on it. I very new to shaders so my understanding is minimal. One other thing I'd like to do is have the possibility to add a rotation property for the shader that when "toggled" rotates the top face or y axis texture by 90 degrees. I've tried using an if statement and then simply changing xz to zx on the y axis for the UV's however the if statement causes the #if statements to no longer have uvY be defined.

    Side note, I'd love if you had a beginners tutorial I could go through. Shaders has always been a weakness of mine and I'd like to change that.
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Code (csharp):
    1. // define property
    2. [Toggle] _RotateTop ("Rotate Top", Float) = 0 // materials can't save bools, so it's faked with a float
    3.  
    4. // setup uniform bool, auto converted from float to bool
    5. bool _RotateTop;
    6.  
    7. // inline switch
    8. UV = _RotateTop ? IN.worldPos.xz : IN.worldPos.zx
     
  7. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    I don't, but there are plenty of threads on the forum on this. I like pointing people at Alan Zucconi's beginner tutorial:
    https://www.alanzucconi.com/2015/06/10/a-gentle-introduction-to-shaders-in-unity3d/

    After that there are a ton of shader tutorials on YouTube, and crawling through Shader Toy can be useful in just getting a sense for what's possible. The problem is there's a big gap between understanding the basics and being able to sit down and write code to do exactly what you want. To be honest for a lot things it's still trial and error or searching online for examples ... just like any kind of programming really. ;)
     
  8. Nishtana

    Nishtana

    Joined:
    Jan 5, 2017
    Posts:
    6
    Thank you so much! I knew bools didn't work with shaders so I was trying to find a work around that was neater then just setting up a property with a range (0, 1). That is exactly what I was looking for. I also tweaked the asymmetrical blending to allow me to pinpoint which axis projects on the diagonal planes. I know it's not perfect but it's doing exactly what we need it to. Please let me know what you think, I always appreciate constructive criticism.

    Code (CSharp):
    1. Shader "Triplanar/Surface Shader (RNM)" {
    2.     Properties {
    3.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    4.         _Emission("Emission", Color) = (0,0,0,1)
    5.         [NoScaleOffset] _BumpMap("Normal Map", 2D) = "bump" {}
    6.         _Specular ("Specular (G)", 2D) = "black" {}
    7.         [NoScaleOffset] _OcclusionMap("Occlusion", 2D) = "white" {}
    8.         _OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
    9.         [Toggle] _Rotation("Top Rotation", Float) = 0
    10.         _Scale("Texture Scale", Float) = 1.0
    11.        
    12.     }
    13.     SubShader {
    14.         Tags { "RenderType"="Opaque" }
    15.         LOD 200
    16.        
    17.         CGPROGRAM
    18.         // Physically based Standard lighting model, and enable shadows on all light types
    19.         #pragma surface surf StandardSpecular
    20.  
    21.         // Use shader model 3.0 target, to get nicer looking lighting
    22.         #pragma target 3.0
    23.  
    24.         #include "UnityStandardUtils.cginc"
    25.  
    26.         // flip UVs horizontally to correct for back side projection
    27.         #define TRIPLANAR_CORRECT_PROJECTED_U
    28.  
    29.         // offset UVs to prevent obvious mirroring
    30.         // #define TRIPLANAR_UV_OFFSET
    31.  
    32.         // Reoriented Normal Mapping
    33.         // http://blog.selfshadow.com/publications/blending-in-detail/
    34.         // Altered to take normals (-1 to 1 ranges) rather than unsigned normal maps (0 to 1 ranges)
    35.         half3 blend_rnm(half3 n1, half3 n2)
    36.         {
    37.             n1.z += 1;
    38.             n2.xy = -n2.xy;
    39.  
    40.             return n1 * dot(n1, n2) / n1.z - n2;
    41.         }
    42.  
    43.         sampler2D _MainTex;
    44.         float4 _MainTex_ST;
    45.  
    46.         sampler2D _BumpMap;
    47.         sampler2D _OcclusionMap;
    48.         sampler2D _Specular;
    49.         float4 _Emission;
    50.         float _Scale;
    51.         bool _Rotation;
    52.        
    53.        
    54.         half _OcclusionStrength;
    55.  
    56.         struct Input {
    57.             float3 worldPos;
    58.             float3 worldNormal;
    59.             INTERNAL_DATA
    60.         };
    61.  
    62.         float3 WorldToTangentNormalVector(Input IN, float3 normal) {
    63.             float3 t2w0 = WorldNormalVector(IN, float3(1,0,0));
    64.             float3 t2w1 = WorldNormalVector(IN, float3(0,1,0));
    65.             float3 t2w2 = WorldNormalVector(IN, float3(0,0,1));
    66.             float3x3 t2w = float3x3(t2w0, t2w1, t2w2);
    67.             return normalize(mul(t2w, normal));
    68.         }
    69.  
    70.         void surf (Input IN, inout SurfaceOutputStandardSpecular o) {
    71.             // work around bug where IN.worldNormal is always (0,0,0)!
    72.             IN.worldNormal = WorldNormalVector(IN, float3(0,0,1));
    73.            
    74.  
    75.  
    76.        
    77.             // Asymmetric Triplanar Blend
    78.             float3 triblend = 0;
    79.             // Blend for sides only
    80.             triblend.z = saturate((abs(IN.worldNormal.z) - 0.8) * 10.0);
    81.             triblend.x = saturate((abs(IN.worldNormal.x) - 0.5) * 80.0);
    82.             // Blend for top
    83.             triblend.y = saturate((abs(IN.worldNormal.y) - 0.8) * 80.0);
    84.             triblend.xz *= (1 - triblend.y);
    85.            
    86.             // applying texture scale and offset values ala TRANSFORM_TEX macro
    87.  
    88.  
    89.             float2 uvX = (IN.worldPos.zy * _MainTex_ST.xy + _MainTex_ST.zy);
    90.             float2 uvY = (IN.worldPos.xz * _MainTex_ST.xy + _MainTex_ST.zy);
    91.             float2 uvZ = (IN.worldPos.xy * _MainTex_ST.xy + _MainTex_ST.zy);
    92.  
    93.             //Rotation
    94.             uvY = _Rotation ? IN.worldPos.xz : IN.worldPos.zx;
    95.  
    96.             // offset UVs to prevent obvious mirroring
    97.         #if defined(TRIPLANAR_UV_OFFSET)
    98.             uvY += 0.33;
    99.             uvZ += 0.67;
    100.         #endif
    101.  
    102.             // minor optimization of sign(). prevents return value of 0
    103.             half3 axisSign = IN.worldNormal < 0 ? -1 : 1;
    104.            
    105.             // flip UVs horizontally to correct for back side projection
    106.         #if defined(TRIPLANAR_CORRECT_PROJECTED_U)
    107.             uvX.x *= axisSign.x;
    108.             uvY.x *= axisSign.y;
    109.             uvZ.x *= -axisSign.z;
    110.         #endif
    111.  
    112.             // albedo textures
    113.             fixed4 colX = tex2D(_MainTex, uvX* _Scale);
    114.             fixed4 colY = tex2D(_MainTex, uvY* _Scale);
    115.             fixed4 colZ = tex2D(_MainTex, uvZ* _Scale);
    116.             fixed4 col = colX * triblend.x + colY * triblend.y + colZ * triblend.z;
    117.  
    118.             // specular textures
    119.             fixed4 specX = tex2D(_Specular, uvX* _Scale);
    120.             fixed4 specY = tex2D(_Specular, uvY* _Scale);
    121.             fixed4 specZ = tex2D(_Specular, uvZ* _Scale);
    122.             fixed4 spec = specX * triblend.x + specY * triblend.y + specZ * triblend.z;
    123.  
    124.             // occlusion textures
    125.             half occX = tex2D(_OcclusionMap, uvX* _Scale).g;
    126.             half occY = tex2D(_OcclusionMap, uvY* _Scale).g;
    127.             half occZ = tex2D(_OcclusionMap, uvZ* _Scale).g;
    128.             half occ = LerpOneTo(occX * triblend.x + occY * triblend.y + occZ * triblend.z, _OcclusionStrength);
    129.  
    130.             // tangent space normal maps
    131.             half3 tnormalX = UnpackNormal(tex2D(_BumpMap, uvX* _Scale));
    132.             half3 tnormalY = UnpackNormal(tex2D(_BumpMap, uvY* _Scale));
    133.             half3 tnormalZ = UnpackNormal(tex2D(_BumpMap, uvZ* _Scale));
    134.  
    135.             // flip normal maps' x axis to account for flipped UVs
    136.         #if defined(TRIPLANAR_CORRECT_PROJECTED_U)
    137.             tnormalX.x *= axisSign.x;
    138.             tnormalY.x *= axisSign.y;
    139.             tnormalZ.x *= -axisSign.z;
    140.         #endif
    141.  
    142.             half3 absVertNormal = abs(IN.worldNormal);
    143.  
    144.             // swizzle world normals to match tangent space and apply reoriented normal mapping blend
    145.             tnormalX = blend_rnm(half3(IN.worldNormal.zy, absVertNormal.x), tnormalX);
    146.             tnormalY = blend_rnm(half3(IN.worldNormal.xz, absVertNormal.y), tnormalY);
    147.             tnormalZ = blend_rnm(half3(IN.worldNormal.xy, absVertNormal.z), tnormalZ);
    148.  
    149.             // apply world space sign to tangent space Z
    150.             tnormalX.z *= axisSign.x;
    151.             tnormalY.z *= axisSign.y;
    152.             tnormalZ.z *= axisSign.z;
    153.  
    154.             // sizzle tangent normals to match world normal and blend together
    155.             half3 worldNormal = normalize(
    156.                 tnormalX.zyx * triblend.x +
    157.                 tnormalY.xzy * triblend.y +
    158.                 tnormalZ.xyz * triblend.z
    159.                 );
    160.  
    161.             // set surface ouput properties
    162.             o.Albedo = col.rgb;
    163.             o.Specular = spec.a;
    164.             o.Emission = _Emission;
    165.             o.Occlusion = occ;
    166.  
    167.             // convert world space normals into tangent normals
    168.             o.Normal = WorldToTangentNormalVector(IN, worldNormal);
    169.         }
    170.         ENDCG
    171.     }
    172.     FallBack "Diffuse"
    173. }
     
  9. Nishtana

    Nishtana

    Joined:
    Jan 5, 2017
    Posts:
    6
    @bgolus quick question. I'm trying to eliminate the artifacting on the diagonal faces, what's in my head is to expand the x axis projection into the z axis or slightly offset it so that these diagonal faces will show one or the other projection and not both. Is there a way to do that modifying your asymmetrical blending code?

    I added this little snippet to it after playing around and it appears to be working. Do you think it's going to break something down the road?
    Code (CSharp):
    1. IN.worldNormal.x = WorldNormalVector(IN, float3(0,0,1)+0.15);
     
    Last edited: Oct 19, 2019
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,352
    Sure. You can try multiplying one axis before the normalize. That should skew the angle a bit.