셰이더 기초 - 고냥이도 이해하는 그래픽스와 셰이더 기초편 - 1/3 by 우덜 :: WD ART.

위 링크는 가볍게 보기 좋은것 같아 가져왔습니다.

툰 셰이더 튜토리얼 - .Unity Toon Shader Tutorial - Roystan

툰 셰이더 튜토리얼을 해보고 적용 시킬 생각입니다. (기본적인 글의 그림은 위 링크가 출처입니다.)

일단 기본적인 유니티 셰이더로 조명과 상호작용하는 Surface Shaders(서피스 셰이더)를 일반적으로 사용합니다.

방향성 조명 (Directional lighting)

조명 데이터를 받기 위해 코드를 추가합니다.

Tags
{
  // 포워드 렌더링에 사용, 앰비언트 라이트, 주 방향 광원, 버텍스/SH 라이트 및 라이트맵이 적용
  "LightMode" = "ForwardBase"
  // 포워드 렌더링, 위 값의 주방향 광원, 앰비언트/라이트프로브 데이터만 셰이더에 전달됩니다.
  "PassFlage" = "OnlyDirectional"
}

Tag는 기본적으로 SubShader 안 Pass 밖에서 사용 "키 태그 이름(TagName)" = "값 명령어(Value)" 이며 , 를 사용하지 않고 갯수 제한이 없음.

조명 파이프라인(앰비언트, 버텍스 리트, 픽셀 릿 등)에서 Pass가 수행하는 역할과 몇 가지 옵션을 제어하는데 사용됩니다. (ShaderLab: 패스 태그 - Unity 매뉴얼 )

조명을 계산하기 위해 Blinn-Phong에 몇 가지 추가 필터를 적용하여 툰 셰이더를 사용할것입니다. 일반적 Phong 보다 포퍼먼스가 좋고 현실적입니다.

Blinn-Phong, 음영 벡터, 여기서 L은 광원(vector to the light source)에 대한 벡터이고 N(normal of the surface)은 노멀값입니다. (번역기)

자세한 자료는 두가지로 유니티OpenGL입니다. 유니티 링크에서 개념적부분을 잡고 OpenGL 링크에서 좀 더 자세히 알아가면 됩니다.

셰이더 내에서 개체의 일반 데이터에 액세스해야합니다. 아래 코드를 추가하십시오.

// struct appdata에 추가해줍니다.
float3 normal : NORMAL;

// struct v2f에 추가해줍니다.
float3 worldNormal : NORMAL;

appdata의 노말 값은 자동으로 입력되지만 v2f의 노말 값은 직접 값을 줘야합니다.

버텍스 셰이더에 채워집니다. 또한 빛의 방향이 월드 공간에서 제공되므로 노멀을 로컬에서 월드 공간으로 변환하려고합니다. 버텍스 셰이더에 다음 줄을 추가합니다.

o.worldNormal = UnityObjectToWorldNormal(v.normal);

이제 프래그먼트 셰이더(Fragment Shader)에서 월드 노말 값을 사용해 내적 연산을 통하여 빛의 방향을 비교 할 수 있습니다.

내적(Dot Product)

길이에 관계없이 두 벡터를 받고 단일 숫자를 반환합니다. 벡터가 평행한 상태이고 방향 벡터(길이가 1 인 벡터)인 경우값은 1입니다. 또는 벡터가 수직이면 0을 반환합니다. 벡터를 평행에서 수직으로 이동하면 내적 결과가 비선형 형태로 1에서 0으로 이동합니다. 벡터 사이의 각도가 90보다 크면 내적은 음수가됩니다.

프래그먼트 셰이더(Fragment Shader)에 다음을 추가합니다.  아래 NdotL 은 수정한 코드입니다.

// 프래그먼트 셰이더 맨 위
float3 normal = normalize(i.worldNormal);
float NdotL = dot(_WorldSpaceLightPos0, normal);

…

// 기존 return 문 수정
return _Color * sample * NdotL;

 

결과물

사실적인 조명을 구현한 Blinn-Phong 셰이더를 렌더링한 모습입니다.

이제 툰 셰이딩으로 수정하기 위해 밝고 어두운 두 영역으로 나누는 작업입니다.

// NdotL이 계산 후 들어가야함으로 NdotL 아래에 적어줍니다.
float lightIntensity = NdotL > 0 ? 1 : 0;

// return 문 수정
return _Color * sample * lightIntensity;

결과물

엠비언트 라이트 (Ambient light)

어두운 면이 너무 어둡습니다. 주변 광을 추가하겠습니다. 

// property에 추가
[HDR]
_AmbientColor("Ambient Color", Color) = (0.4,0.4,0.4,1)

…

// 이름 일치해야함, 프래그먼트 셰이더 위에 기입
float4 _AmbientColor;

// 수정
return _Color * sample * (_AmbientColor + lightIntensity);

결과물

씬에서 Directional Light(조명) 강도나 색상을 수정해도 셰이더에 영향을 주지 않습니다, 이를 조명 계산에 포함하기 코드를 더 추가합니다.

// #include "UnityCG.cginc" 아래
#include "Lighting.cginc"

…

// lightIntensity 아래에 추가
float4 light = lightIntensity * _LightColor0;

// 수정
return _Color * sample * (_AmbientColor + light);

기존 lightIntensity 값을 곱하여 float4에 저장하여 계산에 조명의 색상을 포함합니다. _LightColor0은 주 방향 조명의 색상입니다. Lighting.cginc 파일에 선언 된 fixed4이므로 값을 활용하기 위해 위의 파일을 포함합니다.

입력 하고 조명 색을 바꿀 시 구의 색도 같이 변합니다. 어둡게 할 시 어두워지고요.

더 진행하기 전에 밝음과 어두움 사이의 가장자리를 부드럽게 만듭니다. 빛에서 어둠으로의 전환은 즉각적이며 단일 픽셀에서 발생합니다. 대신 smoothstep 함수를 사용하여 값을 1에서 0으로 부드럽게 블렌딩합니다.

smoothstep (왼쪽)과 선형 함수 (오른쪽)의 비교. 값은 회색조 배경과 빨간색 곡선에 매핑됩니다.

smoothstep은 선형이 아닙니다. 값이 0 -> 0.5로 가속되고 0.5 -> 1로 감속됩니다. 이것은 매끄럽게 블렌딩하는데 이상적이며, 라이트 강도 값을 블렌딩하는 데 사용할 방법입니다.

// 수정
float lightIntensity = smoothstep(0, 0.01, NdotL);

좌 <변경 전> 우 <변경 후>

하한값과 상한값 인 0과 0.01은 가깝습니다. 이는 상대적으로 왜곡된 가장자리를 유지하는 데 도움이됩니다. 잘 모르겠으면 0.01 값을 바꿔볼 시 단번에 이해가 가능합니다.

정반사 (Specular reflection)

광원에 의해 만들어진 개별적이고 뚜렷한 반사를 모델링합니다. 이 반사는 표면이 보이는 각도의 영향을 받는다는 점에서 시점에 따라 움직입니다. 정점 셰이더에서 월드 뷰 방향을 계산하고 이를 프래그먼트 셰이더로 전달합니다. 현재 정점에서 카메라를 향하는 방향입니다.

// struct v2f 추가
float3 viewDir : TEXCOORD1;
// vertex shader 추가
o.viewDir = WorldSpaceViewDir(v.vertex);

이제 Blinn-Phong의 specular 구성 요소를 구현하겠습니다. 표면에서 두 가지 속성인 반사를 착색하는 반사광 색상과 반사 크기를 제어하는 ​​광택을 가져옵니다.

// properties에 추가
[HDR]
_SpecularColor("Specular Color", Color) = (0.9,0.9,0.9,1)
_Glossiness("Glossiness", Float) = 32

// 변수명 주의
float _Glossiness;
float4 _SpecularColor;

정반사의 강도는 Blinn-Phong에서 표면의 법선과 half vector 사이의 내적으로 정의됩니다. half vector는 보는 방향(카메라)과 광원 사이의 벡터입니다. 이 두 벡터를 합하고 결과를 정규화하여 얻을 수 있습니다.

// 프래그먼트 셰이더에 _MainTex 위에 작성
float3 viewDir = normalize(i.viewDir);

float3 halfVector = normalize(_WorldSpaceLightPos0 + viewDir);
float NdotH = dot(normal, halfVector);

float specularIntensity = pow(NdotH * lightIntensity, _Glossiness * _Glossiness);

// 리턴 값 수정
return _Color * sample * (_AmbientColor + light + specularIntensity);

pow 함수를 사용하여 정반사 크기를 제어합니다. NdotH에 lightIntensity(빛의 세기)를 곱하여 표면의 빛 들어올때만 반사가 되도록합니다. _Glossiness(광택)는 그 자체로 곱해 지므로 머티리얼 에디터에서 더 작은 값이 더 큰 효과를 내고 셰이더 작업을 더 쉽게 할 수 있습니다.

다시 smoothstep을 사용하여 반사를 툰화하고 최종 출력에 _SpecularColor를 곱합니다.

림 라이트 (Rim lighting)

반사광이나 역광을 시뮬레이션하기 위해 물체의 가장자리에 조명을 추가하는 것입니다.툰 셰이더가 평평한 음영 표면에서 오브젝트의 실루엣을 돋보이게하는 데 특히 유용합니다.

개체의 "Rim"은 카메라에서 멀리 향하는 표면으로 정의됩니다. 그러므로 우리는 법선과 뷰 방향의 내적을 취하고 그것을 반전시켜 림을 계산할 것입니다. 

// fragment shader에 specular 밑에 작성
float4 rimDot = 1 - dot(viewDir, normal);

// 수정
return _Color * sample * (_AmbientColor + light + specular + rimDot);

 

다시 smoothstep으로 값을 임계 값으로 설정하여 효과를 돋보이게합니다.

// properties에 작성
[HDR]
_RimColor("Rim Color", Color) = (1,1,1,1)
_RimAmount("Rim Amount", Range(0, 1)) = 0.716

…

// 변수 선언
float4 _RimColor;
float _RimAmount;

…

// rimDot 아래 선언
float rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimDot);
float4 rim = rimIntensity * _RimColor;

// 수정
return _Color * sample * (_AmbientColor + light + specular + rim);

개체 주위에 테두리가 그려지면 조명 효과 보단 윤곽선과 비슷합니다. 오브젝트의 조명 표면에만 생기게 수정합니다.

// rimIntensity 위에 선언 후 rimIntensity 수정
float rimIntensity = rimDot * NdotL;
rimIntensity = smoothstep(_RimAmount - 0.01, _RimAmount + 0.01, rimIntensity);

해결 했지만 림이 조명 표면을 따라 얼마나 멀리 확장되는지 제어 할 수 있으면 유용합니다. pow 함수를 사용하여 림 크기를 조정합니다.

// property에 작성
_RimThreshold("Rim Threshold", Range(0, 1)) = 0.1

…

// 변수
float _RimThreshold;

// 수정
float rimIntensity = rimDot * pow(NdotL, _RimThreshold);

 

그림자 (Shadows)

마지막 단계로 셰이더가 그림자를 받는 기능을 추가합니다. 그람자 투사는 간단합니다. 패스 끝(중괄호 외부)에 다음 코드 줄을 추가합니다.

// 패스가 끝나는 곳 중괄호 밖에 입력합니다.
UsePass "Legacy Shaders/VertexLit/SHADOWCASTER"

지금 끝내도 좋지만 디테일을 위해 프래그먼트 셰이더에서 표면에 그림자가 있는 지 여부를 알고 조명 계산에 반영해보겠습니다.

빛에 의해 투사되는 그림자 맵을 샘플링하려면 버텍스 셰이더에서 프래그먼트 셰이더로 텍스처 좌표를 전송해야합니다.

// include 기존 항목 아래에 추가
#include "AutoLight.cginc"

// v2f 구조체에 추가
SHADOW_COORDS(2)

// vertex shader에 추가
TRANSFER_SHADOW(o)

그림자를 샘플링하는 데 사용할 여러 매크로가 포함 된 파일 인 Autolight.cginc를 포함합니다.

SHADOW_COORDS (2)는 다양한 정밀도(대상 플랫폼에 따라 다름)로 4차원 값을 생성하고 제공된 인덱스(이 경우 2)에서 TEXCOORD 의미 체계에 할당합니다.

TRANSFER_SHADOW는 입력 정점의 공간을 그림자 맵의 공간으로 변환 한 다음 선언 한 SHADOW_COORD에 저장합니다.

그러나 그림자 맵을 샘플링하기 전에 셰이더가 두 가지 다른 조명 케이스를 처리하도록 설정되어 있는지 확인해야합니다. 주 방향 조명이 그림자를 투사하거나 투사하지 않는 경우입니다. Unity는 각 사용 사례에 대해이 셰이더의 여러 변형을 컴파일하여이 두 가지 구성을 처리하는 데 도움이됩니다.

#pragma multi_compile_fwdbase

 Unity가 포워드베이스 렌더링에 필요한 모든 변형을 컴파일하도록 지시합니다. 이제 그림자 맵에서 값을 샘플링하여 조명 계산에 적용 할 수 있습니다.

// 프래그먼트 셰이더에서 lightIntensity 위에 추가
float shadow = SHADOW_ATTENUATION(i);
// 수정
float lightIntensity = smoothstep(0, 0.01, NdotL * shadow);

SHADOW_ATTENUATION은 0과 1 사이의 값을 반환하는 매크로입니다. 여기서 0은 그림자가 없음을 나타내고 1은 완전히 그림자가 있음을 나타냅니다. 주 방향 광에서받은 빛의 양을 저장하는 변수이기 때문에 NdotL에이 값을 곱합니다.

완료.

툰 셰이더는 다양한 그래픽 스타일로 제공되지만 효과를 얻으려면 일반적으로 표준 조명 설정 (Blin-Phong에서했던 것처럼)을 취하고 단계적으로 셰이더 함수를 적용하는 것이 중심입니다.

나중에 공부해볼만한 것 (실제로 노멀과 라이팅 데이터를 사용할 수있는 경우 포스트 프로세스 효과로 수행 할 수 있습니다. 이에 대한 예는 Unreal Engine 4 튜토리얼에서 찾을 수 있습니다.)

+ Recent posts