Deferred Rendering

Date:     Updated:

카테고리:

태그:

Main Reference
- Metal by Tutorials - 4th edition


  • The first pass renders the shadow map.
  • The second pass constructs G-buffer textures containing theses values
    • : albedo(with shadows), normals, positions, and depths
  • Uisng a full-screen quad, the third and final pass processes the lights.
    • In the same pass, accumulates lights information.

Apple GPUs can combine the second and third passes. Chapter 15, “Tile-Based Deferred Rendering”

G-Buffer Pass

let textures = [
  albedoTexture,
  normalTexture,
  positionTexture
]
for (index, texture) in textures.enumerated() {
	let attachment =
		descriptor?.colorAttachments[RenderTargetAlbedo.index + index]
		attachment?.texture = texture
		attachment?.loadAction = .clear
		attachment?.storeAction = .store
		attachment?.clearColor =
    MTLClearColor(red: 0.73, green: 0.92, blue: 1, alpha: 1)
}
descriptor?.depthAttachment.texture = depthTexture
descriptor?.depthAttachment.storeAction = .dontCare
  • G-Buffer Pass가 끝난 이후 Lighting Pass에서는 Depth Texture를 사용할 필요가 없음
    • depthAttachment.storeAction = .dontCare
  • 예제에서는 G-Buffer Pass의 descriptor를 draw 함수에서 매번 초기화하는 구조네.. (굳이..?)
fragment GBufferOut fragment_gBuffer(
  VertexOut in [[stage_in]],
  depth2d<float> shadowTexture [[texture(ShadowTexture)]],
  constant Material &material [[buffer(MaterialBuffer)]])
{
  GBufferOut out;
  out.albedo = float4(material.baseColor, 1.0);
  out.albedo.a = calculateShadow(in.shadowPosition, shadowTexture);
  out.normal = float4(normalize(in.worldNormal), 1.0);
  out.position = float4(in.worldPosition, 1.0);
  return out;
}

Since you don’t use the alpha channel of the albedo texture, you can store the shadow value there. 이런 식으로 빈 채널에 필요한 데이터들 추가해주면 효율적임.

1

Lighting Pass

struct LightingRenderPass: RenderPass {
  let label = "Lighting Render Pass"
  var descriptor: MTLRenderPassDescriptor?
  var directionalLightPSO: MTLRenderPipelineState
  var pointLightPSO: MTLRenderPipelineState
  var spotLightPSO: MTLRenderPipelineState
  let depthStencilState: MTLDepthStencilState?
  weak var albedoTexture: MTLTexture?
  weak var normalTexture: MTLTexture?
  weak var positionTexture: MTLTexture?

  func resize(view: MTKView, size: CGSize) {}

  func draw(
    commandBuffer: MTLCommandBuffer,
    scene: GameScene,
    uniforms: Uniforms,
    params: Params
  ) {
  // ...

  drawDirectionalLight()
  drawPointLight();
  drawSpotLight();
  }

  func drawDirectionalLight() {}
  func drawPointLight() {}
  func drawSpotLight() {}
}

You’ll accumulate output from all light types when running the lighting pass. Each type of light needs a different fragment function, so you’ll need multiple pipeline states.

// LightingRenderPass.draw()...
renderEncoder.drawPrimitives(
	type: .triangle,
	vertexStart: 0,
	vertexCount: 6)


// deffered.metal
constant float3 vertices[6] = {
    float3(-1,  1,  0),    // triangle 1
    float3( 1, -1,  0),
    float3(-1, -1,  0),
    float3(-1,  1,  0),    // triangle 2
    float3( 1,  1,  0),
    float3( 1, -1,  0)
};

vertex VertexOut vertex_quad(uint vertexID [[vertex_id]])
{
    VertexOut out {
        .position = float4(vertices[vertexID], 1)
    };
    return out;
}

fragment float4 fragment_deferredSun(...)
{
	uint2 coord = uint2(in.position.xy);
    float4 albedo = albedoTexture.read(coord);
    float3 normal = normalTexture.read(coord).xyz;
    Material material {
        .baseColor = albedo.xyz,
        .ambientOcclusion = 1.0
    };

    float3 color = 0;
    for (uint i = 0; i < params.lightCount; i++) {
        Light light = lights[i];
        color += calculateSun(light, normal, params, material);
    }
    color *= albedo.a;
    return float4(color, 1);
}

마지막 Lighting Pass에서는 화면 전체를 덮는 quad를 그려야 함. 이때 별도의 vertex buffer를 생성하고 바인딩할 필요가 없고, 바로 drawPrimitives 함수를 호출하면 된다. 그 대신 vertex shader가 직접 vertex data를 생성해야 하므로, metal 파일 내에 quad의 vertex data가 포함되어야 한다. 또한 quad size가 screen size [-1, 1]에 대응되므로, vertex의 position을 texture coordinate로 바로 사용이 가능하다.

1

Lighting Pass에서 한 가지 문제가 발생하는데, 하늘이 clear color로 렌더링 된다는 것이다. 원래 아무 geometry가 없기 때문에 fragment function이 돌면 안되는데, deffered rendering 특성 상 모든 fragment에 대해 lighting 연산이 실행되기 때문이다. 이러한 문제는 stencil masks를 통해 해결할 수 있음.

Instancing

If you had one thousand point lights, a draw call to render the geometry for each light volume would bring your system to a crawl. Instancing is a great way to tell the GPU to draw the same geometry a specific number of times.

// Lighting Pass
static func buildDepthStencilState() -> MTLDepthStencilState? {
  let descriptor = MTLDepthStencilDescriptor()
  descriptor.isDepthWriteEnabled = false
  return Renderer.device.makeDepthStencilState(descriptor: descriptor)
}

// ...
renderEncoder.drawIndexedPrimitives(
  type: .triangle,
  indexCount: submesh.indexCount,
  indexType: submesh.indexType,
  indexBuffer: submesh.indexBuffer,
  indexBufferOffset: submesh.indexBufferOffset,
  instanceCount: scene.lighting.pointLights.count)

// .metal file
struct PointLightIn {
  float4 position [[attribute(Position)]];
};

struct PointLightOut {
  float4 position [[position]];
  uint instanceId [[flat]];
};

vertex PointLightOut vertex_pointLight(
  PointLightIn in [[stage_in]],
  constant Uniforms &uniforms [[buffer(UniformsBuffer)]],
  constant Light *lights [[buffer(LightBuffer)]],
  uint instanceId [[instance_id]])
{
  float4 lightPosition = float4(lights[instanceId].position, 0);
  float4 position = uniforms.projectionMatrix * uniforms.viewMatrix
				    * (in.position + lightPosition);
  PointLightOut out {
    .position = position,
    .instanceId = instanceId
  };
  return out;
}
  • Light를 위한 icosahedrons는 항상 render되기 원하기 때문에, depthStencilDescriptor 오버라이드.
  • fragment에서는 rendering을 위한 position과, 적절한 light 정보를 가져오기 위한 instanceID가 필요.
  • instanceID 값은 interpolation되면 안되기 때문에 [[flat]] attribute 명시
  • point light의 position의 경우 rotation, scaling은 의미가 없음
    • 따라서 model matrix를 곱하지 않고 단순히 lightPoition 값만 더해줘도 괜찮음

1

let attachment = pipelineDescriptor.colorAttachments[0]
attachment?.isBlendingEnabled = true
attachment?.rgbBlendOperation = .add
attachment?.alphaBlendOperation = .add
attachment?.sourceRGBBlendFactor = .one
attachment?.sourceAlphaBlendFactor = .one
attachment?.destinationRGBBlendFactor = .one
attachment?.destinationAlphaBlendFactor = .zero
attachment?.sourceRGBBlendFactor = .one
attachment?.sourceAlphaBlendFactor = .one
  • PointLightPSO에 blending 옵션 설정

1

10000 Point Lights

Deffered 1
Forward 1



맨 위로 이동하기

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

댓글 남기기