본문 바로가기
IT/Graphics

Metal 스터디 6주차 (23.01.19)

by YEON-DU 2023. 2. 6.
반응형

Function Specialization

두 가지 다른 대안을 처리해야 하는 Fragment Shader에 문제가 있다.

Model의 Texture에 UV 좌표가 있는 경우 Rendering에 Texture 색상을 사용한다.

그렇지 않은 경우 Material 색상을 사용한다.

 

예시는 단순한 상황이고, 대부분의 Model에는 Bump Map, Ambiens Occlusion 및 Spercularity에 대해 4개 또는 5개의 서로 다른 Texutre가 존재하기도 한다.

Model은 사용 가능한 Texture 중 일부를 사용할 수 있지만 모든 Texture가 동일한 Texture를 사용하지는 않는다.

조건 분기와 GPU Shader 함수는 비용이 크므로 배제한다면, 함수 전문화Function Specialization를 통해 원하는 것을 달성할 수 있다.

 

GPU에서 사용하려는 두 개의 Shader가 포함된 Metal Library로 GPU Pipeline을 설정했다.

vertex_main / fragment_main

Renderer 대신 Submesh에서 Pipeline State를 유지하도록 앱으로 재설정한다면,

Submesh에 Texture가 있는지 여부를 테스트하기 위해 Pipeline State에서 Boolean 특수 상수를 설정할 수 있다.

이 상수에는 연관된 인수 테이블Argument Table이 있으므로 Index 번호를 부여한다.

Fragment Function을 설정할 때 Library에 hasColorTexture 상수가 있고 Library가 자동으로 두 가지 다른 함수를 설정할 수 있다고 알린다.

Boolean에 따라 하나는 True이고 다른 것은 False이다.

 

Mesh.swift

static func createPipelineState(textures: Textures) -> MTLRenderPipelineState {
        let functionConstants = MTLFunctionConstantValues()
        var property = textures.baseColor != nil
        functionConstants.setConstantValue(&property,
                                           type: .bool,
                                           index: 0)

        let vertexFunction = Renderer.library.makeFunction(name: "vertex_main")
        // 2가지 버전으로 함수를 컴파일 한다.
        // index 0의 함수 상수가 True인 버전과, False인 버전이다.
        // 더 많은 Texture가 포함된 경우 컴파일러는 더 많은 Shader를 빌드하여 Constance 함수의 가능한 모든 조합을 만든다.
        let fragmentFrunction = try! Renderer.library.makeFunction(name: "fragment_main",
                                                                   constantValues: functionConstants)

        // pipeline state properties
        let pipelineStateDescriptor = MTLRenderPipelineDescriptor()
        pipelineStateDescriptor.colorAttachments[0].pixelFormat = .bgra8Unorm
        pipelineStateDescriptor.vertexFunction = vertexFunction
        pipelineStateDescriptor.fragmentFunction = fragmentFrunction
        pipelineStateDescriptor.vertexDescriptor = MTLVertexDescriptor.defaultVertexDescriptor()
        pipelineStateDescriptor.depthAttachmentPixelFormat = .depth32Float

        return try! Renderer.device.makeRenderPipelineState(descriptor: pipelineStateDescriptor)
    }

 

Instancing Introduction

Untitled 4

 

모두 동일한 여러 개체를 Rendering할 때 필요하지 않은 경우 동일한 Geometry를 여러 번 Load하고 싶지 않다.

현재 상태의 게임 엔진을 사용하여 임의의 위치에 20개의 기차를 Rendering했다.

이렇게 하면 Model과 Texture가 모두 20번 Load된다.

앱이 매우 느리게 실행되며, 2.1GB의 RAM을 사용했으며 FPS는 6이었다. 즉, 앱은 초당 60개의 Frame을 Rendering하는 대신 초당 6개의 Frame을 Rendering한다.

 

“This performance is terrible.”

동일한 Model을 20번 Load할 필요는 없다. 한 번 Load하고 20번 Rendering할 수 있다.

한 번 Load하고 여러 번 Rendering하는 방법을 Instancing이라고 한다.

 

하나의 기차를 Load하고 임의의 위치에 있는 기차의 각 Instance로 100번 Rendering한다. 하나의 Texture만 Load하고 있기 때문에 메모리 사용량이 2GB에서 140MB로 줄었다.

그리고 Frame당 하나의 Model만 Rendering하기 때문에 FPS가 60으로 보여진다.

현재 Position, Rotation 및 Scale Transform 속성이 있는 Model Class가 있다.

 

가장 효율적인 Rendering은 Geometry를 한 번 Load하지만 모두 동일한 Geometry를 사용하는 여러 Transform 속성을 갖는 것이다. 각 Instance에 대해 하나씩 Transform 배열을 갖도록 만든다. 배열뿐만 아니라 이러한 Transform에 대한 행렬을 Metal Buffer에 보관하고 각각의 Instance에 대한 Buffer를 GPU에 전달한다.

 

Metal은 Instancing을 허용한다. InstanceCount를 포함하도록 draw call을 변경하고 Instance Buffer, Instance 속성과 함께 현재 InstanceID를 제공하는 새로운 Vertex Function을 작성한다.

 

Rendering 전 각 Frame은 현재 Transform 속성으로 Metal Buffer를 Update한다. Metal Buffer의 정보에 접근하려면 Swift Pointer를 사용해야 한다. Instance가 사용하는 모든 Memory를 Instance의 구조체 형식으로 InstanceBuffer.contents()의 Pointer에 Binding한다. 이 Instance의 구조체에는 각 Instance에 대한 Model 행렬이 포함된다. 이제 Pointer가 첫 번째 Instance를 가리킨다. Pointer의 pointee 속성을 사용하여 변환 배열을 처리한다.

 

Instance의 구조체 Member에 접근하고 각 Instance를 현재 Transform Matrix로 Update할 수 있다. Loop의 끝에서 Pointer를 하나씩 앞으로 이동하여 이제 Pointer가 Buffer의 다음 Member를 가리키도록 한다.

이제 하나가 아닌 100개의 나무를 포함하도록 게임 장면을 Update한다.

Instancing

Untitled 11

 

Instance.swift

import Foundation
import MetalKit

class Instance: Model {
    var transforms: [Transform]
    var instanceCount: Int
    var instanceBuffer: MTLBuffer

    init(name: String, instanceCount: Int = 1) {
        transforms = [Transform](repeatElement(Transform(), count: instanceCount))
        self.instanceCount = instanceCount
        instanceBuffer = Renderer.device.makeBuffer(length: instanceCount * MemoryLayout<Instances>.stride,
                                                    options: [])!
        super.init(name: name)
    }

    override func render(commandEncoder: MTLRenderCommandEncoder, submesh: Submesh) {
        var pointer = instanceBuffer.contents().bindMemory(to: Instances.self, capacity: instanceCount)

        for transform in transforms {
            pointer.pointee.modelMatrix = transform.matrix
            pointer = pointer.advanced(by: 1)
        }
        commandEncoder.setVertexBuffer(instanceBuffer, offset: 0, index: 20)
        commandEncoder.setRenderPipelineState(submesh.instancedPipelineState)

        let mtkSubmesh = submesh.mtkSubmesh

        commandEncoder.drawIndexedPrimitives(type: .triangle,
                                             indexCount: mtkSubmesh.indexCount,
                                             indexType: mtkSubmesh.indexType,
                                             indexBuffer: mtkSubmesh.indexBuffer.buffer,
                                             indexBufferOffset: mtkSubmesh.indexBuffer.offset,
                                             instanceCount: instanceCount)
    }

}

instance

 

Challenge: Render all the trains

Untitled 12

 

임의의 위치에 있는 100대의 기차 모습을 따라해본다.

성능 차이를 확인하려면 먼저 Instance 없이 10개의 기차를 Rendering하고 Performance를 확인한다.

그런 다음 Instancing을 사용하여 100개의 기차를 Rendering하고 성능 차이를 확인한다.

기차를 배치할 때에는 x 방향으로 -5 ~ 5, z 방향으로 0~10 범위의 float.random을 사용한다.

또한 기차의 Y 회전을 무작위로 지정한다.

 

GameScene.swift

import Foundation

class GameScene: Scene {

    override func setupScene() {
        camera.target = [0, 0.8, 0]
        camera.distance = 8
        camera.rotation = [-0.4, -0.4, 0]

        let train = Instance(name: "train", instanceCount: 100)
        add(node: train)
        for i in 0..<100 {
            train.transforms[i].position.x = Float.random(in: -5..<5)
            train.transforms[i].position.z = Float.random(in: 0..<10)
            train.transforms[i].rotation.y = Float.random(in: 0..<radians(fromDegrees: 359))
        }
    }
}

Untitled 14

 

Conclusion

Untitled 15

 

이제 간단한 게임 엔진을 완성했다. GameScene Class에 Model과 게임 로직을 추가하기만 하면 매우 쉽게 GameScene을 만들 수 있다. Rendering은 게임 로직에서 완전히 추상화된다. 이제 게임 엔진을 사용하여 사운드, 음악 및 동작이 포함된 실제 게임을 만들어보자. 다음 부분은 Breakout 클론을 만들 것이다.

반응형

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

Metal 스터디 8주차 (23.02.02)  (0) 2023.04.16
Metal 스터디 7주차 (23.01.26)  (0) 2023.02.11
Metal 스터디 5주차 (23.01.12)  (0) 2023.01.15
Metal 스터디 4주차 (23.01.06)  (0) 2023.01.15
Metal 스터디 3주차 (22.12.22)  (0) 2022.12.28

댓글