---
title: "호수에 비친 태양은 내가 걸어가면 따라온다"
description: "네 항 중 유일하게 보는 방향이 들어가는 항입니다. Diffuse 와 Specular 를 가르는 것이 그 시선 의존성 하나입니다."
author: "유니얼"
pubDate: 2026-09-14
category: "game-dev/graphics"
tags: ["렌더링", "셰이더"]
canonical: https://blog.unialgames.com/posts/lighting-specular/
---
호수에 비친 태양을 보면서 물가를 걸어가 보세요. 그 반짝이는 자리가
나를 따라 움직입니다. 태양도 안 움직였고 호수도 그대로인데, **내가
움직여서** 반짝이는 자리가 바뀐 것입니다.

금속, 젖은 바닥, 광택 나는 플라스틱에 생기는 밝은 점이 전부 이렇습니다.
이걸 담당하는 항이 Specular 입니다.

## 하나만 기억하면 이것입니다

> **Diffuse 는 보는 방향을 안 쓰고, Specular 는 씁니다.**

두 항이 헷갈리는 이유는 둘 다 "빛을 받으면 밝아진다" 로 요약되기
때문인데, 갈리는 지점은 그 한 줄입니다.

거친 표면은 들어온 빛을 사방으로 고르게 흩습니다. 어디서 봐도 비슷하게
돌아오니 보는 방향이 필요 없습니다. 매끈한 표면은 거울처럼 **한쪽으로만**
튕겨 보냅니다. 그 튕겨 나가는 길 위에 내 눈이 있을 때만 밝게 보입니다.

<x-shade>
<script type="application/json">
{
  "label": "빛은 고정하고 시점만 움직이면",
  "terms": ["ambient", "diffuse", "specular"],
  "motion": "camera",
  "probe": [-0.16, 0.5],
  "note": "광원은 한 번도 안 움직였습니다. 움직이는 것은 보는 사람뿐입니다."
}
</script>
</x-shade>

빛을 고정해 두고 시점만 돌렸습니다. **Diffuse 로 칠해진 부분은 한 칸도
안 움직이고 하이라이트만 표면 위를 미끄러집니다.** 오른쪽 막대에서
Diffuse 폭이 그대로인 채로 Specular 칸만 커졌다 사라집니다.

## 어떻게 계산하나

들어온 빛이 표면에서 튕겨 나가는 방향을 구하고, 그게 내 시선과 얼마나
가까운지를 봅니다. 이것도 내적입니다.

```hlsl
float3 R = reflect(-L, N);
float  spec = pow(saturate(dot(R, V)), shininess);
```

`V` 는 표면에서 카메라 쪽을 가리키는 방향입니다. 반사 방향과 시선이
정확히 겹치면 1, 벌어질수록 0으로 갑니다.

요즘은 반사 벡터 대신 **하프 벡터**를 씁니다. 빛 방향과 시선 방향의
한가운데를 가리키는 벡터입니다.

```hlsl
float3 H = normalize(L + V);
float  spec = pow(saturate(dot(N, H)), shininess);
```

표면이 그 하프 벡터 쪽을 보고 있으면 빛이 정확히 눈으로 튕겨 온다는
뜻입니다. `reflect` 를 부르는 것보다 싸고, 스치는 각도에서 하이라이트가
뚝 끊기는 문제도 덜합니다. 앞쪽을 퐁, 뒤쪽을 블린퐁이라고 부릅니다.

## 거듭제곱이 광택을 만듭니다

`pow` 의 지수가 광택도입니다. 내적 결과는 0에서 1 사이라 거듭제곱할수록
빠르게 0으로 떨어집니다. 그래서 **지수가 클수록 하이라이트가 좁아집니다.**

<x-shade>
<script type="application/json">
{
  "label": "광택도를 바꿔 보기",
  "terms": ["ambient", "diffuse", "specular"],
  "motion": "camera",
  "shine": 32,
  "shineOptions": [4, 32, 256],
  "probe": [-0.16, 0.5],
  "note": "값만 바뀌었습니다. 지수가 커지면 같은 빛이 좁은 자리에 몰립니다."
}
</script>
</x-shade>

4쯤이면 넓고 흐릿하게 퍼져서 고무나 거친 플라스틱처럼 보입니다.
256쯤이면 바늘로 찌른 것처럼 좁아져서 금속이나 유리처럼 보입니다.

## 하이라이트는 재질 색을 안 따릅니다

Diffuse 는 재질 색을 곱하는데 Specular 는 보통 **광원 색만** 곱합니다.

파란 플라스틱 공에 흰 빛을 쏘면 몸통은 파랗고 하이라이트는 흽니다.
표면에서 바로 튕겨 나온 빛이라 재질 속으로 들어갔다 나온 게 아니기
때문입니다. 금속은 예외입니다. 금이나 구리의 하이라이트는 금색과
구릿빛을 띱니다.

## 왜 항을 나누나

여기까지 오면 나누는 이유가 보입니다. **재질마다 비중이 다르기**
때문입니다.

| 재질 | Diffuse | Specular |
| --- | --- | --- |
| 천, 종이, 벽지 | 크다 | 거의 없다 |
| 플라스틱 | 중간 | 중간 넓이로 조금 |
| 광택 금속 | 작다 | 강하고 아주 좁다 |
| 젖은 바닥 | 중간 | 강하고 좁다 |

계수를 재질마다 다르게 주면 이 질감들이 전부 나옵니다. 하나로 뭉뚱그리면
그 조절이 불가능합니다. **항을 나누는 것 자체가 재질을 표현하는 손잡이를
만드는 일입니다.**

## 물리 기반이 아닙니다

여기가 이 모델의 한계가 드러나는 자리입니다.

`pow(dot(N, H), shininess)` 에는 물리적 근거가 없습니다. "이렇게 하니까
그럴듯해 보이더라" 에서 나온 식입니다. 그래서 **에너지 보존이 안 지켜집니다.**

Diffuse 계수와 Specular 계수를 둘 다 크게 잡으면 표면이 받은 빛보다 많은
빛을 반사합니다. 위 애니메이션에서 더한 값이 1.0 금을 넘어가는 순간이
그겁니다. 실제로는 있을 수 없는 상태입니다.

당장 화면이 깨지지는 않습니다. 문제는 **재질이 조명 환경에 따라 다르게
보인다**는 데서 터집니다. 실내에서 잘 맞춰 놓은 재질을 야외에 가져다
놓으면 허옇게 뜹니다. 조명을 바꿀 때마다 재질을 다시 만져야 합니다.

## 그래서 PBR 로 갔습니다

요즘 엔진의 기본 셰이더는 계수 대신 **금속성과 거칠기**를 파라미터로
둡니다. 거칠기 하나가 하이라이트의 넓이와 세기를 같이 정하고, 반사가
세지면 확산이 그만큼 줄도록 식이 묶여 있습니다. 에너지 보존이 식 안에
들어가 있는 것입니다.

덕분에 **한 번 만든 재질이 어떤 조명 환경에서도 일관되게 보입니다.**
아티스트가 "이건 거칠기 0.4짜리 플라스틱" 이라고 정하면 실내든 야외든
그 재질입니다. Unity 의 Standard 셰이더와 URP Lit 이 이쪽입니다.

이름은 달라졌어도 하는 일은 같습니다. 확산으로 돌아오는 빛과 표면에서
바로 튕기는 빛을 갈라서 계산하고 더합니다. 시선이 뒤쪽에만 들어간다는
구조도 그대로입니다.

## 정리

- Specular 는 매끈한 표면에서 빛이 한쪽으로 튕겨 나가 생기는 밝은 점입니다.
- **네 항 중 유일하게 보는 방향이 들어갑니다.** 카메라가 움직이면 하이라이트가 표면 위를 미끄러집니다. Diffuse 와 갈리는 지점이 이 하나입니다.
- 반사 벡터와 시선의 내적, 또는 법선과 하프 벡터의 내적을 거듭제곱해서 구합니다. 지수가 클수록 좁고 날카로워집니다.
- 재질 색이 아니라 광원 색을 곱합니다. 표면에서 바로 튕긴 빛이라서 그렇습니다. 금속은 예외입니다.
- 항을 나누는 이유는 재질마다 비중이 달라서입니다. 계수를 따로 주면 천부터 금속까지 나옵니다.
- 이 식은 경험적이라 에너지 보존이 안 됩니다. 그래서 조명 환경이 바뀌면 재질을 다시 만져야 하고, 그 문제를 없앤 것이 PBR 입니다.