Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Triplanar Shader, using World Normals & Bump Mapping: HELP!

Discussion in 'Shaders' started by phr00t, Aug 19, 2016.

  1. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    I can't seem to get a working shader that takes in 3 textures, 3 bump maps and successfully shades an object with triplanar shading. Anytime I write to o.Normal, my objects simply go black. To simplify things, I'm trying to just apply 1 bump map as follows to otherwise standard triplanar shader:

    Code (CSharp):
    1. Shader "Tri-Planar World" {
    2.  
    3.     Properties {
    4.         _Side("Side", 2D) = "white" {}
    5.         _Top("Top", 2D) = "white" {}
    6.         _Bottom("Bottom", 2D) = "white" {}
    7.         _BumpMap("BumpMap", 2D) = "bump" {}
    8.     }
    9.  
    10.     SubShader {
    11.  
    12.         Tags {
    13.             "Queue" = "Geometry"
    14.             "IgnoreProjector" = "False"
    15.             "RenderType" = "Opaque"
    16.         }
    17.  
    18.         Cull Back
    19.         ZWrite On
    20.  
    21.         CGPROGRAM
    22.  
    23.         #pragma surface surf Standard
    24.         #pragma exclude_renderers flash
    25.         #pragma shader_feature _NORMALMAP_ON
    26.  
    27.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    28.  
    29.         struct Input {
    30.             float3 worldPos;
    31.             float3 worldNormal;
    32. #if _NORMALMAP_ON
    33.             INTERNAL_DATA
    34. #endif
    35.         };
    36.  
    37.         void surf(Input IN, inout SurfaceOutputStandard o) {
    38.             float3 projNormal = saturate(pow(IN.worldNormal * 1.4, 4));
    39.  
    40.             // SIDE X
    41.             float3 x = tex2D(_Side, frac(IN.worldPos.zy * 0.7)) * abs(IN.worldNormal.x);
    42.  
    43.             // TOP / BOTTOM
    44.             float3 y = 0;
    45.             float absny = abs(IN.worldNormal.y);
    46.             float2 fraczx = frac(IN.worldPos.zx * 0.7);
    47.             if (IN.worldNormal.y > 0) {
    48.                 y = tex2D(_Top, fraczx) * absny;
    49.             }
    50.             else {
    51.                 y = tex2D(_Bottom, fraczx) * absny;
    52.             }
    53.  
    54.             // SIDE Z
    55.             float3 z = tex2D(_Side, frac(IN.worldPos.xy * 0.7)) * abs(IN.worldNormal.z);
    56.  
    57. #if _NORMALMAP_ON
    58.             o.Normal = UnpackNormal(tex2D(_BumpMap, fraczx) * absny);
    59. #endif
    60.             o.Albedo = z;
    61.             o.Albedo = lerp(o.Albedo, x, projNormal.x);
    62.             o.Albedo = lerp(o.Albedo, y, projNormal.y);
    63.         }
    64.  
    65.         ENDCG
    66.     }
    67.     Fallback "Diffuse"
    68. }
    If I comment out the o.Normal line, my stuff looks fine (but no bump mapping, as expected). If I set o.Normal to apparently anything, even (0,1,0) -- things still go black.

    Here is a shader that supposedly does what I want:

    http://forum.unity3d.com/threads/tr...se-spec-versions-as-well.195334/#post-1335059

    ... but the "world normal" it calculated in the vertex shader didn't seem stable.. as I moved around the scene, the normals of my objects would go whack.

    If I modify my shader to try calculating the world normal as the linked one above is doing:

    Code (CSharp):
    1. Shader "Tri-Planar World" {
    2.  
    3.     Properties {
    4.         _Side("Side", 2D) = "white" {}
    5.         _Top("Top", 2D) = "white" {}
    6.         _Bottom("Bottom", 2D) = "white" {}
    7.         _BumpMap("BumpMap", 2D) = "bump" {}
    8.     }
    9.  
    10.     SubShader {
    11.  
    12.         Tags {
    13.             "Queue" = "Geometry"
    14.             "IgnoreProjector" = "False"
    15.             "RenderType" = "Opaque"
    16.         }
    17.  
    18.         Cull Back
    19.         ZWrite On
    20.  
    21.         CGPROGRAM
    22.  
    23.         #pragma surface surf Standard vert:vertWorld
    24.         #pragma exclude_renderers flash
    25.         #pragma shader_feature _NORMALMAP_ON
    26.  
    27.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    28.  
    29.         struct Input {
    30.             float3 worldPos;
    31.             float3 thisNormal;
    32. //#if _NORMALMAP_ON
    33. //            INTERNAL_DATA
    34. //#endif
    35.         };
    36.  
    37.         void vertWorld(inout appdata_full v, out Input o) {
    38.             o.thisNormal = mul(unity_ObjectToWorld, float4(v.normal, 0.0f)).xyz;
    39.         }
    40.  
    41.         void surf(Input IN, inout SurfaceOutputStandard o) {
    42.             float3 projNormal = saturate(pow(IN.thisNormal * 1.4, 4));
    43.  
    44.             // SIDE X
    45.             float3 x = tex2D(_Side, frac(IN.worldPos.zy * 0.7)) * abs(IN.thisNormal.x);
    46.  
    47.             // TOP / BOTTOM
    48.             float3 y = 0;
    49.             float absny = abs(IN.thisNormal.y);
    50.             float2 fraczx = frac(IN.worldPos.zx * 0.7);
    51.             if (IN.thisNormal.y > 0) {
    52.                 y = tex2D(_Top, fraczx) * absny;
    53.             }
    54.             else {
    55.                 y = tex2D(_Bottom, fraczx) * absny;
    56.             }
    57.  
    58.             // SIDE Z
    59.             float3 z = tex2D(_Side, frac(IN.worldPos.xy * 0.7)) * abs(IN.thisNormal.z);
    60.  
    61. #if _NORMALMAP_ON
    62.             o.Normal = UnpackNormal(tex2D(_BumpMap, fraczx) * absny);
    63. #endif
    64.             o.Albedo = z;
    65.             o.Albedo = lerp(o.Albedo, x, projNormal.x);
    66.             o.Albedo = lerp(o.Albedo, y, projNormal.y);
    67.         }
    68.  
    69.         ENDCG
    70.     }
    71.     Fallback "Diffuse"
    72. }
    ... all upward facing textures are smeared in one direction & the normals (as I said before) are not stable (because I notice flickering surfaces in relation to my directional light).

    I'm pulling my hair out & I can't seem to figure out how to write a triplanar shader, that uses the world normal to blend 3 textures, and then apply a bump map.

    Any help is greatly appreciated!
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    What's the #pragma shader_feature _NORMALMAP_ON in there for? I would remove that, it's unnecessary in a surface shader and may even be part of the problem.

    Additionally this is going to be very hard to get working in a surface shader as it expects the o.Normal to be a tangent space normal that matches the tangent space of the original mesh. That is to say you're using a normal map that uses the exact same UVs as the model was saved with, and not the UVs you're generating from the world position. Anything you pass to o.Normal is going to get transformed from the model's tangent space to world space or whatever space it needs for lighting. That means to use a surface shader you have to transform your normal maps into the models tangent space from your custom UV tangent space which is possible, but means some additional math.
     
  3. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    Thank you for the response!

    I have _NORMALMAP_ON because some materials may not have a normal map set, so I want that section removed if that is the case.

    Is there a better way to go about this that wouldn't require significant math? I'm not sure how to go about tangent spaces (never really understood that part).

    I wonder if that other shader I linked ever worked with world position UVs?

    Triplanar texturing with bump maps seems like it'd be a common situation for procedural terrain...
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Are you toggling the _NORMALMAP_ON from code yourself? If not then it's likely never even turning on normal maps. The standard shader has a custom editor that toggles the keyword on and off, and you can write your own custom editor to do the same or add a [Toggle] _NormalMap ("Enable Normal Map", Float) = 0 to your property block.

    As for if the shader in that link ever worked, looking through there's one shader that'll do something, but it won't be remotely correct in any way. It would probably have looked fine with a sufficiently noisy normal map. I'm pretty sure the one Farfarer linked probably works and is doing things more correct, but I've not looked at the shader in his package. Also he probably isn't using a surface shader which makes this particular problem a bit easier to deal with.

    The short version is normal maps require some amount of math per tangent space to transform the direction from tangent to world space, and with potentially 6 different tangent spaces (optimizable down to 3 at a time) that's math you will have to do for a triplanar shader with normal maps. The problem with surface shaders is you are adding two more transforms that you cannot avoid as you have to convert from the triplanar tangent space to world space, back to the mesh's tangent space so the surface shader can conver it back to world space. You can do some of that before hand in the vertex shader so you only have to do the transform from triplanar tangent space to mesh UVs tangent space once per side in the surface shader function, but it's annoying and again way easier to just go straight to world in a vertex / fragment shader.
     
  5. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    Yes, I am toggling the _NORMALMAP_ON keyword from code myself when a bump map gets set.

    GOOD NEWS! I made some progress & I feel SOOOOO CLOSE, but it still isn't working 100%. If I look mostly down at the surface, it works exactly as I want: I see the bumping on the top surface & it is using my world normals & world position as the UV maps. However, if I turn a certain way or look up, the bump map seems to disappear. It seems to disappear & reappear immediately, based on the angle / rotation I'm looking at the surface (it doesn't fade in and out). Overall lighting doesn't change -- just the bump map's visibility:

    Code (CSharp):
    1. Shader "Tri-Planar World" {
    2.  
    3.     Properties {
    4.         _Side("Side", 2D) = "white" {}
    5.         _Top("Top", 2D) = "white" {}
    6.         _Bottom("Bottom", 2D) = "white" {}
    7.         _BumpMap("NormalMap 1 (_Y_X)", 2D) = "bump" {}
    8.     }
    9.  
    10.     SubShader {
    11.  
    12.         Tags {
    13.             "Queue" = "Geometry"
    14.             "IgnoreProjector" = "False"
    15.             "RenderType" = "Opaque"
    16.         }
    17.  
    18.         Cull Back
    19.         ZWrite On
    20.  
    21.         CGPROGRAM
    22.  
    23.         #include "UnityCG.cginc"
    24.  
    25.         #pragma surface surf BlinnPhong vertex:vertWorld
    26.         #pragma exclude_renderers flash
    27.         #pragma shader_feature _NORMALMAP_ON
    28.  
    29.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    30.  
    31.         struct Input {
    32.             float3 worldPos;
    33.             float3 thisNormal;
    34.         };
    35.  
    36.         // Vertex program is determined in pragma above
    37.         void vertWorld(inout appdata_full v, out Input o)
    38.         {
    39.             o.thisNormal = UnityObjectToWorldNormal(v.normal.xyz);
    40.             o.worldPos = mul(unity_ObjectToWorld, v.vertex);
    41.         }
    42.  
    43.         void surf(Input IN, inout SurfaceOutput o) {
    44.             float3 projNormal = saturate(pow(IN.thisNormal * 1.4, 4));
    45.  
    46.             // SIDE X
    47.             float3 x = tex2D(_Side, frac(IN.worldPos.zy * 0.7)) * abs(IN.thisNormal.x);
    48.  
    49.             // TOP / BOTTOM
    50.             float3 y = 0;
    51.             float absny = abs(IN.thisNormal.y);
    52.             float2 fraczx = frac(IN.worldPos.zx * 0.7);
    53.             if (IN.thisNormal.y > 0) {
    54.                 y = tex2D(_Top, fraczx) * absny;
    55.             }
    56.             else {
    57.                 y = tex2D(_Bottom, fraczx) * absny;
    58.             }
    59.  
    60.             // SIDE Z  
    61.             float3 z = tex2D(_Side, frac(IN.worldPos.xy * 0.7)) * abs(IN.thisNormal.z);
    62.  
    63. #if _NORMALMAP_ON
    64.             // Sample bump maps too, and generate bump vectors. (Note: this uses an oversimplified tangent basis.)
    65.             // Using Unity packed normals (_Y_X), but we don't unpack since we don't need z.                  
    66.             half2 bumpVec1 = tex2D(_BumpMap, fraczx).wy * 2 - 1;      // To use standard normal maps change wy to xy
    67.  
    68.             half3 bump1 = half3(0, bumpVec1.x, bumpVec1.y);
    69.  
    70.             o.Normal = normalize(half3(0, 0, 1) + bump1);
    71. #endif
    72.  
    73.             o.Albedo = z;
    74.             o.Albedo = lerp(o.Albedo, x, projNormal.x);
    75.             o.Albedo = lerp(o.Albedo, y, projNormal.y);
    76.         }
    77.  
    78.         ENDCG
    79.     }
    80.     Fallback "Diffuse"
    81. }
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    That'll get you something in the normals, but not what you really want. You'll randomly get normals that are inverted, or flatten out depending on where on the model you're looking.

    Here's a version of your shader with a bunch of stuff cleaned up, and "good enough" triplanar normals.
    Code (CSharp):
    1.  
    2. Shader "Tri-Planar World" {
    3.  
    4.     Properties {
    5.         _Side("Side", 2D) = "white" {}
    6.         _Top("Top", 2D) = "white" {}
    7.         [NoScaleOffset] _Bottom("Bottom", 2D) = "white" {}
    8.         [NoScaleOffset] _BumpMap("NormalMap", 2D) = "bump" {}
    9.     }
    10.  
    11.     SubShader {
    12.  
    13.         Tags {
    14.             "Queue" = "Geometry"
    15.             "IgnoreProjector" = "False"
    16.             "RenderType" = "Opaque"
    17.         }
    18.  
    19.         Cull Back
    20.         ZWrite On
    21.  
    22.         CGPROGRAM
    23.  
    24.         #include "UnityCG.cginc"
    25.  
    26.         #pragma surface surf BlinnPhong vertex:vertWorld
    27.         #pragma exclude_renderers flash
    28.         #pragma target 3.0
    29.  
    30.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    31.         float4 _Side_ST, _Top_ST;
    32.  
    33.         struct Input {
    34.             float4 worldToTangent0;
    35.             float4 worldToTangent1;
    36.             float4 worldToTangent2;
    37.             float3 worldPos;
    38.             INTERNAL_DATA
    39.         };
    40.  
    41.         // Vertex program is determined in pragma above
    42.         void vertWorld(inout appdata_full v, out Input o)
    43.         {
    44.             UNITY_INITIALIZE_OUTPUT(Input,o);
    45.  
    46.             // really shouldn't have to do this, but looks like surface shaders are bugged
    47.             // and aren't passing the world normal to the surface shader with using:
    48.             // struct Input { float3 worldNormal; }
    49.             // luckily we have free float3 so we don't need another interpolator
    50.             float3 worldNormal = UnityObjectToWorldNormal(v.normal);
    51.  
    52.             // unity macro for getting the object to tangent space matrix as var "rotation"
    53.             TANGENT_SPACE_ROTATION;
    54.  
    55.             // calculate the full world to tangent matrix
    56.             float3x3 worldToTangent = mul(rotation, (float3x3)unity_WorldToObject);
    57.             o.worldToTangent0 = float4(worldToTangent[0], worldNormal.x);
    58.             o.worldToTangent1 = float4(worldToTangent[1], worldNormal.y);
    59.             o.worldToTangent2 = float4(worldToTangent[2], worldNormal.z);
    60.         }
    61.  
    62.         void surf(Input IN, inout SurfaceOutput o) {
    63.             // extract world normal from the unused w component of world to tangent matrix
    64.             float3 worldNormal = float3(IN.worldToTangent0.w, IN.worldToTangent1.w, IN.worldToTangent2.w);
    65.             float3 projNormal = saturate(pow(worldNormal * 1.4, 4));
    66.  
    67.             // "normalize" projNormal x+y+z to equal 1, ensures even blend
    68.             projNormal /= projNormal.x + projNormal.y + projNormal.z;
    69.  
    70.             // SIDE X
    71.             float xsign = sign(worldNormal.x);
    72.             float2 zy = IN.worldPos.zy * float2(xsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    73.             float3 xAlbedo = tex2D(_Side, zy);
    74.             float3 xNorm = UnpackNormal(tex2D(_BumpMap, zy));
    75.             // xNorm.z *= xsign; // flip normal based on +/- x
    76.  
    77.             // TOP / BOTTOM
    78.             float ysign = sign(worldNormal.y);
    79.             float2 zx = IN.worldPos.zx * _Top_ST.xy + _Top_ST.zw;
    80.             float yAlbedo = ysign > 0 ? tex2D(_Top, zx) : tex2D(_Bottom, zx);
    81.             float3 yNorm = UnpackNormal(tex2D(_BumpMap, zx));
    82.             // yNorm.z *= ysign;
    83.  
    84.             // SIDE Z
    85.             float zsign = sign(worldNormal.z);
    86.             float2 xy = IN.worldPos.xy * float2(-zsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    87.             float3 zAlbedo = tex2D(_Side, xy);
    88.             float3 zNorm = UnpackNormal(tex2D(_BumpMap, xy));
    89.             // zNorm.z *= zsign; // flip normal based on +/- z
    90.  
    91.             // use "UDN" style normal blending to wrap normal map to surface normal
    92.             // prevents normals from just being on a "box"
    93.             // world normal components are swizzled so their major axis is along "z", normal map-like
    94.             xNorm = normalize(float3(xNorm.xy * float2(xsign, 1.0) + worldNormal.zy, worldNormal.x));
    95.             yNorm = normalize(float3(yNorm.xy + worldNormal.zx, worldNormal.y));
    96.             zNorm = normalize(float3(zNorm.xy * float2(-zsign, 1.0) + worldNormal.xy, worldNormal.z));
    97.  
    98.             // reorient normal maps to their world axis by swizzling the components
    99.             xNorm = xNorm.zyx; // hackily swizzle channels to match unity "right"
    100.             yNorm = yNorm.yzx; // hackily swizzle channels to match unity "up"
    101.             zNorm = zNorm.xyz; // no swizzle needed!
    102.  
    103.             // blend normals together
    104.             float3 wNorm = xNorm * projNormal.x + yNorm * projNormal.y + zNorm * projNormal.z;
    105.  
    106.             // transform world space normals back to tangent space so that the surface shader
    107.             // can transform them back to world space.
    108.             o.Normal = normalize(
    109.                 float3(
    110.                     dot(wNorm, IN.worldToTangent0.xyz),
    111.                     dot(wNorm, IN.worldToTangent1.xyz),
    112.                     dot(wNorm, IN.worldToTangent2.xyz)
    113.                     )
    114.                 );
    115.  
    116.             // blend albedos together
    117.             o.Albedo = xAlbedo * projNormal.x + yAlbedo * projNormal.y + zAlbedo * projNormal.z;
    118.         }
    119.  
    120.         ENDCG
    121.     }
    122.     Fallback "Diffuse"
    123. }
    124.  
    Note: This is terrible and no one should use this.

    There's a ton of unnecessary computation being done just to work around surface shaders which is why when people do these kinds of shaders they don't use surface shaders. Also, this still isn't actually correct, but it's close enough that it's hard to tell it's wrong.
     
    antonkudin and Kusras like this.
  7. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    If that is terrible & shouldn't be done as a surface shader, how should it be done?
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    With a straight vertex fragment shader.

    Most of that code is fine, hacky but fine. It's a fairly decent approximation of world space triplanar normal maps and perfectly valid to use. The stuff that's ugly are lines 53-59 and lines 108 - 114, they only exist to get stuff working in a surface shader.

    One way you could go is to take that surface shader, select it in the editor, click on "show generated code" and cut that shader down. That can be kind of scary though as surface shaders generate some fairly dense and occasionally obtuse code. A better route would be to learn to write an equivalent shader from scratch, or close to.

    This page is a great resource for understanding how shaders work. It starts a little scary looking, but work through it. All 5 parts.
    http://www.alanzucconi.com/2015/06/10/a-gentle-introduction-to-shaders-in-unity3d/

    And this page has most of the bits needed to make a shader that's equivalent to the functionality of the surface shader, though not all in one shader and is missing deferred support which the surface shader does have.
    https://docs.unity3d.com/Manual/SL-VertexFragmentShaderExamples.html
     
  9. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    I've written vertex and fragment shaders before for non-Unity titles. I had to do everything in those cases, like my own lighting. I want to leverage Unity's automation wherever possible. I also want this to work with single-pass VR rendering. Would writing my own vertex and fragment shaders require getting my hands dirty in those areas?

    Another thought... can I relatively easily detect when the normals would be flat or inverted in my version, and counteract it? Ultimately I don't care how correct it is, I just want it to be applied somehow & stable (so my surfaces are not always flat).
     
  10. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Adding support for single pass rendering can be done in about 3 lines of shader code. Almost all of it is done in macros; a line in the v2f, a line in the vertex function, and a line in the fragment shader.

    The way to detect if the normals are going to be flipped is by calculating the inverse tangent matrix, in which case it ends up being faster / cheaper / more accurate to just apply that rotation matrix than try decode it to see if it'll be wrong with out it... which is exactly what that example shader does.

    If you're set on using surface shaders the code above does work, and can be used, but even if you were to write the shader out "from scratch" you would end up using a lot of built in functions and macros for the majority of the more complex bits of code so it's not really from scratch, it's more a case of knowing what to use. That example page is a great reference, as is looking at the generated shader code if for nothing else but seeing what it's using if not trying to modify it.

    All of this boils down to surface shaders expecting the value set on o.Normal to be in mesh UV tangent space, which it then transforms into world space before doing lighting. There's no avoiding that, it's just what surface shaders are going to do. This is unfortunate because it's usually fairly easy to generate world space normals when doing world space triplanar mapping, so for example in that above shader by line 104 we already have the world space normal Unity wants for lighting.

    Maybe you need to get your head around normal maps and tangent space for this to make sense.

    Tangent space is just the surface normal (the direction a surface is facing) and the orientation of the texture UVs. If you were to draw a texture with a red arrow pointing to the right and a green arrow pointing up and applied that to a surface the direction the arrows point are literally the tangent and binormal (sometimes called bitangent) directions. A normal map is then storing how far left & right or up & down off from the surface normal direction, towards those red and green arrows, it should look like the surface is facing.

    The math is minorly more complex than that, but only a little. The math for calculating those directions to begin with is more complex which is why it's usually stored in the mesh as the normal and tangent values.

    When you're doing world space triplanar mapping because the UVs you're generating don't match the mesh's original UVs usually you can just ignore them and the tangent directions that were calculated for those UVs. Because you're working in world space you can fake the tangent and binormal directions by just using world space directions. The common thing to do then is to adjust the tangent and binormal by using some vector math (a couple of cross() calls), which will result in essentially perfect normal mapping, but I went with a normal blending technique that gets you 95% of the way with less math.
     
  11. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    I am not set on using surface shaders, it is just what I tried to use first. If it is more efficient to use vertex and fragment shaders, then that is what I should use. A little disappointing, though, since one reason for switching to Unity was to avoid writing my own raw shaders.

    Every example I've seen thus far has been of a surface shader. A bit daunting, and feeling like more uncharted area to delve into, to write straight vertex and fragment shaders with Unity.

    I'd use your surface shader, but I'm worried it is inefficient... especially when I'm planning on bringing this title to VR.

    Thank you for all of your help this far... was hoping this wasn't going to be so complicated :-/
     
  12. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Surface shaders are great for a lot of things. You just picked one of the few things surface shaders aren't great at.
     
  13. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    @bgolus, I tried out your shader above & it suffered the same problem mine has been having: instability. As I turn around, the bumps on a surface will blink in & out. It also appears lighting is screwy, as when bumps blink in & out, the overall brightness of the surface changes. It seems to happen at specific rotations, either it will look good, flat or act like the normal is looking in a completely different direction. If it helps, I'm sharing the material across many different objects that are being resized & rotated -- but isn't that suppose to be the benefit of world positioning triplanar shaders, you can apply them to anything at any rotation?
     
  14. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    That is ... odd. Sounds like batching going wrong.
     
  15. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    I do share the material on low-poly models next to eachother. I did this to hopefully & specifically benefit from dynamic batching. I'm heading to bed so I can't test disabling that feature (player option, I think?) at the moment. Perhaps I should somehow statically batch my objects once I am done creating them? Any suggestions to get around this if batching is to blame? Disabling batching may bring a nasty performance hit...
     
  16. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Here's that shader as a vertex / fragment shader (sans support for lightmaps or deferred lighting). It's also just Lambert diffuse instead of Blinn Phong specular (but since your original shader wasn't using Specular or Gloss values meant it was just Lambert anyway).

    Curious if you still have problems with this code.

    Code (CSharp):
    1. Shader "Triplanar Bumped"
    2. {
    3.     Properties {
    4.         _Side("Side", 2D) = "white" {}
    5.         _Top("Top", 2D) = "white" {}
    6.         [NoScaleOffset] _Bottom("Bottom", 2D) = "white" {}
    7.         [NoScaleOffset] _BumpMap("NormalMap", 2D) = "bump" {}
    8.     }
    9.     SubShader
    10.     {
    11.         Tags { "RenderType"="Opaque" }
    12.  
    13.         CGINCLUDE
    14.         #include "HLSLSupport.cginc"
    15.         #include "UnityShaderVariables.cginc"  
    16.         #include "UnityCG.cginc"
    17.    
    18.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    19.         float4 _Side_ST, _Top_ST;
    20.  
    21.         void GetTriplanarTextures(float3 worldPos, float3 worldNormal, out fixed4 albedo, out half3 normal)
    22.         {
    23.                 // extract world normal from the unused w component of world to tangent matrix
    24.                 float3 projNormal = saturate(pow(worldNormal * 1.4, 4));
    25.  
    26.                 // "normalize" projNormal x+y+z to equal 1, ensures even blend
    27.                 projNormal /= projNormal.x + projNormal.y + projNormal.z;
    28.    
    29.                 // SIDE X
    30.                 float xsign = sign(worldNormal.x);
    31.                 float2 zy = worldPos.zy * float2(xsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    32.                 fixed4 xAlbedo = tex2D(_Side, zy);
    33.                 half3 xNorm = UnpackNormal(tex2D(_BumpMap, zy));
    34.                 // xNorm.z *= xsign; // flip normal based on +/- x
    35.    
    36.                 // TOP / BOTTOM
    37.                 float ysign = sign(worldNormal.y);
    38.                 float2 zx = worldPos.zx * _Top_ST.xy + _Top_ST.zw;
    39.                 fixed4 yAlbedo = ysign > 0 ? tex2D(_Top, zx) : tex2D(_Bottom, zx);
    40.                 half3 yNorm = UnpackNormal(tex2D(_BumpMap, zx));
    41.                 // yNorm.z *= ysign;
    42.    
    43.                 // SIDE Z
    44.                 float zsign = sign(worldNormal.z);
    45.                 float2 xy = worldPos.xy * float2(-zsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    46.                 fixed4 zAlbedo = tex2D(_Side, xy);
    47.                 half3 zNorm = UnpackNormal(tex2D(_BumpMap, xy));
    48.                 // zNorm.z *= zsign; // flip normal based on +/- z
    49.  
    50.                 // use normal blending to wrap normal map to surface normal
    51.                 xNorm = normalize(half3(xNorm.xy * float2(xsign, 1.0) + worldNormal.zy, worldNormal.x));
    52.                 yNorm = normalize(half3(yNorm.xy + worldNormal.zx, worldNormal.y));
    53.                 zNorm = normalize(half3(zNorm.xy * float2(-zsign, 1.0) + worldNormal.xy, worldNormal.z));
    54.  
    55.                 // reorient normals to their world axis
    56.                 xNorm = xNorm.zyx; // hackily swizzle channels to match unity "right"
    57.                 yNorm = yNorm.yzx; // hackily swizzle channels to match unity "up"
    58.                 zNorm = zNorm.xyz; // no swizzle needed
    59.  
    60.                 // blend normals together
    61.                 normal = xNorm * projNormal.x + yNorm * projNormal.y + zNorm * projNormal.z;
    62.    
    63.                 // blend albedos together
    64.                 albedo = xAlbedo * projNormal.x + yAlbedo * projNormal.y + zAlbedo * projNormal.z;
    65.         }
    66.         ENDCG
    67.  
    68.         Pass
    69.         {
    70.             Tags { "LightMode"="ForwardBase" }
    71.  
    72.             CGPROGRAM
    73.             #pragma vertex vert
    74.             #pragma fragment frag
    75.             #pragma target 3.0
    76.             #pragma multi_compile_fog
    77.             #pragma multi_compile_fwdbase
    78.             #include "Lighting.cginc"
    79.             #include "AutoLight.cginc"
    80.  
    81.             struct appdata
    82.             {
    83.                 float4 vertex : POSITION;
    84.                 float3 normal : NORMAL;
    85.             };
    86.  
    87.             struct v2f
    88.             {
    89.                 float4 pos : SV_POSITION;
    90.                 float3 worldNormal : NORMAL;
    91.                 float3 worldPos : TEXCOORD0;
    92.                 SHADOW_COORDS(1)
    93.                 UNITY_FOG_COORDS(2)
    94.             };
    95.          
    96.             v2f vert (appdata v)
    97.             {
    98.                 v2f o;
    99.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    100.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    101.                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
    102.                 TRANSFER_SHADOW(o);
    103.                 UNITY_TRANSFER_FOG(o,o.pos);
    104.                 return o;
    105.             }
    106.          
    107.             half4 frag (v2f i) : SV_Target
    108.             {
    109.                 fixed4 albedo;
    110.                 half3 normal;
    111.                 GetTriplanarTextures(i.worldPos, i.worldNormal, albedo, normal);
    112.              
    113.                 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    114.                 half3 lighting = saturate(dot(normalize(normal), _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb * atten;
    115.                 lighting += ShadeSH9(half4(normal,1));
    116.  
    117.                 half3 col = albedo.rgb * lighting;
    118.  
    119.                 UNITY_APPLY_FOG(i.fogCoord, col);
    120.                 return half4(col, 1);
    121.             }
    122.             ENDCG
    123.         }
    124.  
    125.         Pass
    126.         {
    127.             Tags { "LightMode"="ForwardAdd" }
    128.             ZWrite Off Blend One One
    129.  
    130.             CGPROGRAM
    131.             #pragma vertex vert
    132.             #pragma fragment frag
    133.             #pragma target 3.0
    134.             #pragma multi_compile_fog
    135.             #pragma multi_compile_fwdadd_fullshadows
    136.             #include "Lighting.cginc"
    137.             #include "AutoLight.cginc"
    138.  
    139.             struct appdata
    140.             {
    141.                 float4 vertex : POSITION;
    142.                 float3 normal : NORMAL;
    143.             };
    144.  
    145.             struct v2f
    146.             {
    147.                 float4 pos : SV_POSITION;
    148.                 float3 worldNormal : NORMAL;
    149.                 float3 worldPos : TEXCOORD0;
    150.                 SHADOW_COORDS(1)
    151.                 UNITY_FOG_COORDS(2)
    152.             };
    153.          
    154.             v2f vert (appdata v)
    155.             {
    156.                 v2f o;
    157.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    158.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    159.                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
    160.                 TRANSFER_SHADOW(o);
    161.                 UNITY_TRANSFER_FOG(o,o.pos);
    162.                 return o;
    163.             }
    164.          
    165.             half4 frag (v2f i) : SV_Target
    166.             {
    167.                 fixed4 albedo;
    168.                 half3 normal;
    169.                 GetTriplanarTextures(i.worldPos, i.worldNormal, albedo, normal);
    170.              
    171.                 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    172.  
    173.             #ifndef USING_DIRECTIONAL_LIGHT
    174.                 fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
    175.             #else
    176.                 fixed3 lightDir = _WorldSpaceLightPos0.xyz;
    177.             #endif
    178.                 half3 lighting = saturate(dot(normalize(normal), lightDir)) * _LightColor0.rgb * atten;
    179.  
    180.                 half3 col = albedo.rgb * lighting;
    181.  
    182.                 UNITY_APPLY_FOG(i.fogCoord, col);
    183.                 return half4(col, 1);
    184.             }
    185.             ENDCG
    186.         }
    187.  
    188.         Pass
    189.         {
    190.             Tags {"LightMode"="ShadowCaster"}
    191.  
    192.             CGPROGRAM
    193.             #pragma vertex vert
    194.             #pragma fragment frag
    195.             #pragma multi_compile_shadowcaster
    196.             #include "UnityCG.cginc"
    197.  
    198.             struct v2f {
    199.                 V2F_SHADOW_CASTER;
    200.             };
    201.  
    202.             v2f vert(appdata_base v)
    203.             {
    204.                 v2f o;
    205.                 TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
    206.                 return o;
    207.             }
    208.  
    209.             float4 frag(v2f i) : SV_Target
    210.             {
    211.                 SHADOW_CASTER_FRAGMENT(i)
    212.             }
    213.             ENDCG
    214.         }
    215.     }
    216. }
    217.  
     
    phr00t likes this.
  17. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    That one works & is stable! :D

    I'm OK skipping gloss & specular, but I do want to make sure single-pass VR rendering will work. You mention it being done in a few lines. It doesn't look like it is included yet? I presume there is an include & macro use somewhere...
     
  18. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    In the v2f struct:
    UNITY_VERTEX_OUTPUT_STEREO

    In the vert function:
    // at the start of the vert function
    UNITY_SETUP_INSTANCE_ID(v);
    ...
    // at the end of the vert function just before return o;
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
     
    phr00t likes this.
  19. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    Looks familiar :) I wrote my own VR instancing library for jMonkeyEngine. I didn't have to modify the fragment shaders & you didn't mention a change required above. You did mention a line in the fragment shader a few posts back, though. Just to confirm, that is all that is needed are those vertex macros?
     
  20. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    No, nothing needed in the fragment shader, just misremembering what was needed. (and I have done some custom shaders that do extra stuff in the fragment shader per eye).
     
  21. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    Do I need the VR stereo macros in the ShadowCaster? I'm presuming yes?
     
  22. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    It does no harm to add it, but I don't know if the depth pass uses it. Shadow map rendering for sure does not (yet, as sadly there's some possible win there for rendering cascades).
     
  23. phr00t

    phr00t

    Joined:
    Apr 19, 2016
    Posts:
    41
    This should be the final shader, then:

    Code (CSharp):
    1. Shader "Triplanar Bumped" {
    2.     Properties{
    3.         _Side("Side", 2D) = "white" {}
    4.         _Top("Top", 2D) = "white" {}
    5.         [NoScaleOffset] _Bottom("Bottom", 2D) = "white" {}
    6.         [NoScaleOffset] _BumpMap("NormalMap", 2D) = "bump" {}
    7.     }
    8.  
    9.     SubShader {
    10.         Tags{ "RenderType" = "Opaque" }
    11.  
    12.         CGINCLUDE
    13.         #include "HLSLSupport.cginc"
    14.         #include "UnityShaderVariables.cginc"
    15.         #include "UnityCG.cginc"
    16.  
    17.         sampler2D _Side, _Top, _Bottom, _BumpMap;
    18.         float4 _Side_ST, _Top_ST;
    19.  
    20.         void GetTriplanarTextures(float3 worldPos, float3 worldNormal, out fixed4 albedo, out half3 normal) {
    21.             // extract world normal from the unused w component of world to tangent matrix
    22.             float3 projNormal = saturate(pow(worldNormal * 1.4, 4));
    23.  
    24.             // "normalize" projNormal x+y+z to equal 1, ensures even blend
    25.             projNormal /= projNormal.x + projNormal.y + projNormal.z;
    26.  
    27.             // SIDE X
    28.             float xsign = sign(worldNormal.x);
    29.             float2 zy = worldPos.zy * float2(xsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    30.             fixed4 xAlbedo = tex2D(_Side, zy);
    31.             half3 xNorm = UnpackNormal(tex2D(_BumpMap, zy));
    32.             // xNorm.z *= xsign; // flip normal based on +/- x
    33.  
    34.             // TOP / BOTTOM
    35.             float ysign = sign(worldNormal.y);
    36.             float2 zx = worldPos.zx * _Top_ST.xy + _Top_ST.zw;
    37.             fixed4 yAlbedo = ysign > 0 ? tex2D(_Top, zx) : tex2D(_Bottom, zx);
    38.             half3 yNorm = UnpackNormal(tex2D(_BumpMap, zx));
    39.             // yNorm.z *= ysign;
    40.  
    41.             // SIDE Z
    42.             float zsign = sign(worldNormal.z);
    43.             float2 xy = worldPos.xy * float2(-zsign, 1.0) * _Side_ST.xy + _Side_ST.zw;
    44.             fixed4 zAlbedo = tex2D(_Side, xy);
    45.             half3 zNorm = UnpackNormal(tex2D(_BumpMap, xy));
    46.             // zNorm.z *= zsign; // flip normal based on +/- z
    47.  
    48.             // use normal blending to wrap normal map to surface normal
    49.             xNorm = normalize(half3(xNorm.xy * float2(xsign, 1.0) + worldNormal.zy, worldNormal.x));
    50.             yNorm = normalize(half3(yNorm.xy + worldNormal.zx, worldNormal.y));
    51.             zNorm = normalize(half3(zNorm.xy * float2(-zsign, 1.0) + worldNormal.xy, worldNormal.z));
    52.  
    53.             // reorient normals to their world axis
    54.             xNorm = xNorm.zyx; // hackily swizzle channels to match unity "right"
    55.             yNorm = yNorm.yzx; // hackily swizzle channels to match unity "up"
    56.             zNorm = zNorm.xyz; // no swizzle needed
    57.  
    58.                                // blend normals together
    59.             normal = xNorm * projNormal.x + yNorm * projNormal.y + zNorm * projNormal.z;
    60.  
    61.             // blend albedos together
    62.             albedo = xAlbedo * projNormal.x + yAlbedo * projNormal.y + zAlbedo * projNormal.z;
    63.         }
    64.         ENDCG
    65.  
    66.         Pass {
    67.             Tags{ "LightMode" = "ForwardBase" }
    68.  
    69.             CGPROGRAM
    70.             #pragma vertex vert
    71.             #pragma fragment frag
    72.             #pragma target 3.0
    73.             #pragma multi_compile_fog
    74.             #pragma multi_compile_fwdbase
    75.             #include "Lighting.cginc"
    76.             #include "AutoLight.cginc"
    77.  
    78.             struct appdata {
    79.                 float4 vertex : POSITION;
    80.                 float3 normal : NORMAL;
    81.             };
    82.  
    83.             struct v2f {
    84.                 float4 pos : SV_POSITION;
    85.                 float3 worldNormal : NORMAL;
    86.                 float3 worldPos : TEXCOORD0;
    87.                 SHADOW_COORDS(1)
    88.                 UNITY_FOG_COORDS(2)
    89.                 UNITY_VERTEX_OUTPUT_STEREO
    90.             };
    91.  
    92.             v2f vert(appdata v)    {
    93.                 v2f o;
    94.                 UNITY_SETUP_INSTANCE_ID(v);
    95.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    96.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    97.                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
    98.                 TRANSFER_SHADOW(o);
    99.                 UNITY_TRANSFER_FOG(o,o.pos);
    100.                 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    101.                 return o;
    102.             }
    103.  
    104.             half4 frag(v2f i) : SV_Target {
    105.                 fixed4 albedo;
    106.                 half3 normal;
    107.                 GetTriplanarTextures(i.worldPos, i.worldNormal, albedo, normal);
    108.  
    109.                 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    110.                 half3 lighting = saturate(dot(normalize(normal), _WorldSpaceLightPos0.xyz)) * _LightColor0.rgb * atten;
    111.                 lighting += ShadeSH9(half4(normal,1));
    112.  
    113.                 half3 col = albedo.rgb * lighting;
    114.  
    115.                 UNITY_APPLY_FOG(i.fogCoord, col);
    116.                 return half4(col, 1);
    117.             }
    118.         ENDCG
    119.         }
    120.  
    121.         Pass {
    122.             Tags{ "LightMode" = "ForwardAdd" }
    123.             ZWrite Off Blend One One
    124.  
    125.             CGPROGRAM
    126.             #pragma vertex vert
    127.             #pragma fragment frag
    128.             #pragma target 3.0
    129.             #pragma multi_compile_fog
    130.             #pragma multi_compile_fwdadd_fullshadows
    131.             #include "Lighting.cginc"
    132.             #include "AutoLight.cginc"
    133.  
    134.             struct appdata {
    135.                 float4 vertex : POSITION;
    136.                 float3 normal : NORMAL;
    137.             };
    138.  
    139.             struct v2f {
    140.                 float4 pos : SV_POSITION;
    141.                 float3 worldNormal : NORMAL;
    142.                 float3 worldPos : TEXCOORD0;
    143.                 SHADOW_COORDS(1)
    144.                 UNITY_FOG_COORDS(2)
    145.                 UNITY_VERTEX_OUTPUT_STEREO
    146.             };
    147.  
    148.             v2f vert(appdata v)
    149.             {
    150.                 v2f o;
    151.                 UNITY_SETUP_INSTANCE_ID(v);
    152.                 o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
    153.                 o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
    154.                 o.worldNormal = UnityObjectToWorldNormal(v.normal);
    155.                 TRANSFER_SHADOW(o);
    156.                 UNITY_TRANSFER_FOG(o,o.pos);
    157.                 UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
    158.                 return o;
    159.             }
    160.  
    161.             half4 frag(v2f i) : SV_Target {
    162.                 fixed4 albedo;
    163.                 half3 normal;
    164.                 GetTriplanarTextures(i.worldPos, i.worldNormal, albedo, normal);
    165.  
    166.                 UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    167.  
    168. #ifndef USING_DIRECTIONAL_LIGHT
    169.                 fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
    170. #else
    171.                 fixed3 lightDir = _WorldSpaceLightPos0.xyz;
    172. #endif
    173.                 half3 lighting = saturate(dot(normalize(normal), lightDir)) * _LightColor0.rgb * atten;
    174.  
    175.                 half3 col = albedo.rgb * lighting;
    176.  
    177.                 UNITY_APPLY_FOG(i.fogCoord, col);
    178.                 return half4(col, 1);
    179.             }
    180.             ENDCG
    181.         }
    182.  
    183.         Pass {
    184.             Tags{ "LightMode" = "ShadowCaster" }
    185.  
    186.             CGPROGRAM
    187.             #pragma vertex vert
    188.             #pragma fragment frag
    189.             #pragma multi_compile_shadowcaster
    190.             #include "UnityCG.cginc"
    191.  
    192.             struct v2f {
    193.                 V2F_SHADOW_CASTER;
    194.             };
    195.  
    196.             v2f vert(appdata_base v) {
    197.                 v2f o;
    198.                 TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
    199.                 return o;
    200.             }
    201.  
    202.             float4 frag(v2f i) : SV_Target {
    203.                 SHADOW_CASTER_FRAGMENT(i)
    204.             }
    205.             ENDCG
    206.         }
    207.     }
    208. }
    Looks good? Looking forward to using it & hopefully others find this helpful!
     
    JoeStrout likes this.
  24. Jroel

    Jroel

    Joined:
    Oct 25, 2013
    Posts:
    14
    After finding this awesome shader, I wanted normals as well. This shader is great, thank you! It looks like shadows are setup according to this, but I can't seem to get them to function. Any ideas?

    I would also like to know how to improve on this shader to get normals for the sides separate from normals on the top/bottom?
     
  25. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214

    I tried this, and got a "too many texture interpolators.... (12 out of max 10)" error; and haven't been able to slim down the rest of the code enough to get it to work. Is there a way to move the "calculate the full world to tangent matrix" code out of the vertex shader and into the surf function? Or some other workaround? I think I need to use a surface shader because I may need reflection probe reflections (although I suppose I could try to implement that myself).
     
  26. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329