이전 챕터에서 Diffuse 항에 대해 물체의 난반사 재질을 구현하였다면
이번 챕터에서는 그 다음으로 빛의 정반사 재질에 대한 Specular 항을 구현해 볼 것이다.
Specular항에 대하여도
Diffuse 모델과 마찬가지로 여러가지의 라이팅 모델이 존재한다.

기본적으로 빛은 입자성과 파동성을 갖고 있으며 광자의 에너지에 대하여,
물리적으로 올바른 라이팅에 대하여 논한다면 빛이 발산하는 광원에서의 에너지와
반사, 충돌, 굴절, 회절 등에 발상하는 애너지의 합이 서로 같아야만 한다. (에너지 보존 법칙)
빛이 물체 표면에 충돌하게 되면 일부 파장은 흡수하게 되고 일부 파장은 반사하게 되는데, 이때 반사하는 빛에 대한 에너지에 있어서 난반사 재질과 정반사 재질의 에너지 총 합이 같아야만 한다.
이를 표현하기 위해 PBR에서는
Roughness와 Metalic 이라고 하는 파라메터에 근간하여 난반사 재질과 정반사 재질을 모델링한다.
한편, NPR에 대해서
NPR은 사실적인 물리 기반 라이팅을 구현할 필요성과 중요도가 상대적으로 떨어지기 때문에
간단한 라이팅으로 물체의 재질을 표현하는데 초점을 두며 에너지의 보존과 관계 없이
연출이 이쁘게 나오도록 하는 것을 주 목표로 하는 경우가 많다.
이전 챕터에 대해서Diffuse 항을 구현할 때 Lambert Lighting이라는,
빛방향과 법선 벡터의 유사도에 입각한 내적 연산으로 비교적 저렴하고 간단한 라이팅을 구현하였었다.
이번Specular항에 대해서도 이와 유사하게 PBR에 의존하지 않고 저렴한 라이팅 방식을 사용하여 구현해 볼 것이다.
준비물
A. Specular 모델 선정
B. LightMap -> specular mask
C. MatCap Texture
A. Specular 모델 선정
간단한 Specular항을 모델링 하는데 주로 많이 쓰이는 모델은
Phong 모델과 이를 경량화 한 Blinn-Phong 모델이다.
1. Phong Lighting
스펙큘러가 발생하는 물리적인 원리는 간단하게 말해서,
빛이 표면에 입사하고
물체의 극소 평면에 대한 법선벡터에 대하여 입사각과 반사각이 같다는 물리 법칙과
반사된 빛을 우리가 어느 각도로 바라느냐에 따라서 반사정도가 달라진다.
이를 수학적으로 모델링하면 다음과 같다.
1.빛이 표면에 반사되어 나간 벡터 R을 구한다.

헷갈리지 말아야 할 것은,
라이트 벡터에 대해서 다룰 때에는
표면을 기준으로 광원이 어디 있는지에 대한 벡터를 L 벡터로 설정하기 때문에 이를 유심하여
r = 2s - l
projection을 통해( N,L은 정규화된 벡터이므로)
s = (N·L)/ (N·N) N = (N·L)N
R = 2(N·L)N − L
라는 사실을 통해 유도한다.
2. View Vector와의 유사도를 측정하기 위해 dot(V,R) 를 하고, 값이 -1이 되면 안되므로
Phong = MAX(dot(V,R), 0)

이를 구현하면 다음과 같이 Specular가 차지하는 면적이 큰 비중을 차지한다는 것을 알 수 있다.
이는 dot(V,R)에 대한 세타 각도의 범위에 따른 값이 그대로 표면에 표현되기 때문인데
pow 연산을 통해 1에 가까운 값에 대해서는 어느정도의 밝기를 유지하고 특정 구간부터는 암부로 값을 매핑한다.
half3 V = GetWorldSpaceNormalizeViewDir(i.posWS);
//half3 V = SafeNormalize(_WorldSpaceCameraPos - i.posWS);
Light mainLight = GetMainLight();
half3 LightDirection = SafeNormalize(mainLight.direction);
half3 N = normalize(i.normalWS);
half3 R = 2 * N * dot(N, LightDirection) - LightDirection;
half NdotR = saturate(dot(V, R));
float Phong = pow(NdotR, _SpecularIntensity);

pow 값을 적절하게 조절하여 하이라이트 영역을 시각적으로 자연스럽게 보이도록 한다.
한편, 해당 식에서 fragment shader에 normal을 사용할때는 normalize를 꼭 하고 사용해야 한다.
rasterizer 단계에서 정점 정보들을 통해 barycentric 을 통한 interpolationi 시 vertex 사이의 fragment에 대해서
보간된 벡터의 크기가 1이 되지 않기 때문에, 만약 이를 무시한다면 modle을 이루는 vertex들의 plane의 normal과 유사하게
마치 Gouraud shading을 한 듯한 결과물이 발생한다.

2. Blinn-Phong Lighting
상기 설명한 Phong 모델은 직관적이고 입사각 = 반사각이라는 물리 법칙에 기반한 스펙큘러 모델이지만,
RTR 환경에서는 몇 가지 한계를 가진다.
우선, 반사벡터를 직접 계산해야 하므로 수치적으로 불안정할 수 있으며, 비싸고
특정 각도에서(ex, 라이트 반대방향에 카메라가 있을 때) 하이라이트가 과도하게 퍼지거나 튀는 현상이 발생한다.
이러한 문제를 개선하기 위해 Jim Blinn은 Phong 모델을 변형한 Blinn-Phong 모델을 제안하였다.
Blinn-Phong은 반사벡터 R 대신 뷰벡터와 Light 벡터의 반각 벡터를 사용 함으로써
계산을 단순화하고, 하이라이트의 안정성과 일관성을 높였다.
half3 V = GetWorldSpaceNormalizeViewDir(i.posWS);
//half3 V = SafeNormalize(_WorldSpaceCameraPos - i.posWS);
Light mainLight = GetMainLight();
half3 LightDirection = SafeNormalize(mainLight.direction);
half3 N = normalize(i.normalWS);
half3 H = SafeNormalize(V + LightDirection);
half NdotH = dot(N, H);
float BlinnPhong = pow(NdotH, _SpecularIntensity);

같은 임계 값을 설정하였을 때 Specular의 영역이 더 넓게 퍼진다는 특징이 있다.
3. Phong VS Blinn Phong
블린 퐁 모델과 퐁 모델에 대해서 결과적으로 ,형태 적으로 봤을 때 개인적으로 어느 큰 차이점을 잘 느낄 수 없었고
연산량 관점에서 두 모델 크게 dot product에 대한 비용이 다이나믹한 차이를 발생하지는 않을 것으로 보고
Blinn Phong 모델이 주로 사용되는 정확한 이유에 대해서 탐구를 해 볼 필요가 있었다.
1. phong 모델은 이상적인 거울 반사에 가깝다.
입사각과 반사각이 서로 같다는 물리 법칙은 모든 빛에 대해서 동일하게 성립하지만, 물체의 미세 표면에 대하여
반사 표면이 완전히 평면을 형성하는 오브젝트는 존재하지 않는다.

Phong 모델에서의 reflection 벡터는 반사 표면에 대해 완전한 정반사를 가정한 모델이기 때문에 오히려 더욱 물체의 사실적인 라이팅을 표현하지 못한 인위적인 라이팅처럼 보이게 되는 것이다.
2. Blinn Phong은 PBR의 microfacet 모델을 일부 반영한것처럼 보이는 효과를 만들어 주는 모델이다.
반면, Blinn-Phong 모델은 표면의 반사에서 V벡터와 L 벡터의 반각벡터 H를 사용하기 때문에
표면의 microfacet(미세 거울면)을 근사하는 형태로 해석된다.
N·H는 본질적으로 N 벡터와 H벡터의 유사도를 나타내는데, 이 뜻은 법선이 H에 가까운 microfacet의 분포가 얼만나 많은지에 대해서 측정하는 것으로 해석할 수 있다.
3. BLinnphong 하이라이트가 Phong 모델보다 자연스럽게 퍼지는 효과를 보여준다.
Blinn-Phong은 N·H 기반이기 때문에
하이라이트가 표면 전체에 더 부드럽게 분포한다.
반면, Phong은 V·R 기반이라 특정 각도에서 급격히 밝아지는 구간이 생긴다.
4. Blinn Phong은 각도 변화에 덜 민감하다.
반각벡터를 사용하기 때문에 view 벡터에 대한 각도 민감도가 Phong 모델보다 덜하여
하이라이트가 특정 각도 조건에서 튀는 현상이 줄어들고 안정적이다.
다양한 이유가 있지만, 결과론적으론 blinn phong 과 phong 모델에 대해서
Power에 대한 Specular Intensity를 적절히 조절했을때 크게 다르게 보이지 않는다는게 결론이며 연산량 또한
현대 하드웨어에서는 큰 차이가 발생하지 않는다.


확연하게 다른점은 빛과 뷰 벡터의 각도 차이가 커질때
Phong과 BlinnPhong 의 민감도가 다르다는 것이며
NPR 쉐이딩에서는 하이라이트의 변화가 튀지 않고 안정적인 라이팅을 선호하기 때문에
간단한 라이팅에 있어서 대부분 Phong 라이팅 보다는 Blinn-Phong을 선택하는 것으로 보인다.
B. LightMap
NPR에서 스펙큘러를 효과적으로 구현하는데 있어서 Diffuse 구현과 마찬가지로 커스텀 텍스쳐를 통한 마스킹을 활용한다.









Diffuse/ LightMap.R / LightMap.B
각 텍스쳐가 의미하는 바에 대해서 먼저 분석해 본다.
참고: https://gamebanana.com/tuts/19171
LightMap.R
1값인 흰색 부분에 해당하는 텍스쳐는 금속성(Metalic)을 띄는 오브젝트에 대한 마스크 값임을 알 수 있다.
0~1의 중간 회색에 대해서는 스펙큘러 효과가 적용되는 재질에 대한 마스크 값임을 알 수 있다.
0값인 검은색 부분에 해당하는 텍스쳐는 피부와 같이 난반사 재질이 강한 텍스쳐에 해당하는 마스크 값임을 알 수 있다.
즉, LightMap.R은 재질 타입에 대한 마스크 역할로써 값이 사용된다.
- 1에 가까움 → 금속성 영역 (MatCap 사용)
- 0~1 중간값 → 스펙큘러가 적용되는 재질
- 0에 가까움 → 난반사 위주의 영역(피부 등)
LightMap.B
스펙큘의 강도를 나타내는 항목이다.
0일때는 스펙큘러의 강도가 적고 1로 갈수록 스펙큘러의 강도가 세진다.
이를 통해 LightMap.B는 specular Intensity로써 사용된다는 점을 확인해 볼 수 있다.
해당 텍스쳐에서 가장 특징적으로 보여지는 부분은 머리카락의 하이라이트 부분에 대한 마스킹 값이다.
NPR 쉐이딩에서 머리카락의 하이라이트를 구현하는데에는 여러가지 방식이 있는데 대표적으로
dynamic specular, pre-masked specular, plane specular 가 있다.
래퍼런스 모델에서는 미리 스펙큘러의 디자인과 스펙큘러가 형성되는 위치에 대해서 설정해 놓은 pre-maksed 방식이므로 원하는 위치에 대해서 하이라이트가 나타나도록 제어하기 쉽다.
하지만 카메라의 위치에 따라서 머리카락 등의 하이라이트 위치가 변하지 않고 정해지기 때문에
다이나믹한 스펙큘러를 묘사하는데에는 적합하지 않고 보다 덜 사실적으로 묘사되는 부분이 있다.
요즘 NPR게임의 경우 하이라이트는 대부분 동적 하이라이트 방식을 사용하는 경우가 많으며 이를 위한 모델로
kajiya-kay, marschner 모델 등이 있다.
C. MatCap
매트캡은 material capture의 약어이다.
matcap 방식은 환경 조명 등을 뷰 공간 기반으로 그럴싸 하게 보이도록 trick을 사용한 방식이라고 볼 수 있다.
일반적인 matcap texture는 다음과 같이 생겼다.



오브젝트의 world normal을 view공간에 대한 normal로 변환한 후 해당 값에 대한 xy벡터를
texture의 uv 좌표로 사용하여 해당 normal의 색상정보를 해당 이미지를 통해 얻어내는 방식이다.
즉, matap은 view based fake lighting이라고 볼 수 있다.
이렇게 구현하였을때 얻을 수 있는 장점은 큰 성능 제약과 기술력 없이도 금속과 같은 재질을 손쉽게 표현할 수 있고
specular에 대한 물성이 금속성과 비금속성을 가르는 가장 큰 기준이 되기 때문에 잘못된 라이팅이여도 사실적인 묘사에 대해서
큰 ROI가 없는 NPR에 대해서는 효과적으로 텍스쳐를 활용할 수 있다는 장점이 있다.
가장 오른쪽 사진이 레퍼런스 모델에서 사용하는 matcap texture이며 이를 활용해 금속성의 재질 묘사를 표현할 것이다.
구현
다음 문제를 해결하는데 있어서 우선적으로 스펙큘러를 구현하는데에는
여러가지 방식과 아이디어가 있고 정확한 정답이 없다는 것을 먼저 알고 가야 한다는 생각이 들었다.
PBR 환경에서는 microfact 기반의 GGX등을 많이 사용하지만, NPR에서는 구현 수식 자체보다는
어떤 아트적 목표를 가지고, 제약을 두고 설계하였는지에 따라서 방향성이 달라질 수 있다.
라이트방향에 대해서 의존하는 NdotH 방법도 있지만,
NPR에서는 L벡터를 뺀 NdotV 기반 라이팅도 있다는 것을 자료 조사를 통해 찾아 볼 수 있었으며
항상 스펙큘러가 화면 방향에 대해서 보이도록 설계하기도 한다는 점이 인상적이었다.
여기서부터는 아트적으로 어떠한 방법을 선택하느냐에 따라 선택지가 달라질 수 있고
레퍼런스 모델에 대해서 Diffuse를 구현할때와는 달리 텍스쳐 기반이 아닌
파라메터 기반으로 값을 조정하기 때문에 정확하게 어떻게 구현을 했을지에 대하여
추적하기가 쉽지 않다는 점을 먼저 전제로 한다.
다음은 npr에서 specular를 구현하는데 있어서의 기준으로 생각한 항목들이다.
1. 룩의 안정성
하이라이트가 형성되는 위치가 카메라나 환경 등에 따라서 변동이 커져선 안된다.
환경, 라이트 세팅의 변동에도 인상이 크게 변하지 않도록 한다.
2. 아트 통제
텍스쳐, 파라메터 값을 통해 어느 부분에 어느 시점에서 하이라이트가 생성 될지를 제어하고 설계할 수 있어야 한다.
파라메터는 최소한으로 유지하면서 적절한 프로덕션을 만들어 낼 수 있도록 한다.
Lightmap.R을 통해 먼저 금속 비금속을 나눈다.
lightmap.R의 값에 대해서 1일때는 matcap으로 적용되도록,
1이 아닌 값일 때는 스펙큘러가 적용되도록 한다.
half3 specular = lerp(nonMetallic, metallic, step(0.99, lightMap.r));
metalic에 대한 항에 대해서는 matcap을 통해 금속 재질 묘사를 표현하고
nonMetalic의 경우에는 lightmap.b의 수치에 따른 Intensity강도를 부여하여 스펙큘러 강도를 다르게 설정한다.
MatCap
half Intensity = lightmap.b;
half3 normalVS = TransformWorldToViewNormal(input.normalWS, true);
// 뷰 노말에 대해서 uv값으로 사용
half2 matcapUV = 0.5 * normalVS.xy + 0.5;
half3 metalMap = SAMPLE_TEXTURE2D(_MetalMap, sampler_MetalMap, matcapUV);
half3 metallic = blinnPhong * Intensity * albedo * metalMap * _MetallicIntensityOffset;
TransformWorldToViewNormal은 유니티의SpaceTransforms.hlsl에 정의되어 있다.
내부적으로
world 노말값에 대해하여 world -> view projection을 한것과 같으며
float3 normalVS = mul((float3x3)UNITY_MATRIX_V, normalWS);
로 구현된다.
true에 대한건 정규화에 대한 옵션이고 일반적으로 true로 설정한다.
ViewNormal은 view space에 대해서 x,y축에 대하여 각각 -1~1의 범위를 갖지만 텍스쳐의 UV 값은 u,v에 대하여
0~1 사이의 값을 갖는다.
따라서 해당 값을 매핑하기 위해 0.5*viewnormal + 0.5로 처리한다.
금속 재질에서 하이라이트 반사가 표현되기 위해 전제조건이 있다.
1. Phong Lighting 영역 안에 들어와 있어야 한다.
2. Intensity에 해당하는 Lightmap.b에 수치에 영향을 받아야 한다
3. 금속성 재질은 기본적으로 자기 자신의 albedo 본연의 색상값을 specular의 색상값으로 사용한다.
이를 간단하게 조합한 부분이
half3 metallic = blinnPhong * Intensity* albedo * metalMap * _MetallicIntensityOffset;
가 된다
마지막 Offset은 밝기를 적절히 조절하기 위한 파라메터로 사용된다.
Specular
(해당 부분은 구현에 있어서 가장 까다로운 부분이였고 일단 임시방편으로 먼저 구현해 놓았음을 알린다.)
레퍼런스의 스펙큘러에서 사용 되는 값은 상기 서술했듯이 두 텍스쳐
Lightmap.r와 Lightmap.b을 이용한다.
Lghtmap.r에서는 0에서 1로 갈수록 정반사 성질이 커지는 재질이며 Lightmap.b는 스펙큘러의 세기를 뜻하는 텍스쳐임을 암시적으로 확인해 볼 수 있다.
일반적으로 metalic 과 같은 광택 재질에 대해서는 specular의 범위가 좁은대신 강도가 높고 ,
옷과 같은 난반사 재질에 대해서는 specular의 범위가 넓은 대신 강도가 낮다.
본 단계에서는 lightmap.r / lightmap.b가 재질 분리에 관여한다는 점을 확인했으나,
해당 채널들이 재질id인지 강도/거칠기 파라미터인지,
그리고 금속, 실크가 어떤 방식으로 서로 다른 응답곡선을 갖는지에 대한 근거를 충분히 확보하지 못했다.
따라서 추정 기반으로 모델을 변경하는 대신, 레퍼런스의 구현을 그대로 유지하여
스펙큘러라는 특징을 나타내는 정도에 대해서만 만족하는 것을 우선하였다.
이후 추가 분석(텍스처 채널 의미 확정, 샘플링 조건 확인, 인게임 조명 조건 변화 테스트)을 통해
재질 분리 모델을 개선하는 것을 지속 시도해 볼 필요가 있다.
half Specularity = lightMap.r;
half Intensity = lightMap.b;
half gate = smoothstep(_SpecularThreshold - _SpecWidth, _SpecularThreshold+ _SpecWidth, Intensity*Specularity + blinnPhong);
half outSpec = gate * blinnPhong;
half nonMetallic = outSpec * Intensity * _NonmetallicIntensityOffset;
gate은 Specular가 형성되는 영역에 대한 마스킹 값으로
BlinnPhong 모델과
Intensity에 해당하는(추정) 텍스쳐 Lightmap.b과
Specular의 정반사도를 나타내는(추정) Lightmap.r을 합하여
스펙큘러가 나타나 지는 영역을 threshold를 통해 조절하도록 구현하였다.
해당 값을 블린 퐁의 기본 반사 specular와 곱하여 마스킹 된 영역에 대해서만
블린퐁 효과가 나타도록 설계한 것이 outSpec에 대한 부분이다.
최종적으로 해당 값을 Specular Intensity에 대한 Lightmap.b에 대한 값과 Offset 값을 곱하여
적절히 스펙큘러 강도가 나타나도록 설정하였다.
마지막으로 Specular가 그림자 음영이 진 곳에 형성이 되어선 안되므로
이전 Diffuse항에서 암부에 대한 영역에 대해서는
Specular값이 반영되지 않도록 처리한다.
half Grayscale(half3 color )
{
return (color.r + color.g + color.b)/3;
}
half shadowvalue = smoothstep(0.96,0.99,Grayscale(shadowColor));
specular = lerp(0, specular, shadowvalue );
#if _IS_FACE
specular = 0;
#endif
grayscale함수는
r,g,b 색상의 value 차이를 고려할 필요 없이 단순 암부와 명부의 구분용으로 사용하는 것이 목적이다.
얼굴에 대해서는 스펙큘러가 적용되어선 안되므로 shader variant를 사용해 0으로 둔다.
각 파라메터들을 적절히 조합하여 나온 specular항의 모습은 다음과 같다.


스펙큘러 항 only / diffuse + specular


스펙큘러 적용 on / 스펙큘러 적용 off
'NPR Project Demo' 카테고리의 다른 글
| 06. 2 Pass Outline(Inverted Hull)_[NPR Project] (0) | 2026.02.27 |
|---|---|
| 05. Rim Light_[NPR Project] (0) | 2026.02.22 |
| 03. Normal Map_[NPR Project] (0) | 2026.02.11 |
| 02. Ramp Color Diffuse_[NPR Project] (0) | 2026.02.06 |
| 01. Face SDF_[NPR Project] (0) | 2026.02.04 |