Triplanar 기본 구현

월드 포지션 2개 축을 조합해서 UV 좌표로 활용 할 수 있습니다.

아래는 예시입니다.

Varyings vert(Attributes input)
{
    Varyings output = (Varyings)0;

    float4 positionOS = input.positionOS;
    float3 positionWS = TransformObjectToWorld(positionOS.xyz);
    float4 positionCS = TransformWorldToHClip(positionWS);
    ...
    
    
float4 frag(Varyings input) : SV_Target
{
    float3 positionWS = input.positionWS;
    return float4(positionWS.xy + 0.5f, 0,0);
    return float4(positionWS.zx + 0.5f, 0,0);
    return float4(positionWS.zy + 0.5f, 0,0);

positionWS.XY,    positionWS.ZX,    positionWS.ZY

 

 

다음 월드 노말 벡터를 픽셀 쉐이더로 확인하면 아래와 같이 각 방향 별로 마스킹 가능합니다.

Varyings vert(Attributes input)
{
    Varyings output = (Varyings)0;

    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);
    ...
    
    
float4 frag(Varyings input) : SV_Target
{
    float3 normalWS = NormalizeNormalPerPixel(input.normalWS);
    return float4(normalWS, 0);

 

 

월드 포지션 기반 UV와 월드 노말 각 축을 곱하면 아래와 같이 깔끔하게 UV 좌표 계산 가능합니다.

 float4 frag(Varyings input) : SV_Target
{
    float3 positionWS = input.positionWS;
    float3 normalWS = NormalizeNormalPerPixel(input.normalWS);

    return float4((positionWS.xy + 0.5f) * normalWS.z, 0,0);
    return float4((positionWS.xz + 0.5f) * normalWS.y, 0,0);
    return float4((positionWS.zy + 0.5f) * normalWS.x, 0,0);

positionWS.XY,    positionWS.ZX,    positionWS.ZY

 

 

 

위에서 계산한 UV 좌표로 각각 3개 샘플링에 사용하면 Triplanar 기본형 완성입니다.

float4 frag(Varyings input) : SV_Target
{
    float3 positionWS = input.positionWS + 0.5f;
    float3 normalWS = NormalizeNormalPerPixel(input.normalWS);

    float4 texColor_X = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, positionWS.zy * _BaseMap_ST.xy + _BaseMap_ST.zw);
    float4 texColor_Y = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, positionWS.zx * _BaseMap_ST.xy + _BaseMap_ST.zw);
    float4 texColor_Z = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, positionWS.xy * _BaseMap_ST.xy + _BaseMap_ST.zw);
    float4 triplanar = texColor_X * normalWS.x + texColor_Y * normalWS.y + texColor_Z * normalWS.z;

    float4 finalColor = triplanar;
    return finalColor;
}

 

 

하지만 아래와 같이 월드 노말 반대 편은 0 이하 값이 되기 때문에 월드 노말에 abs처리하여 음수를 양수로 변환합니다.

 

 

다음 아래 Shader Graph의 Triplanar 코드를 참고하여 함수 형태로 만들고 기능을 개선합니다.

https://docs.unity3d.com/Packages/com.unity.shadergraph@16.0/manual/Triplanar-Node.html

float4 TriplanarSample(float3 positionWS, float3 normalWS, float blend, 
                        TEXTURE2D_PARAM(Texture_X, sampler_Texture_X), float4 tilingOffsetX,
                        TEXTURE2D_PARAM(Texture_Y, sampler_Texture_Y), float4 tilingOffsetY,
                        TEXTURE2D_PARAM(Texture_Z, sampler_Texture_Z), float4 tilingOffsetZ)
{
    float3 Node_Blend = pow(abs(normalWS), blend);
    Node_Blend /= dot(Node_Blend, 1.0);
    float scrollSpeed = 0;
    scrollSpeed = _Time.y;
    float4 texColor_X = SAMPLE_TEXTURE2D(Texture_X, sampler_Texture_X, positionWS.zy * tilingOffsetX.xy + (tilingOffsetX.zw * scrollSpeed));
    float4 texColor_Y = SAMPLE_TEXTURE2D(Texture_Y, sampler_Texture_Y, positionWS.zx * tilingOffsetY.xy + (tilingOffsetY.zw * scrollSpeed));
    float4 texColor_Z = SAMPLE_TEXTURE2D(Texture_Z, sampler_Texture_Z, positionWS.xy * tilingOffsetZ.xy + (tilingOffsetZ.zw * scrollSpeed));

    float4 triplanar = texColor_X * Node_Blend.x + texColor_Y * Node_Blend.y + texColor_Z * Node_Blend.z;
    return triplanar;
}


float4 frag(Varyings input) : SV_Target
{
    float3 positionWS = input.positionWS;
    float3 normalWS = NormalizeNormalPerPixel(input.normalWS);

    float4 triplanar = TriplanarSample(positionWS + 0.5f, normalWS, 1, 
                                        TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                        TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                        TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0));

    float4 finalColor = triplanar;
    return finalColor;
}

 

 

 

Triplanar 월드 포지션 고정하기

Triplanar 기본은 구현되었지만 오브젝트 좌표가 움직여도 월드 포지션 UV는 변하지 않기 때문에 위 결과물 예시와 같이 텍스처 UV가 움직이지 않는 현상이 있습니다.

보통 좌표가 이동하지 않는 배경 오브젝트에는 문제 없지만, 캐릭터와 같이 움직이는 오브젝트에는 문제가 발생할 수 있습니다.

 

그래서 이동행렬을 고정한 상태로 월드 포지션을 계산하여 Triplanar에 사용한다면 개선 가능합니다.

아래 예시는 이전 빌보드 구현 과정에서 Rotation Transform을 고정하는 부분이 있었는데, 해당 구현 부분을 응용합니다.

https://darkcatgame.tistory.com/137

 

void GetCustomModelMatrix(out float4x4 outMoveMatrix, out float4x4 outRotationMatrix, out float4x4 outScaleMatrix)
{
    float4x4 scaleMatrix;
    float4 sx = float4(UNITY_MATRIX_M._m00, UNITY_MATRIX_M._m10, UNITY_MATRIX_M._m20, 0);
    float4 sy = float4(UNITY_MATRIX_M._m01, UNITY_MATRIX_M._m11, UNITY_MATRIX_M._m21, 0);
    float4 sz = float4(UNITY_MATRIX_M._m02, UNITY_MATRIX_M._m12, UNITY_MATRIX_M._m22, 0);
    float scaleX = length(sx);
    float scaleY = length(sy);
    float scaleZ = length(sz);
    scaleMatrix[0] = float4(scaleX, 0, 0, 0);
    scaleMatrix[1] = float4(0, scaleY, 0, 0);
    scaleMatrix[2] = float4(0, 0, scaleZ, 0);
    scaleMatrix[3] = float4(0, 0, 0, 1);
    outScaleMatrix = scaleMatrix;

    float4x4 rotationMatrix;
    rotationMatrix[0] = float4(UNITY_MATRIX_M._m00 / scaleX, UNITY_MATRIX_M._m01 / scaleY, UNITY_MATRIX_M._m02 / scaleZ, 0);
    rotationMatrix[1] = float4(UNITY_MATRIX_M._m10 / scaleX, UNITY_MATRIX_M._m11 / scaleY, UNITY_MATRIX_M._m12 / scaleZ, 0);
    rotationMatrix[2] = float4(UNITY_MATRIX_M._m20 / scaleX, UNITY_MATRIX_M._m21 / scaleY, UNITY_MATRIX_M._m22 / scaleZ, 0);
    rotationMatrix[3] = float4(0, 0, 0, 1);
    outRotationMatrix = rotationMatrix;

    float4x4 moveMatrix;
    moveMatrix[0] = float4(1, 0, 0, UNITY_MATRIX_M._m03);
    moveMatrix[1] = float4(0, 1, 0, UNITY_MATRIX_M._m13);
    moveMatrix[2] = float4(0, 0, 1, UNITY_MATRIX_M._m23);
    moveMatrix[3] = float4(0, 0, 0, UNITY_MATRIX_M._m33);
    outMoveMatrix = moveMatrix;
}

float3 TransformObjectToWorld_StaticPosition(float3 positionOS)
{
    float4x4 scaleMatrix;
    float4x4 rotationMatrix;
    float4x4 moveMatrix;
    GetCustomModelMatrix(moveMatrix, rotationMatrix, scaleMatrix);
    float4x4 modelMatrix_Static = mul(mul(1, rotationMatrix), scaleMatrix);
    return mul(modelMatrix_Static, float4(positionOS.xyz, 1)).xyz;
}

Varyings vert(Attributes input)
{
    Varyings output = (Varyings)0;

    float4 positionOS = input.positionOS;
    float3 positionWS = TransformObjectToWorld(positionOS.xyz);
    float4 positionCS = TransformWorldToHClip(positionWS);

    float3 positionWS_Static = TransformObjectToWorld_StaticPosition(positionOS.xyz);

    float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

    output.normalWS = normalWS;
    output.positionCS = positionCS;
    output.uv = input.uv;
    output.positionWS_Static = positionWS_Static.xyz;
    return output;
}

 

float4x4 modelMatrix_Static = mul(mul(1, rotationMatrix), scaleMatrix);

빌보드 자료에는 rotationMatrix를 고정했지만 이번엔 이동행렬을 고정하기 때문에 위와 같이 코드를 작성합니다.

 

 

 


Shader "CatDarkGame/Triplanar"
{
    Properties
    { 
        [MainTexture] _BaseMap("Texture", 2D) = "white" {}
        [MainColor] _BaseColor("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "Queue"="Geometry" "RenderPipeline" = "UniversalPipeline" }
        LOD 100

        Pass
        {
            Name  "URPUnlit"
            Tags {"LightMode" = "SRPDefaultUnlit"}

            HLSLPROGRAM
            #pragma target 4.5
            #pragma exclude_renderers gles d3d9
            
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _BaseColor;
            CBUFFER_END

            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);


            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS_Static : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
            }; 
            

            void GetCustomModelMatrix(out float4x4 outMoveMatrix, out float4x4 outRotationMatrix, out float4x4 outScaleMatrix)
            {
                float4x4 scaleMatrix;
                float4 sx = float4(UNITY_MATRIX_M._m00, UNITY_MATRIX_M._m10, UNITY_MATRIX_M._m20, 0);
                float4 sy = float4(UNITY_MATRIX_M._m01, UNITY_MATRIX_M._m11, UNITY_MATRIX_M._m21, 0);
                float4 sz = float4(UNITY_MATRIX_M._m02, UNITY_MATRIX_M._m12, UNITY_MATRIX_M._m22, 0);
                float scaleX = length(sx);
                float scaleY = length(sy);
                float scaleZ = length(sz);
                scaleMatrix[0] = float4(scaleX, 0, 0, 0);
                scaleMatrix[1] = float4(0, scaleY, 0, 0);
                scaleMatrix[2] = float4(0, 0, scaleZ, 0);
                scaleMatrix[3] = float4(0, 0, 0, 1);
                outScaleMatrix = scaleMatrix;

                float4x4 rotationMatrix;
                rotationMatrix[0] = float4(UNITY_MATRIX_M._m00 / scaleX, UNITY_MATRIX_M._m01 / scaleY, UNITY_MATRIX_M._m02 / scaleZ, 0);
                rotationMatrix[1] = float4(UNITY_MATRIX_M._m10 / scaleX, UNITY_MATRIX_M._m11 / scaleY, UNITY_MATRIX_M._m12 / scaleZ, 0);
                rotationMatrix[2] = float4(UNITY_MATRIX_M._m20 / scaleX, UNITY_MATRIX_M._m21 / scaleY, UNITY_MATRIX_M._m22 / scaleZ, 0);
                rotationMatrix[3] = float4(0, 0, 0, 1);
                outRotationMatrix = rotationMatrix;

                float4x4 moveMatrix;
                moveMatrix[0] = float4(1, 0, 0, UNITY_MATRIX_M._m03);
                moveMatrix[1] = float4(0, 1, 0, UNITY_MATRIX_M._m13);
                moveMatrix[2] = float4(0, 0, 1, UNITY_MATRIX_M._m23);
                moveMatrix[3] = float4(0, 0, 0, UNITY_MATRIX_M._m33);
                outMoveMatrix = moveMatrix;
            }

            float3 TransformObjectToWorld_StaticPosition(float3 positionOS)
            {
                float4x4 scaleMatrix;
                float4x4 rotationMatrix;
                float4x4 moveMatrix;
                GetCustomModelMatrix(moveMatrix, rotationMatrix, scaleMatrix);
                float4x4 modelMatrix_Static = mul(mul(1, rotationMatrix), scaleMatrix);
                return mul(modelMatrix_Static, float4(positionOS.xyz, 1)).xyz;
            }

            float4 TriplanarSample(float3 positionWS, float3 normalWS, float blend, 
                                    TEXTURE2D_PARAM(Texture_X, sampler_Texture_X), float4 tilingOffsetX,
                                    TEXTURE2D_PARAM(Texture_Y, sampler_Texture_Y), float4 tilingOffsetY,
                                    TEXTURE2D_PARAM(Texture_Z, sampler_Texture_Z), float4 tilingOffsetZ)
            {
                float3 Node_Blend = pow(abs(normalWS), blend);
                Node_Blend /= dot(Node_Blend, 1.0);
                float scrollSpeed = 0;
                scrollSpeed = _Time.y;
                float4 texColor_X = SAMPLE_TEXTURE2D(Texture_X, sampler_Texture_X, positionWS.zy * tilingOffsetX.xy + (tilingOffsetX.zw * scrollSpeed));
                float4 texColor_Y = SAMPLE_TEXTURE2D(Texture_Y, sampler_Texture_Y, positionWS.zx * tilingOffsetY.xy + (tilingOffsetY.zw * scrollSpeed));
                float4 texColor_Z = SAMPLE_TEXTURE2D(Texture_Z, sampler_Texture_Z, positionWS.xy * tilingOffsetZ.xy + (tilingOffsetZ.zw * scrollSpeed));

                float4 triplanar = texColor_X * Node_Blend.x + texColor_Y * Node_Blend.y + texColor_Z * Node_Blend.z;
                return triplanar;
            }


            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;

                float4 positionOS = input.positionOS;
                float3 positionWS = TransformObjectToWorld(positionOS.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);

                float3 positionWS_Static = TransformObjectToWorld_StaticPosition(positionOS.xyz);
 
                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

                output.normalWS = normalWS;
                output.positionCS = positionCS;
                output.uv = input.uv;
                output.positionWS_Static = positionWS_Static.xyz;
                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                float3 positionWS = input.positionWS_Static;
                float3 normalWS = NormalizeNormalPerPixel(input.normalWS);
             
                float4 triplanar = TriplanarSample(positionWS + 0.5f, normalWS, 1, 
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0));
                
                float4 finalColor = triplanar;
                return finalColor;
            }
            
            ENDHLSL
        }
    }
}

 

 

 

worldPivot계산하는 방식

Shader "CatDarkGame/Triplanar"
{
    Properties
    { 
        [MainTexture] _BaseMap("Texture", 2D) = "white" {}
        [MainColor] _BaseColor("Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags { "RenderType" = "Opaque" "Queue"="Geometry" "RenderPipeline" = "UniversalPipeline" }
        LOD 100

        Pass
        {
            Name  "URPUnlit"
            Tags {"LightMode" = "SRPDefaultUnlit"}

            HLSLPROGRAM
            #pragma target 4.5
            #pragma exclude_renderers gles d3d9
            
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"


            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _BaseColor;
            CBUFFER_END

            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);


            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 positionWS_Static : TEXCOORD1;
                float3 normalWS : TEXCOORD2;
            }; 
            

            float4 TriplanarSample(float3 positionWS, float3 normalWS, float blend, 
                                    TEXTURE2D_PARAM(Texture_X, sampler_Texture_X), float4 tilingOffsetX,
                                    TEXTURE2D_PARAM(Texture_Y, sampler_Texture_Y), float4 tilingOffsetY,
                                    TEXTURE2D_PARAM(Texture_Z, sampler_Texture_Z), float4 tilingOffsetZ)
            {
                float3 Node_Blend = pow(abs(normalWS), blend);
                Node_Blend /= dot(Node_Blend, 1.0);
                float scrollSpeed = 0;
                scrollSpeed = _Time.y;
                float4 texColor_X = SAMPLE_TEXTURE2D(Texture_X, sampler_Texture_X, positionWS.zy * tilingOffsetX.xy + (tilingOffsetX.zw * scrollSpeed));
                float4 texColor_Y = SAMPLE_TEXTURE2D(Texture_Y, sampler_Texture_Y, positionWS.zx * tilingOffsetY.xy + (tilingOffsetY.zw * scrollSpeed));
                float4 texColor_Z = SAMPLE_TEXTURE2D(Texture_Z, sampler_Texture_Z, positionWS.xy * tilingOffsetZ.xy + (tilingOffsetZ.zw * scrollSpeed));

                float4 triplanar = texColor_X * Node_Blend.x + texColor_Y * Node_Blend.y + texColor_Z * Node_Blend.z;
                return triplanar;
            }


            Varyings vert(Attributes input)
            {
                Varyings output = (Varyings)0;

                float4 positionOS = input.positionOS;
                float3 positionWS = TransformObjectToWorld(positionOS.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);
                float3 pivotWS = float3(unity_ObjectToWorld._m03,  unity_ObjectToWorld._m13,  unity_ObjectToWorld._m23);

                float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

       
                output.normalWS = normalWS;
                output.positionCS = positionCS;
                output.uv = input.uv;
                output.positionWS_Static = positionWS.xyz - pivotWS;
                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                float3 positionWS = input.positionWS_Static;
                float3 normalWS = NormalizeNormalPerPixel(input.normalWS);
             
                float4 triplanar = TriplanarSample(positionWS + 0.5f, normalWS, 1, 
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0),
                                                    TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), float4(_BaseMap_ST.xy, 0, 0));
                
                float4 finalColor = triplanar;
                return finalColor;
            }
            
            ENDHLSL
        }
    }
}

 

 

 

'Unity > Shader & VFX' 카테고리의 다른 글

URP ScreenSpaceDecal Shader  (0) 2023.07.18
LinearEyeDepth와 Linear01Depth  (0) 2023.07.13
[URP] HLSL SoftParticles Shader  (0) 2023.03.07
자동 UV타일링 쉐이더  (1) 2022.11.11
유니티 빌보드 쉐이더 구현 & 분석  (1) 2022.11.04

WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,