MetalByTutorials/12-environment/projects/skybox.playground/Sources/Renderer.swift

220 lines
8.1 KiB
Swift

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<float4x4>.stride, index: 1)
var tiling: float2 = [30, 30]
renderEncoder.setFragmentBytes(&tiling, length: MemoryLayout<float2>.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<float4x4>.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()
}
}