3줄 요약

  1. pixiv에서 일하는 개발자 norio(Norio Shimizu)가 2026년 9월 6일 three-gtvbao를 공개했다. three.js의 WebGPU와 WebGL2 백엔드에서 돌아가는 스크린 스페이스 앰비언트 오클루전 라이브러리이고, 셰이더는 전부 TSL로 작성되어 있다. 라이선스는 MIT다.
  2. 계산의 뼈대는 픽셀마다 32비트 정수 하나를 두고 이미 가려진 방향을 비트로 적어 나가는 가시성 비트마스크(visibility bitmask)다. 여기에 Mirko Salm의 GT-VBAO 보정을 얹는다. 지평선 각도는 비트로 자르기 전에 누적분포함수를 통과시킨다. 그러면 각도가 균등한 32개 구획이 가중치까지 균등해지고, 켜진 비트를 세어 32로 나눈 값이 그대로 올바르게 가중된 가시성이 된다.
  3. 그 밖의 구성 요소는 대부분 성능을 확보하는 데 쓰인다. 선형 깊이 MIP 체인, 절반 해상도 렌더와 깊이를 고려한 업샘플, TRAA 또는 경계를 보존하는 디노이저, 품질 프리셋 다섯 종, TypeScript 선언, 트리 셰이킹 테스트가 함께 들어 있다.

무엇이 공개되었나

저장소는 2026년 9월 6일에 만들어졌고, 커밋이 둘뿐이다. 첫 커밋 메시지는 “Initial commit for open-source release"이고, 두 번째는 일본어로 “example을 GitHub Pages에 공개하고 README에 데모를 덧붙임"이다. npm 패키지 three-gtvbao 0.1.0도 같은 날 올라갔다.

항목내용
저자GitHub 사용자 norio. 프로필에 이름은 Norio Shimizu, 소속은 pixiv, 위치는 일본이라고 적혀 있다
공개일2026-09-06 (저장소 생성, npm 0.1.0 게시)
라이선스MIT
언어TypeScript와 JavaScript, 셰이더는 TSL
요구 사항three.js r184 이상, WebGPURenderer, PerspectiveCamera
문서README 영어판과 일본어판 두 벌

요구 사항이 꽤 까다롭다. three.js r184는 2026년 4월에 나온 판이다. 렌더러는 반드시 WebGPURenderer여야 한다. 이 렌더러가 WebGPU를 못 잡으면 알아서 WebGL2 백엔드로 내려가는데, 예전부터 쓰던 WebGLRenderer는 아예 지원하지 않는다고 명시되어 있다. WebGL2로 돌릴 때는 EXT_color_buffer_float 확장이 필요하고, 카메라는 원근 카메라만 받는다.

AO를 껐다 켜면

나는 저장소에 딸린 라이브 데모를 헤드리스 브라우저로 열어 세 가지 표시 상태를 같은 구도로 찍었다. 화면 아래 토글이 각각 어느 상태인지 알려 준다.

AO를 끈 상태. 아치 안쪽, 기둥 밑동, 판과 판 사이가 모두 같은 밝기로 평평하게 칠해져 있다.

AO 채널만 뽑아 본 화면. 물체의 색이 사라지고 가려짐의 정도만 회색조로 남는다. 매듭 표면에 남은 얼룩은 시간 누적이 덜 된 샘플링 노이즈다.

AO를 적용한 전체 렌더. 아치 안쪽이 깊어지고, 매듭이 좌대에 닿는 자리와 판 사이 틈에 접촉 그늘이 생긴다.

차이가 가장 잘 보이는 곳은 얇은 판이 줄지어 선 배열과 아치 안쪽이다. AO를 끄면 판 사이 틈이 바깥면과 같은 밝기로 칠해져서 물체가 바닥에서 떠 있는 것처럼 보이고, 켜면 틈이 가라앉으면서 판이 좌대에 붙는다.

캡처 조건은 밝혀 두어야겠다. GPU가 없는 환경에서 소프트웨어 래스터라이저로 WebGL2 백엔드를 강제했다. 그래서 화면 오른쪽 위 프레임 수치가 초당 1프레임으로 나오고, AO 채널 화면에는 시간 누적이 진행되지 않아 디더링 얼룩이 남아 있다. 이 캡처는 라이브러리의 성능이나 품질을 보여 주려는 자료가 아니다. 저자가 저장소에 넣어 둔 표지 화면에는 WebGPU 백엔드에서 초당 145프레임이 찍혀 있는데, 어떤 기기에서 잰 것인지는 적혀 있지 않다.

코드의 출처

README의 크레딧이 계보를 꽤 정직하게 밝혀 놓았다.

  • 뼈대는 three.js 예제에 들어 있는 SSGINode.js의 AO 경로와 DenoiseNode.js다. 둘 다 MIT다.
  • SSGINode는 다시 Olivier Therrien의 SSRT3를 옮긴 것이고, SSRT3는 논문 Screen Space Indirect Lighting with Visibility Bitmask(Therrien, Levesque, Gilet, 2023)의 구현이다.
  • GT-VBAO 보정은 Mirko Salm이 Shadertoy에 올린 것을 가져왔다.
  • 깊이 MIP 프리필터는 Intel의 XeGTAO에서 왔다.

그러니까 이 저장소는 없던 기법을 발명하지 않았다. 흩어져 있던 세 갈래를 three.js의 TSL 위에서 하나의 노드로 합쳤다. 이어서 three.js가 제공하는 AO 라이팅 훅에 결과를 흘려 넣는 배선을 짰다. 옮기는 과정에서 손을 댄 흔적은 적지 않다. 지평선 리맵은 원래 식을 지평선 코사인 값으로 다시 풀어 초월 함수를 덜어 냈고, 깊이 프리필터와 업샘플, 셰이더 변형 관리는 각각 별도 모듈로 나뉘어 있다.

AO가 걸리는 범위는 three.js의 규칙을 그대로 따른다. MeshStandardNodeMaterial처럼 AO 라이팅 훅을 쓰는 재질에만 적용되고, 조명을 받지 않는 재질은 영향을 받지 않는다.

비트 서른두 개로 그늘을 세는 법

앰비언트 오클루전은 표면의 한 점에서 하늘이 얼마나 보이는지를 재는 계산이다. 화면 공간에서 이걸 하려면 그 점 주위로 방향을 훑으면서 무언가에 막혔는지 확인해야 하는데, 가시성 비트마스크 계열은 그 결과를 32비트 정수 하나에 담는다. 픽셀마다 몇 갈래(slice)를 잡고, 각 갈래에서 반구를 32개 구획으로 쪼갠 다음, 막힌 구획에 해당하는 비트를 켠다. 훑기가 끝나면 꺼져 있는 비트는 보이는 방향을 뜻한다.

문제는 구획의 크기에서 생긴다. 32개 구획은 각도로는 똑같이 나뉘어 있지만, 반구 위에서 차지하는 몫은 똑같지 않다. 저자가 GTVBAOHorizonRemap.js에 남긴 주석이 이 사정을 그대로 설명한다.

32개 구획은 각도로는 균등하지만, 한 갈래 안에서 반구가 갖는 측도는 균등하지 않다. 입체각에는 모든 갈래가 공유하는 극인 시선 벡터 둘레에 |sin| 야코비안이 붙고, 코사인 가중 AO에는 법선과 이루는 각의 cos가 더 붙는다. 어느 쪽도 비트 하나에는 적용할 수 없으므로, 대신 지평선 각도를 그 측도의 누적분포에 통과시킨다.

비트는 0 아니면 1이므로 가중치를 실을 자리가 없다. GT-VBAO는 값을 나중에 보정하는 길을 버리고, 각도가 들어오는 좌표 쪽을 비틀어 둔다. 각도를 그대로 자르지 않고 누적분포를 한 번 통과시킨 뒤에 자르면, 폭이 같은 구획이 차지하는 몫도 같아진다. 그러고 나면 countOneBits(mask) / 32 한 줄로 계산이 끝난다.

구획에 가중치를 매기는 방식은 셋 중에 고를 수 있다.

sectorMeasure성격
cosine (기본값)GTAO식 가중. 카메라를 위아래로 기울일 때 생기는 편향이 줄어든다
solidAngle입체각 가중
angle원래 VBAO의 등각 구획을 그대로 쓴다

계산량도 손을 봤다. acosPI로 나눈 꼴은 이 코드를 쓰는 쪽에서 모두 필요로 한다. 그래서 다항식 계수를 미리 정규화해 두고, 지평선을 훑는 매 걸음에서 나눗셈 하나를 없앴다. 균등 가중 쪽 리맵은 초월 함수가 아예 필요 없고, 코사인 가중 쪽도 각도에 선형인 항 때문에 acos 하나만 남는다. 게다가 리맵은 한 샘플의 양쪽 경계를 vec2로 한 번에 처리한다.

프리셋과 손잡이

품질 프리셋이 다섯 개 들어 있고, 기본값은 Balanced다.

프리셋해상도 배율갈래 × 걸음시간 누적디노이즈
Low0.51 × 8사용미사용
Balanced0.52 × 6사용미사용
High13 × 12사용미사용
No Temporal Low0.52 × 6미사용사용
No Temporal High13 × 12미사용사용

한 픽셀이 최대로 쓰는 샘플 수는 갈래 수와 걸음 수를 곱한 뒤 다시 2를 곱해 정해진다. Balanced라면 24개, High라면 72개가 된다. 해상도 배율 0.5는 AO 버퍼의 가로와 세로를 절반으로 잡는다는 뜻이다. 픽셀 수는 4분의 1로 줄어든다. 여기서 생기는 경계 번짐은 선형 깊이 MIP과 깊이를 고려한 업샘플로 눌렀다고 저자가 적어 두었다.

한 가지 주의할 점이 있다. applyGtvbaoPreset이 하는 일은 설정값 대입까지다. 렌더 그래프를 어떻게 잇는지는 호출하는 쪽의 몫으로 남는다. 시간 누적을 쓰는 프리셋에는 TRAA 같은 해소 단계가 따로 필요하고, 디노이즈를 쓰는 프리셋은 조명 패스가 디노이저의 출력을 읽도록 직접 배선해야 한다.

속성은 세 부류로 나뉜다. 그냥 대입하는 값, .value로 넣는 유니폼, 그리고 바꾸면 셰이더를 다시 빌드하는 variant다. 갈래 수와 걸음 수, 구획 가중 방식, 깊이 MIP 사용 여부가 마지막 부류에 속한다. 여러 개를 한꺼번에 바꿀 때는 batchVariantChanges로 묶어서 재빌드를 한 번으로 줄인다. aoIntensity가 1이고 expFactor가 2일 때는 빠른 경로로 들어간다. 두 값 중 하나가 기본값에서 벗어나거나 기본값으로 돌아올 때만 재빌드가 일어난다.

파이프라인에 끼워 넣기

README는 빠른 시작을 네 단계로 나눠 놓았다.

  1. 불투명 프리패스로 깊이, 뷰 공간 법선, TRAA용 속도를 뽑는다.
  2. 완성된 프리패스를 읽어 AO를 계산한다.
  3. builtinAOContext로 조명 패스의 각 픽셀에 AO를 적용한다.
  4. TRAA로 시간 누적을 해소한다.

빠른 시작 예제에는 보조 함수가 둘 붙어 있다. renderPassAfter는 매 프레임 의존 패스를 먼저 그리게 해서 깊이 입력이 준비된 상태를 보장한다. createPassthroughAoContext는 프리패스를 조명 패스의 AO 컨텍스트로부터 떼어 놓아서, 프리패스 자체에 AO 훅이 적용되는 일을 막는다. 둘 다 빼서는 안 된다고 README가 분명히 밝혀 두었다.

시간 누적 없이 쓰려면 gtvbaoDenoise를 붙인다. AO 해상도에서 실행되는 16탭 경계 보존 디노이저이고, 반경과 밝기, 깊이, 법선 세 가지 임계값을 조절할 수 있다.

npm 패키지는 sideEffects: false로 표시되어 있고, 테스트에 트리 셰이킹 검사가 따로 들어 있다. README에는 런타임 옵션을 꺼 두는 것만으로는 번들에서 코드가 빠지지 않으니 필요한 이름만 가져다 쓰라는 안내가 따로 붙어 있다.

저자가 밝힌 한계

README 마지막 절에는 한계 세 가지가 적혀 있다.

  • 화면 공간의 깊이만 쓰기 때문에, 가려져 있거나 화면 밖에 있는 형상은 그늘에 기여하지 못한다.
  • 두께는 어디까지나 근사다. 각 샘플의 시선 광선을 따라 적용하는 원근 보정 오프셋은 구현되어 있지 않다.
  • 기본값인 코사인 가중은 같은 샘플 수에서 어두운 영역이 더 지저분해질 수 있다. 기울임 편향보다 노이즈가 더 신경 쓰이면 angle을 써 보라고 권했다.

가장 흥미로운 지점

이 우회로가 사 주는 것은 마지막 한 명령이다. 구획마다 다른 가중치를 곱해야 한다면 32개 비트를 하나씩 풀어 보아야 하고, 갈래마다 그 일을 되풀이해야 한다. 누적분포를 각도 쪽으로 미리 옮겨 둔 덕에 마지막 단계가 비트를 세는 명령 하나로 남는다. 절반 해상도와 깊이 MIP으로 아낀 몫을 여기서 도로 내놓지 않아도 된다는 뜻으로 이해했다.

그러면서도 이 정교함이 화면 공간이라는 울타리를 넘어서지는 못한다. 각도를 아무리 정확하게 가중해도 카메라에 잡히지 않은 형상은 애초에 마스크에 들어오지 못한다. 저자가 그 한계를 자기 손으로 README에 적어 둔 대목이 오히려 이 저장소를 믿게 만들었다.

커밋은 둘뿐이다. 그런데 TypeScript 선언, 여섯 벌의 단위 테스트, 브라우저 GPU 회귀 검사, 트리 셰이킹 검사, 영일 두 벌의 README, GitHub Pages 자동 배포는 이미 갖춰져 있다. 처음부터 공개를 염두에 두고 다듬어 온 사내 코드를 한 번에 내놓았다고 읽는 편이 자연스럽다. 공개된 날짜만 보고 갓 시작한 프로젝트로 오해하기 쉬운 종류다.

출처

Norio Shimizu(norio), three-gtvbao, 2026-09-06 공개, MIT. 원문: https://github.com/norio/three-gtvbao 라이브 데모: https://norio.github.io/three-gtvbao/