Notice
Recent Posts
Recent Comments
Link
반응형
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

RenderLog

VolumeLight 본문

Graphics/Graphics

VolumeLight

scahp 2020. 6. 24. 09:26

 

VolumeLight

 

최초작성 : 2020-06-02

마지막수정 : 2020-06-24

최재호

목표

[Nvidia]VolumeLight 번역 의 구현을 이해해봅니다.

 

내용

이 문서를 한번 정도 읽었다고 가정하고 진행되므로 [Nvidia]VolumeLight 번역를 읽어보시길 권장 합니다.

분석할 코드는 Nvidia 홈페이지 코드로 진행했습니다. Nvidia 코드

 

OpenGL 을 기준으로 작동할 수 있도록 변경하였습니다.

 

코드를 보면 사용되는 렌더타겟과 렌더링 패스가 상당히 많습니다.

이 부분을 먼저 이해하는 것이 이해를 더 쉽게 해주기 때문에 먼저 내용을 보겠습니다.

렌더패스는 Git에 올려둔 코드에 똑같은 번호가 붙어있어서 참고하시면 됩니다.

 

[기본 정보]

메인 렌더타겟 크기 SCR_WIDTH, SCR_HEIGHT : (1280, 720)
쉐도우 맵 크기 SM_WIDHT, SM_HEIGHT : (2048, 2048)
Directional Light 정보
 - Orthogonal Projection (반드시)
 - Width, Height : (2500.0f, 2500.0f)

[렌더타겟 종류]

1. ShadowMapWorldRT (R32) : (SM_WIDHT)
 - ShadowMap Depth를 World Space 기준으로 변경함
2. ShadowMapWorldMipsRT (RG32) : (SM_WIDHT / 2), (SM_WIDHT / 4) (SM_WIDHT / 8) (SM_WIDHT / 16)
 - ShadowMapWorldRT의 일정 타일 크기당 Min, Max를 계산
3. ShadowMapWorldScaledOptRT (R32) : (SM_WIDHT / 16)
 - ShadowMapWorldMipsRT 최종 결과
4. ShadowMapHoleRT (R32) : (SM_WIDHT / 16) (SM_WIDHT / 16)
 - 일정 크기 이하의 구멍들을 모두 제거
 - Indoor 의 LightVolume이 더 강한 빛을 만들어 주는데 사용. (Light Density 조정용)
5. MainSceneRT (RGBA32) : (SCR_WIDTH, SCR_HEIGHT)
 - MainScene Color/Depth 렌더
6. LightVolumeHDRRT (RGBA32) : (SCR_WIDTH / 4, SCR_HEIGHT / 4)
 - 라이트 볼륨 렌더링
7. ConvertDepthWorldNormalizedRT (R32F) : (SCR_WIDTH, SCR_HEIGHT)
 - MainScene의 Depth를 World Scale로 변경
8. EdgeDetectionRT (RG32F) : (SCR_WIDTH / 4, SCR_HEIGHT / 4)
 - Sobel Filter로 Edge Detection 결과 담김
9. EdgeBlurRT (R32F) : (SCR_WIDTH / 4, SCR_HEIGHT / 4)
 - EdgeDetectionRT(Sobel Filter) 결과에 Edge의 방향 기준 Blur
10. EdgeGradientBlur (R32F) : (SCR_WIDTH / 4, SCR_HEIGHT / 4)
 - EdgeBlurRT에 9x9 일반 블러


[렌더링 패스 순서]

1. ShadowMapRender
2. MainSceneRender
 - MainSceneRT
3. ConvertDepthToWorldScale
 - ShadowMapWorldRT
4. Generate Min/Max ShadowMap Mip level
 - ShadowMapWorldMipsRT[0] ~ [3]
 - ShadowMapWorldScaledOptRT
5. Fill the holes
 - Min 3 회
 - Max 5 회
6. Generate LightVolume
7. MainSceneDepth To WorldScale
8. EdgeDetectionSobel
9. GradientBlur
- Sobel filter로 얻어진 Edge 방향으로 필터링
10. ImageBlur
11. Final : 최종 장면에 ImageBlur 까지 마친 LightVolume 적용

 

렌더링 패스에서 주목할 만한 부분만 소개하겠습니다. 전체 코드는 Git 에 공개 되어있습니다.

 

Depth 에서 World Position 구하기 (Depth -> WorldScale Z 구하기)

#version 330 core

precision mediump float;

uniform sampler2D tex_object;
uniform vec3 LightPos;
uniform vec3 LightForward;
uniform mat4 LightVPInv;

in vec2 TexCoord_;
out vec4 color;

void main()
{
    float depth = texture2D(tex_object, TexCoord_).x;

    vec4 clipPos;
    clipPos.x = 2.0 * TexCoord_.x - 1.0;
    clipPos.y = 2.0 * TexCoord_.y - 1.0;
    clipPos.z = 2.0 * depth - 1.0;          // OpenGL은 NDC 공간이 Z : -1 ~ 1
    clipPos.w = 1.0;

    vec4 posWS = LightVPInv * clipPos;
    posWS /= posWS.w;

    float WorldZ = dot(posWS.xyz - LightPos, LightForward);
    color = vec4(WorldZ, WorldZ, WorldZ, 1.0);
}

 

Fill the holes

작은 구멍을 막아버린 Texture와 그렇지 않은 Texture에 같은 UV 텍셀을 Fetch 하여 Depth 차이가 나는 경우 Light Volume Density를 더 높게 설정해주는 용도. Indoor 의 경우 Light Volume을 더 두드러지게 해줍니다.

주변 픽셀중 Min Pixel을 선택 3회 후, 주변 픽셀중 Max Pixel 선택 5회

그림1. Fill the holes (Min)

 

그림2. Fill the holes (Max)

 

그림3. Min/Max(좌) 부터 Fill the holes(우) 되는 과정

 

Light Volume 생성

1. MainScene Depth Buffer로 부터 WorldPosition을 복원합니다.

float SceneDepth = texture2D(tex_object, TexCoord_).x;

vec4 clipPos;
clipPos.x = 2.0 * TexCoord_.x - 1.0;
clipPos.y = 2.0 * TexCoord_.y - 1.0;
clipPos.z = 2.0 * SceneDepth - 1.0;
clipPos.w = 1.0;

vec4 posWS = CameraVPInv * clipPos;
posWS /= posWS.w;

2. Eye 위치 기준 현재 픽셀의 방향과 거리를 구합니다. 여기서 구헌 거리가 LightVolume의 Density를 계산하는데 쓰임

vec3 vecForward = normalize(posWS.xyz - EyePos.xyz);
float traceDistance = dot(posWS.xyz - (EyePos.xyz + vecForward * CameraNear), vecForward);
traceDistance = clamp(traceDistance, 0.0, 2500.0); // Far trace distance

posWS.xyz = EyePos.xyz + vecForward * CameraNear;
vecForward *= 2.0 * (1.0 / 4.0);

3. vecForward 기준 몇번이나 LightVolume Density를 계산해야할지 계산

int stepsNum = int(min(traceDistance / length(vecForward), float(MAX_STEPS)));

4. vecForward 방향으로 이동시에 ShadowMap 기준으로 얼마만큼 이동하는지 텍스쳐 UV 및 WorldDepth 기준 Delta 계산

// NearPlane에서 위치를 얻어낸다. 이 좌표의 x, y를 텍스쳐 좌표, z는 월드의 z값.
float jitter = 1.0;
vec3 curPosition = posWS.xyz;
shadowUV = LightVP * vec4(curPosition, 1.0);
coordinates = shadowUV.xyz / shadowUV.w;
coordinates.xy = (coordinates.xy + vec2(1.0)) * 0.5;
coordinates.z = dot(curPosition - LightPos, LightForward);

// NearPlane에서 Forward 방향으로 (한 스탭 + 스탭 나아간 위치)를 얻어낸다. 이 좌표의 x, y를 텍스쳐 좌표, z는 월드의 z값.
curPosition = posWS.xyz + vecForward;
shadowUV = LightVP * vec4(curPosition, 1.0);
vec3 coordinateEnd = shadowUV.xyz / shadowUV.w;
coordinateEnd.xy = (coordinateEnd.xy + vec2(1.0)) * 0.5;
coordinateEnd.z = dot(curPosition - LightPos, LightForward);

vec3 coordinateDelta = coordinateEnd - coordinates;

5. LightVolume Step의 최적화를 위한 코드입니다.

 5.1) vecForward 가 ShadowMap Texture UV 기준 얼마만큼의 Offset인지구합니다.

 5.2) CoarseDepthTexlSize : SM_WIDTH / 128 으로, Fill the hole, min/max texture의 사이즈가 128이므로 텍스쳐를 이 크기 단위로 Fetch 하여 중복 계산을 피하는 것이 목표입니다.

 5.3). longStepScale_1 이 longStepScale - 1인 것은 뒤에 코드에 나오겠지만 기본적으로 1회 연산 이외의 부분을 고려하기 위해서 입니다. [ ex. light += scale * sampleFine * (1.0 + isLongStep * longStepScale_1); ]

vec2 vecForwardProjection;
vecForwardProjection.x = dot(LightRight, vecForward);
vecForwardProjection.y = dot(LightUp, vecForward);

// Calculate coarse step size
float longStepScale = int(CoarseDepthTexelSize / length(vecForwardProjection));
longStepScale = max(longStepScale, 1);
float longStepScale_1 = longStepScale - 1;

6. LightVolume의 Light Density 계산

6.1) LightVolume Density 계산을 위해 Tracing 하는 방향

그림4. LightVolume Tracing 과 ShadowMap에서의 Tracing Delta 비교

 

그림5. Min/Max 최적화를 위해 Tile 범위 내에서의 Min Max 비교하고 Min/Max 범위를 벗어나는 위치에 있다면 타일 범위 내의 모든 픽셀을 Shadow or Lit 으로 판단 가능함, 화살표는 Tracing 방향, 동그라미는 각각 현재와 다음의 Coarse Step 위치. 동그라미의 위치보다 위에 가려지는 것이 있는지 여부로 Light or Shadow 판단. 

float sampleFine;
float light = 0.0;
float coordinateZ_end;
bool optimizeOn = true;

int i = 0;
for (; i < stepsNum; i++)
{
    // 텍스쳐 UV 영역을 벗어나는 부분 회피
    if ((coordinates.x > 1.0 || coordinates.x < 0.0) || (coordinates.y > 1.0 || coordinates.y < 0.0))
    {
        if (optimizeOn)
        {
            coordinates += coordinateDelta * (1.0 + longStepScale_1);
            i += int(longStepScale_1);
        }
        else
        {
            coordinates += coordinateDelta;
        }
        continue;
    }

    // Min/Max Texture Fetch (SM_WIDHT / 16)
    vec2 sampleMinMax = textureLod(tex_object3, coordinates.xy, 0).xy;

    // ShadowMap WorldZ Fetch (SM_WIDTH)
    float ShadowMapWorld = texture2D(tex_object2, coordinates.xy).x;
    sampleFine = float(ShadowMapWorld > coordinates.z);

    // Fill the hole Texture Fetch (SM_WIDHT / 16)
    float zStart = textureLod(tex_object4, coordinates.xy, 0).x;

    // Light Volume Attenuation
    const float transactionScale = 100.0;

    float attenuation = (coordinates.z - zStart) / ((sampleMinMax.y + transactionScale) - zStart);
    attenuation = clamp(attenuation, 0.0, 1.0);
    attenuation = 1.0 - attenuation;
    attenuation *= attenuation;

    float attenuation2 = ((zStart + transactionScale) - coordinates.z) * (1.0 / transactionScale);
    attenuation2 = 1.0 - clamp(attenuation2, 0.0, 1.0);
    attenuation *= attenuation2;

    // Fill the hole texture 와 현재 Depth 중 현재 Depth가 더 크다는 말은 ShadowMap에서 거리다 더 말다는 뜻이며
    // 이 것은 Hole 이 채워져서 막혔다는 의미가 됨. 그래서 Density를 더 높혀줘서 잘 보이도록 함.
    float density = float(zStart < coordinates.z);
    density *= 10.0 * attenuation;
    //density += 0.25;
    sampleFine *= density;

    if (optimizeOn)
    {
        // 2048/128 에 비례한 텍스쳐 크기를 고려한, Min/Max Depth 값으로 Coarse Step 진행 가능 여부 체크
        coordinateZ_end = coordinates.z + coordinateDelta.z * longStepScale;

        // 현재 Min/Max 타일 영역의 Min 값과 비교하여 Light, Shadow 여부 파악
        // MinZ < CurZ < MaxZ 의 경우는 Min/Max 타일 영역 내에서 isLight, isShadow 여부가 변경될 수 있기 때문에 그냥 보통 Step을 진행하지만 
        // CurZ < MinZ, CurZ > MAxZ 의 경우는 타일 영역 전체의 극소, 극대 값 범위 밖이기 때문에 isLight, isShadow 가 확실하다고 볼 수 있음.

        float comparisonValue = max(coordinates.z, coordinateZ_end);
        float isLight = float(comparisonValue < sampleMinMax.x); // .x stores min depth values

        comparisonValue = min(coordinates.z, coordinateZ_end);
        float isShadow = float(comparisonValue > sampleMinMax.y); // .y stores max depth values

        // We can perform coarse step if all samples are in light or shadow
        float isLongStep = isLight + isShadow;

        longStepsNum += isLongStep;
        realStepsNum += 1.0;

        light += scale * sampleFine * (1.0 + isLongStep * longStepScale_1);		// longStepScale should be >= 1 if we use a coarse step
        coordinates += coordinateDelta * (1.0 + isLongStep * longStepScale_1);
        i += int(isLongStep * longStepScale_1);
    }
    else
    {
        light += scale * sampleFine;
        coordinates += coordinateDelta;
    }
}

light -= scale * sampleFine * (i - stepsNum);

color = vec4(vec3(light), 1.0);

 

Edge Detection 후 Blur 처리

LIghtVolume을 만들어 낼때 사용한 해상도와 실제 MainScene 렌더링시 해상도 차이로 읺해 발생하는 앨리어싱을 최소화 하기 위해서 Edge 부분을 검출하고 검출한 Edge의 방향을 기준으로 Blur 처리 합니다.

그림6. 디테일이 떨어지는 LightVolume 텍스쳐
그림7. 좌측이 Edge 앨리어싱, 우측이 필터와 3x3 커널 블러를 이용하여 개선한 결과

 

그림8. 소벨 에지 디텍션

 

그림9. LightVolume(블러 전) -> GradientBlur -> Blur 후 최종

 

이미지 블러 처리

#version 330 core

precision mediump float;

uniform sampler2D tex_object;		// EdgeDetectionSobel
uniform sampler2D tex_object2;		// LightVolumeTexture
uniform float CoarseTextureWidthInv;
uniform float CoarseTextureHeightInv;

in vec2 TexCoord_;
out vec4 color;

void main()
{
	vec2 gradient = texture(tex_object, TexCoord_).xy;
	
	vec2 offset;
	offset.x = CoarseTextureWidthInv * gradient.y;
	offset.y = CoarseTextureHeightInv * gradient.x;

	float result = 0.0f;

	// 에지의 방향을 기준으로 블러를 진행한다. Sobel 은 Edge가 아닌 부분은 모두 0이 됩니다.
    // 또한 X와 Y의 Edge 탐지를 별도로 합니다.
	for (int iSample = -7; iSample < 8; iSample++)
		result += texture(tex_object2, TexCoord_ + offset * iSample).x;

	result *= (1.0 / 15.0);
	color.x = result;
}

 

결과

그림9. 최종 결과

 

구현 코드

https://github.com/scahp/Shadows/tree/LightVolume

 

 

레퍼런스

[번역][Nvidia White Paper] Volume Light

Nvidia Volume Light

https://github.com/scahp/Shadows/tree/LightVolume

반응형

'Graphics > Graphics' 카테고리의 다른 글

Smooth Min (IQ's polynomial smooth minimum)  (3) 2020.07.17
Cascade Shadow Map  (2) 2020.07.10
Forward Plus Rendering  (0) 2020.05.26
PCSS(Percentage-Closer Soft Shadow)  (0) 2020.05.22
Tiled Forward Rendering  (0) 2020.05.12