Shadows

Date:     Updated:

카테고리:

태그:

Main Reference
- Metal by Tutorials - 4th edition


Shadow Maps

: Textures containing a scene’s shadow information. Render your scene from the light source’s location.

1

  • First Pass: You’ll render from the light’s point of view. Since the sun directional, you’ll use an orthographic camera rather than a perspective camera. You’re only interested in the depth of objects that the sun can see, so you won’t render a color texture.

  • Second Pass: You’ll render using the scene camera as usual, but you’ll compare the camera fragment with each shadow map fragment. If the camera fragment’s depth is less than the shadow map fragment at that position, the fragment is in the shadow.

Shadow Render Pass

init() {
  pipelineState =
    PipelineStates.createShadowPSO()
  shadowTexture = Self.makeTexture(
    size: CGSize(
    width: 2048,
    height: 2048),
  pixelFormat: .depth32Float,
  label: "Shadow Depth Texture")
}

일반적으로 Shadow Map은 Square 형태의 Texture를 사용하기 때문에 window가 resize될 때마다 Texture 크기를 조정할 필요가 없음.

    func draw(
        commandBuffer: MTLCommandBuffer,
        scene: GameScene,
        uniforms: Uniforms,
        params: Params
    ) {
        guard let descriptor = descriptor else { return }
        descriptor.depthAttachment.texture = shadowTexture
        descriptor.depthAttachment.loadAction = .clear
        descriptor.depthAttachment.storeAction = .store

        guard let renderEncoder =
                commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
            return
        }
        renderEncoder.label = "Shadow Encoder"
        renderEncoder.setDepthStencilState(depthStencilState)
        renderEncoder.setRenderPipelineState(pipelineState)
        for model in scene.models {
            renderEncoder.pushDebugGroup(model.name)
            model.render(
                encoder: renderEncoder,
                uniforms: uniforms,
                params: params)
            renderEncoder.popDebugGroup()
        }
        renderEncoder.endEncoding()

    }
  • Shadow Pass 내부에서 Descriptor를 세팅하니까 attachment 세팅이 매우 간단하네..
    • (IS엔진에서는 ResourceManager를 통해 별도로 불러오는 형태)
  • Shadow Map은 다음 Pass에서 사용해야 하기 때문에 load/sotre action은 clear/sotre로 지정

Shadow Pass

// in renderer
var shadowCamera = OrthographicCamera()
shadowCamera.viewSize = 16
shadowCamera.far = 16
let sun = scene.lighting.lights[0]
shadowCamera.position = sun.position

// in shader(uniforms)
matrix_float4x4 shadowProjectionMatrix;
matrix_float4x4 shadowViewMatrix;
  • Light 종류에 맞게 orthographic/perspective camera 지정
  • ShadowMap을 그리기 위해 Projection, View Matrix를 shader로 넘겨줘야 함
  • 대부분의 경우 light는 고정이지만, 움직이는 dynamic light라면 매 프레임 update
#import "Common.h"

struct VertexIn {
  float4 position [[attribute(0)]];
};

vertex float4
  vertex_depth(const VertexIn in [[stage_in]],
  constant Uniforms &uniforms [[buffer(UniformsBuffer)]])
{
  matrix_float4x4 mvp =
    uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix
    * uniforms.modelMatrix;
  return mvp * in.position;
}

1

ShadowPSO를 지정할 때 colorAttachment.pixelFormat = .invalid 로 설정하고, depthAttachment만 렌더하도록 설정했기 때문에 fragment function 없이 vertex function만 지정해주면 ShadowMap Texture가 렌더된다.

Main Pass

// in Vertex Function
VertexOut out {
	// ...
	.shadowPosition =
	  uniforms.shadowProjectionMatrix * uniforms.shadowViewMatrix
	  * uniforms.modelMatrix * in.position
};


// in Fragment Function
// ... Lighting Calculation
float3 shadowPosition
  = in.shadowPosition.xyz / in.shadowPosition.w;
float2 xy = shadowPosition.xy;
xy = xy * 0.5 + 0.5;
xy.y = 1 - xy.y;
xy = saturate(xy);

constexpr sampler s(
			  coord::normalized, filter::linear,
			  address::clamp_to_edge,
			  compare_func:: less);
float shadow_sample = shadowTexture.sample(s, xy);

if (shadowPosition.z > shadow_sample) {
  diffuseColor *= 0.5;
}
  • VertexOut에 Light 기준 transformed position 정보 추가
  • Shadow Position 원근 계산 / [-1, 1] 범위 screen space로 변환 / reverse upside
  • Shadow Texture에 사용할 sampler는 clamp_to_edge 옵션을 사용
    • shadow map 경계 밖은 그림자가 없는 공간(1.0)으로 인식해야 함
  • Shadow Texture와 값 비교 후 그림자 영역 판별

Shadow Acne

1

위 코드를 실행시키면 화면이 심하게 flickering 되는 현상을 발견할 수 있다. 이를 shadow acne 또는 surface acne라고 부르는데, float precision에 따른 self-shadowing이 원인이다.

   
1 1
  • 첫 번째 원인으로는 Render Pass에 따라 interpolation된 depth 값이 다르기 때문
    • 해상도, 좌표 변환 등으로 인해 rasterize가 미묘하게 달라지기 때문
  • 두 번째 원인으로는 카메라 변환 기준으로 서로 다른 pixel들이 shadow map 기준으로는 동일한 texel인 경우가 존재
    • 이러한 현상에 의한 depth 차이는 normal과 light 사이의 각도가 클 수록 더 극명하게 나타남
// ...
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);
if (shadowPosition.z > shadow_sample + bias) {
	diffuseColor *= 0.5;
}

정리하자면 이런 문제는 아주 미세한 depth 값 차이에 의해 발생하므로 shadow를 판별할 때 작은 bias값을 추가해주면 간단하게 해결된다. 두 번째 현상을 해결하기 위해 bias값을 normal과 light 사이의 각도에 비례하게 설정하기도 한다.

1

Visualize Problem

1

// Fragment Function
float3 shadowPosition = in.shadowPosition.xyz / in.shadowPosition.w;
float2 xy = shadowPosition.xy;
xy = xy * 0.5 + 0.5;
xy.y = 1 - xy.y;
if (xy.x < 0.0 || xy.x > 1.0 || xy.y < 0.0 || xy.y > 1.0) {
	return float4(1, 0, 0, 1);
}

위 캡처는 Shadow Map이 커버하지 못하는 부분을 red로 렌더링한 결과이다. 이를 보면 Shadow Map이 실제 Main Camera에 렌더링되는 영역 전체를 커버하지 못하는 것을 알 수 있다. 그렇다고 해서 모든 영역을 커버하기 위해 Light Camera를 멀리 두면 shadow map의 해상도가 떨어져 그림자 품질이 나빠진다.

-> Cascaded Shadow Mapping

Balance perfomance and shadow depth



맨 위로 이동하기

Metal 카테고리 내 다른 글 보러가기

댓글 남기기