import MetalKit public class Renderer: NSObject { public var isWireframe = false let skyboxDimensions: int2 = [256, 256] let device: MTLDevice let commandQueue: MTLCommandQueue! var depthStencilState: MTLDepthStencilState! var texture: MTLTexture? let camera = Camera() lazy var terrain = { return Terrain(device: device) }() lazy var skybox: MTKMesh = { do { let allocator = MTKMeshBufferAllocator(device: device) let cube = MDLMesh(boxWithExtent: [1,1,1], segments: [1, 1, 1], inwardNormals: true, geometryType: .triangles, allocator: allocator) let model = try MTKMesh(mesh: cube, device: device) return model } catch { fatalError("failed to create skybox mesh") } }() var skyboxTexture: MTLTexture? var skyboxPipelineState: MTLRenderPipelineState! public struct SkyboxSettings { public var turbidity: Float = 0.28 public var sunElevation: Float = 0.6 public var upperAtmosphereScattering: Float = 0.1 public var groundAlbedo: Float = 4 } public var skyboxSettings = SkyboxSettings() { didSet { skyboxTexture = loadGeneratedSkyboxTexture(dimensions: skyboxDimensions) } } public init(metalView: MTKView) { guard let device = MTLCreateSystemDefaultDevice() else { fatalError("GPU not available") } metalView.device = device self.device = device commandQueue = device.makeCommandQueue()! super.init() metalView.clearColor = MTLClearColor(red: 1, green: 1, blue: 0.8, alpha: 1) metalView.depthStencilPixelFormat = .depth32Float metalView.delegate = self buildPipelineState() buildDepthStencilState() skyboxTexture = loadGeneratedSkyboxTexture(dimensions: [256, 256]) camera.transform.position = [0, 0.5, 3] terrain.rotation.x = -.pi / 2 terrain.scale = [50, 50, 50] terrain.texture = Renderer.loadTexture(imageName: "grass.png", device: device) } func buildPipelineState() { do { guard let library = device.makeDefaultLibrary() else { fatalError("Can't make default library") } let fragmentFunction = library.makeFunction(name: "fragment_main") let descriptor = MTLRenderPipelineDescriptor() descriptor.colorAttachments[0].pixelFormat = .bgra8Unorm descriptor.depthAttachmentPixelFormat = .depth32Float descriptor.fragmentFunction = fragmentFunction // terrain pipeline state descriptor.vertexFunction = library.makeFunction(name: "vertex_terrain") try terrain.pipelineState = device.makeRenderPipelineState(descriptor: descriptor) // skybox pipeline state descriptor.vertexFunction = library.makeFunction(name: "vertex_skybox") descriptor.fragmentFunction = library.makeFunction(name: "fragment_skybox") descriptor.vertexDescriptor = MTKMetalVertexDescriptorFromModelIO(skybox.vertexDescriptor) try skyboxPipelineState = device.makeRenderPipelineState(descriptor: descriptor) } catch let error { fatalError(error.localizedDescription) } } func buildDepthStencilState() { let descriptor = MTLDepthStencilDescriptor() descriptor.depthCompareFunction = .lessEqual descriptor.isDepthWriteEnabled = true depthStencilState = device.makeDepthStencilState(descriptor: descriptor) } static func loadTexture(imageName: String, device: MTLDevice) -> MTLTexture { let textureLoader = MTKTextureLoader(device: device) let textureLoaderOptions: [MTKTextureLoader.Option: Any] = [.origin: MTKTextureLoader.Origin.bottomLeft, .SRGB: false, .generateMipmaps: NSNumber(booleanLiteral: true)] let fileExtension = URL(fileURLWithPath: imageName).pathExtension.isEmpty ? "png" : nil guard let url = Bundle.main.url(forResource: imageName, withExtension: fileExtension) else { fatalError("No texture found") } let texture: MTLTexture do { texture = try textureLoader.newTexture(URL: url, options: textureLoaderOptions) } catch let error { fatalError(error.localizedDescription) } return texture } func loadGeneratedSkyboxTexture(dimensions: int2) -> MTLTexture? { var texture: MTLTexture? let skyTexture = MDLSkyCubeTexture(name: "sky", channelEncoding: .uInt8, textureDimensions: dimensions, turbidity: skyboxSettings.turbidity, sunElevation: skyboxSettings.sunElevation, upperAtmosphereScattering: skyboxSettings.upperAtmosphereScattering, groundAlbedo: skyboxSettings.groundAlbedo) do { let textureLoader = MTKTextureLoader(device: device) texture = try textureLoader.newTexture(texture: skyTexture, options: nil) } catch let error { print(error.localizedDescription) } return texture } public func zoomUsing(delta: CGFloat) { let sensitivity = Float(0.1) let rotation = camera.transform.rotation let dx = Float(delta) * sensitivity * sin(rotation.y) let dz = Float(delta) * sensitivity * cos(rotation.y) camera.transform.position.x += dx camera.transform.position.z -= dz } public func rotateUsing(translation: NSPoint) { let sensitivity: Float = 0.01 camera.transform.rotation.x += Float(translation.y) * sensitivity camera.transform.rotation.y -= Float(translation.x) * sensitivity } } extension Renderer: MTKViewDelegate { public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { camera.aspect = Float(size.width/size.height) } public func draw(in view: MTKView) { guard let drawable = view.currentDrawable, let descriptor = view.currentRenderPassDescriptor, let commandBuffer = commandQueue.makeCommandBuffer(), let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else { return } // render planebuffer renderEncoder.setCullMode(.none) renderEncoder.setFrontFacing(.counterClockwise) renderEncoder.setRenderPipelineState(terrain.pipelineState) renderEncoder.setDepthStencilState(depthStencilState) renderEncoder.setVertexBuffer(terrain.buffer, offset: 0, index: 0) renderEncoder.setVertexBuffer(terrain.uvBuffer, offset: 0, index: 3) var modelViewProjectionMatrix = camera.projectionMatrix * camera.viewMatrix * terrain.modelMatrix renderEncoder.setVertexBytes(&modelViewProjectionMatrix, length: MemoryLayout.stride, index: 1) var tiling: float2 = [30, 30] renderEncoder.setFragmentBytes(&tiling, length: MemoryLayout.stride, index: 1) renderEncoder.setFragmentTexture(terrain.texture, index: 0) renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: terrain.vertices.count/3) // render skybox renderEncoder.setCullMode(.back) renderEncoder.setFrontFacing(.counterClockwise) renderEncoder.setRenderPipelineState(skyboxPipelineState) renderEncoder.setDepthStencilState(depthStencilState) renderEncoder.setVertexBuffer(skybox.vertexBuffers[0].buffer, offset: 0, index: 0) var matrix = camera.viewMatrix matrix.columns.3 = [0, 0, 0, 1] var viewProjectionMatrix = camera.projectionMatrix * matrix renderEncoder.setVertexBytes(&viewProjectionMatrix, length: MemoryLayout.stride, index: 1) renderEncoder.setFragmentTexture(skyboxTexture, index: 0) let submesh = skybox.submeshes[0] renderEncoder.drawIndexedPrimitives(type: .triangle, indexCount: submesh.indexCount, indexType: submesh.indexType, indexBuffer: submesh.indexBuffer.buffer, indexBufferOffset: 0) renderEncoder.endEncoding() commandBuffer.present(drawable) commandBuffer.commit() } }