[Volume Rendering] 구름 볼륨 렌더링 + SDF
구름 볼륨 렌더링을 할 때, 3가지 방식을 진행합니다. CloudDensity-> CoudLighting-> Scattering이죠. 우리가 흔히 무엇을 그리는 과정은 2d texture에다가 했습니다. 하지만 3d texture에다 노이즈를 생성하여 형성하는 방법이죠.
1) CloudDensity
density의 방법을 살펴보겠습니다. density란 밀도라는 뜻입니다. 노이즈를 형성하여 3d텍스춰에 노이즈를 형성합니다. perlinWorley방식을 사용하여 노이즈를 생성하였습니다. (참고링크 https://www.shadertoy.com/view/3dVXDc)
구름의 움직임을 표현하기 위해서
[numthreads(16, 16, 4)]
void main(uint3 dtID : SV_DispatchThreadID)
{
uint width, height, depth;
densityTex.GetDimensions(width, height, depth);
float3 uvw = dtID / float3(width, height, depth) + uvwOffset; // 노이즈 생성을 위해 uvwOffset 사용
densityTex[dtID] = cloudDensity(uvw);
}
uvwOffset을 형성하였습니다.
2) CloudLighting 계산입니다
다음 그림을 먼저 보고 이해해봅시다.
CloudLighting을 구하는 과정은 해당 지점에서 태양사이에 얼마나 많은 밀도에 의해서 빛이 가려지는가를 구하는 과정입니다. 여기서 구름쪽에서 태양을 바라보는 선을 봐봅시다. 점과 점사이를 stepsize로 생각하고, 임의로 stepsize를 구합니다. 그리고 texture3D밖에 나가지 않는 선에서 점마다 density를 통해 light를 감소시켜줍니다. 코드는 다음과 같습니다.
float LightRay(float3 posModel, float3 lightDir)
{
// 근처만 탐색
int numSteps = 64 / 4;
float stepSize = 2.0 / float(numSteps);
// float absorptionCoeff = 5.0;
float alpha = 1.0; // visibility 1.0으로 시작
[loop] // [unroll] 사용 시 쉐이더 생성이 너무 느림
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;
}
// alpha가 0에 가까울 수록 조명으로부터 빛을 잘 못 받음
return alpha;
}
[numthreads(16, 16, 4)]
void main(uint3 dtID : SV_DispatchThreadID)
{
// float3 lightDir = float3(0, 1, 0);
uint width, height, depth;
lightingTex.GetDimensions(width, height, depth);
float3 uvw = dtID / float3(width, height, depth); //+ uvwOffset; 라이트맵은 주어진 밀도장에 대해 계산하는 것이라서 uvwOffset 미사용
// uvw는 [0, 1]x[0,1]x[0,1]
// 모델 좌표계는 [-1,1]x[-1,1]x[-1,1]
lightingTex[dtID] = LightRay((uvw - 0.5) * 2.0, lightDir);
}
3) Scattering
산란이라는 뜻입니다.
태양에서 어떤 지점을 향해 빛을 쏘면 그 지점에서 빛이 산란되죠. 여기서 주의해야할 점은 화살표가 길 수록 그쪽방향으로 산란될 확률이 높다는 의미입니다. HenyeyGreenstein함수에 반영되어 있습니다.
코드를 살펴봅시다.
float4 main(PixelShaderInput input) : SV_TARGET
{
float3 eyeModel = mul(float4(eyeWorld, 1), worldInv).xyz; // 월드->모델 역변환
float3 dirModel = normalize(input.posModel - eyeModel);
int numSteps = 64;
float stepSize = 2.0 / float(numSteps); // 박스 길이가 2.0
float3 volumeAlbedo = float3(1, 1, 1);
float4 color = float4(0, 0, 0, 1); // visibility 1.0으로 시작
float3 posModel = input.posModel + dirModel * 1e-6; // 살짝 들어간 상태에서 시작
// 주의: color.a에 "투명도"로 사용하다가 마지막에 "불투명도"로 바꿔줌
[loop] // [unroll] 사용 시 쉐이더 생성이 너무 느림
for (int i = 0; i < numSteps; i++)
{
float3 uvw = GetUVW(posModel); // +uvwOffset; 미사용
float density = densityTex.SampleLevel(linearClampSampler, uvw, 0).r;
float lighting = lightingTex.SampleLevel(linearClampSampler, uvw, 0).r;
if (density > 1e-3)
{
float prevAlpha = color.a;
color.a *= BeerLambert(densityAbsorption * density, stepSize);
float absorptionFromMarch = prevAlpha - color.a;
color.rgb += absorptionFromMarch * volumeAlbedo * lightColor
* density * lighting
* HenyeyGreensteinPhase(lightDir, dirModel, aniso);
//HenYey는 빛의 산란정도를 리턴하고, absorptionFromMarch는 step에서 얼마나
//빛을 흡수했는지를 나타낸다. 많이 흡수할수록 결국 그 흡수한만큼 산란을 많이 시키는 것이다
//즉 흡수정도*산란정도로 흡수해서 산란하는 값을 color.rgb에 반환한다
//추가적으로 Henyey에서 lightDir과 dirModel이 각도가 0에 가까울수록 더 많은 산란값을 반환한다
}
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; // a는 불투명도
//처음 a값은 1이었다. a값을 많이 흡수했다면 a가 0에 가까워졌을 것이고, 1-color.a해서 흡수한만큼 색을 더 불투명으로 뽑아낸다
//만약에 흡수를 안했다면? color.a가 1이었고 1-color.a는 0이 돼서 투명값을 반환한다
return color;
}
여기서
float prevAlpha = color.a;
color.a *= BeerLambert(densityAbsorption * density, stepSize);
float absorptionFromMarch = prevAlpha - color.a;
color.rgb += absorptionFromMarch * volumeAlbedo * lightColor
* density * lighting
* HenyeyGreensteinPhase(lightDir, dirModel, aniso);
이부분을 이해해봅시다.
color.a는 투명도를 의미합니다. 첫 color.a는 1로 시작합니다. 나중에 color.a = 1.0 - color.a;여기서 빼는 과정이 있으니 반대로 생각해야합니다. 어쨌든 color.a가 처음에 1에서 BeerLambert함수를 통해 감소합니다. density와 densityAbsorption이 클수록 더 많이 감소한다는 뜻이죠. 그 값은 absorptionFromMarch에 저장됩니다.
color.rgb += absorptionFromMarch * (이것저것) * density * lighting * Henyey함수가 있습니다. absorptionFromMarch는 감소한 값이니 감소한만큼 흡수가 많이 이뤄졌다는 뜻입니다. 그 만큼 구름의 color가 증가해야겠죠? (흡수가 안일어나면 즉 density가 0이라면 구름이 없이 맑은 하늘이 보인다를 생각하면 됩니다) 여기서 density를 곱한것은 밀도가 높으면 흡수가 더 많이 일어나니 BeerLambert에서 계산한것 뿐 아니라 여기서도 한번 더 곱해주는 것 같습니다. lighting은 해당 지점의 밀도에 의한 빛의 세기였죠? 여기서 곱해줍니다. Henyey함수에서는 scattering을 반환해주는 함수인데, 빛의 방향과 dirModel(해당지점에서 eye로의 방향)이 각도가 0에 가까울수록 강한 세기를 반환해줍니다. 위의 Henyey함수를 설명하는 scattering그림을 참고하면 이해가 쉬울 것입니다.
이렇게 texture3d를 통해 구름을 표현할 수 있습니다.
추가적으로 볼륨모델링에서 SDF기법으로 두개 이상의 물체를 결합하여 물체 주위에 구름을 형성하는 방식을 사용할 수 있습니다. density를 변형해서 이용하는 기법인데요. density쉐이더 자체를 변형하진 않고 이미 다 형성된 density에서 픽쉘쉐이더부분에서 코드를 수정하여 그리는 기법입니다.
구를 하나 형성하는 코드는 다음과 같습니다.
float density = densityTex.SampleLevel(linearClampSampler, uvw, 0).r;
float sdf = length(posModel) - 0.5;
if (sdf <= 0.0)
{
}
else
{
density *= saturate(1.0 - sdf * 10.0);
}
이렇게 거리를 조정하여 그려줍니다. 구를 벗어나면 밀도가 감소하여 구름이 미세하게 지는 것을 볼 수 있습니다.
sdf뒤에 곱해진 수를 조정하여 날카롭게 할지 부드럽게 할지 결정할 수 있습니다.
그럼 두개이상의 물체를 그릴 때는 어떻게 할까요? 바로 min을 이용하면 됩니다. 하나의 구를 표현하는 함수 f1과 f2가 있다고 생각해봅시다. f1안의 점을 f1에 넣으면 f1<0이 되고, 바깥의 점을 넣으면 f1>0가 됩니다. 이 원리를 이용하여 이해하면 좋을 것 같습니다. 코드는 다음과 같습니다.
float density = densityTex.SampleLevel(linearClampSampler, uvw, 0).r;
float sdf1 = length(posModel - float3(-0.3, 0.0, 0.0)) - 0.4;
float sdf2 = length(posModel - float3(0.3, 0.0, 0.0)) - 0.4;
if (min(sdf1,sdf2) <= 0.0)
{
}
else
{
density *= saturate(1.0 - min(sdf1, sdf2) * 10.0);
}
참고자료 :
SIGGRAPH 논문 :
SIGGRAPH 2022 Advances in Real-Time Rendering in Games course (realtimerendering.com)
SIGGRAPH 2022 Advances in Real-Time Rendering in Games course
Natalya Tatarchuk (@mirror2mask) is a graphics engineer and a rendering enthusiast at heart, currently focusing on driving the state-of-the-art rendering technology and graphics performance for the Unity engine as a Distinguished Technical Fellow and Chief
www.advances.realtimerendering.com
Perlin-Worley Noise코드 :
https://www.shadertoy.com/view/3dVXDc
Shadertoy
0.00 00.0 fps 0 x 0
www.shadertoy.com
Volume fog 생산 Henyey-Greenstein phase function :
https://www.shadertoy.com/view/7s3SRH
Shadertoy
0.00 00.0 fps 0 x 0
www.shadertoy.com
SDF볼륨 렌더링 물체 합치기 아이디어 :
2D SDF Combination
In the last tutorial we learned how to create and move simple shapes with signed distance functions. In this one we will learn how to combine several shapes to make more complex distance fields. I learned most of the techniques described here from a glsl s
www.ronja-tutorials.com
강의 출처 :
홍정모 그래픽스 새싹코스 Part4(격자시뮬레이션- 구름모델링)
https://www.honglab.ai/courses
honglab
그래픽스 새싹코스 파트2,3,4 묶음판매 Bundle [그래픽스 새싹코스 번들] 파트1 공부 후 더 자세하게 배우고 싶은 분들을 위해 10% 할인된 가격으로 파트2,3,4 묶음판매를 진행합니다.
www.honglab.ai