TECHARTNOMAD | TECHARTFLOWIO.COM

TECH.ART.FLOW.IO

[번역] MatCap 박막 간섭 비눗방울 렌더링

jplee 2025. 11. 11. 23:39

역자의 말: 오랫만에 가벼운 거 하나 번역해서 올립니다. 맷켑은 정말 오래 된 기술이지만 여전히 실무 필드에서는 활용되어지고 있는 것이죠. 쉽다면 쉽지만 또 어렵다면 어려운 거죠. 툰 스타일 렌더링에서는 여전히 혼용 되어지는 방법 중의 하나입니다. 菠萝很甜 님이 작성해 준 글 가볍게 읽어보고 넘어가도 좋겠습니다.

 

저자: 菠萝很甜

최근 MatCap의 원리를 학습하면서, MatCap과 NoiseMap을 활용하여 간단한 비눗방울 렌더링을 구현해보았습니다.


MatCap 샘플링

MatCap 샘플링은 매우 간단합니다. 카메라 공간에서의 법선 벡터 xy를 0-1 범위로 매핑한 후, 해당 값으로 미리 제작된 MatCap 텍스처를 샘플링하면 됩니다. 이 방법은 계산 비용이 낮으면서도 우수한 결과를 얻을 수 있습니다.

URP에서의 코드는 다음과 같습니다:

float3 normalVS = TransformWorldToViewNormal(v.normalWS);
float2 matCapUV = (normalVS.xy + 1) * 0.5;
float4 matCapColor = tex2D(_MatCap, uv);

이러한 샘플링 방식은 가장자리 부분에서 샘플링 결과가 정확하지 않다는 문제가 있습니다. xy의 제곱합이 1을 초과하여 샘플링 영역이 원형 외부에 위치하게 됩니다. 이에 대한 개선된 방법은 향후 학습 후 보완하겠습니다.


비눗방울 렌더링

반사 효과

반사 부분은 MatCap을 직접 샘플링하여 반사 효과를 시뮬레이션하고, 프레넬(Fresnel)을 사용하여 투명도를 제어합니다.

실제 비눗방울을 관찰해보면 표면의 색상 층이 더욱 풍부합니다.

단일 MatCap 레이어로는 다소 단조로워 보이므로, 추가로 다른 MatCap 레이어를 중첩시킬 수 있습니다.

박막 간섭 효과

실제 비눗방울 표면 두께의 복잡한 변화를 시뮬레이션하기 위해, 노이즈 맵을 여러 번 샘플링하는 방법으로 교란 효과를 강화하여 표면의 유동성을 표현합니다. 프레넬을 사용하여 간섭 효과의 범위를 제어합니다.

구체적인 코드는 다음과 같습니다:

//주어진 방향 샘플링 FlowMap
 float2 flowNoise_uv = (_NoiseFlowControl.xy * _Time.y * 0.01+v.uv) * _FlowNoise_ST.xy + _FlowNoise_ST.zw;
//그런 다음 FlowMap 샘플 값을 uv 값으로 사용하여 NoiseMap을 샘플링합니다.
float2 noise_uv =  tex2D(_FlowNoise,flowNoise_uv).xy * _Noise_ST.xy + _Noise_ST.zw;
//프레넬 계산
float NdotV = saturate( dot(v.normalWS,viewDir));
float fresnel = smoothstep(_RimControl.x,_RimControl.y,1-NdotV) * _FresnelPower;
float3 noiseColor = tex2D(_Noise,noise_uv) * fresnel * _NoiseIntensity;

사용된 두 개의 텍스처:

최종적으로 반사 효과와 중첩하여 다음과 같은 결과를 얻었습니다:

위에서 구현한 방식은 결국 작은 트릭에 불과하며, 많은 시간을 들여 파라미터를 조정했지만 이 정도의 결과를 얻는 데 그쳤습니다.


소스 코드

전체 셰이더 코드는 다음과 같습니다:

Shader "Pineapple/MatCap_foam"
{
    Properties
    {
        _MatCap_1 ("Mat Cap ",2D) = "white"{}
        _MatCap_2 ("Mat Cap Add ",2D) = "white"{}
        _Noise("Noise",2D) = "white"{}
        _FlowNoise("Flow Noise",2D) = "white"{}
        _MatCapIntensity("MatCap Intensity",Range(0,3)) = 1.0
        _MatCapAddIntensity("MatCap Add Intensity",Range(0,1)) = 0.1
        _NoiseFlowControl("Noise Flow Control",Vector) = (0,0,0,0)
        _NoiseIntensity("Noise Intensity",Range(0,2)) =1.0 
        _FresnelPower("Fresnel Power",Range(0,3)) = 2.0
        _InnerAlpha("Inner Alpha",Range(0,1)) = 0.1
        _RimControl("Rin Controll",Vector) = (0,0,0,0)
    }
    SubShader
    {
        Tags{"Queue" = "Transparent"}
        HLSLINCLUDE
        
        float2 ClampUV(float2 uv)
        {
            float x = (uv.x + 1) * 0.5;
            float y = (uv.y + 1) * 0.5;
            return float2(x,y);
        }
        
        ENDHLSL
        
        
        Pass
        {
            Blend SrcAlpha One
            HLSLPROGRAM
            
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #pragma  vertex vertexCompute
            #pragma  fragment fragShading

            sampler2D _MatCap_1;
            sampler2D _MatCap_2;
            
            sampler2D   _Noise;
            float4 _Noise_ST;
            
            sampler2D _FlowNoise;
            float4 _FlowNoise_ST;
            
            half4 _NoiseFlowControl;
            float4 _RimControl;
            float _MatCapAddIntensity;
            float _MatCapIntensity;
            float _FresnelPower;
            float _InnerAlpha;
            float _NoiseIntensity;
          
            struct Attributes
            {
                float4 positionOS : POSITION;
                float3 normalOS : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
                float3 normalWS : TEXCOORD0;
                float3 positionWS :TEXCOORD1;
                float2 uv : TEXCOORD3;
            };

            Varyings vertexCompute(Attributes a)
            {
                Varyings o;
                o.positionCS = TransformObjectToHClip(a.positionOS);
                o.normalWS = TransformObjectToWorldNormal(a.normalOS);
                o.positionWS = TransformObjectToWorld(a.positionOS);
                o.uv = a.uv;
                return o;
            }

            float4 fragShading(Varyings v) : SV_Target
            {
                float3 normalVS = TransformWorldToViewNormal(v.normalWS);
                float3 viewDir = normalize(_WorldSpaceCameraPos - v.positionWS);
                //给定方向采样FlowMap
                float2 flowNoise_uv = (_NoiseFlowControl.xy * _Time.y * 0.01+v.uv) * _FlowNoise_ST.xy + _FlowNoise_ST.zw;
                //然后使用FlowMap采样值作为uv值 对NoiseMap采样
                float2 noise_uv =  tex2D(_FlowNoise,flowNoise_uv).xy * _Noise_ST.xy + _Noise_ST.zw;
                //fresnel计算
                float NdotV = saturate( dot(v.normalWS,viewDir));
                float fresnel = smoothstep(_RimControl.x,_RimControl.y,1-NdotV) * _FresnelPower;
                float3 noiseColor = tex2D(_Noise,noise_uv) * fresnel * _NoiseIntensity;

                float2 uv =ClampUV(normalVS.xy);
                float4 matCapColor1 = tex2D(_MatCap_1,uv) * _MatCapIntensity;
                float4 matCapColor2 = tex2D(_MatCap_2,uv) * _MatCapAddIntensity;
                float3 final_Color = float3(matCapColor1.xyz + matCapColor2.xyz) +noiseColor ;
                return  float4(final_Color.xyz,fresnel + _InnerAlpha ) ;
            }
            ENDHLSL
        }
    }
}

원문 출처: zhuanlan.zhihu.com