본문 바로가기

프로그래밍/iOS,macOS

[Metal] MetalKit 플로우 분석

공통헤더

RederType.h
쉐이더에서 공통적으로 사용할 타입들을 위해 별도 헤더 정의
스위프트 프로젝트에서는 프로젝트.h 에 이 헤더를 import 해주어야 한다.

#include <simd/simd.h>

 

 

버텍스 : Vertex

import MetalKit

struct Vertex {
  var position: float3
}

 

쉐이더/텍스처

var vertexFunction:MTLFunction
var fragmentFunction:MTLFunction
var diffuseTexture:MTLTexture

 

Mesh

var vertexBuffer:MTLBuffer
var indexBuffer:MTLBuffer

 

초기화

렌더러 생성

simd::float4x4 modelViewProjectionMatrix;
simd::float4x4 modelViewMatrix;

MetalKit clear color 설정
var clearColor: UIColor
self.clearColor = [UIColor clorWithWhite:0.95 alpha:1];

 

메탈 디바이스

var device:MTLDevice!
device = MTLCreateSystemDefaultDevice();

 

메탈 레이어

UIView에 렌더링하기 위한 레이어

var metalLayer:CAMetalLayer!
metalLayer = CAMetalLayer()
metalLayer.device = self.device
metalLayer.pixelFormat = .bgra8Unorm
metalLayer.framebufferOnly = true
metalLayer.frame = view.layer.frame
view.layer.addSublayer( metalLayer )

 

라이브러리

var library: MTLLibrary
library = self.device.makeDeafaultLibrary()

 

명령큐

var commandQueue: MTLCommandQueue
commandQueue = self.device.makeCommandQueue()

 

샘플러

var sampler: MTLSamplerState
var samplerDescriptor: MTLSamplerDescriptor

samplerDescriptor = MTLSamplerDescriptor()
samplerDescriptor.minFilter = MTLSamplerMinMagFilter.nearest
samplerDescriptor.magFilger = MTLSamplerMagFilter.linear
samplerDescriptor.sAddressMode = MTLSamplerAddressMode.repeat
samplerDescriptor.tAddressMode = MTLSamplerAddressMode.repeat
sampler = self.device.makeSamplerState( descriptior: samplerDescriptor )

근데 보통은 그냥 쉐이더 샘플러 사용


버텍스

let vertexData: [Vertex]= [ Vertex(0,0,0), Vertex(0,1,0), ..... ]
let indexData:[Int] = [ ..... ]

let dataSize = vertexData.count * MemoryLayout<Vertex>.stride
let indexSize = vertexData.count * MemoryLayout<Int>.size

var vertexBuffer: MTLBuffer!
var indexBuffer: MTLBuffer!

// 버퍼 생성
vertexBuffer = self.device.makeBuffer( bytes: vertexData, length: dataSize, options:MTLResourceOptionCPUCacheModeDefault )
indexBuffer = self.device.makeBuffer( bytes: indexData, length: indexSize, options: MTLResourceOptionCPUCacheModeDefault )

// Mesh 객체에 저장
var mesh: Mesh = Mesh( vertexBuffer:vertexBuffer, indexBuffer:indexBuffer )

 

쉐이더

var vertexFunction:MTLFunction = self.library.makeFunction( name: "vertex_main" )
var fragmentFunction: MTLFunction = self.library.makeFunction( name: "fragment_main" )

쉐이더 텍스처를 위한 이미지 변환
쉐이더에서 사용할 텍스처 이미지를 생성한다

UIImage는 이미지의 raw 데이터가 접근할 수 없으므로, UIImage로부터 CGImageRef 를 생성
UIKit은 다른 이미지와 마찬가지로 좌상단이 원점이므로
OpenGL 좌표계로 변경하려면 상하를 뒤집어야함

var image: UIImage = UIImage(named:textureName )
CGImageRef imageRef = image.cgImage

NSUInteger width = CGImageGetWidth( imageRef );
NSUInteger height = CGImageGetHeight( imageRef );
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();


// 이미지 포맷별 상하 뒤집기 위한 부분
uint8_t *rawData = (uint8_t *)calloc(height * width * 4, sizeof(uint8_t);
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitPerComponent = 8;
CGContextRef context = 
    CGBitmapContextCreate( 
        rawData, 
        width, 
        bitsPerComponent, 
        bytesPerRow, 
        colorSpace,
        kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
        
CGColorSpaceRelease(colorSpace);

CGContextTranslateCTM( context, 0, height )
CGContextScaleCTM( context, 1, -1 );

CGContetDrawImage( context, CGRectMake(0, 0, width, height), imageRef);
CGContextRelease( context );


// 텍스처 생성
var textureDescriptor: MTLTextureDescriptor =
        MTLTextureDescriptor.texture2DDescriptor (
            pixelFormat:MTLPixelFormat.rgba8Unorm,
            width:width,
            height:height,
            mipmapped:fale
        )

var texture: MTLTexture = self.device.makeTexture(descriptior: textureDescriptor )

let region: MTLRegion = MTLRegionMake2D(0, 0, width, height);
texture.replaceRegion( region, mipmapLevel:0 withBytes: rawData, bytesPerRow:bytesPerRow )

// rawData 해제
free( rawData );



var material:Material = 
        Material( 
            vertexFunction: vertexFunction,
            fragmentFunction: fragmentFunction,
            diffuseTexture: texture
        )

 

깊이버퍼 텍스처

var depthTexture:MTLTexture

CGSize drawableSize = self.metalLayer.drawableSize;
var depthDesc: MTLTextureDescriptor = 
        MTLtextureDescriptor.texture@DDescriptor(
            pixelFormat:MTLPixelFormat.depth32Float,
            width:drawableSize.width,
            height:drawableSize.height,
            mipmapped:false
        )
self.depthTexture = self.device.makeTexture( descriptor:depthDesc )

 

버텍스 정보

// MTLVertexDescriptor
// vertex, normal, uv
var vertexDescriptor: MTLVertexDescriptor = MTLVertexDescriptor()

// vertex
vertexDescriptor.attributes[0].format = MTLvertexFormat.float4;
vertexDescriptor.attributes[0].bufferIndex = 0;
vertexDescriptor.attributes[0].offset = MemoryLayout<Vector>.offset(of: \.position )

// normal
vertexDescriptor.attributes[1].format = MTLVertexFormat.float3;
vertexDescriptor.attributes[1].bufferIndex = 0;
vertexDescriptor.attributes[1].offset = MemoryLayout<Vector>.offset(of: \.normal )

// uv
vertexDescriptor.attributes[2].format = MTLVertexFormat.float2;
vertexDescriptor.attributes[2].bufferIndex = 0;
vertexDescriptor.attributes[2].offset = MemoryLayout<Vector>.offset(of: \.texCoords )

vertexDescriptor.layouts[0].stride = MemoryLayout<Vector>.stride
vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunction.perVertex;

Vertex는 쉐이더와 동일하게 정의된 struct 객체

 

 

 

 

 

렌더 파이프라인

// MTLRenderPipelineDescriptor
// 쉐이더 함수설정 및 vertex descriptor 설정
var pipelineDescriptor: MTLRenderPipelineDescriptor = MTLRenderPipelineDescriptor()
pipelineDescriptor.vertexFunction = self.material.vertexFunction
pipelineDescriptor.fragmentFunction = self.material.fragmentFunction
pipelineDescriptor.vertexDescritor = vertexDescriptor;

pipelineDescriptor.colorAttachments[0].pixelFormat = MTLPixelFormat.bgra8Unorm;
pipelineDescriptor.depthAttachmentPixelFormat = MTLPixelFormat.depth32Float;


// 렌더 파이프라인 생성
self.pipeline = try? self.device.makeRenderPipelineState( descriptor: pipelineDescriptor )

 

 

게임루프

// 생성 : viewDidApper

CADisplayLink

var dispLink: CADisplayLink
dispLink = CADisplayLink( target:self, selector:#selector(dispLinkCallback) )
dispLink.add( to: .current, forMode: .defaultRunLoopMode )


// 콜백
func dispLinkCallback( displaylink: CADisplayLink ) {
    // 게임루프 작성
    // MVC 변환
    // 프레임 업데이트
}

// 해제 : viewWillDisapper
dispLink.invalidata()

 


게임루프

MVC 변환을 위한 행렬 생성

const CGSize size = self.view.bound.size;
const CGFloat aspectRatio = size.width / size.height;
const CGFloat verticalFOV = (aspectRation > 1) ? 45:90;
static const CGFloat near = 0.1;
static const CGFloat far = 100;

simd::float4x4 projectionMatrix = PerspectiveProjection( aspectRatio, verticalFOV * (M_PI / 180), near, far);

static const simd::float3 X = {1,0,0};
static const simd::float3 Y = {0,1,0};

simd::float4x4 modelViewMatrix = Identity();
modelViewMatrix = modelViewMatrix * Rotation( X, -self.angle.y);
modelViewMatrix = modelViewMatrix * Rotation( Y, -self.angle.x);

modelViewMatrix.columns[3].z = -1.5;

modelViewProjectionMatrix = projectionMatrix * modelViewMatrix;

 

프레임 그리기

CAMetalDrawable 얻어오기

guard let drawable = self.metalLayer?.nextDrawable() else {
    //drawable 얻기 실패
    return
}

 

 

uniform

// update uniforms
id<MTLBuffer> uniformBuffer;
if( !self.uniformBuffer ) {
    self.uniformBuffer = [self.device newBufferWithLength:sizeof(Uniforms) options: MTLResourceOptionCPUCacheModeDefault]
}
Uniforms uniforms;
uniforms.modelViewMatrix = self.modelViewMatrix;
uniforms.modelViewProjectionMatrix = self.modelViewProjectionMatrix;
uniforms.normalMatrix = simd::inverse(simd::transpose(UpperLeft3x3(self.modelViewMatrix)));
memcpy([self.uniformBuffer contents], &uniforms, sizeof(Uniforms));

 

렌더패스
쉐이더의 렌더 패스를 설정한다.

CGFloat r,g,b,a;
[self.clearColor getRead:&r gree:&g blue:&b alpha:&a];


// MTLRenderPassDescriptor
// texture, depth
var renderPass: MTLRenderPassDescriptor = MTLRenderPassDescriptor()
renderPass.colorAttachments[0].texture = drawable.texture;
renderPass.colorAttachments[0].loadAction = MTLLoadAction.clear;
renderPass.colorAttachments[0].storeAction = MTLStoreAction.store
renderPass.colorAttachments[0].clearColor = MTLClearColorMake(r,g,b,a);

renderPass.depthAttachment.texture = self.depthTexture;
renderPass.depthAttachment.loadAction = MTLLoadAction.clear;
renderPass.depthAttachment.storeAction = MTLStoreAction.store;
renderPass.depthAttachment.clearDepth = 1;

 

커맨드 인코더

let commandBuffer: MTLCommandBuffer = self.commandQueue.makeCommandBuffer()
var commandEncoder: MTLRenderCommandEncoder = commandBuffer.makeRenderCommandEncoder( descriptor: renderPass )

commandEncoder.setVertexBuffer( mesh.vertexBuffer, offset:0, index:0 )
commandEncoder.setVertexBuffer( self.uniformBuffer, offset:0, index:1 )
commandEncoder.setFragmentBuffer( self.uniformBuffer,offset:0, index:0];
commandEncoder.setFragmentTexture( material.diffuseTexture, index:0 )
commandEncoder.setFragmentSamplerState( self.sampler, index:0 )

commandEncoder.setRenderPipelineState( self.pipeline )
commandEncoder.setCullMode( MTLCullMode.back )
commandEncoder.setFrontFacingWinding ( MTLWinding.counterClockwise )



// MTLDepthStencil 
var depthStencilDescriptor: MTLDepthStencilDescriptor = MTLDepthStencilDescriptor()
depthStencilDescriptor.depthCompareFunction = MTLCompareFunction.less;
depthStencilDescriptor.depthWriteEnabled = YES;

var depthStencilState: MTLDepthStencilState = self.device.makeDepthStencilState( descriptor:depthStencilDescriptor )

commandEncoder.setDepthStencilState( depthStencilState )
commandEncoder.drawIndexedPrimitives(
                    type: MTLPrimitiveType.triangle,
                    indexCount: mesh.indexBuffer.length() / MemoryLayout<UInt16>().size
                    indexType: MTLIndexType.uInt16
                    indexBuffer: mesh.indexBuffer
                    indexBufferOffset:0
                )

commandEncoder.endEncoding()
commandBuffer.commit()

 

Present

var commandBuffer: MTLCommandBuffer = self.commandQueue.makeCommandBuffer()
commandBuffer.present( drawable )
commandBuffer.commit()