Shader "CatDarkGame/ScreenSpaceDecal"
{
    Properties
    { 
        _BaseColor("Color", Color) = (1, 1, 1, 1)
        _BaseMap("Texture", 2D) = "white" {}
    }

    SubShader
    {
        Tags { "RenderType" = "Transparent" "Queue" = "Transparent-499" "DisableBatching" = "True" }
        

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

            Cull back
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha 

            HLSLPROGRAM
            
            #pragma target 4.5
            
            
            #pragma vertex vert 
            #pragma fragment frag

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


            CBUFFER_START(UnityPerMaterial)
                float4 _BaseMap_ST;
                float4 _BaseColor;
            CBUFFER_END

            TEXTURE2D(_BaseMap);    SAMPLER(sampler_BaseMap);


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

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float2 uv : TEXCOORD0;
                float4 positionSS : TEXCOORD1;
                float4 viewRayOS : TEXCOORD2; 
                float3 cameraPosOS : TEXCOORD3;
            }; 
            
            
            void GetObjectSpaceViewTransform(float3 positionVS, out float4 objectToViewDir, out float3 viewPositionOS)
            {
                float viewZ = positionVS.z;
                float4x4 viewToObjectMatrix = mul(UNITY_MATRIX_I_M, UNITY_MATRIX_I_V);
                objectToViewDir.xyz = mul((float3x3)viewToObjectMatrix, positionVS.xyz * -1).xyz;
                objectToViewDir.w = viewZ;
                viewPositionOS.xyz = mul(viewToObjectMatrix, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz; 
            }


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

                float4 positionOS = input.positionOS;
                float3 positionWS = TransformObjectToWorld(positionOS.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);
                float3 positionVS = TransformWorldToView(positionWS);
                float4 positionSS = ComputeScreenPos(positionCS);
               
                float4 objectToViewDir = 0;
                float3 viewPositionOS = 0;
                GetObjectSpaceViewTransform(positionVS, objectToViewDir, viewPositionOS);
                
                output.viewRayOS = objectToViewDir;
                output.cameraPosOS = viewPositionOS;
                output.positionSS = positionSS;
                output.positionCS = positionCS;
                output.uv = input.uv;
                return output;
            }

            float4 frag(Varyings input) : SV_Target
            {
                float sceneRawDepth = SampleSceneDepth(input.positionSS.xy / input.positionSS.w).r; 
                float sceneDepthVS = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
                float3 viewRayOS = (input.viewRayOS.xyz / input.viewRayOS.w) * sceneDepthVS;
                float3 decalSpaceScenePos = input.cameraPosOS.xyz + viewRayOS.xyz;
                
                float3 absDecalSpaceScenePos = abs(decalSpaceScenePos);
                float decalClipAlpha = max(max(absDecalSpaceScenePos.x, absDecalSpaceScenePos.y), absDecalSpaceScenePos.z);
                clip(0.5f - decalClipAlpha);
               
                float2 decalSpaceUV = decalSpaceScenePos.xy + 0.5f;
                float2 uv = decalSpaceUV.xy * _BaseMap_ST.xy + _BaseMap_ST.zw;
                uv = frac(uv);
              
                float4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
                float4 finalColor = texColor * _BaseColor;
                return finalColor;
            }
            
            ENDHLSL
        }
    }
}

 


구현 원리

1. View Space 기준 Object Transform 계산

View Space, 즉 카메라가 오브젝트를 보는 기준으로 변형된 Transform를 다시 ObjectSpace로 변환합니다.

나중에 Fragment 쉐이더에서 깊이값과 Screen Position 기준으로 데칼 맵핑을 하기 위함입니다.

void GetObjectSpaceViewTransform(float3 positionVS, out float4 objectToViewDir, out float3 viewPositionOS)
{
    float viewZ = positionVS.z;
    
    // 월드 공간 -> 뷰 공간 -> 오브젝트 공간으로 변환하기 위한 역행렬 계산
    float4x4 viewToObjectMatrix = mul(UNITY_MATRIX_I_M, UNITY_MATRIX_I_V);
    
    // View -> 오브젝트가 아닌 오브젝트 -> View 방향의 Transform으로 역행렬 계산
    objectToViewDir.xyz = mul((float3x3)viewToObjectMatrix, positionVS.xyz * -1).xyz;
    
    // w에는 뷰 공간 깊이값 정보 저장
    objectToViewDir.w = viewZ;
    
    // 카메라 Transform를 오브젝트 공간 변환
    viewPositionOS.xyz = mul(viewToObjectMatrix, float4(0.0f, 0.0f, 0.0f, 1.0f)).xyz; 
}

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

    float4 positionOS = input.positionOS;
    float3 positionWS = TransformObjectToWorld(positionOS.xyz);
    float3 positionVS = TransformWorldToView(positionWS);
    float4 objectToViewDir = 0;
    float3 viewPositionOS = 0;
    GetObjectSpaceViewTransform(positionVS, objectToViewDir, viewPositionOS);

 

 

 

2. 데칼 맵핑 포지션 계산

다음 카메라 공간 기준, 거리 값을 이용해서 화면 상의 정확한 위치에 데칼 맵핑 포지션을 계산합니다.

float4 frag(Varyings input) : SV_Target
{
	// SceneDepth 샘플링, ScreenPos XY에 W를 나눠서 원근 기준 값을 평면 기준으로 변환 
    float sceneRawDepth = SampleSceneDepth(input.positionSS.xy / input.positionSS.w).r; 
    
    // 카메라에서 Depth픽셀까지 거리 값 계산
    float sceneDepthVS = LinearEyeDepth(sceneRawDepth, _ZBufferParams);
    
    // xyz/w 계산으로 오브젝트 스페이스의 포지션 값을 원점으로 변환, 다음 depth값을 곱하여 올바른 거리 위치로 좌표 계산
    float3 viewRayOS = (input.viewRayOS.xyz / input.viewRayOS.w) * sceneDepthVS;
    
    // 카메라 원점 기준으로 Offset처리하여 올바른 XY위치로 위치 변환
    float3 decalSpaceScenePos = input.cameraPosOS.xyz + viewRayOS.xyz;

설명이 복잡하지만 예시 자료처럼 화면 기준으로 위치 값을 계산해서 카메라로 부터 뎁스 깊이 위치까지 레이저를 쏘는 흐름으로 이해하시면 됩니다. 

 

 

 

3. AlphaClip으로 데칼 외부 픽셀 제거

데칼은 Mesh가 겹쳐진 부분에 맵핑됩니다, 따라서 아래와 같이 실제 데칼 픽셀보다 큰 사이즈의 메쉬를 사용하게 되는데 실제 데칼 맵핑 외부 픽셀은 Clip함수로 제거해야합니다.

float3 absDecalSpaceScenePos = abs(decalSpaceScenePos);
float decalClipAlpha = max(max(absDecalSpaceScenePos.x, absDecalSpaceScenePos.y), absDecalSpaceScenePos.z);
clip(0.5f - decalClipAlpha);

 

이전에 계산했던 데칼 포지션은 오브젝트 공간 기준으로 계산되었기 때문에 UV값이 중심에서 밖으로 커집니다.

쉽게 말해서 메쉬 사이즈 밖으로 나가는 픽셀을 제거하면 되기 때문에 위와 같이 xyz의 최대 값 기준으로 일정 값이상 커지는 픽셀을 clip처리 합니다.

(추후 쉽게 설명할 수 있게 되면 글 업데이트 예정)

 

 

 

참고자료

https://github.com/ColinLeung-NiloCat/UnityURPUnlitScreenSpaceDecalShader/blob/master/URP_NiloCatExtension_ScreenSpaceDecal_Unlit.shader


WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,