A volumetric cloud with a volumetric light, plus an object (volumetric too) inside with a higher density. The denser object must cast (volumetric) shadows on the cloud (i.e you must manage the light absorption inside the participating media). Example for the cloud, without the object.
과제의 이부분을 구현하였다.
레이 마칭(Ray Marching)을 활용해 구름을 렌더링 해야한다.
구름의 밀도 계산, 조명(diffuse), 빛 산란(scattering)을 결합하여 볼륨 렌더링 기반의 구름을 표현했다.
레이 마칭을 활용한 구름 구현
구름을 렌더링하기 위해, 화면의 각 픽셀에서 레이를 발사해 구름 내부의 밀도와 빛의 상호작용을 계산했다.
기본 밀도 표현
구름의 기본 구조는 밀도 계산 함수(scene)로 구름의 밀도를 구하고, 이를 기반으로 색상을 계산했다.
코드: 밀도 계산 함수
float scene(vec3 p) {
float distance = sdSphere(p, 1.0); // 구 형태의 기본 거리 함수
float f = fbm(p); // 구름의 세부 구조를 표현할 Fractal Brownian Motion
return (-distance + f); // 구의 내부 밀도를 구한 결과 반환
}
sdSphere
는 구의 표면과의 거리를 계산하는 함수로, 구름의 기본 형태를 정의한다.fbm
은 노이즈를 이용해 구름의 내부 세부 구조를 생성한다.
밀도만 표현된 구름
- 구의 형태와 구름의 기본 밀도를 확인할 수 있다.
- 아직 조명이나 산란이 적용되지 않은 상태다.
Diffuse 계산
float diffuse = clamp(
(scene(p - uCenter) - scene((p - uCenter) + 0.3 * sunDirection)) / 0.3,
0.0, 1.0
);
이 식은 구름 표면에서 조명(diffuse) 효과를 계산한다.
이론적으로는 표면의 법선 벡터와 광원 방향 벡터의 내적을 통해 조명을 계산하는 방식과 유사하다. 하지만 구름은 명확한 표면이 없는 볼륨 오브젝트이므로, 밀도의 변화량을 활용한 간접적인 조명 계산 방식을 사용한다.
- 밀도의 변화량 계산
scene(p - uCenter)
는 현재 위치의 밀도를 반환하고,scene((p - uCenter) + 0.3 * sunDirection)
는 광원의 방향으로 약간 이동한 위치의 밀도를 반환한다.- 이 두 값의 차이를 통해 광원 방향에서 밀도의 변화량을 구한다.
- Diffuse 근사값
- 변화량을
0.3
으로 나누어 근사적으로 표면의 법선을 추정한다. - 광원이 있는 방향에서 밀도가 급격히 감소하면 해당 부분은 더 밝아지고, 밀도가 일정하면 어두워진다.
- 변화량을
- clamp
- 결과 값을
0.0
에서1.0
사이로 제한해 조명의 강도를 안정적으로 유지한다.
- 결과 값을
이 방식은 밀도의 변화량이 큰 부분을 강조해 광원이 있는 방향에서 구름이 더 밝아지도록 한다. 이를 통해 구름에 입체감을 부여한다.
조명이 적용된 구름
Scattering 계산
float phase = 0.75 * (1.0 + pow(dot(normalize(rayDirection), sunDirection), 2.0));
phase *= 0.15; // 강도 조절
vec3 scatterColor = lin * phase;
이 식은 Rayleigh 산란 이론을 기반으로 빛이 구름 내부에서 산란되는 효과를 계산한다.
Rayleigh 산란 이론
Rayleigh 산란은 빛이 작은 입자에 의해 산란될 때 발생하는 현상을 설명하는 이론이다. 이 산란의 강도는 빛의 입사 방향과 관측 방향 사이 각도에 따라 달라진다. 특히,
- 앞쪽 산란(forward scattering): 입사 방향과 관측 방향이 비슷할수록 강해진다.
- 뒤쪽 산란(backward scattering): 입사 방향과 관측 방향이 반대일 때 약해진다.
Phase Function (단계 함수)
dot(normalize(rayDirection), sunDirection)
은 관측 방향(rayDirection
)과 광원 방향(sunDirection
)의 코사인 값을 반환한다.- 이 값을 제곱하여 앞쪽 산란이 더 강하고 뒤쪽 산란은 약한 형태의 산란 분포를 생성한다.
0.75
는 Rayleigh 산란의 강도를 조절하는 계수로, 이를 통해 전체 산란 강도를 조절한다.
float phase = 0.75 * (1.0 + pow(dot(normalize(rayDirection), sunDirection), 2.0));
산란 색상 계산
- 산란의 강도(
phase
)를 구름의 기본 조명 색상(lin
)에 곱해 최종 산란 색상을 계산한다. lin
은 조명에 의해 생성된 기본 색상을 나타내며, 산란 효과와 결합해 구름이 빛에 반응하는 모습을 표현한다.
vec3 scatterColor = lin * phase;
3. 최종 색상 조합
vec4 color = vec4(mix(vec3(1.0,1.0,1.0), scatterColor, density), density);
mix
함수는 구름의 기본 색상(흰색)과 산란된 색상을 밀도(density
)에 따라 혼합한다.- 밀도가 높을수록 산란된 색상이 강조된다.
- 밀도가 낮을수록 기본 색상이 유지된다.
density
는 알파값으로 설정되어, 구름의 투명도를 조절한다.
산란이 적용된 구름
전체 레이 마칭 함수
아래는 구름의 모든 효과를 적용한 레이 마칭 함수이다. 각 단계에서 밀도, 조명, 산란 효과를 계산해 구름의 최종 색상을 결정했다.
vec4 raymarch(vec3 rayOrigin, vec3 rayDirection) {
float depth = 0.0;
vec3 p = rayOrigin + depth * rayDirection;
vec3 sunDirection = normalize(SUN_POSITION - uCenter);
vec4 res = vec4(0.0);
for (int i = 0; i < MAX_STEPS; i++) {
float density = scene(p - uCenter);
if (density > 0.0) {
float diffuse = clamp((scene(p - uCenter) - scene((p - uCenter) + 0.3 * sunDirection)) / 0.3, 0.0, 1.0 );
vec3 lin = vec3(0.60, 0.65, 0.75) * 1.1 + 0.8 * vec3(1.0,0.6,0.3) * diffuse;
// Rayleigh scattering
float phase = 0.75 * (1.0 + pow(dot(normalize(rayDirection), sunDirection), 2.0));
phase *= 0.15;
vec3 scatterColor = lin * phase;
vec4 color = vec4(mix(vec3(1.0,1.0,1.0), scatterColor, density), density);
color.rgb *= lin;
color.rgb *= color.a;
res += color * (1.0 - res.a); // 누적된 색상과 투명도
}
depth += MARCH_SIZE;
p = rayOrigin + depth * rayDirection;
}
return res;
}
최적화
구름을 색구슬을 구현한것과 같이 매쉬로 구름이 표현될 지역을 미리 할당해주고 구름을 그리려고 했었는데,
구름의 끝부분 처리가 매끄럽지 않아 (검은색으로 표현되었었다.) 지금까지 그린걸 프레임버퍼에 저장 후 거기에 덮어 그리는 방식으로 그렸다.
프레임버퍼에 덮어그리는 방식은 결국 구름을 하나 그리기위해 해상도만큼 모든 픽셀 방향으로 레이를 쏘는 것이기 때문에 처음 최적화 하기 전에는 원래 프레임 120프레임에서 80까지 떨어지는 것을 보였다.
120프레임으로 돌려놓기 위해 최적화 하는 작업 과정이다.
MAX_STEPS
감소
- 레이 마칭의 반복 횟수를
100
에서50
으로 줄여 성능을 개선했다.
레이마칭을 활용
bool hit = false;
// 그릴 공간까지 이동
for (int i = 0; i < 2; i++) {
float dist = sdSphere(rayPos - uCenter, 4.1);
if (dist < 0.01) {
hit = true;
break;
}
rayPos += rayDir * dist;
}
if (hit == false)
fragColor = pixel;
////
- 구름의 범위를 구 형태로 정의해, 구름 내부로 레이가 빠르게 진입하도록 구현했다.
한계점
오브젝트들을 그리고 구름을 렌더링 하는데
구름을 렌더링하는 과정에서 최적화 때문에
프래그먼트 셰이더에서 구름만 찾아서 앞에 오브젝트가 있어 가려져야하는데도 구름이 무조건 그려진다.
처음부터 고려했었던 문제인데 오브젝트를 드디어 두개 그려서 이제야 보인다 ㅠㅠ
이건 카메라와 오브젝트간 거리를 계산해서 뒤에서 부터 렌더링을 해 고칠 예정이다.
'Computer Graphics > ShaderPixel' 카테고리의 다른 글
ShaderPixel - 8. mandelbox 추가 (0) | 2024.11.21 |
---|---|
ShaderPixel - 7. 구름 안 장애물 추가 (0) | 2024.11.18 |
ShaderPixel - 5. 색 구슬 구현 (0) | 2024.11.17 |
ShaderPixel - 4. 버텍스 셰이더에서 계산한 노말 값과 월드 좌표는 실제로 프래그먼트에 그려지는 픽셀의 월드 좌표와는 다르다 (2) | 2024.11.15 |
ShaderPixel - 3. 렌더링 전략 고민.. (1) | 2024.11.13 |