☑️ 셀 쉐이딩의 대표적인 특징
- 외곽선이 있다.
- 음영이 끊어져 있다.
PR(Plastic Shader) / NPR(Toon Shader)
☑️ 2Pass로 외곽선 제작
- 첫 번째 패스로 오브젝트를 그리고, 똑같은 자리에 오브젝트를 한번 더 그린다. 이때, 두번째 오브젝트는 완전히 검게 그리고, 크기를 키워서 노멀을 뒤집는다.
- 노멀을 뒤집는 이유는 노멀 방향으로 확장하여 균일한 외곽선을 얻기 위함이다.
❗ 2Pass의 문제점
- 두 번 그리는 것이므로 무겁다.
- Plane으로 끝난 오브젝트에는 외곽선이 생기지 않는다. (완전히 닫힌 오브젝트가 아닌경우)
- 면을 뒤집었을 뿐, 온전한 오브젝트 이기 때문에 메쉬끼리 침범하거나 찌꺼기가 보일 수 있다.
- 각진 면(Hard Edge)에서는 선이 끊어진다.
☑️ Frsenel으로 외곽선 제작
- Rim 라이트를 만들었던 결과물을 단순히 뒤집거나, if문을 이용해서 외각 라인을 검출해 외곽선을 칠해준다.
- 의도적으로 선의 강약이 들어가도록 모델링이 가능하고, 품질이 좋다.
❗ Fresnel의 문제점
: 완전한 평면을 가진 오브젝트에서는 이상한 모양으로 보인다.
(각 픽셀의 노멀 방향의 시선 방향의 차이로 계산되기 때문)
Shader "Custom/2PassShader"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
CGPROGRAM
#pragma surface surf Lambert
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"
}
☑️ Backface Culling
- 3D에서는 흔히 뒷면을 그리지 않는다. 예를 들어 Plane을 만들고 뒤집으면 보이지 않는데, 이 현상을 Backface Culling이라고 한다.
- 현재 쉐이더에서는 자동으로 작동되어 있다.
cull back
이며, 이와 반대로 앞면을 날리고 뒷면을 보이게 해주는 FrontFace Culling은 cull front
라고 작성한다.vert()
함수를 작성해서 가동시킨다.surf()
는 Pixel Shader의 함수, vert()
는 Vertex Shader의 함수이다.)
vertex: vert
- 버텍스 쉐이더가 vert라는 함수 이름으로 시작됨을 알려줌appdata_full
- surf함수에서 사용한 SurfaceOutput처럼 미리 선언되어있는 특정한 구조체.
appdata
구조체 (3가지 종류가 있으며 작은 것을 선택할수록 가볍다.)//위치, 노멀과 하나의 텍스쳐 좌표게 struct appdata_base { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; //위치, 탄젠트, 노멀과 하나의 텍스쳐 좌표계 struct appdata_tan { float4 vertex : POSITION; float4 tangent : TANGENT; float3 normal : NORMAL; float4 texcoord : TEXCOORDO; UNITY_VERTEX_INPUT_INSTANCE_ID }; //위치, 탄젠트, 노멀, 4개의 텍스쳐 좌표계와 버텍스 컬러 struct appdata_full { float4 vertex : POSITION; //버텍스 위치 float4 tangent : TANGENT; //접선 방향 float3 normal : NORMAL; //버텍스의 노멀 float4 texcoord : TEXCOORDO; //UV좌표 float4 texcoordi : TEXCOORD1; float4 texcoord2 : TEXCOORD2; float4 texcoord3 : TEXCOORD3; fixed4 color: COLOR; //버텍스 컬러 UNITY_VERTEX_INPUT_INSTANCE_ID };
- 원래 쉐이더에서는 Vertex Shader에서 로컬 > 월드 > 카메라 > 프로젝션 좌표계로 변환하는 작업을 거치지만, 유니티의 Surface Shader의 Vertex Shader는 알아서 자동 변환을 해주므로 필요한 것만 가져다 쓰면 된다.
- 만약 버텍스의 위치를 위로 올리고 싶다면 아래와 같이 작성할 수 있다.
void vert(inout appdata_full v) { v.vertex.y += 1; //실제 오브젝트의 이동이 아닌 버텍스만 이동 }
void vert(inout appdata_full v) {
v.vertex.xyz += v.normal.xyz;
}
엄청나게 확장되어 공처럼 되어버렸는데, 기본적으로 모든 벡터는 길이가 1로 되어있으므로 각 버텍스는 노멀 방향으로 1유닛(1m)씩 이동한 것이다.
또한, 그림자는 작은 상태의 모델링을 인식하여 만들어지고 있다. 위의 함수에서 다뤄지지 않는 영역이기 때문이다.
당장은 그림자가 필요 없으므로 noshadow
구문을 추가하고, 확장할 정도를 1이 아닌 0.01(1cm)정도로 줄여준다.
#pragma surface surf Lambert vertex:vert noshadow
···
void vert(inout appdata_full v) {
v.vertex.xyz += v.normal.xyz * 0.01;
}
다시 cull front
로 면을 뒤집고, 오브젝트를 검게 만든다.
2nd Pass의 주석을 풀고 2nd Pass는 면이 뒤집히지 않도록 시작하기 전에 cull back
을 선언한다.
Shader "Custom/2PassShader"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
cull front
//1st Pass
CGPROGRAM
#pragma surface surf Nolight vertex:vert noshadow noambient
sampler2D _MainTex;
void vert(inout appdata_full v) {
v.vertex.xyz += v.normal.xyz * 0.01;
}
struct Input
{
float4 color:COLOR;
};
void surf (Input IN, inout SurfaceOutput o)
{
}
float4 LightingNolight(SurfaceOutput s, float3 lightDir, float atten) {
return float4(0, 0, 0, 1);
}
ENDCG
cull back
//2nd Pass
CGPROGRAM
#pragma surface surf Lambert
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"
}
cull back
//2nd Pass
CGPROGRAM
#pragma surface surf Toon noambient
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;
}
float4 LightingToon(SurfaceOutput s, float3 lightDir, float atten) {
return float4(1, 0, 0, 1);
}
ENDCG
0
~1
음영을 출력한다.float4 LightingToon(SurfaceOutput s, float3 lightDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
return ndot1;
}
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
if (ndot1 > 0.5) ndot1 = 1;
else ndot1 = 0.3;
return ndot1;
cell
올림 함수를 사용해서 나타낼 수도 있다. float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
ndot1 *= 5;
ndot1 = ceil(ndot1) / 5;
return ndot1;
Shader "Custom/2PassShader"
{
Properties
{
_MainTex ("Albedo (RGB)", 2D) = "white" {}
_BumpMap ("NormalMap", 2D) = "bump" {}
}
SubShader
{
Tags { "RenderType"="Opaque" }
cull front
//1st Pass
CGPROGRAM
#pragma surface surf Nolight vertex:vert noshadow noambient
void vert(inout appdata_full v) {
v.vertex.xyz += v.normal.xyz * 0.01;
}
struct Input
{
float4 color:COLOR;
};
void surf (Input IN, inout SurfaceOutput o)
{
}
float4 LightingNolight(SurfaceOutput s, float3 lightDir, float atten) {
return float4(0, 0, 0, 1);
}
ENDCG
cull back
//2nd Pass
CGPROGRAM
#pragma surface surf Toon
sampler2D _MainTex;
sampler2D _BumpMap;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Albedo = c.rgb;
o.Alpha = c.a;
}
float4 LightingToon(SurfaceOutput s, float3 lightDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
ndot1 *= 5;
ndot1 = ceil(ndot1) / 5;
float4 final;
final.rgb = s.Albedo * ndot1 * _LightColor0.rgb;
final.a = s.Alpha;
return final;
}
ENDCG
}
FallBack "Diffuse"
}
Shader "Custom/FresnalShader"
{
Properties
{
_MainTex("Albedo (RGB)", 2D) = "white" {}
_BumpMap("NormalMap", 2D) = "bump" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
cull back
CGPROGRAM
#pragma surface surf Toon
sampler2D _MainTex;
sampler2D _BumpMap;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Albedo = c.rgb;
o.Alpha = c.a;
}
float4 LightingToon(SurfaceOutput s, float3 lightDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
ndot1 *= 5;
ndot1 = ceil(ndot1) / 5;
float4 final;
final.rgb = s.Albedo * ndot1 * _LightColor0.rgb;
final.a = s.Alpha;
return final;
}
ENDCG
}
FallBack "Diffuse"
}
rim
을 반환해서 확인한다.Shader "Custom/FresnalShader"
{
Properties
{
_MainTex("Albedo (RGB)", 2D) = "white" {}
_BumpMap("NormalMap", 2D) = "bump" {}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
cull back
CGPROGRAM
#pragma surface surf Toon
sampler2D _MainTex;
sampler2D _BumpMap;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Albedo = c.rgb;
o.Alpha = c.a;
}
float4 LightingToon(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
ndot1 *= 5;
ndot1 = ceil(ndot1) / 5;
float rim = abs(dot(s.Normal, viewDir));
if (rim > 0.3) rim = 1;
else rim = -1;
float4 final;
final.rgb = s.Albedo * ndot1 * _LightColor0.rgb * rim;
final.a = s.Alpha;
return final;
}
ENDCG
}
FallBack "Diffuse"
}
ndot1
을 구현하는 쉐이더에서 시작Shader "Custom/DiffuseWrapingShader"
{
Properties
{
_MainTex("Albedo (RGB)", 2D) = "white" {}
_RampTex("RampTex", 2D) = "white"{}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf warp noambient
sampler2D _MainTex;
sampler2D _RamTex;
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;
}
float4 Lightingwarp(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
return ndot1;
}
ENDCG
}
FallBack "Diffuse"
}
_RampTex
에는 아래와 같은 이미지를 넣는다.
이번에는 텍스쳐 연산을 커스텀 라이트 함수에서 진행한다.
float4 Lightingwarp(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir);
//텍스쳐 연산, UV자리에 (0.1, 0.1)를 넣었다.
float4 ramp = tex2D(_RampTex, float2(0.1, 0.1));
return ramp;
}
- UV자리에
float2(0.1, 0.1)
이 들어가 있으니 UV가 0.1, 0.1인 자리의 컬러를 출력한 것이다.
ndot1
으로 밝은 부분은 1, 어두운 부분은 0으로 나오는 상태이고, 이것을 ramp 텍스쳐 UV 자리에 U로 넣는다.float4 ramp = tex2D(_RampTex, float2(ndot1, 0.5));
☑️ Ramp 텍스쳐
- Wrap Mode를 Repeat로 하면 마지막 텍스쳐 색이 처음 부분에 반복되어 묻어나올 수 있기 때문에 Clamp를 사용하는 것이 좋다.
- 텍스쳐의 사이즈를 줄이고 압축을 None으로 하여 컬러의 손실이 생기지 않도록 한다.
Shader "Custom/DiffuseWrapingShader"
{
Properties
{
_MainTex("Albedo (RGB)", 2D) = "white" {}
_BumpMap("NormalMap", 2D) = "bump"{}
_RampTex("RampTex", 2D) = "white"{}
}
SubShader
{
Tags { "RenderType" = "Opaque" }
CGPROGRAM
#pragma surface surf warp noambient
sampler2D _MainTex;
sampler2D _BumpMap;
sampler2D _RampTex;
struct Input
{
float2 uv_MainTex;
float2 uv_BumpMap;
};
void surf(Input IN, inout SurfaceOutput o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex);
o.Normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap));
o.Albedo = c.rgb;
o.Alpha = c.a;
}
float4 Lightingwarp(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
float4 ramp = tex2D(_RampTex, float2(ndot1, 0.5));
float4 final;
final.rgb = s.Albedo.rgb * ramp.rgb;
final.a = s.Alpha;
return final;
}
ENDCG
}
FallBack "Diffuse"
}
1. Specular
float4 Lightingwarp(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
//specular
float3 H = normalize(lightDir + viewDir);
float spec = saturate(dot(s.Normal, H));
//X에 N·L, Y에 H·V
float4 ramp = tex2D(_RampTex, float2(ndot1, spec));
float4 final;
final.rgb = (s.Albedo.rgb * ramp.rgb) + (ramp.rgb * 0.1); //ramp 텍스쳐를 10% 강하게
final.a = s.Alpha;
return final;
}
2. Fresnel
float4 Lightingwarp(SurfaceOutput s, float3 lightDir, float3 viewDir, float atten) {
float ndot1 = dot(s.Normal, lightDir) * 0.5 + 0.5;
//Fresnel
float rim = abs(dot(s.Normal, viewDir));
//X에 N·L, Y에 N·V
float4 ramp = tex2D(_RampTex, float2(ndot1, rim));
float4 final;
final.rgb = (s.Albedo.rgb * ramp.rgb) + (ramp.rgb * 0.1);
final.a = s.Alpha;
return final;
}