Skip to content

20260609


어제 SSR 관련 글을 쓰다가,

문득 든 생각을 글로 정리해본다.

이런 아이디어는

하필 바쁠 때 딴짓하면서 잘 나온다.


Hybrid SSR

Note

이 기술이 이미 실제로 존재하는지는 모른다.
분명 1bounce가 아닌 SSR과
단점을 보완한 SSR 기술이 몇 가지 있었던 것으로 기억하지만,
지금부터 내가 설명할 기술과 유사한지는 조사해보지 않았다.
아래 설명은 모두 외부 도움 없이 생각한 것이다.
참고로, 6월 9일 기준 아직 구현해보지 않았기 때문에
성능이 괜찮을지는 확실치 않다.
하지만, 이론상으로는 괜찮을 듯 하다.
구현을 일단은 해 보고,
이 조건들에 논리적 오류가 없다는 보장이 없기 때문에
언제든 때려치울 수 있다.


목차

  • 이 기술을 고안하게 된 과정
  • 하이브리드 SSR 과정
  • 조건
  • 이 기술의 장점
  • 이 기술의 한계
  • 여담

0609_0.png

새벽에 잠을 못자고 끄적인 거라서

두서가 없는 점 양해바란다.


이 기술을 고안하게 된 과정

기존의 SSR이 게임에서 사용될 때는 -

대개 '바닥' 반사 표면을 구현하기 위해서 사용된다.

왜냐면, 중력에 의해서

바닥을 딛고 서 있는 물체는

'바닥과 닫는 면' 을 반사할 필요가 없기 때문이다.

SSR을 '벽' 반사에 안 쓰는 이유는,

SSR의 Ray가 대충 이렇게 동작하기 때문이다.

  1. 현재 픽셀에 대한, ViewFragPos 를 정규화시켜, 입사각으로 둔다.
  2. GBuffer에서 해당 픽셀의, 뷰 공간의 Normal 을 법선으로 둔다.
  3. reflect(I, N) 을 해서 나온 (단위) 벡터가 Ray의 방향이다.
  4. Ray의 출발점은 ViewFragPos 자체이다.

위 상황을 생각해 보면,

위 종이 사진에서도 알 수 있다시피

'그려지지 않은 옆면' 에 대해서는

레이마칭이 실패하고 그냥 아무것도 안 나온다.

그래서 보통 게임에서는

전역 반사 - 특히 벽에 대한 - 를 구현하기 위해서

레이 트레이싱이나 GI 기법을 도입한다.

근데 나는 문득 이런 생각이 들었다.

실패한 이유는, 물론 아무것도 없는 공간으로 광선을 전진시키다가

실패한 것일수도 있지만,

충분히 물체에 닿을 수 있는데도

Depth정보와 Albedo 정보가 없었기 때문이지 않나?

그러면 그냥

직관적으로 '뒷면' 에 대한 뎁스와 알베도를 저장해두면 되는 것 아닌가?


이런 생각을 한 과정을 차근차근 설명해 보겠다.

아래 사진을 보자.

카메라가 중앙 하단에 존재하고,

그 앞에 정육면체 1개와

Plane 메시 2개가 있는 씬을

위에서 내려다 본 것이다.

빨간색이 기존에 사용하던 전통적인 뎁스값,

초록색이 내가 고안한 뒷면 뎁스이다.

0609_1.png

기존의 SSR이 레이를 쏘았을 때,

성공하는 경우는

주황색 Ray의 경우이다.

Ray를 천천히 전진시키면서,

Projected_RayPoint.z > Current_Pixel_Depth

인 경우만?

성공하고 해당 RayPoint에 해당하는

알베도를 gbuffer에서 가져온다.


실제 코드는 밑의 ComputeDepthDelta 를 해서 나온 델타 값이

'0보다 크고 BIAS 보다 작으면'

충돌이 일어난 것으로 판정한다.

float ComputeDepthDelta(vec3 pos, vec2 uv) {
    float sceneDepth = abs(
        ReconstructPosition(uv, texture(uGDepth, uv).x).z
    );
    return abs(pos.z) - sceneDepth;
}

이제 실패하는 경우를 생각하자.

실패하는 경우는,

파란색 Ray의 경우이다.

아까와 똑같이 반사 벡터를 방향으로 잡고

전진하다가,

레이마칭 iteration 이 끝나거나?

(rayPoint 를 2D 스크린 기준으로 보았을 때) 화면 영역 밖으로 나가면?

종료한다.

파란색 광선이 그 경우이다.

분명 씬 전체를 아는 사람 입장에서는,

'큐브의 뒷면이 있잖아! 당연히 그곳에 충돌해야지' 라는 생각을 하겠지만,

광선 입장에서는

자신을 멈출 뎁스 경계선이 없기 때문에

실패로 돌아간다.


그래서 생각한게 뒷면 뎁스이다.

이 뒷면 뎁스는 다음과 같이 기록된다.

  1. 기존 뎁스를 모두 채워, DepthTex 를 만들어 둔다. (GL_LESS 사용한다)

  2. 렌더 타겟으로 BackwardDepthTex 와 BackwardAlbedo 를 지정하고,
    전체 씬 뎁스 + 알베도를 그리되, 이때 페이스 컬링, glCullFace 를 "GL_FRONT" 로 두어서?
    메시의 뒷면만 그려지게 한다.
    이때, Backward Depth 를 기록하는 조건은
    "기존의 Depth 보다 값이 클 때(=카메라보다 멀리 있을 때)" 이다.
    (이때도 마찬가지로 depthFunc로 GL_LESS 사용한다)

(2) 에서 조건 적용하는 방법은

(2) 패스에서 uniform sampler2D uDepthTex; 를 참조해서

texture(uDepthTex, UV) > gl_FragDepth 인 경우 discard; 해주면 된다.


참고로, 위 사진에서

뒷면 뎁스, 즉 초록색 부분이

뒤쪽 2개의 plane에 대해서는 기록되지 않은 이유가,

plane이 반드시 한 방향 (만 렌더링) -

특히 카메라 방향을 바라보고 있다고 했을 때,

glCullFace(GL_FRONT) 를 해 버리면

그냥 아무것도 안 그려지고 -

따라서 뎁스 버퍼가 갱신되지도 않는다.

(그 때문이 아니더라도,

이전에 기록된 뎁스와 똑같을 것이기 때문에

조건에 의해서 dwrite가 무시된다)


하이브리드 SSR 과정

우선 셋업 과정에서,

기존 SSR에 쓰이는 gbuffer 가 필요하다.

fbo + albedoTex + depthTex + reflectionMaskTex(또는 opacity)

여기다가 뒷면 정보를 추가로 기록한다.

fbo2 + backwardAlbedoTex + backwardDepthTex


렌더 루프에선,

반드시

기존 gbuffer -> 뒷면 gbuffer 순으로

씬 전체를 기록해준다.

dFunc 는 두 개 다 Less로 해준다.

그 후,

기존의 SSR 과정과 똑같이

메시를 SSR적용해서 그리면 된다.

그 셰이더에 fallback으로

backwardDepth + backwardAlbedo 타겟으로

광선 한번 더 추적하는걸 넣기만 하면 된다.


이때, 매우 중요한 건,

기존 SSR 에서

"Projected_RayPoint.z > Current_Pixel Depth 이면 충돌이 일어났다"

라고 판단했다면,

fallback 을 할 때는

"projRayPoint.z < currentPixelDepth 이면 충돌"

이렇게

조건을 뒤집어줘야 한다.

왜 이런지는 위에 그림을 보면 직관적으로 나온다.

기존이 앞 뎁스를 뚫고 들어가야 한다면

fallback에서는 뒷 뎁스를

뒤에서 뚫어야 한다.

조건

  1. 뒷면 뎁스의 값은 그 프레임에 기록된 기존 뎁스값보다 반드시 크다(카메라 기준 더 멀리에 있다).
  2. fallback 수행 시에, 충돌 조건은 기존과 반대이다.
  3. SSR을 적용하는 모든 메시는 한 방향으로만 그려진다. (twoSided 아님. 방향성을 가짐.)
  4. 메시 안에 메시가 있는 상황은 존재하지 않는다.

이 기술의 장점

가장 큰 장점은 레이트레이싱 없이도

평면에 대한 SSR을 (이론상) 수행할 수 있다는 점이다.

또 플레이너 / RT보다는

비용이 비교적 저렴하다.

반사(reflection) 말고도 굴절(refraction) 로도 확장할 수 있을 것 같다.

이 기술의 한계

우선 내가 발견한 가장 치명적인 결함은,

"메시 안에 또다른 메시가 있을 경우"

Backward Depth 를 기록할 때

카메라 기준으로

안에 있는 메시의 뎁스가 기록되기 때문에

나중에 SSR의 광선은

실질적으로

'안에 있는 메시' 를 보지 못하는 데도

그 뎁스를 참조할 수밖에 없다.

이 문제는,

대충 둘러대자면

"애초에 오버드로우 나게 모델링하면 안된다"

라던가,

"메시 안에 메시가 있는 건 이상적인 상황이 아니다"

라는 걸로 무마할 수 있다.

애초에 앞에다가 가려질 건데,

안에다가 메시를 넣어두는건

정상적인 상황은 아니다.


결함은 아니지만

예상되는 단점을 말해 보자면,

우선 GBuffer를 1개 만든 다음에

Albedo + Depth 조합의 미니 gbuffer를 채우게 되는데,

이게 메모리가 장난이 아닐 수 있다.

또한, 기존의 SSR이 fallback 되어서

"레이 마칭을 처음부터 다시 한다" 는 점 때문에

최악의 경우에는 레이마칭 비용이

2배가 된다.

근데 이거는 파라메터로 타협 보면 될 것 같다.

SS로 (거의) 완벽한 반사가 만들어지는데,

저정도 성능은 감수하는게 맞지 않나?


또한, 이 설명 자체가

2-bounce는 아니다.

그냥 1-bounce를 최대 2번하는 거다.


마지막으로 가장 큰 단점은

SSR의 '화면 밖 정보는 모른다' 는 특징 자체를 개선하진 않았기에,

물체 A 뒤에 있고, 물체 C 앞에 있는 물체 B 는 반사할 수 있지만

화면 밖의 지오메트리에 대해서는 -

특히 아예 절두체에 포함되지 않는 물체는

반사가 불가능하다.

여담

이거 아이디어 나온 이유가

SSR 글 쓴것도 있지만

글 쓰기 전에

Depth Peeling 에 대한 글을 읽어보고 있었다.

한 씬에 대한 뎁스를

"가장 앞에 있는 거 1개" 가 아니라

최대 N개의 뎁스를 저장할 수 있도록 해서

나중에 반투명 렌더링 (OIT) 할 때 쓰이는

기법이다.

뎁스를 여러개 저장한다는 점이 영감을 주었다.