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

Box / Triplanar mapping following object rotation

Discussion in 'Shaders' started by Azial, Oct 22, 2017.

  1. Azial

    Azial

    Joined:
    Mar 19, 2013
    Posts:
    17
    Hi,

    I need a very simple shader that projects a texture on all sides of the cube, but the texture should not get stretched by the cubes size. If the cube is not rotated, this is simple:

    Code (CSharp):
    1. Shader "Custom/BoxMap" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _TexScale ("Texture scale", Float) = 1
    6.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    7.         _Metallic ("Metallic", Range(0,1)) = 0.0
    8.     }
    9.     SubShader {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 200
    12.      
    13.         CGPROGRAM
    14.         #pragma surface surf Standard fullforwardshadows
    15.         #pragma target 3.0
    16.  
    17.         sampler2D _MainTex;
    18.         float _TexScale;
    19.  
    20.         struct Input {
    21.             float3 worldPos;
    22.             float3 worldNormal;
    23.         };
    24.  
    25.         half _Glossiness;
    26.         half _Metallic;
    27.         fixed4 _Color;
    28.  
    29.         void surf (Input IN, inout SurfaceOutputStandard o) {
    30.             // box mapping uv
    31.             float2 uv;
    32.             if (abs(IN.worldNormal.x) > 0.5) {
    33.                 uv = IN.worldPos.yz;
    34.             } else if (abs(IN.worldNormal.z) > 0.5) {
    35.                 uv = IN.worldPos.xy;
    36.             } else {
    37.                 uv = IN.worldPos.xz;
    38.             }
    39.             fixed4 c = tex2D(_MainTex, uv * (1/_TexScale));
    40.             o.Albedo = c.rgb * _Color;
    41.             // Metallic and smoothness come from slider variables
    42.             o.Metallic = _Metallic;
    43.             o.Smoothness = _Glossiness;
    44.             o.Alpha = c.a;
    45.         }
    46.         ENDCG
    47.     }
    48.     FallBack "Diffuse"
    49. }

    However, when the cube is rotated, the texture is still following the worldPos of course. Do I have to multiply the worldPos coords with the objects rotation matrix and if yes, how do I get this information in a shader? Or is there even a simpler way?
     
    Last edited: Oct 22, 2017
  2. brownboot67

    brownboot67

    Joined:
    Jan 5, 2013
    Posts:
    375
    Pass local position (v.vertex.xyz) in from a custom vert program and use that instead.
     
  3. Azial

    Azial

    Joined:
    Mar 19, 2013
    Posts:
    17
    You mean like:

    #pragma surface surf Standard fullforwardshadows vertex:vert
    ...
    struct Input {
    ...
    float2 boxUV;
    };

    void vert (inout appdata_full v, out Input o) {
    UNITY_INITIALIZE_OUTPUT(Input, o);
    o.boxUV = v.vertex.xyz;
    }

    void surf (Input IN, inout SurfaceOutputStandard o) {
    fixed4 c = tex2D(_MainTex, IN.boxUV * (1/_TexScale));
    ...
    }

    But then the texture is stretched again, because the uv map is based on the mesh (which is affected by scale).
     
  4. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Basically you need to find a way to extract the scale from the object to world matrix and apply it to the local position with out the rotation.

    If you're using uniform scaling on your object, this is pretty easy. If you're using non-uniform scaling its still possible but a little more work. If you're using compound scaling, ie you have a parent game object that is also scaled and rotated such that it into introduces skewing, this gets a lot harder to get right, but don't the same thing as the non-uniform scale might be good enough.

    All that information is in the unity_ObjectToWorld matrix. You need to get the length of the first three columns to get the scale.
     
  5. Azial

    Azial

    Joined:
    Mar 19, 2013
    Posts:
    17
    Hm, is this matrix a TRS-matrix in homogeneous coordinates? If I know the matrix-layout I could try to get the rotation out of this instead of the scale. If I do it externally in a script, it looks good:

    Script:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class PassRotationToShader : MonoBehaviour {
    6.  
    7.     // Use this for initialization
    8.     void Start() {
    9.         Material m = GetComponent<Renderer>().material;
    10.         Quaternion rotReversedQ = Quaternion.Inverse(transform.rotation);
    11.         Matrix4x4 rotReversedMat = QuaternionToMatrix(rotReversedQ);
    12.         m.SetMatrix("_WorldRotation", rotReversedMat);
    13.     }
    14.    
    15.     Matrix4x4 QuaternionToMatrix(Quaternion q) {
    16.         // Converts a quaternion rotation to rotation matrix
    17.         // algorithm taken from http://www.euclideanspace.com/maths/geometry/rotations/conversions/quaternionToMatrix/
    18.  
    19.         Matrix4x4 mat = new Matrix4x4();
    20.         float sqw = q.w*q.w;
    21.         float sqx = q.x*q.x;
    22.         float sqy = q.y*q.y;
    23.         float sqz = q.z*q.z;
    24.  
    25.         // invs (inverse square length) is only required if quaternion is not already normalised
    26.         float invs = 1.0f / (sqx + sqy + sqz + sqw);
    27.         mat.m00 = ( sqx - sqy - sqz + sqw)*invs; // since sqw + sqx + sqy + sqz =1/invs*invs
    28.         mat.m11 = (-sqx + sqy - sqz + sqw)*invs;
    29.         mat.m22 = (-sqx - sqy + sqz + sqw)*invs;
    30.  
    31.         float tmp1 = q.x*q.y;
    32.         float tmp2 = q.z*q.w;
    33.         mat.m10 = 2.0f * (tmp1 + tmp2)*invs;
    34.         mat.m01 = 2.0f * (tmp1 - tmp2)*invs;
    35.  
    36.         tmp1 = q.x*q.z;
    37.         tmp2 = q.y*q.w;
    38.         mat.m20 = 2.0f * (tmp1 - tmp2)*invs;
    39.         mat.m02 = 2.0f * (tmp1 + tmp2)*invs;
    40.         tmp1 = q.y*q.z;
    41.         tmp2 = q.x*q.w;
    42.         mat.m21 = 2.0f * (tmp1 + tmp2)*invs;
    43.         mat.m12 = 2.0f * (tmp1 - tmp2)*invs;
    44.  
    45.         return mat;
    46.     }
    47. }

    Shader:
    Code (CSharp):
    1. Shader "Custom/BoxMap" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _TexScale ("Texture scale", Float) = 1
    6.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    7.         _Metallic ("Metallic", Range(0,1)) = 0.0
    8.     }
    9.     SubShader {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 200
    12.        
    13.         CGPROGRAM
    14.         // Physically based Standard lighting model, and enable shadows on all light types
    15.         #pragma surface surf Standard fullforwardshadows
    16.  
    17.         // Use shader model 3.0 target, to get nicer looking lighting
    18.         #pragma target 3.0
    19.  
    20.         sampler2D _MainTex;
    21.         float _TexScale;
    22.  
    23.         struct Input {
    24.             float3 worldPos;
    25.             float3 worldNormal;
    26.         };
    27.  
    28.         half _Glossiness;
    29.         half _Metallic;
    30.         fixed4 _Color;
    31.  
    32.         float4x4 _WorldRotation;
    33.  
    34.         void surf (Input IN, inout SurfaceOutputStandard o) {
    35.             // box mapping uv
    36.             float2 uv;
    37.             float3 pos = mul(_WorldRotation, float4(IN.worldPos, 0)).xyz;
    38.             if (abs(IN.worldNormal.x) > 0.5) {
    39.                 uv = pos.yz;
    40.             } else if (abs(IN.worldNormal.z) > 0.5) {
    41.                 uv = pos.xy;
    42.             } else {
    43.                 uv = pos.xz;
    44.             }
    45.             fixed4 c = tex2D(_MainTex, uv * (1/_TexScale));
    46.             o.Albedo = c.rgb * _Color;
    47.             // Metallic and smoothness come from slider variables
    48.             o.Metallic = _Metallic;
    49.             o.Smoothness = _Glossiness;
    50.             o.Alpha = c.a;
    51.         }
    52.         ENDCG
    53.     }
    54.     FallBack "Diffuse"
    55. }
    56.  
     
  6. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    If you're going to calculate a matrix from a quaternion there's no need to calculate that yourself. There's a Matrix4x4.Rotate(quat) you can use instead. You could always just pass lossyScale to the shader and use that to scale the local vertex position too.
     
  7. Azial

    Azial

    Joined:
    Mar 19, 2013
    Posts:
    17
    Thanks for the hints so far. I think I have all the parts of the puzzle, but I'm not that much into shaders and the result is still not what I expect. I tried all kinds of combinations multiplying v.vertex or v.texcoord or worldPos with parts of the transformation matrix. The best I can get is this, but it works only for either scaling OR rotating the object. For both at the same time, the texture gets sheared:

    Code (CSharp):
    1. Shader "Custom/BoxMap" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _TexScale ("Texture scale", Float) = 1
    6.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    7.         _Metallic ("Metallic", Range(0,1)) = 0.0
    8.     }
    9.     SubShader {
    10.         Tags { "RenderType"="Opaque" }
    11.         LOD 200
    12.        
    13.         CGPROGRAM
    14.         #pragma surface surf Standard fullforwardshadows
    15.         #pragma target 3.0
    16.  
    17.         sampler2D _MainTex;
    18.         float _TexScale;
    19.  
    20.         struct Input {
    21.             float3 worldPos;
    22.             float3 worldNormal;
    23.         };
    24.  
    25.         half _Glossiness;
    26.         half _Metallic;
    27.         fixed4 _Color;
    28.  
    29.         void surf (Input IN, inout SurfaceOutputStandard o) {
    30.             // get objects inverse transformation
    31.             float4x4 m = unity_WorldToObject;
    32.             // get translation from matrix
    33.             float3 pos = float3(m[0][3], m[1][3], m[2][3]);
    34.             // get scale from matrix
    35.             float3 scale;
    36.             scale.x = length(float4(m[0][0], m[1][0], m[2][0], m[3][0]));
    37.             scale.y = length(float4(m[0][1], m[1][1], m[2][1], m[3][1]));
    38.             scale.z = length(float4(m[0][2], m[1][2], m[2][2], m[3][2]));
    39.             // get rotation from matrix
    40.             float4x4 rot = float4x4(
    41.                 m[0][0]/scale.x, m[0][1]/scale.y, m[0][2]/scale.z, 0,
    42.                 m[1][0]/scale.x, m[1][1]/scale.y, m[1][2]/scale.z, 0,
    43.                 m[2][0]/scale.x, m[2][1]/scale.y, m[2][2]/scale.z, 0,
    44.                 0, 0, 0, 1);
    45.  
    46.             // make box mapping with rotation preserved
    47.             float3 map = mul(rot, float4(IN.worldPos+pos, 0)).xyz;
    48.             float3 norm = mul(rot, float4(IN.worldNormal, 0)).xyz;
    49.             float2 uv;
    50.             if (abs(norm.x) > 0.5) {
    51.                 uv = map.yz;
    52.             } else if (abs(norm.z) > 0.5) {
    53.                 uv = map.xy;
    54.             } else {
    55.                 uv = map.xz;
    56.             }
    57.  
    58.             fixed4 c = tex2D(_MainTex, uv * (1/_TexScale));
    59.             o.Albedo = c.rgb * _Color;
    60.             // Metallic and smoothness come from slider variables
    61.             o.Metallic = _Metallic;
    62.             o.Smoothness = _Glossiness;
    63.             o.Alpha = c.a;
    64.         }
    65.         ENDCG
    66.     }
    67.     FallBack "Diffuse"
    68. }
     
  8. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    You were close. I simplified some of the code structures; using the _m## notation to access a matrix element inline and using a float3x3 rotation matrix instead of a float4x4.

    Biggest issue was switching from column to row. Also changing how the object position was factored in.

    Code (CSharp):
    1. Shader "Custom/BoxMap" {
    2.     Properties {
    3.         _Color ("Color", Color) = (1,1,1,1)
    4.         _MainTex ("Albedo (RGB)", 2D) = "white" {}
    5.         _TexScale ("Texture scale", Float) = 1
    6.         _Glossiness ("Smoothness", Range(0,1)) = 0.5
    7.         _Metallic ("Metallic", Range(0,1)) = 0.0
    8.     }
    9.     SubShader {
    10.         Tags { "RenderType"="Opaque" "DisableBatching"="True" }
    11.         LOD 200
    12.    
    13.         CGPROGRAM
    14.         #pragma surface surf Standard fullforwardshadows
    15.         #pragma target 3.0
    16.         sampler2D _MainTex;
    17.         float _TexScale;
    18.         struct Input {
    19.             float3 worldPos;
    20.             float3 worldNormal;
    21.         };
    22.         half _Glossiness;
    23.         half _Metallic;
    24.         fixed4 _Color;
    25.         void surf (Input IN, inout SurfaceOutputStandard o) {
    26.  
    27.             // get scale from matrix
    28.             float3 scale = float3(
    29.                 length(unity_WorldToObject._m00_m01_m02),
    30.                 length(unity_WorldToObject._m10_m11_m12),
    31.                 length(unity_WorldToObject._m20_m21_m22)
    32.                 );
    33.  
    34.             // get translation from matrix
    35.             float3 pos = unity_WorldToObject._m03_m13_m23 / scale;
    36.  
    37.             // get unscaled rotation from matrix
    38.             float3x3 rot = float3x3(
    39.                 normalize(unity_WorldToObject._m00_m01_m02),
    40.                 normalize(unity_WorldToObject._m10_m11_m12),
    41.                 normalize(unity_WorldToObject._m20_m21_m22)
    42.                 );
    43.             // make box mapping with rotation preserved
    44.             float3 map = mul(rot, IN.worldPos) + pos;
    45.             float3 norm = mul(rot, IN.worldNormal);
    46.  
    47.             float3 blend = abs(norm) / dot(abs(norm), float3(1,1,1));
    48.             float2 uv;
    49.             if (blend.x > max(blend.y, blend.z)) {
    50.                 uv = map.yz;
    51.             } else if (blend.z > blend.y) {
    52.                 uv = map.xy;
    53.             } else {
    54.                 uv = map.xz;
    55.             }
    56.             fixed4 c = tex2D(_MainTex, uv * (1/_TexScale));
    57.             o.Albedo = c.rgb * _Color;
    58.  
    59.             // Metallic and smoothness come from slider variables
    60.             o.Metallic = _Metallic;
    61.             o.Smoothness = _Glossiness;
    62.             o.Alpha = c.a;
    63.         }
    64.         ENDCG
    65.     }
    66.     FallBack "Diffuse"
    67. }
    necro edit: adding DisableBatching tag
     
    Last edited: Aug 22, 2018
  9. Azial

    Azial

    Joined:
    Mar 19, 2013
    Posts:
    17
    Ah, thanks! That's exactly what I was looking for.

    Unfortunately, in play mode and when built, the rotation on some object seems not applied. In edit mode everything looks fine.

    BoxMapping.jpg

    Btw, is there a particular reason for your changes on the face selecting (blend vs norm)? I tried both methods and didn't noticed any differences.
     
    Last edited: Oct 26, 2017
  10. mh114

    mh114

    Joined:
    Nov 17, 2013
    Posts:
    295
    Perhaps the cubes are batched together (statically or dynamically)?
     
    bgolus likes this.
  11. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    This. Batching will cause all knowledge of local rotation to be lost as the actual mesh being rendered has no rotation in the object to world matrix. You can try adding "DisableBatching"="True" to the tags.

    For a box there won't be any noticable difference. Try it on a sphere though...
     
  12. Jick87

    Jick87

    Joined:
    Oct 21, 2015
    Posts:
    124
    @bgolus Sorry to necro this post but, I was just trying out your code and it works great! Not exactly as-is because I'm using it in a vert/frag shader rather than a surface shader, but your basic code is unchanged. However, I noticed one thing about it that I was wondering if you had any tips for...

    Basically, animated/modified vertexes... Earlier on in my shader, in the vertex function, I have some vertex modification going on for a special animation effect. It just moves the vertexes randomly over time. Then later, in the fragment function, I have basic texture/color stuff which is where I added your code. As I said, it all works fine, except for the animated vertexes. In those places the texture seems to stay static where it was rather than move with the vertexes, like it does for non-animated vertexes.

    I'm still new to the world of shaders, so I'm not sure how I could/would make changes to effect that. I figured maybe it was worth a shot to ask here. This isn't a total deal breaker so I can live with it if it's not possible, but I figured what's the harm in asking? :p

    Thanks!
     
  13. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    If you're using a vertex fragment shader there's a lot of that shader you don't need as like a third of it is trying to get local vertex positions and normals.

    As for why your shader is having problems. No idea. There's no reason what you're describing can't work like you want, but I have no idea what the problem would be. The question I would ask is in what space are you modifying the vertices, and what version of the vertices are you passing into the triplanar code.
     
  14. Jick87

    Jick87

    Joined:
    Oct 21, 2015
    Posts:
    124
    Best as I can tell, my vertex modifying code is working in local(object?) space. It takes in a raw vertex at the very beginning of the vertex function and modifies it without changing the space it's working in at all (never calls unity_ObjectToWorld etc) and then returns it and assigns it directly to the same vertex value. Something like this:
    Code (CSharp):
    1. v2f vert( appdata_t v )
    2. {
    3.     v2f o;
    4.     UNITY_SETUP_INSTANCE_ID( v );
    5.     UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO( o );
    6.     UNITY_TRANSFER_INSTANCE_ID( v, o );
    7.  
    8.     v.vertex = FunctionToAnimateVertex( v.vertex );
    9.  
    10.     o.pos = UnityObjectToClipPos( v.vertex );
    11.     o.worldPos = mul( unity_ObjectToWorld, v.vertex ).xyz;
    12.     o.worldNormal = mul( unity_ObjectToWorld, v.normal ).xyz;
    13.  
    14.     return o;
    15. }
    This code is obviously stripped way down, but it should illustrate what I mean.

    As a matter of fact, speaking of stripped down code... I was trying this code before I came across your code in this thread:
    Code (CSharp):
    1. float3 bf = normalize( abs( i.localNormal ) );
    2. bf /= dot( bf, (float3)1 );
    3.  
    4. col = saturate( ( ( tex2D( _MainTex, ( i.localPos.yz * _TexScale ) ) * bf.x ) +
    5.       ( tex2D( _MainTex, ( i.localPos.zx * _TexScale ) ) * bf.y ) +
    6.       ( tex2D( _MainTex, ( i.localPos.xy * _TexScale ) ) * bf.z ) ) * _TintColor );
    It seems to work almost as good as your version, except it too has an issue with the animated vertexes, plus it also has an additional issue with stretching rather than tiling when dealing with scaled meshes. Your version doesn't seem to suffer from that issue, which is why I was excited when I found it.

    My overall shader code is a bit too bulky to post in its entirety, but perhaps I can post a stripped-down barebones version if it could help. But again, this isn't the hugest of deals. I can live with it if I have to. Just not sure where I'm going wrong with my shader code...

    Anyway, thanks for replying! I appreciate your help here and elsewhere. I've found a lot of useful things in many of your posts throughout this forum. A treasure trove of knowledge. :p
     
  15. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    You’re moving the vertices, then using those positions to calculate the UVs. Try setting worldPos before you apply the noise so you’re using the original vertex positions.
     
  16. Jick87

    Jick87

    Joined:
    Oct 21, 2015
    Posts:
    124
    Oh... my... gosh... That was it! Now I feel like a real fool... :p


    As a last thing, you said that a good portion of your code wasn't necessary when using it in a vertex/fragment shader as opposed to a surface shader. I'm wondering, what parts exactly are not needed? I've been going over it but I'm not sure I understand. I'm learning... Just, slowly! :p


    Thanks again for your help! :)
     
  17. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    In a Surface Shader, the world position and world normal are both readily available in the surface function, so I used that. I could have passed the object space position and normal instead using a custom vertex function, but Surface Shaders already pass a ton of data from the vertex to the fragment shader, so I try to avoid that when possible.

    For a straight vertex fragment shader there's not really a point to calculating the world position and normal if you don't need to, as the object space triplanar code is just undoing that work. The only thing needed is extracting the scale and applying it to the object space vertex position and normal in the vertex function, then passing that on.

    Code (CSharp):
    1. Shader "Unlit/ObjectSpaceTriplanar"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" "DisableBatching"="True" }
    10.         LOD 100
    11.  
    12.         Pass
    13.         {
    14.             CGPROGRAM
    15.             #pragma vertex vert
    16.             #pragma fragment frag
    17.             #pragma multi_compile_fog
    18.          
    19.             #include "UnityCG.cginc"
    20.  
    21.             struct appdata
    22.             {
    23.                 float4 vertex : POSITION;
    24.                 float3 normal : NORMAL;
    25.             };
    26.  
    27.             struct v2f
    28.             {
    29.                 float4 pos : SV_POSITION;
    30.                 float3 uvPos : TEXCOORD0;
    31.                 float3 uvBlend : TEXCOORD1;
    32.                 UNITY_FOG_COORDS(2)
    33.             };
    34.  
    35.             sampler2D _MainTex;
    36.             float4 _MainTex_ST;
    37.          
    38.             v2f vert (appdata v)
    39.             {
    40.                 v2f o;
    41.                 o.pos = UnityObjectToClipPos(v.vertex);
    42.  
    43.                 float3 scale = float3(
    44.                     length(unity_ObjectToWorld._m00_m10_m20),
    45.                     length(unity_ObjectToWorld._m01_m11_m21),
    46.                     length(unity_ObjectToWorld._m02_m12_m22)
    47.                     );
    48.  
    49.                 o.uvPos = v.vertex.xyz * scale;
    50.                 o.uvBlend = v.normal.xyz / scale;
    51.                 o.uvBlend /= dot(abs(o.uvBlend), float3(1,1,1));
    52.  
    53.                 UNITY_TRANSFER_FOG(o,o.pos);
    54.                 return o;
    55.             }
    56.          
    57.             fixed4 frag (v2f i) : SV_Target
    58.             {
    59.                 float3 blend = abs(i.uvBlend);
    60.                 float2 uv;
    61.                 if (i.uvBlend.x > max(i.uvBlend.y, i.uvBlend.z)) {
    62.                     uv = i.uvPos.yz;
    63.                 } else if (i.uvBlend.z > i.uvBlend.y) {
    64.                     uv = i.uvPos.xy;
    65.                 } else {
    66.                     uv = i.uvPos.xz;
    67.                 }
    68.                 fixed4 col = tex2D(_MainTex, TRANSFORM_TEX(uv, _MainTex));
    69.  
    70.                 UNITY_APPLY_FOG(i.fogCoord, col);
    71.                 return col;
    72.             }
    73.             ENDCG
    74.         }
    75.  
    76.     }
    77.  
    78.     Fallback "VertexLit"
    79. }
    You have to extract the scale in a slightly different way as you're applying it to the object space position rather than removing it from the world space position, but there are no matrix multiplies needed.
     
  18. Jick87

    Jick87

    Joined:
    Oct 21, 2015
    Posts:
    124
    Oh, I understand now. Thanks for the code and the explanation. :)

    One issue I noticed with that code was some stretching on certain sides of my objects, but I think I solved it. In the code you defined a float3 called "blend" like this:
    Code (CSharp):
    1. float3 blend = abs( i.uvBlend );
    But then don't actually use it in the code that follows it. After I changed the instances of
    i.uvBlend.[XYZ]
    to
    blend.[XYZ]
    the stretching issues went away.

    So basically, I changed your code to this:
    Code (CSharp):
    1. Shader "Unlit/ObjectSpaceTriplanar"
    2. {
    3.     Properties
    4.     {
    5.         _MainTex ("Texture", 2D) = "white" {}
    6.     }
    7.     SubShader
    8.     {
    9.         Tags { "RenderType"="Opaque" "DisableBatching"="True" }
    10.         LOD 100
    11.  
    12.         Pass
    13.         {
    14.             CGPROGRAM
    15.             #pragma vertex vert
    16.             #pragma fragment frag
    17.             #pragma multi_compile_fog
    18.  
    19.             #include "UnityCG.cginc"
    20.  
    21.             struct appdata
    22.             {
    23.                 float4 vertex : POSITION;
    24.                 float3 normal : NORMAL;
    25.             };
    26.  
    27.             struct v2f
    28.             {
    29.                 float4 pos : SV_POSITION;
    30.                 float3 uvPos : TEXCOORD0;
    31.                 float3 uvBlend : TEXCOORD1;
    32.                 UNITY_FOG_COORDS(2)
    33.             };
    34.  
    35.             sampler2D _MainTex;
    36.             float4 _MainTex_ST;
    37.  
    38.             v2f vert (appdata v)
    39.             {
    40.                 v2f o;
    41.                 o.pos = UnityObjectToClipPos(v.vertex);
    42.  
    43.                 float3 scale = float3(
    44.                     length(unity_ObjectToWorld._m00_m10_m20),
    45.                     length(unity_ObjectToWorld._m01_m11_m21),
    46.                     length(unity_ObjectToWorld._m02_m12_m22)
    47.                     );
    48.  
    49.                 o.uvPos = v.vertex.xyz * scale;
    50.                 o.uvBlend = v.normal.xyz / scale;
    51.                 o.uvBlend /= dot(abs(o.uvBlend), float3(1,1,1));
    52.  
    53.                 UNITY_TRANSFER_FOG(o,o.pos);
    54.                 return o;
    55.             }
    56.  
    57.             fixed4 frag (v2f i) : SV_Target
    58.             {
    59.                 float3 blend = abs(i.uvBlend);
    60.                 float2 uv;
    61.                 if (blend.x > max(blend.y, blend.z)) {
    62.                     uv = i.uvPos.yz;
    63.                 } else if (blend.z > blend.y) {
    64.                     uv = i.uvPos.xy;
    65.                 } else {
    66.                     uv = i.uvPos.xz;
    67.                 }
    68.                 fixed4 col = tex2D(_MainTex, TRANSFORM_TEX(uv, _MainTex));
    69.  
    70.                 UNITY_APPLY_FOG(i.fogCoord, col);
    71.                 return col;
    72.             }
    73.             ENDCG
    74.         }
    75.  
    76.     }
    77.  
    78.     Fallback "VertexLit"
    79. }
    Figured I should tell about the changes in case someone else comes along trying to use the code.

    But anyway, thanks again for the help. I really appreciate it. :)
     
    bgolus likes this.
  19. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Yep, I originally was doing that abs in the vertex shader. I figured it would be a little safer to do it in the fragment shader, but I'm not positive that's true. Ideally both the sum normalization (divide by the dot product) and the abs should be done in the fragment shader and only the scaling of the normal should be done in the vertex, but for most use cases there won't be a noticeable difference.

    Honestly, for this type of shader, you could probably skip the dot product line entirely and not notice a difference. It's more important if you plan on doing blending.
     
    Last edited: Aug 30, 2018
    Jick87 likes this.
  20. Jick87

    Jick87

    Joined:
    Oct 21, 2015
    Posts:
    124
    Yeah, I was actually playing around trying to get a better understanding of the code and, I noticed that commenting out that line had no visual difference, at least as far as I can tell. But then the game I'm working on is a low poly one with no really round/smooth shapes (almost everything is made out of flat/cube shapes) so I guess that makes sense based on your info. :)

    Thanks again for the great help.
     
  21. CCTTINOVEM

    CCTTINOVEM

    Joined:
    Feb 11, 2019
    Posts:
    2
    Hi everyone! i know it's an old thread but I currently need a shader like the one mentionned in this discussion, with the addition of normal map and specular map.

    I'm a total newbie with shaders, I really want to understand how to add these features. I read the article from bgolus, it was really interesting, but every time I tried to add normal map to this shader, my object become either all white or pink...

    If someone is interested in helping me create this shader it would be really nice!
     
    Last edited: Jul 8, 2019
  22. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    The example shader above is an unlit shader, normal mapping won't really be that useful to add since the shader doesn't get any lighting information to use with the normals.
     
  23. CCTTINOVEM

    CCTTINOVEM

    Joined:
    Feb 11, 2019
    Posts:
    2
    Thanks for your answer. I wasn't aware that it would receive any lighting.

    What I need is a shader that :
    • receives lighting information, so it can be used with ARCore LightEstimation feature ( it has a directional light )
    • is triplanar, most of my objects are created/stretched by the user at runtime, so I can't make UVs ahead.
    • follow object rotation, like the shader from this thread
    • use a normal map texture
    • use a specular map texture, with a slider for intensty/shinyness
    • is not unlit
    • is as mobile friendly as possible
    Is there a shader somewhere that does this? Or is there an easy tutorial help me create one like this?
     
  24. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    Well, the first shader example I posted in 2017 does do lighting. It's only the ones from 2018 that are unlit. But...
    Normal mapping a triplanar shader is a bigger task than object space triplanar alone. Plenty of people handle triplanar normal maps the hacky way by just taking the triplanar mapped normal map and setting the o.Normal of the surface shader and calling it good. There's some semi-official Unity shaders that do that ... but it's wrong.

    I wrote a long article about why it's wrong, and how to do it correctly here:
    https://medium.com/@bgolus/normal-mapping-for-a-triplanar-shader-10bf39dca05a

    The shader code in that article is all done in world space rather than object space, but most of the code will be the same. The key differences will be the object relative normal and position you feed into the triplanar code, which the above shaders show how to get, and the need to transform the resulting normals from object space back into world space before transforming into tangent space if needed.

    That's a lot, and I don't know of any good "tutorials" for the object space parts of all that beyond what's in this thread.

    Lastly, there's this problem:
    Triplanar adds a lot of code to a shader on it's own. Triplanar increases that significantly. Something like a Surface Shader using the Standard lighting model with normal mapping is somewhere around 160~200 instructions for the fragment shader, and proper world space triplanar normal mapping can increase that by as much as 50%. Object space probably adds another 20 instructions alone. For performance reference, the older Mobile/Bump Specular shader is only around ~40 instructions in total. Triplanar shaders are not "mobile friendly" in that regard.

    There might be some assets on the store that already do object space triplanar normal maps, but I have no idea how good they are, so I can't really give you any recommendations.
     
  25. HonoraryBob

    HonoraryBob

    Joined:
    May 26, 2011
    Posts:
    1,214

    I tried to combine your method of compensating for object rotation along with another triplanar method that I was using (one which avoids IF statements), but the end result causes the pattern to shift as the object is rotated, and I can't find the problem. Here's the code (note that there are some other things going on, such as the ability to select between object and world coordinates by setting shader properties, in case you're wondering what that code is for):

    Code (CSharp):
    1.             // get scale from matrix
    2.             float3 scale = float3(
    3.                 length(_World2Object._m00_m01_m02),
    4.                 length(_World2Object._m10_m11_m12),
    5.                 length(_World2Object._m20_m21_m22)
    6.                 );
    7.             // get translation from matrix
    8.             float3 pos = _World2Object._m03_m13_m23 / scale;
    9.             // get unscaled rotation from matrix
    10.             float3x3 rot = float3x3(
    11.                 normalize(_World2Object._m00_m01_m02),
    12.                 normalize(_World2Object._m10_m11_m12),
    13.                 normalize(_World2Object._m20_m21_m22)
    14.                 );
    15.             // make box mapping with rotation preserved
    16.             float3 map = mul(rot, IN.worldPos) + pos;
    17.             float3 norm = mul(rot, IN.worldNormal);
    18.             float3 blend = abs(norm) / dot(abs(norm), float3(1,1,1));
    19.                                                    
    20.             //lerp between ObjCoords and worldcoords
    21.             float3 TriCoords = lerp(map,IN.worldPos.xyz,_TriplanarObjOrWorldCoords);                    
    22.             // find normal in objectspace
    23.             float3 ObjectNormal = normalize( mul(float4(worldNormal,0.0), _World2Object).xyz );
    24.        
    25.             // lerp between object normal and world normal          
    26.             float3 TriNormal = lerp(ObjectNormal,worldNormal, _TriplanarObjectOrWorldNormal);
    27.             // clamp (saturate) and increase(pow) the worldnormal value to use as a blend between the projected textures
    28.             float3 blendNormal = saturate(pow(TriNormal * 1.4,4));          
    29.    
    30.             // triplanar for top texture for x, y, z sides
    31.             float3 xm = tex2D(_TriplanarTex1, TriCoords.zy * _TriplanarScale);
    32.             float3 ym = tex2D(_TriplanarTex1, TriCoords.xz * _TriplanarScale);    
    33.             float3 zm = tex2D(_TriplanarTex1, TriCoords.xy * _TriplanarScale);              
    34.             // lerp together all sides for top texture
    35.             float3 toptexture = zm;
    36.             toptexture = lerp(toptexture, xm, blendNormal.x);
    37.             toptexture = lerp(toptexture, ym, blendNormal.y);          
    38.             // triplanar for side and bottom texture, x,y,z sides
    39.             float3 x = tex2D(_TriplanarTex2, TriCoords.zy * _TriplanarSideScale);
    40.             float3 y = tex2D(_TriplanarTex2, TriCoords.xz * _TriplanarSideScale);
    41.             float3 z = tex2D(_TriplanarTex2, TriCoords.xy * _TriplanarSideScale);          
    42.             // lerp together all sides for side bottom texture
    43.             float3 sidetexture = z;
    44.             sidetexture = lerp(sidetexture, x, blendNormal.x);
    45.             sidetexture = lerp(sidetexture, y, blendNormal.y);
    46.            
    47.             // final color
    48.             float3 TriC = (toptexture+sidetexture)*_TriplanarColor;
    49.  
     
  26. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,329
    You calculate the proper blend factor on line 18, then never actually use it...

    Also, you shouldn't be lerping between the sides. The blend factor if properly setup should let you do:
    fixed3 col = xcol * blend.x + ycol * blend.y + zcol * blend.z;

    That's what that divide by the dot product is for. The blend factor you calculate on line 28 isn't doing that, which probably why you're having to use the lerp.
     
    HonoraryBob likes this.
  27. adamgryu

    adamgryu

    Joined:
    Mar 1, 2014
    Posts:
    186
    I just got object space triplanar mapping with normal maps working, with help from this thread and @bgolus's blog post.

    To help others, here's my solution. (Written for URP)
    Code (CSharp):
    1.  
    2. float2 triplanarUV(
    3.     float3 positionWS, float3 normalOS, out float3 tangent, out float3 bitangent)
    4. {
    5.     // get scale from matrix
    6.     float3 scale = float3(
    7.         length(unity_WorldToObject._m00_m01_m02),
    8.         length(unity_WorldToObject._m10_m11_m12),
    9.         length(unity_WorldToObject._m20_m21_m22));
    10.  
    11.     // get translation from matrix
    12.     float3 pos = unity_WorldToObject._m03_m13_m23 / scale;
    13.     pos += float3(_MainTex_ST.z, _MainTex_ST.w, _MainTex_ST.z);
    14.  
    15.     // get unscaled rotation from matrix
    16.     float3x3 rot = float3x3(
    17.         normalize(unity_WorldToObject._m00_m01_m02),
    18.         normalize(unity_WorldToObject._m10_m11_m12),
    19.         normalize(unity_WorldToObject._m20_m21_m22));
    20.  
    21.     // make box mapping with rotation preserved
    22.     float3 map = mul(rot, positionWS) + pos;
    23.     float3 blend = abs(normalOS) / dot(abs(normalOS), float3(1, 1, 1));
    24.  
    25.     // do cross product tangent reconstruction (see blog post)
    26.     half3 axisSign = sign(normalOS);
    27.     float2 uv;
    28.     if (blend.x > max(blend.y, blend.z))
    29.     {
    30.         uv = map.zy / _MainTex_ST.xy;
    31.         tangent = normalize(cross(normalOS, half3(0, axisSign.x, 0)));
    32.         bitangent = normalize(cross(tangent, normalOS)) * axisSign.x;
    33.     }
    34.     else if (blend.z > blend.y)
    35.     {
    36.         uv = map.xy / _MainTex_ST.xy;
    37.         tangent = normalize(cross(normalOS, half3(0, -axisSign.z, 0)));
    38.         bitangent = normalize(-cross(tangent, normalOS)) * axisSign.z;
    39.     }
    40.     else
    41.     {
    42.         uv = map.xz / _MainTex_ST.xy;
    43.         tangent = normalize(cross(normalOS, half3(0, 0, axisSign.y)));
    44.         bitangent = normalize(cross(tangent, normalOS)) * axisSign.y;
    45.     }
    46.  
    47.     return uv;
    48. }
    49.  
    50. MyVaryings vert (ToonAttributes input)
    51. {
    52.     MyVaryings output;
    53.     TOON_VERTEX(input, output); // Replace with your own vert function etc...
    54.  
    55.     float3 tangent;
    56.     float3 bitangent;
    57.     output.uv = triplanarUV(
    58.         output.positionWSAndFogFactor.xyz, input.normalOS, tangent, bitangent);
    59.  
    60.     #if defined(_NORMAL_MAP)
    61.         output.tangentWS = TransformObjectToWorldDir(tangent);
    62.         output.bitangentWS = TransformObjectToWorldDir(bitangent);
    63.     #endif
    64.  
    65.     return output;
    66. }
    67.  
    68. half4 frag (MyVaryings input) : SV_Target
    69. {
    70.     half4 diffuse = tex2D(_MainTex, input.uv) * _Color.rgba;
    71.     clip(diffuse.a - 0.3);
    72.  
    73.     #if defined(_NORMAL_MAP)
    74.         float3 tnormal = UnpackNormalScale(tex2D(_Normals, input.uv), _NormalStrength);
    75.  
    76.         // Construct tangent to world matrix.
    77.         half3x3 tbn = half3x3(input.tangentWS, input.bitangentWS, input.normalWS);
    78.         input.normalWS = clamp(mul(tnormal, tbn), -1, 1);
    79.     #endif
    80.  
    81.     return TOON_LIGHTING(diffuse, input); // Replace this with your own lighting etc...
    82. }
    Note that it does the tangent / uv calculations in the vertex shader, so this only works with boxy meshes with hard edges. You can move it to the fragment shader to fix that. The normals might also be off on non-boxy meshes, but you might be able to use RNM Blending (?) to address that.
     
    Last edited: Jun 11, 2021