12 – 01 ~ 02 Rim 라이트의 기초 (Unity Shader)
주변을 자세히 보면 모든 물체는 기울어 질수록 반사가 심해집니다.
실제로 모든 물체는 기울어 질수록 심하게 반사가 되며 그 반사율은 재질에 따라 다르게 나타납니다.
만약 역광이 있을 떄, 털이나 반투명의 재질을 가진 물체들은 이 현상이 매우 강렬하게 나타날 수 있고
재질에 따라 때로는 잘 보이지 않게 나타날 수도 있습니다.
사진 분야에서는 이것을 Rim Light ( 림 라이트 ) 라고 부릅니다.
우리가 사용하는 Standard Shader와 같은 물리 기반의 쉐이더는 이러한 재질의 특성에 따른 반사율이
BRDF(Bidirectional Reflectance Distribution Function – 양방향 반사 분포 함수) 를 이용하여 구현되어 있습니다.
이러한 반사 공식을 fresnel (프레넬) 이라 부릅니다.
이번에 설명할 fresnel 공식은 그보다는 간단하게, 빛의 방향과는 전혀 상관이 없게 만드는 효과입니다.
기본적으로 배경과 캐릭터의 분리나 강조를 위해, 혹은 선택되었을때나 특정 상태를 표현하기 위한 특수한 효과를 위해 사용합니다.
직접 확인해보기 위해서 아래 에셋을 사용했습니다. (무료)
https://assetstore.unity.com/packages/3d/characters/humanoids/barbarian-warrior-75519 <- TestAsset
fresnel 공식 구현
텍스쳐를 한장만 받는 기본형으로 쉐이더를 변경합니다.
Shader "Custom/RimLight" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert noambient #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex); o.Albedo = c.rgb; o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
o.Albedo 를 0 으로 만들어서 검게 만들고 시작하는 것이 좋습니다.
이전에 배운 Lambert 공식은 노멀 벡터와 조명 벡터를 dot 연산한 공식입니다.
이 공식을 간단하게 정의하면 두 벡터, 즉 노멀벡터와 조명 벡터의 각도가 같으면 밝고, 90도가 되면 어두워져서 검정색이 된다는 것입니다.
그렇다면 여기에서 input 구조체에서 다른 값을 받아 봅시다.
https://docs.unity3d.com/Manual/SL-SurfaceShaders.html <- 영문
https://docs.unity3d.com/kr/2021.3/Manual/SL-SurfaceShaders.html <- 한글
표면 셰이더 입력 구조
입력 구조 Input에는 일반적으로 셰이더가 필요로 하는 텍스처 좌표가 있습니다. 텍스처 좌표의 이름은 텍스처 이름 앞에 “uv”가 붙는 형식으로 지어야 합니다(또는 두 번째 텍스처 좌표 세트를 사용하려면 “uv2”로 시작해야 합니다.)
다음 값을 입력 구조에 추가할 수 있습니다.
float3 viewDir – 뷰 방향을 포함합니다. 패럴랙스 이펙트, 림 조명 등의 컴퓨팅에 사용합니다.
float4와 COLOR 시맨틱 – 보간된 버텍스당 컬러를 포함합니다.
float4 screenPos – 반사 또는 스크린 공간 효과를 위한 스크린 공간 포지션을 포함합니다. GrabPass에는 적합하지 않습니다. ComputeGrabScreenPos 함수를 사용하여 커스텀 UV를 직접 계산해야 합니다.
float3 worldPos – 월드 공간 포지션을 포함합니다.
float3 worldRefl – 표면 셰이더가 o.Normal에 기록하지 않는 경우 월드 반사 벡터를 포함합니다. 예를 들어 리플렉트-디퓨즈(Reflect-Diffuse) 셰이더가 있습니다.
float3 worldNormal – 표면 셰이더가 o.Normal에 기록하지 않는 경우 월드 노멀 벡터를 포함합니다.
float3 worldRefl; INTERNAL_DATA – 표면 셰이더가 o.Normal에 기록하는 경우 월드 반사 벡터를 포함합니다. 픽셀당 노멀 맵을 기반으로 반사 벡터를 얻으려면 WorldReflectionVector (IN, o.Normal)를 사용해야 합니다. 예를 들어 리플렉트-범프드(Reflect-Bumped) 셰이더가 있습니다.
float3 worldNormal; INTERNAL_DATA – 표면 셰이더가 o.Normal에 기록하는 경우 월드 노멀 벡터를 포함합니다. 픽셀당 노멀 맵에 기반한 노멀 벡터를 얻으려면 WorldNormalVector (IN, o.Normal)를 사용해야 합니다.
우리가 사용할 값은 float3 viewDir 입니다.
float3 는 3차원 벡터 Input 구조체 안에 있는 것으로 봐서 엔진에서 받을 수 있는 데이터입니다.
float3 lightDir가 버텍스에서 바라보는 조명의 방향을 나타내는 길이가 1인 벡터라면
float3 viewDir 은 버텍스에서 바라보는 카메라의 방향을 나타냅니다.
그럼 Normal dot lightDir 대신 Normal dot viewDir 을 하면
카메라가 마치 조명처럼 인식되어 내가 바라보는 방향이 계속 밝아진다는 것입니다.
반전
Shader "Custom/RimLight" { Properties { _MainTex ("Albedo (RGB)", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma surface surf Lambert noambient #pragma target 3.0 sampler2D _MainTex; struct Input { float2 uv_MainTex; float3 viewDir; }; void surf (Input IN, inout SurfaceOutput o) { fixed4 c = tex2D (_MainTex, IN.uv_MainTex); float rim = dot(o.Normal, IN.viewDir); o.Emission = 1 - rim; // 결과 뒤집기 o.Alpha = c.a; } ENDCG } FallBack "Diffuse" }
위에서 조심할 내용은 dot 연산은 -1 까지 내려가서 나중에 결과가 이상해 질 수 가 있습니다. saturate() 를 잊지마세요
보는 각도를 바꿔도 언제나 외각이 밝아 보이면서 이제 Rim 라이트처럼 보이기 시작합니다.
그런데 Rim라이트가 너무 두꺼워서 Rim 라이트 느낌이 살아나지 않습니다.
흰테두리를 더 얇게 만들어줄 필요가 있습니다.
검정부터 흰색까지 변하는 음영을 표로 보면 아래와 같습니다.
가로축은 1 – dot(o.Normal, IN.viewDir) 이고 세로축은 색상 (흑백)을 의미합니다. 두 벡터의 각도 차이가 벌어질수록, 색상도 흰색이 됩니다.
pow 를 이용하면 지수함수와 같은 그래프로 어두운 부분이 한참동안 존재하다가 갑자기 밝아지는 모양이 됩니다.
이 그래프를 이용하여 흰 테두리를 얇게 만듭니다.
이제 rim 라이트에 색상을 추가합니다.
두께 조절 프로퍼티도 만들어줍니다.
NormalMap 추가
Rim 연산에서 o.Normal을 사용하기 때문에 반드시 Rim라이트 연산 이전에 NormalMap 을 적용시켜줘야 합니다.
이제 dot 연산에서 -1로 넘어가는 부분을 막아주고 noambient 까지 지워서 완성합니다.
이것이 Rim 라이트라고 부르는 Fresnel 이 적용된 결과 입니다.
게임에서 캐릭터가 데미지를 입었을 때나 선택할 때, 혹은 단순히 배경과 분리된 느낌을 원할 때 자주 사용되는 방식이지만
실제 존재하는 Rim 라이트를 구현했다고 부르기에는 약간 부족합니다.
우리가 만든 Rim 라이트는 조명의 방향과 전혀 연관이 없습니다.
이상적인 Rim 라이트는 역광일 때 주로 나타나고 직광일 때는 보이지 않습니다. (나중에 커스텀 라이트를 만들 때 가능합니다.)