💠 Screen Space Fluid Rendering
기존의 유체 렌더링 방식 중 하나인 Marching Cubes와 같은 볼륨 기반 표면 재구성 기법은, 복잡한 연산량으로 인해 성능적인 한계를 보인다.
이를 해결하기 위해 본 프로젝트에서는 GLSL 쉐이더를 활용한 Screen Space Fluid Rendering 기법을 채택한다. 이 방식은 GPU의 병렬 처리 능력을 활용하여 연산을 하드웨어 가속하고, 화면 공간 상에서 직접 유체의 깊이와 두께(Thickness)를 재구성함으로써 렌더링 성능을 비약적으로 향상시키는 동시에 시각적 품질 또한 유지 또는 개선할 수 있습니다.
과정은 다음과 같다:
- 1. 입자 정보 수집
- 2. Depth 이미지 버퍼 생성
- 3. Depth smoothing (optional blur)
- 4. Normal 계산
- 5. Lighting 계산
- 6. Thickness 이미지 버퍼 생성
- 7. Surface Shader로 시각적 처리
🧲 입자 정보 수집
Shader를 위한 입자 정보는 기존의 GPU에서 렌더링하기 위해 CPU 메모리로 복사하던 것을 OpenGL을 통해 VBO 바인딩을 수행하여 GLSL 쉐이더에서 접근 가능하도록 했다.
📸 Depth 버퍼 생성
Depth 버퍼 생성을 위해 다음 과정을 거친다:
- 1. Sphere Rendering
- 2. Depth Rendering
🟢 Sphere Rendering
먼저 VBO에 저장된 각 입자의 위치 정보를 활용해 구(Sphere) 형태로 렌더링하는 과정이 필요하다.
이때 vertex shader에서 카메라로부터 각 입자까지의 거리를 계산하고, 설정한 구의 반지름에 스케일링을 수행한다:
다음으로, fragment shader에서 정점의 텍스처 좌표를 $(0,1) \rightarrow (-1,1)$ 범위로 변경하고, 해당 좌표의 magnitude를 계산하여 설정한 point Radius보다 바깥 쪽에 있는 경우 무시한다.
또한, 구 방정식을 활용해 법선 벡터의 Z 성분을 계산하여 lighting을 설정할 수 있다.
💾 Depth 이미지 버퍼 생성
먼저, depth 계산을 위해 vertex shader에서 ModelViewProjection 행렬을 활용해 각 정점을 clipping space 좌표로 이동시킨다:
이를 통해 fragment shader에서 depth를 clipping space에서의 z 좌표를 원근 투영에서 발생하는 w 좌표로 나누어 계산할 수 있다:
이렇게 계산한 depth 값을 다음 normal 계산에 사용하기 위해 FBO(Fame Buffer Object)를 활용해 depth 값을 텍스처에 저장 및 로드하는 과정을 거친다.
🧹 Depth Smoothing
depth smoothing 과정은 크게 두 가지 방식으로 진행할 수 있다.
- Gaussian Filtering
- Bilateral Filtering
이때, gaussian filtering의 경우 입자의 edge 정보가 smoothing되어 사라진다는 문제가 있기에, bilateral filter를 채택했다.
- 모든 픽셀을 순회하며 공간 도메인과 값 도메인에 대해 필터링을 진행한다.
- Spatial Domain (공간 도메인):
- 픽셀 간 거리 r에 따라 가까운 이웃에 더 높은 가중치를 준다.
- Range Domain (값 도메인):
- 현재 픽셀의 깊이 값과 이웃 픽셀 s의 깊이 값을 활용
- 깊이 값의 차이가 너무 크면(ex 입자의 경계) 가중치를 줄인다.
- 물체의 경계가 smoothing 되지 않도록 보호
🧭 Normal 계산
normal을 계산하기 위해 다음과 같은 과정을 거친다:
- uv 좌표에서 카메라 좌표계로의 변환
- 주변 픽셀의 값과 비교하여 normal 계산
이를 코드로 표현하면 다음과 같다:
- 이는 화면의 픽셀 위치와 깊이 값을 입력 받아 해당 픽셀이 3D 공간 어디에 위치하는지 계산한다.
- 주요 로직은 투영 행렬의 역행렬을 곱해서 clip space -> view space로 복원하는 것이다.
- 이때, perspective를 고려하여 w 좌표를 나눠준다.
- 좌,우,상,하(=zl, zr, zt, zb) 방향으로 한 픽셀 떨어진 곳의 3D 위치를 각각 계산한다.
- 이후, x축 y축을 표현하는 벡터를 설정하기 위해 depth 값을 고려하여 더 가까운 방향(depth 값이 작은) 값으로 설정한다.
- 최종적으로, x축과 y축을 의미하는 벡터를 외적하여 normal을 계산한다.
💡 Lighting 계산
앞서 계산한 normal를 활용해 lighting을 적용할 수 있다. lighting은 크게 세 가지로 나눌 수 있다:
- Diffuse Lighting
- Specular Lighting
- Fresnel
☀️ Diffuse Lighting
난반사라고도 불리며, 빛이 표면에 부딪힌 뒤 여러 방향으로 퍼지는 빛을 표현한다. 특히, 거친 표면에서 주로 발생한다.
✨ Specular Lighting
정반사라고도 불리며, 빛이 표면에 부딪힌 뒤 거울처럼 특정 방향으로 튕겨 나가는 빛을 의미한다. 주로 매끄러운 표면에서 발생한다.
🌈 Fresnel
Fresnel (프레넬) 효과는 빛이 표면에 닿을 때 반사와 투과(굴절)의 비율이 시선 각도에 따라 달라지는 현상이다.
이는 특히 유체, 유리, 물, 플라스틱처럼 투명하거나 반투명한 물체를 렌더링할 때 굉장히 중요한 시각 효과이다.
$$
\Huge
R(\theta) = R_0 + (1 - R_0)(1 - \cos\theta)^5
$$
🧪 Thickness 이미지 버퍼 생성
thickness 값을 계산하기 위해 다음과 같이 fragment shader를 작성한다:
- gl_PointCoord: 각 입자가 그려질 때, 그 입자를 덮는 각 픽셀에 대해 해당 픽셀이 점 내부 어디에 위치하는지 $(0, 1)$ 범위로 반환
- $(0,1) \rightarrow (-1,1)$ 범위로 변경
- $x^2 + y^2 = r^2$ 을 계산 -> 중심에 가까울수록 0, 멀수록 1 값을 가짐
- $\sqrt{1-r^2}$ 을 통해 얇은 곳은 0, 두꺼운 곳은 1 값을 가짐
이를 활용해 두께에 따른 빛 투과량을 계산하여 다음과 같이 렌더링 한다:
🖼️ 결과
참고 자료:
https://developer.download.nvidia.com/presentations/2010/gdc/Direct3D_Effects.pdf
깃허브:
https://github.com/qkrdmstn/flip3d-turbulence-cuda.git
GitHub - qkrdmstn/flip3d-turbulence-cuda
Contribute to qkrdmstn/flip3d-turbulence-cuda development by creating an account on GitHub.
github.com