이번 포스팅에서는 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로 짠 로직을 실제 코드로 옮길 때의 구조입니다.
- Properties: 텍스처, Color, Rim Power, Flow Speed 등의 변수를 선언합니다.
- Vertex Shader:
- 월드 공간 노멀(NormalWorld)과 시선 벡터(ViewDir)를 계산합니다.
- 월드 공간 위치(PosWorld)를 계산해서 Pixel Shader로 넘깁니다.
- 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
}
}
}반응형
'자료는 자료지 > 외부에서 퍼온자료' 카테고리의 다른 글
| 5. Unity 셰이더 : 버텍스 오프셋을 이용한 촉수 성장 애니메이션 (1) | 2026.01.05 |
|---|---|
| 4. Unity 셰이더: MatCap과 박막 간섭 구현하기 (0) | 2026.01.04 |
| 02 Shader 코드 입문 기초 (0) | 2026.01.02 |
| 실시간 렌더링 파이프라인 기초 (6) | 2026.01.02 |
| ASTC_User_Guide 번역 6 (0) | 2025.12.15 |
댓글