본문 바로가기
자료는 자료지/외부에서 퍼온자료

03. 캐릭터 림 라이트 및 스캔(유광) 효과 구현과 코드 변환 [ASE 강좌]

by 대마왕J 2026. 1. 4.

이번 포스팅에서는 Amplify Shader Editor(이하 ASE)를 사용하여 캐릭터의 외각을 빛나게 하는 **림 라이트(Rim Light)**와 위에서 아래로 빛이 훑고 지나가는 스캔(Scan/Flow) 효과를 구현하는 방법을 정리했습니다. 또한, ASE로 짠 노드를 실제 HLSL 코드로 변환하는 과정까지 다룹니다.

1. 씬(Scene) 및 기본 설정

효과가 잘 보이도록 어두운 배경을 세팅합니다.

  • Skybox 교체: 기존 스카이박스 대신 어두운 톤의 환경으로 변경합니다.
  • 바닥 설정: 반사가 없는 어두운 바닥을 깔고, Fog(안개)를 켜서 바닥과 배경의 경계를 자연스럽게 처리합니다.
  • 셰이더 타입: 빛의 영향을 받지 않고 자체 발광만 표현하기 위해 Unlit(무광) 모드를 선택합니다.


2. ASE 노드 구현 과정

A. 반투명(Transparent) 설정과 블렌딩

  • Render Queue: Transparent 모드로 설정합니다.
  • Blend Mode: 빛이 겹칠 때 밝아지는 효과를 위해 Particle Additive (Source Alpha, One) 방식을 사용합니다.

B. 림 라이트 (Rim Light) 구현

캐릭터의 테두리가 빛나는 효과입니다.

  • 월드 공간의 노멀(N)과 시선 벡터(V)를 내적합니다.
  • 기본 공식: 1 - saturate(dot(Normal, ViewDir)).
  • 제어 방식: 보통 Power(지수) 함수를 많이 쓰지만, 여기서는 SmoothStep 함수를 쓰겠습니다. 
    [역자 주] SmoothStep(min, max, x)은 값이 min보다 작으면 0, max보다 크면 1, 그 사이는 부드러운 S자 곡선(Hermite interpolation)으로 보간합니다. Power보다 제어 범위가 명확하고 부드러운 감쇠를 표현하기 좋습니다.
  • 색상 처리: Inner Color(안쪽)와 Rim Color(바깥쪽)를 지정하고, 림 라이트 계산 값을 마스크로 사용하여 Lerp로 두 색상을 섞어줍니다.

C. 스캔/유광 (Flow Light) 구현

빛이 위에서 아래로 흐르는 효과입니다.

  • 좌표계: 모델의 UV가 아닌 World Position을 사용해야 전체를 훑는 효과를 낼 수 있습니다.
  • 상대 좌표 변환 (핵심): 단순히 World Position을 쓰면 캐릭터가 이동할 때 스캔 효과가 제자리에 머무는 문제가 발생합니다.
    • 해결책: World Position - Object Pivot Position.
    • 월드 공간에서의 픽셀 위치에서 오브젝트의 중심점 위치를 빼주어, 오브젝트 기준의 상대 좌표를 만들어야 캐릭터를 따라다닙니다.
  • 애니메이션: Y축 좌표에 Time을 더해 빛이 흐르게 만듭니다.

D. 디테일 및 뎁스 처리

  • 내부 디테일: 텍스처의 R채널 등을 추출해 림 라이트와 더해주면(Add), 단순한 외곽선 외에 내부의 기계적인 디테일이 빛나는 효과를 줄 수 있습니다.
  • 투명도 정렬 문제 해결: 반투명 셰이더 특성상 뒤쪽 면이 비쳐 보일 수 있습니다. ASE의 Extra Depth Pass (ZWrite On) 옵션을 켜서 앞쪽 면만 깔끔하게 렌더링되도록 합니다.

3. 노드를 코드로 변환 (HLSL)

ASE로 짠 로직을 실제 코드로 옮길 때의 구조입니다.

  1. Properties: 텍스처, Color, Rim Power, Flow Speed 등의 변수를 선언합니다.
  2. Vertex Shader:
    • 월드 공간 노멀(NormalWorld)과 시선 벡터(ViewDir)를 계산합니다.
    • 월드 공간 위치(PosWorld)를 계산해서 Pixel Shader로 넘깁니다.
  3. Pixel Shader:
    • 림 라이트: smoothstep(min, max, 1 - dot(N, V))로 계산합니다.
    • 스캔 UV 계산: (i.posWorld.xy - i.pivotWorld.xy) + _Time.y * Speed 로 계산하여 흐르는 UV를 만듭니다.
    • 최종 합성: 림 라이트 색상 + 스캔 텍스처 색상 + 내부 발광(Emission)을 모두 더하여(Add) 최종 색상을 출력합니다.


Shader "Custom/ASE_To_Code_ScanEffect"
{
    Properties
    {
        [Header(Base Settings)]
        _MainTex ("Main Texture", 2D) = "white" {}
        _DetailTex ("Detail Texture (R Channel Emission)", 2D) = "black" {}

        [Header(Rim Light Settings)]
        _InnerColor ("Inner Color", Color) = (0,0,0,1)
        _RimColor ("Rim Color", Color) = (0,0.5,1,1)
        _RimMin ("Rim SmoothStep Min", Range(0, 1)) = 0.2
        _RimMax ("Rim SmoothStep Max", Range(0, 1)) = 0.8
        _RimPower ("Rim Power", Range(0.1, 10)) = 3.0

        [Header(Scan Light Settings)]
        _ScanTex ("Scan Texture", 2D) = "white" {}
        _ScanColor ("Scan Color", Color) = (1,1,1,1)
        _ScanSpeed ("Scan Speed", Vector) = (0, 1, 0, 0)
        _ScanTiling ("Scan Tiling", Float) = 1.0

        [Header(System)]
        [Toggle] _ZWrite ("Extra Depth Pass (ZWrite)", Float) = 1
    }

    SubShader
    {
        // 반투명 렌더링 설정 [cite: 1]
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        
        // 블렌드 모드: Particle Additive (SrcAlpha, One) [cite: 2]
        Blend SrcAlpha One
        
        // 뎁스 쓰기 설정 (Extra Depth Pass) [cite: 8]
        ZWrite [_ZWrite]
        Cull Back

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 worldPos : TEXCOORD1;
                float3 worldNormal : TEXCOORD3;
                float3 viewDir : TEXCOORD4;
            };

            // 변수 선언
            sampler2D _MainTex; float4 _MainTex_ST;
            sampler2D _DetailTex;
            sampler2D _ScanTex; float4 _ScanTex_ST;

            fixed4 _InnerColor;
            fixed4 _RimColor;
            float _RimMin;
            float _RimMax;
            float _RimPower;

            fixed4 _ScanColor;
            float2 _ScanSpeed;
            float _ScanTiling;

            v2f vert (appdata v)
            {
                v2f o;
                // 월드 공간 위치 계산 
                o.pos = UnityObjectToClipPos(v.vertex);
                o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;
                
                // 월드 노멀 계산 
                o.worldNormal = UnityObjectToWorldNormal(v.normal);
                
                // 시선 벡터 계산 (카메라 위치 - 픽셀 위치) [cite: 11]
                o.viewDir = WorldSpaceViewDir(v.vertex);
                
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // 벡터 정규화
                float3 normal = normalize(i.worldNormal);
                float3 viewDir = normalize(i.viewDir);

                // 1. 림 라이트 (Rim Light) 계산 [cite: 3, 4]
                float NdotV = dot(normal, viewDir);
                // 1 - saturate(N.V) 공식
                float rimRaw = 1.0 - saturate(NdotV);
                // SmoothStep을 이용한 부드러운 보간 [cite: 4, 5]
                float rimFactor = smoothstep(_RimMin, _RimMax, rimRaw);
                // Inner/Outer 색상 혼합 (Lerp) [cite: 5]
                float3 rimFinal = lerp(_InnerColor.rgb, _RimColor.rgb, rimFactor) * rimFactor * _RimPower;

                // 2. 디테일 텍스처 (R 채널) 추출 [cite: 7]
                fixed4 detailCol = tex2D(_DetailTex, i.uv);
                float3 detailEmission = detailCol.r * _RimColor.rgb; // 림 컬러와 섞어 자연스럽게

                // 3. 스캔/유광 (Flow Light) 계산 [cite: 6, 15]
                // 오브젝트의 중심점(Pivot) 좌표 가져오기 (행렬의 이동값 활용)
                float3 objectPivot = float3(unity_ObjectToWorld[0][3], unity_ObjectToWorld[1][3], unity_ObjectToWorld[2][3]);
                
                // 핵심: 월드 좌표 - 피벗 좌표 = 로컬 상대 좌표
                float2 relativePos = i.worldPos.xy - objectPivot.xy;
                
                // UV 애니메이션 (Time 더하기)
                float2 scanUV = (relativePos * _ScanTiling) + (_Time.y * _ScanSpeed);
                fixed4 scanCol = tex2D(_ScanTex, scanUV);
                float3 scanFinal = scanCol.rgb * _ScanColor.rgb * scanCol.a; // 알파값도 반영 [cite: 7]

                // 4. 최종 합성 (Add) [cite: 17]
                // 림 라이트 + 디테일 발광 + 스캔 효과
                float3 finalColor = rimFinal + detailEmission + scanFinal;

                // 투명도 처리를 위해 Alpha는 1 혹은 림 팩터에 따라 조절 (Additive라 크게 중요치 않으나 안전하게 처리)
                return fixed4(finalColor, 1.0);
            }
            ENDCG
        }
    }
}
반응형

댓글