Clouds Rendering
카테고리: Graphics
태그: DirectX
홍정모님의 그래픽스 새싹코스 강의를 듣고 정리한 내용입니다.
Other References:
How Big Budget AAA Games Render Clouds - SimonDev
Fog vs Clouds
Fog | Beer’s Law | Cloud |
---|---|---|
이전 포스팅에서 배운 Fog Effect의 경우, 거리에 따라 밝기를 감쇠시킴으로서 구현할 수 있었다. 하지만 Cloud 혹은 Cloud-like object는 같은 방식으로 구현할 수 없다. Boundary가 존재하고, 내부 밀도가 일정하지 않으며, position에 따른 lighting 계산도 해야하기 때문이다. 이번 예제에서는 Object의 Density와 Lighting 정보를 따로 계산해 3D-Texture에 저장해두고 사용할 것이다. 이러한 방식을 이용하면 여러가지 장점이 있다. 먼저 pixel shader에서 interpolation 연산을 진행할 때 중복 연산을 피할 수 있다. 또한 실제 object의 크기보다 작은 texture를 이용해 최적화 할 수 있다는 것도 큰 장점이다.
Density Map
Cloud | Absorption |
---|---|
void main(uint3 dtID : SV_DispatchThreadID)
{
uint width, height, depth;
densityTex.GetDimensions(width, height, depth);
float3 uvw = dtID / float3(width, height, depth) + uvwOffset;
densityTex[dtID] = cloudDensity(uvw);
}
- Perlin-Worley noise를 이용해 Density Map 생성
- Absorption(감쇠) 연산은 object를 step으로 쪼갠 뒤, (Beer’s law * step size) 적용
uvwOffset
을 이용해 구름이 흘러가는 연출 구현
Lighting Map
Before | After |
---|---|
- Density Map으로만 구름을 렌더링하면 빛에 반응하지 않음
- 실제 구름은 해당 위치의 밀도, 빛의 방향 등 여러가지 요소가 결합되어 있음
- 심지어 구름 내 그림자 영역도 존재
float LightRay(float3 posModel, float3 lightDir)
{
int numSteps = 128 / 4;
float stepSize = 2.0 / float(numSteps);
float alpha = 1.0;
[loop]
for (int i = 0; i < numSteps; i++)
{
float prevAlpha = alpha;
float density = densityTex.SampleLevel(linearClampSampler, GetUVW(posModel), 0).r;
if (density > 1e-3)
alpha *= BeerLambert(lightAbsorptionCoeff * density, stepSize);
posModel += lightDir * stepSize;
if (abs(posModel.x) > 1 || abs(posModel.y) > 1 || abs(posModel.z) > 1)
break;
if (alpha < 1e-3)
break;
}
return alpha;
}
- Lighting Map역시 sampling 후 step size로 계산
stepSize
가 2/numSteps인 이유는 Model의 크기가 [-1, 1] 이기 때문- alpha = 1.0 부터 시작. Density Texture에서 density값 가져와 alpha 감쇠
- 범위를 벗어나거나, alpha가 0으로 떨어지면 break.
Volumetric Render
Scattering
Isotropic | Anisotropic |
---|---|
float HenyeyGreensteinPhase(in float3 L, in float3 V, in float aniso)
{
// V: eye - pos
// L: Light Direction
float cosT = dot(L, -V);
float g = aniso;
return (1.0 - g * g) / (4.0 * PI * pow(abs(1.0 + g * g - 2.0 * g * cosT), 3.0 / 2.0));
}
- 빛이 구름 내부로 들어오면 내부 분자들에 의해 빛이 산란됨
- 일반적으로 산란은 모든 방향으로 균등하게 산란하지만, 구름의 경우 빛의 방향에 따라 산란 정도가 다른 모델을 사용함
HenyeyGreenstein
모델 사용
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 eyeModel = mul(float4(eyeWorld, 1), worldInv).xyz; // 월드->모델 역변환
float3 dirModel = normalize(input.posModel - eyeModel);
int numSteps = 128;
float stepSize = 2.0 / float(numSteps);
float3 volumeAlbedo = float3(1, 1, 1);
float4 color = float4(0, 0, 0, 1);
float3 posModel = input.posModel + dirModel * 1e-6;
[loop]
for (int i = 0; i < numSteps; i++)
{
float3 uvw = GetUVW(posModel);
float density = densityTex.SampleLevel(linearClampSampler, uvw, 0).r;
float lighting = lightingTex.SampleLevel(linearClampSampler, uvw, 0).r;
if (density.r > 1e-3)
{
float prevAlpha = color.a;
color.a *= BeerLambert(densityAbsorption * density.r, stepSize);
float absorptionFromMarch = prevAlpha - color.a;
color.rgb += absorptionFromMarch * volumeAlbedo * lightColor
* density * lighting
* HenyeyGreensteinPhase(lightDir, dirModel, aniso);
}
posModel += dirModel * stepSize;
if (abs(posModel.x) > 1 || abs(posModel.y) > 1 || abs(posModel.z) > 1)
break;
if (color.a < 1e-3)
break;
}
color = saturate(color);
color.a = 1.0 - color.a;
return color;
}
- Density, Lighting 값을 가져온 후
HenyeyGreenstein
함수를 이용해 rgb 계산 - 구름 입장에서 alpha값은 visabilty로 사용
- 마지막에 alpha값을 반전시켜야 다른 오브젝트(ex. skybox)와 정상적인 블렌딩 가능
Result
댓글 남기기