- Shadow map (depth shadows)
- 빠르게 계산 가능
- but, Aliasing 많음
- Shadow volume
- gs에서 광원으로부터 생기는 volume을 생성
- hard-edge shadows
1. Shadow Map
two-pass algorithm
- 1pass: shadow map 생성
- 카메라를 광원의 위치에 두고 씬을 렌더링
- 광원의 view/projection matrix
- view mat: 카메라를 광원 위치에 두고 얻음
- projection mat: 카메라를 광원 위치에 두고, 렌더링할 씬의 오브젝트들이 포함되도록 view frustum 정의해서 얻음
- FBO update 안함, depthmap만 만들면 됨 (depth buffer만 텍스처에 저장)
- 2pass: depth 비교
- shadow map과 fragment의 depth를 비교하기 위해 같은 좌표계로 변환
- vertex의 position을 광원을 중심으로한 좌표계로 변환하고 광원의 projection mat으로 투영 (pass1에서 구한 mat)
- shadow이면 ambient light만 사용해서 렌더링, 아니면 기존대로 렌더링
Conversion of fragment's position
- model coordinate에
S
matrix를 곱하여 depthmap과 동일한 좌표계로 만든다
- fragment의 3d 좌표계를 shadow map과 동일한 좌표계로 만들어주기 위해 광원의 view/projection matrix를 곱한다 (
PVM
matrix)
- 그러면 fragment의 3d 좌표가 homogeneous clip 좌표(정사각형 큐브)로 변환됨
[(-1, -1, -1), (1, 1, 1)]
- [-1, 1] 범위를 [0, 1] 범위로 바꿔주기 위해 translation과 rotation을 적용하는
B
matrix 곱함
- w값으로 x, y, z를 나눠주는 perspective division까지 하여 마무리
front-face culling
- 그림자가 아닌 부분에 그림자가 생기는 경우
- depth map의 값과 실제 depth를 비교할 때, 값이 비슷해서 약간의 오차가 생기는 경우
- 이를 방지하기 위해 front-face를 culling하고, back-face만 depth에 기록한다 -> 안정적
📃 Code
OpenGL Application
- depth buffer를 담을 FBO 세팅
GL-CLAMP-TO-BORDER
로 shadow map 범위 바깥의 오브젝트가 텍스처에 접근하려할 때의 값 (테두리 값) 설정
-> 범위 바깥의 오브젝트는 그림자가 아닐 것으로 판단하기위해 테두리값은 큰 값(1)으로 설정
GL-TEXTURE-COMPARE-MODE
는 GL-LESS
로 설정하여 shader에서 텍스처를 읽어올 때, 텍스처에 저장된 값이 비교하는 값보다 작으면 True를 반환하도록 한다
-> shadow map의 값이 fragment의 depth보다 작으면 True (그림자가 아닌 경우)
- depth는 자동으로 버퍼에 저장되므로 fs의 output 설정해줄 필요는 없다
vertex shader
- vertex position을 shadow map과 같은 좌표계로 변환하여 shadow map으로부터 값을 읽어올 수 있도록 함
fragment shader
- subroutine으로 pass1일 때는
recordDepth()
, pass2일 때는 shadeWithShadow()
가 호출
textureProj
함수로 vs에서 넘어온 homogeneous coord의 position과 shadow map texture를 비교하여 shadow일 때 0 반환
❗ issues
Aliasing
- 그림자의 엘리어싱 현상
- shadow map은 어쨋든 텍스처(이미지)이기 때문에 제한된 해상도 안에서 픽셀별로 depth 정보가 저장된다
- 그래서 오브젝트와 depth를 비교할 때, 오브젝트의 depth는 연속적인 값인데 shadow map의 depth는 한 픽셀동안 중앙값을 사용하므로 엘리어싱이 발생하기 쉽다
- 해상도를 높이거나, GL_LINEAR를 사용해서 보완
Shadow acne (일듯..?)
- 위에서 설명한 문제로 그림자가 아닌 부분에 그림자가 생기는 문제
- 오브젝트가 닫힌 물체이면 front를 culling하고 back의 depth만 기록
- 열린 물체라면 이 방법을 사용할 수 없으므로, 1pass에서 물체를 약간 뒤로 보낸다 생각하고 -> depth값에 일정 값을 더해준다
glPolygonOffset
- 두 방법을 합쳐서 사용하면 효과적
👀아래는 Anti-aliasing을 위한 Shadow 2가지
1-1. PCF (percentage-closer filtering)
- 간단하고 많이 사용되는 기술
- 그림자를 그릴 때, 한 픽셀 내에서 여러개의 샘플로 계산하는데, 그림자에 속하지 않는 부분과 속하는 부분의 비율을 계산해서 섞음
- 그림자의 경계를 부드럽게 필터링
📃 Code
OpenGL Application
- depth텍스처를 읽어올 때 주변 4개의 texel으로 평균값을 내는 GL-LINEAR 필터링 사용
fragment shader
shadeWithShadow()
에서 depth를 비교할 때, 주변 texel들과도 비교한 평균값으로 (0또는 1이 아닌) 0~1사이의 값이 나오도록 함
- 그림자 경계에서만 0과 1사이고, 나머지는 0또는 1의 값이다
- GL_LINEAR에서 4개, fs에서 4개 -> 총 16개의 픽셀로 평균내는 효과
❗ issues
- 필터링 영역을 더 넓혀주면 블러 효과가 더 커지면서 soft shadow 효과 줄 수 있다
- 하지만, 영역이 넓어지면 계산이 많아지고, 굳이 필요없는 부분(전부 그림자인/아닌 부분)에도 많은 계산이 들어간다
1-2. Random Sampling
- 규칙성이 생기지 않도록 random sample을 사용하여 필터링
- 모든 점에 대해 항상 random이긴 어려워서 random set 몇개를 만들어서 사용한다
- 또한, 영역 내에서 전부 그림자인지/아닌지를 판별해서 계산이 필요한 부분만 sampling한다
- sampling은 원의 영역에서 하는데, 가장 바깥(원 경계쪽)의 fragment가 전부 그림자라면 안에도 전부 그림자일 확률이 높다 생각하고 계산하지 않는다
(그림자가 아닌 경우도 마찬가지)
- 주안점
- random sample을 어떻게 만들 것인가
- 계산이 필요한 부분과 필요하지 않은 부분 파악
random sampling
- 미리 계산해둔 여러개의 random sample pattern은 texture에 저장
- offsets을 3d texture에 저장 (n x n x d)
- n x n: set의 개수
- d: 한 pattern에서 sample 개수
- offset은 2D 좌표
x, y
이고, texture에는 RGBA 정보를 저장할 수 있으므로 2개의 offset을 저장할 수 있다x1, y1, x2, y2
- 큰 해상도의 frame buffer에 비해 크기가 작은 sample을 담은 texture를 나열하여 적용한다
- screen 좌표를 texture의 크기
n
으로 나눈 나머지를 사용해서 texel을 가져온다
- 장점: 주변 fragment끼리는 다른 sample set을 사용하게 된다
📃 Code
OpenGL Application
- offset texture의 크기, 필터링 영역(radius)을 유니폼 변수로 설정
- 필터링 영역은 sample pattern과 상관 없다 (영역이 커졌다고 sample pattern이 바뀌지 않는다)
- 16개의 sample을 사용, sample들의 위치를 random하게 위치를 바꿔주는 jittering을 적용하고 unit circle상의 점이 되도록 변환해준다
- unit circle로 변환할 때, sample의 x좌표는 원에서의 각도, y좌표는 원점으로부터의 거리를 나타냄
- 텍스처에 저장할 때, 왼쪽 위의 sample부터(y값이 큰 것부터) 저장되도록해서 중심으로부터 먼 sample 데이터가 텍스처에 먼저 저장되도록 한다
fragment shader
- screen 좌표를 texture의 크기로 나눈 나머지를 사용해 sample pattern 중 하나 선택
- 가장 바깥쪽 4개의 sample 먼저 계산하여 완전 그림자인지 아닌지 판단
- 그림자인/아닌 점들이 섞여있는 경우, smaple개수÷2번 반복하여 안쪽 sample도 계산!
❗ issues
- random pattern set의 개수 제한이 있고, texture를 나열하여 set을 선택하므로 여전히 artifacts가 생길 수 있다
- AO때처럼 sample에 random rotation을 적용하면 좀 줄일 수 있다..
- 그림자가 블러되지 않았으면 하는 부분까지 전부 soft-shadow가 된다
- ex) 물체와 가까운 부분에는 좀 더 선명한 그림자
-> fs에서 거리에 따른 Radius의 값을 조절한다면 가능..
2. Shadow Volumes
- 그림자 경계에 aliasing 문제가 발생하지 않음
-> hard shadow (soft shadow 만들기 어려움)
- stencil buffer 사용
Shadow Volume 만들기
- 그림자를 생성하는 영역인 Shadow Volume을 정의하기
- 광원과 그림자를 만드는 물체의 테두리를 연결하는 무한한 쿼드(gs에서 생성)
- 그림자를 만드는 물체가 복잡한 형태인 경우에는 단순 연결이 어려우므로 광원 방향에서의 실루엣 엣지를 활용
- 광원을 카메라로 정의하고, 그림자를 만드는 물체의 실루엣 엣지를 찾는다
- 광원과 실루엣 엣지를 연결하여 만든 쿼드 집합이 Shadow Volume이다
- 쿼드의 두 꼭짓점은 실루엣 엣지의 양 끝점, 나머지 두 꼭짓점은 무한히 멀리 정의된 점
Shadow Volume에 속하는지 판별하기
- 그림자인지 아닌지 판별할 물체를 향하는 카메라로부터의 ray가 shadow voluem를 들어갔다 나오면 그림자가 아닌 것이다!
- ray는 판별할 오브젝트 표면과 만날때까지 시간마다 조금씩 늘려나감
- 카메라로부터 원하는 물체로 향하는 ray와, shadow volume의 쿼드가 만났는지를 판별
- ray가 쿼드의 front를 지나면 (쿼드 안으로 들어가면)
+1
- ray가 쿼드의 back을 지나면 (쿼드 바깥으로 나가면)
-1
- 최종 결과가
0
이면 그림자가 아닌 것이고, 1
이상이면(shadow volume안에 들어갔다 안나오면) 그림자이다
- 단순히 들어갔다 나왔다만 판별하면 여러개의 shadow volume이 겹칠 때를 판별할 수 없으므로 위와 같이 나타내는 것임
- 카운팅 하는 과정에서 stencil buffer를 사용
- 렌더링되는 각 픽셀마다 컬러가 아닌 값을 남길 수 있음 (더하거나 빼거나...)
3pass process
- Pass1
- 3D 씬 렌더링
- ambient 컬러 정보와 diffuse+specular 컬러 정보를 각 텍스처에 저장 (2개의 텍스처)
- 한 fragment에 대해 2개의 data(texture)를 제공
- 그림자인 부분: ambient texture
- 그림자 아닌 부분: (diffuse+specular) texture + ambient texture
- Pass2
- shadow volume을 구성하는 쿼드를 stencil buffer에 렌더링
- depth test를 안하므로 한 점에 여러개의 fragment가 존재할 수 있음
- front/back face에 따라 카운팅한 결과를 stencil buffer에 저장
- 이때, 어쨋든 카메라에서 보이는 애들만 그림자인지 아닌지 판별하면 되므로 Pass1에서의 depth 정보를 사용해서, 보이지 않는 애들까지 카운팅하지 않도록 한다
- Pass3
- screen-filling quad 렌더링
- stencil buffer를 읽어와서 해당 픽셀의 값에 따라 그림자인지 아닌지를 확인하여 컬러 결정
📃 Code
1pass OpenGL Application
- FBO 생성
- ambient color 정보는 텍스처에 따로 저장하지 않고 버퍼에만 저장
- 어쨋든 ambient color는 그림자이든 아니든 사용하므로, pass3에서 screen-filling quad에 전체 복사
- diffuse+specular color 정보는 텍스처에 저장
- 1pass에서 2개의 output이 나오므로 color attatchment 설정
1pass fragment shader
- 1pass에서 3D 씬 렌더링을 위해 shading
- 2개의 output (ambient, diff+spec)
2pass OpenGL Application
- Stencil buffer 세팅
- 모든 면을 그리는데, front이면 increase, back이면 decrease해서 데이터를 작성하도록 함
- 1pass의 결과를 담은 FBO를 default FBO로 복사
- FBO의 ambient buffer를 3pass에서 사용하기 위해
- 복사한 buffer의 값이 2pass에서 바뀌지 않도록 disable
2pass geometry shader
- 광원 기준 실루엣 엣지를 찾기
- 광원을 기준으로 front-face인지 back-face인지 판별
- 실루엣 엣지의 양 끝점을 사용해 쿼드 생성
- 양 끝점
a
, b
는 projection만 해서 그대로 emit
- 무한히 먼 나머지 두 점도 정의 (lightPos와의 차이로 벡터 계산 후 projection해서 무한히 먼 점의 좌표 얻음)
- 기존 모델의 버텍스는 emit 안함
3pass OpenGL Application
- stencil buffer를 다시 세팅
- 2pass에서 저장된 stencil buffer값이 0일 때만 fragment upadate하도록 stencil mask 정의
(그림자가 아닌 경우만 fs를 실행)
GL-BLEND
를 enable해서 buffer를 update할 때 원래 저장된 값(ambient)와 새로 저장할 값(diffspec)이 섞여서 저장되도록 함
- ambient color는 이미 default buffer에 그려져 있음
- 그림자가 아닐 때만 diffspec color를 섞기 위해
3pass fragment shader
- diffspce texture에서 값을 읽어와 그린다~
- 이 fs는 stencil buffer를 통과한 애들만 하므로 그림자가 아닌 경우만 실행되는 것임
- blend를 설정해놨기 때문에 ambient와 섞인다
- 만약 ambient도 texture로 저장했다면, 같이 더해서 내보냈을 것
- 그러면 또 stencil buffer값 확인해서 그림자인지 아닌지 판별해야되니까 비효율적일 수 있음
- 카메라가 shadow volume 바깥에 있으면 z-pass techinique (지금까지 한 방법)
- 카메라가 shadow voluem 안에 있으면 z-fail technique (infinity에서 카메라를 향하는 ray로(반대) depth test에 따라 trace)
❗ issues
- degenerate triangle나 열린 물체의 경우 실루엣 엣지 계산 과정에서 문제가 생길 수 있다
- degenerate triangle은 삼각형이 거의 수평이어서 화면에 면이 보이지 않는 경우, 실루엣 판별을 위한 면 계산이 안될 수 있음