대장장이 모델링, skybox, ground texture 등등 자료 출처 : 아티스트를 위한 유니티 URP 셰이더 입문
좌측에 있는 오브젝트를 우측처럼 만들어주는 쉐이더를 제작해봤다.
코드에 사용된 기능?들은 다음과 같다.
Cast Shadow와 Depth Pass 는 우선 테스트 해보려고 대마왕님의 블로그를 참고하여 가져왔다.
지금 당장은 좀 여력이 안 되어서, 아쉬웠던 부분은 나중에 추가로 작업 해볼 예정이다...
1. Recieve Shadow 구현하기
2. Edge-Detection 방법으로 Outline Shader 구현해보기
3. Point Light 받아오기
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_LightStep ("Light Step", Range(0.01, 10)) = 1
_LightIntensity ("Light Intensity", Range(0.01, 2)) = 1
_CelShade ("Cel Shade", Range(0, 1)) = 0.1
_CelWidth ("Cel Width", Range(0, 1)) = 0.1
// [HDR] _SpecularColor ("Spacular Color", Color) = (0.5, 1, 1, 1)
// _SpecPower ("Spec Power", Float) = 32
[HDR] _RimColor ("Rim Color", Color) = (0.5, 1, 1, 1)
_RimPower ("Rim Power", Range(0, 5)) = 1
_OutlineColor("Outline Color", Color) = (1, 0, 0, 1)
_OutlineDistance("Outline Distance", Float) = 0.1
}
중간에 Specular 넣어보려던 흔적... 막상 넣어서 셀식으로 표현하니까 생각보다 안 예뻐서 패스했다.
라이트를 써야 하니까 Lighting.hlsl도 추가해준다.
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float3 lightDir : TEXCOORD1;
float3 viewDir : TEXCOORD2;
};
normal은 노말을 계산하기 위해, lightDir은 빛의 방향을 구하기 위해, viewDir은 카메라를 향하는 방향을 구하기 위해 선언되었다.
이거 자료를 여러 개 같이 보면서 쓰다가 깨달은 건데, 저 v2f에서 굳이 lightDir을 써줄 필요는 없었다. Gouroud Shading 방식인 버텍스에서 라이트를 만들 게 아니라면....
v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.normal = TransformObjectToWorldNormal(v.normal);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
float3 positionWS = TransformObjectToWorld(v.vertex.xyz);
o.viewDir = normalize(_WorldSpaceCameraPos.xyz - positionWS);
return o;
}
솔직히 프레넬의 구조는 반 정도.. 이해가 됐다. 램버트 라이트의 라이트가 카메라 벡터라고 생각하고, 월드 방향 기준으로 dot 연산해주면 카메라 벡터와 오브젝트의 방향이 맞을 때 1값이 들어가고, 사이드로 갈 수록 0이 된다. 이거를 invert해주면 사이드가 1이고, 마주보는 면이 0값인 프레넬 구조가 완성된다.
다만 왜 뷰 벡터가 카메라 포지션에서 오브젝트의 포지션을 빼야 나오는지 아직도 잘 모르겠다. 이럴 땐 그냥 공식이구나 하고 넘어가는게 정신 건강에 좋은 것 같다.
참고한 자료에는 이렇게 적혀있다. 지정된 객체 공간 정점 위치에서 카메라 방향으로 월드 공간 방향을 계산하고 정규화 함(URP Shader Training 자료, 유튜브 영상) ... 어렵다. 우선 넘어가자.
half4 frag (v2f i) : SV_Target
{
i.normal = normalize(i.normal);
// float3 reflectDir = reflect(-i.lightDir, i.normal);
// half spec = saturate(dot(reflectDir, i.viewDir));
// spec = pow(spec, _SpecPower);
// half3 specColor = spec * _SpecularColor;
// Light
float3 lightDir = _MainLightPosition.xyz;
float3 LightColor = _MainLightColor.rgb;
// texture
half4 color = tex2D(_MainTex, i.uv);
// ambient color calculate
half3 ambient = SampleSH(i.normal);
// light calculate
float NdotL = saturate(dot(i.normal, lightDir) * 0.5 + 0.5);
float3 Light = NdotL + ambient;
//toon calculate
float3 toon = ceil(smoothstep(_CelShade, _CelShade + _CelWidth, NdotL) * _LightStep);
// toonLight calculate
float3 ToonLight = (toon + 0.5) * LightColor *_LightIntensity + ambient;
// fresnel
half fresnel = 1 - saturate(dot(i.normal, i.viewDir));
fresnel = pow(fresnel, _RimPower);
half4 fresnelColor = smoothstep(0.5, 0.8, fresnel) * _RimColor;
fresnelColor.rgb *= toon * LightColor * ambient;
color.rgb *= ToonLight;
color.rgb += fresnelColor.rgb;
return color;
}
주석 처리한 것은 스페큘러 계산했던 거.. 안 예뻐서 지웠다. 잘 쓰려면 노말 텍스쳐 받아와서 써야할 것 같은데 아직 HLSL에서 노말 맵을 받아오는 방법을 모른다. 나중에 도전.. ㄱ
그냥 내 입맛대로 좀 밝은 느낌의 툰 쉐이딩을 구현했다.
막상 해보니, 저 ceil 함수를 써서 음영을 구분하는 게 좀 애매했던 것 같다. 밝음, 중간, 어두움 이렇게 세 단계만 내려고 한다면 굳이 ceil 함수는 안 써도 될 듯. 나중에는 그냥 if else 써서 프로퍼티에서 음영이 시작되는 곳과 더 어둡게 음영을 줄 곳을 설정해서 직접 값을 바꿔가면서 보는 게 좋을 하다.
나중에 링크들 자료 뜯어보면서 더 연구해봐야댐
https://github.com/Unity-Technologies/com.unity.toonshader
참고 자료 링크
Cast Shadow, Depth Pass : https://chulin28ho.tistory.com/692
2Pass Outline : 아티스트를 위한 유니티 URP 셰이더 입문
림 라이트 : 유니티 코리아 URP Shader Training