[GPU프로그래밍] 12. Shadows

jungizz_·2024년 6월 10일
0

GPU Programming

목록 보기
12/15
post-thumbnail
  1. Shadow map (depth shadows)
    • 빠르게 계산 가능
    • but, Aliasing 많음
  2. 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과 동일한 좌표계로 만든다
  1. fragment의 3d 좌표계를 shadow map과 동일한 좌표계로 만들어주기 위해 광원의 view/projection matrix를 곱한다 (PVM matrix)
    • 그러면 fragment의 3d 좌표가 homogeneous clip 좌표(정사각형 큐브)로 변환됨 [(-1, -1, -1), (1, 1, 1)]
  2. [-1, 1] 범위를 [0, 1] 범위로 바꿔주기 위해 translation과 rotation을 적용하는 B matrix 곱함
  3. 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-MODEGL-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)

  • 간단하고 많이 사용되는 기술
  • 그림자를 그릴 때, 한 픽셀 내에서 여러개의 샘플로 계산하는데, 그림자에 속하지 않는 부분과 속하는 부분의 비율을 계산해서 섞음
    • 비율: fragment가 받는 그림자의 양
  • 그림자의 경계를 부드럽게 필터링

📃 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가 전부 그림자라면 안에도 전부 그림자일 확률이 높다 생각하고 계산하지 않는다
      (그림자가 아닌 경우도 마찬가지)
  • 주안점
    1. random sample을 어떻게 만들 것인가
    2. 계산이 필요한 부분과 필요하지 않은 부분 파악

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에서 생성)
  • 그림자를 만드는 물체가 복잡한 형태인 경우에는 단순 연결이 어려우므로 광원 방향에서의 실루엣 엣지를 활용
  1. 광원을 카메라로 정의하고, 그림자를 만드는 물체의 실루엣 엣지를 찾는다
  2. 광원과 실루엣 엣지를 연결하여 만든 쿼드 집합이 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은 삼각형이 거의 수평이어서 화면에 면이 보이지 않는 경우, 실루엣 판별을 위한 면 계산이 안될 수 있음
profile
( •̀ .̫ •́ )✧

0개의 댓글