mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-02 21:00:57 +00:00
renderer: internal support for blend modes
This commit is contained in:
parent
f8a80c6b38
commit
667201fe49
@ -46,6 +46,8 @@ add_executable(Voxelotl MACOSX_BUNDLE
|
|||||||
Renderer/Environment.swift
|
Renderer/Environment.swift
|
||||||
Renderer/Mesh.swift
|
Renderer/Mesh.swift
|
||||||
Renderer/ModelBatch.swift
|
Renderer/ModelBatch.swift
|
||||||
|
Renderer/BlendMode.swift
|
||||||
|
Renderer/BlendFunc.swift
|
||||||
Renderer/ChunkRenderer.swift
|
Renderer/ChunkRenderer.swift
|
||||||
Renderer/Renderer.swift
|
Renderer/Renderer.swift
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ class Game: GameDelegate {
|
|||||||
func create(_ renderer: Renderer) {
|
func create(_ renderer: Renderer) {
|
||||||
self.chunkRenderer = ChunkRenderer(renderer: renderer)
|
self.chunkRenderer = ChunkRenderer(renderer: renderer)
|
||||||
self.chunkRenderer.material = .init(
|
self.chunkRenderer.material = .init(
|
||||||
ambient: Color(rgba8888: 0x4F4F4F00).linear,
|
ambient: Color(rgb888: 0x4F4F4F).linear,
|
||||||
diffuse: Color(rgba8888: 0xDFDFDF00).linear,
|
diffuse: Color(rgb888: 0xDFDFDF).linear,
|
||||||
specular: Color(rgba8888: 0x2F2F2F00).linear,
|
specular: Color(rgb888: 0x2F2F2F).linear,
|
||||||
gloss: 75)
|
gloss: 75)
|
||||||
|
|
||||||
self.resetPlayer()
|
self.resetPlayer()
|
||||||
|
41
Sources/Voxelotl/Renderer/BlendFunc.swift
Normal file
41
Sources/Voxelotl/Renderer/BlendFunc.swift
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
internal enum BlendFunc: Hashable {
|
||||||
|
case off
|
||||||
|
case on(src: BlendFuncSourceFactor = .one, dst: BlendFuncDestinationFactor = .zero, equation: BlendFuncEquation = .add)
|
||||||
|
case separate(
|
||||||
|
srcColor: BlendFuncSourceFactor, srcAlpha: BlendFuncSourceFactor,
|
||||||
|
dstColor: BlendFuncDestinationFactor, dstAlpha: BlendFuncDestinationFactor,
|
||||||
|
equColor: BlendFuncEquation, equAlpha: BlendFuncEquation)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BlendFuncSourceFactor: Hashable {
|
||||||
|
case zero, one
|
||||||
|
case srcColor, oneMinusSrcColor
|
||||||
|
case dstColor, oneMinusDstColor
|
||||||
|
case srcAlpha, oneMinusSrcAlpha
|
||||||
|
case dstAlpha, oneMinusDstAlpha
|
||||||
|
/*
|
||||||
|
case constantColor, oneMinusConstantColor
|
||||||
|
case constantAlpha, oneMinusConstantAlpha
|
||||||
|
*/
|
||||||
|
case srcAlphaSaturate
|
||||||
|
case src1Color, oneMinusSrc1Color
|
||||||
|
case src1Alpha, oneMinusSrc1Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BlendFuncDestinationFactor: Hashable {
|
||||||
|
case zero, one
|
||||||
|
case srcColor, oneMinusSrcColor
|
||||||
|
case dstColor, oneMinusDstColor
|
||||||
|
case srcAlpha, oneMinusSrcAlpha
|
||||||
|
case dstAlpha, oneMinusDstAlpha
|
||||||
|
/*
|
||||||
|
case constantColor, oneMinusConstantColor
|
||||||
|
case constantAlpha, oneMinusConstantAlpha
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
enum BlendFuncEquation: Hashable {
|
||||||
|
case add
|
||||||
|
case subtract, reverseSubtract
|
||||||
|
case min, max
|
||||||
|
}
|
23
Sources/Voxelotl/Renderer/BlendMode.swift
Normal file
23
Sources/Voxelotl/Renderer/BlendMode.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
public enum BlendMode: Hashable {
|
||||||
|
case none
|
||||||
|
case normal
|
||||||
|
case premultiplied
|
||||||
|
case additive
|
||||||
|
case screen
|
||||||
|
case multiply
|
||||||
|
case subtract
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension BlendMode {
|
||||||
|
var function: BlendFunc {
|
||||||
|
switch self {
|
||||||
|
case .none: .off
|
||||||
|
case .normal: .on(src: .srcAlpha, dst: .oneMinusSrcAlpha, equation: .add)
|
||||||
|
case .premultiplied: .on(src: .one, dst: .oneMinusSrcAlpha, equation: .add)
|
||||||
|
case .additive: .on(src: .srcAlpha, dst: .one, equation: .add)
|
||||||
|
case .screen: .on(src: .one, dst: .oneMinusSrcColor, equation: .add)
|
||||||
|
case .multiply: .on(src: .dstColor, dst: .one, equation: .add)
|
||||||
|
case .subtract: .on(src: .oneMinusSrcAlpha, dst: .one, equation: .subtract)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,14 +10,15 @@ fileprivate let depthFormat: MTLPixelFormat = .depth32Float
|
|||||||
|
|
||||||
public class Renderer {
|
public class Renderer {
|
||||||
private var device: MTLDevice
|
private var device: MTLDevice
|
||||||
private var layer: CAMetalLayer
|
private var _layer: CAMetalLayer
|
||||||
private var backBufferSize: Size<Int>
|
private var backBufferSize: Size<Int>
|
||||||
private var _clearColor: Color<Double>
|
private var _clearColor: Color<Double>
|
||||||
private var _aspectRatio: Float
|
private var _aspectRatio: Float
|
||||||
private var queue: MTLCommandQueue
|
private var queue: MTLCommandQueue
|
||||||
private var lib: MTLLibrary
|
private var lib: MTLLibrary
|
||||||
|
private var _defaultShader: Shader, _shader2D: Shader
|
||||||
private let passDescription = MTLRenderPassDescriptor()
|
private let passDescription = MTLRenderPassDescriptor()
|
||||||
private var pso: MTLRenderPipelineState
|
private var _psos: [PipelineOptions: MTLRenderPipelineState]
|
||||||
private var depthStencilState: MTLDepthStencilState
|
private var depthStencilState: MTLDepthStencilState
|
||||||
private let _defaultStorageMode: MTLResourceOptions
|
private let _defaultStorageMode: MTLResourceOptions
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ public class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal init(layer metalLayer: CAMetalLayer, size: Size<Int>) throws {
|
internal init(layer metalLayer: CAMetalLayer, size: Size<Int>) throws {
|
||||||
self.layer = metalLayer
|
self._layer = metalLayer
|
||||||
|
|
||||||
// Select best Metal device
|
// Select best Metal device
|
||||||
guard let device = Self.createMetalDevice() else {
|
guard let device = Self.createMetalDevice() else {
|
||||||
@ -74,8 +75,8 @@ public class Renderer {
|
|||||||
self._defaultStorageMode = .storageModeShared
|
self._defaultStorageMode = .storageModeShared
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
layer.device = device
|
self._layer.device = device
|
||||||
layer.pixelFormat = colorFormat
|
self._layer.pixelFormat = colorFormat
|
||||||
|
|
||||||
// Setup command queue
|
// Setup command queue
|
||||||
guard let queue = device.makeCommandQueue() else {
|
guard let queue = device.makeCommandQueue() else {
|
||||||
@ -116,20 +117,16 @@ public class Renderer {
|
|||||||
} catch {
|
} catch {
|
||||||
throw RendererError.initFailure("Metal shader compilation failed:\n\(error.localizedDescription)")
|
throw RendererError.initFailure("Metal shader compilation failed:\n\(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
let vertexProgram = lib.makeFunction(name: "vertexMain")
|
self._defaultShader = .init(
|
||||||
let fragmentProgram = lib.makeFunction(name: "fragmentMain")
|
vertexProgram: lib.makeFunction(name: "vertexMain"),
|
||||||
|
fragmentProgram: lib.makeFunction(name: "fragmentMain"))
|
||||||
|
self._shader2D = .init(
|
||||||
|
vertexProgram: lib.makeFunction(name: "vertex2DMain"),
|
||||||
|
fragmentProgram: lib.makeFunction(name: "fragment2DMain"))
|
||||||
|
|
||||||
// Set up pipeline state
|
// Set up initial pipeline state
|
||||||
let pipeDescription = MTLRenderPipelineDescriptor()
|
self._psos = try [ .init(colorFormat: self._layer.pixelFormat, depthFormat: depthFormat, shader: self._defaultShader, blendFunc: .off) ]
|
||||||
pipeDescription.vertexFunction = vertexProgram
|
.map { [$0: try $0.createPipeline(device)] }[0]
|
||||||
pipeDescription.fragmentFunction = fragmentProgram
|
|
||||||
pipeDescription.colorAttachments[0].pixelFormat = layer.pixelFormat
|
|
||||||
pipeDescription.depthAttachmentPixelFormat = depthFormat
|
|
||||||
do {
|
|
||||||
self.pso = try device.makeRenderPipelineState(descriptor: pipeDescription)
|
|
||||||
} catch {
|
|
||||||
throw RendererError.initFailure("Failed to create pipeline state: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a default texture
|
// Create a default texture
|
||||||
do {
|
do {
|
||||||
@ -155,6 +152,16 @@ public class Renderer {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func usePipeline(options pipeOpts: PipelineOptions) throws {
|
||||||
|
if let exists = self._psos[pipeOpts] {
|
||||||
|
self._encoder.setRenderPipelineState(exists)
|
||||||
|
} else {
|
||||||
|
let new = try pipeOpts.createPipeline(self.device)
|
||||||
|
self._encoder.setRenderPipelineState(new)
|
||||||
|
self._psos[pipeOpts] = new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func createMesh(_ mesh: Mesh<VertexPositionNormalColorTexcoord, UInt16>) -> RendererMesh? {
|
func createMesh(_ mesh: Mesh<VertexPositionNormalColorTexcoord, UInt16>) -> RendererMesh? {
|
||||||
if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil }
|
if mesh.vertices.isEmpty || mesh.indices.isEmpty { return nil }
|
||||||
|
|
||||||
@ -335,11 +342,11 @@ public class Renderer {
|
|||||||
|
|
||||||
func newFrame(_ frameFunc: (Renderer) -> Void) throws {
|
func newFrame(_ frameFunc: (Renderer) -> Void) throws {
|
||||||
try autoreleasepool {
|
try autoreleasepool {
|
||||||
guard let rt = layer.nextDrawable() else {
|
guard let rt = self._layer.nextDrawable() else {
|
||||||
throw RendererError.drawFailure("Failed to get next drawable render target")
|
throw RendererError.drawFailure("Failed to get next drawable render target")
|
||||||
}
|
}
|
||||||
|
|
||||||
passDescription.colorAttachments[0].clearColor = MTLClearColor(self._clearColor)
|
passDescription.colorAttachments[0].clearColor = MTLClearColor(self._clearColor)
|
||||||
passDescription.colorAttachments[0].texture = rt.texture
|
passDescription.colorAttachments[0].texture = rt.texture
|
||||||
passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame]
|
passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame]
|
||||||
|
|
||||||
@ -359,7 +366,6 @@ public class Renderer {
|
|||||||
|
|
||||||
encoder.setFrontFacing(.counterClockwise) // OpenGL default
|
encoder.setFrontFacing(.counterClockwise) // OpenGL default
|
||||||
encoder.setViewport(Self.makeViewport(rect: self.frame))
|
encoder.setViewport(Self.makeViewport(rect: self.frame))
|
||||||
encoder.setRenderPipelineState(pso)
|
|
||||||
encoder.setDepthStencilState(depthStencilState)
|
encoder.setDepthStencilState(depthStencilState)
|
||||||
encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
|
encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0)
|
||||||
|
|
||||||
@ -383,7 +389,15 @@ public class Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal func setupBatch(environment: Environment, camera: Camera) {
|
internal func setupBatch(environment: Environment, camera: Camera) {
|
||||||
assert(self._encoder != nil, "startBatch can't be called outside of a frame being rendered")
|
assert(self._encoder != nil, "setupBatch can't be called outside of a frame being rendered")
|
||||||
|
|
||||||
|
do {
|
||||||
|
try self.usePipeline(options: PipelineOptions(
|
||||||
|
colorFormat: self._layer.pixelFormat, depthFormat: depthFormat,
|
||||||
|
shader: self._defaultShader, blendFunc: .off))
|
||||||
|
} catch {
|
||||||
|
printErr(error)
|
||||||
|
}
|
||||||
|
|
||||||
var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection)
|
var vertUniforms = VertexShaderUniforms(projView: camera.viewProjection)
|
||||||
|
|
||||||
@ -520,7 +534,7 @@ public struct RendererMesh: Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MTLClearColor {
|
fileprivate extension MTLClearColor {
|
||||||
init(_ color: Color<Double>) {
|
init(_ color: Color<Double>) {
|
||||||
self.init(red: color.r, green: color.g, blue: color.b, alpha: color.a)
|
self.init(red: color.r, green: color.g, blue: color.b, alpha: color.a)
|
||||||
}
|
}
|
||||||
@ -536,6 +550,127 @@ fileprivate extension MTLCullMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate struct Shader: Hashable {
|
||||||
|
let vertexProgram: (any MTLFunction)?, fragmentProgram: (any MTLFunction)?
|
||||||
|
|
||||||
|
static func == (lhs: Shader, rhs: Shader) -> Bool {
|
||||||
|
lhs.vertexProgram?.hash == rhs.vertexProgram?.hash && lhs.fragmentProgram?.hash == rhs.fragmentProgram?.hash
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(self.vertexProgram?.hash ?? 0)
|
||||||
|
hasher.combine(self.fragmentProgram?.hash ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate struct PipelineOptions: Hashable {
|
||||||
|
let colorFormat: MTLPixelFormat, depthFormat: MTLPixelFormat
|
||||||
|
let shader: Shader
|
||||||
|
let blendFunc: BlendFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension PipelineOptions {
|
||||||
|
func createPipeline(_ device: MTLDevice) throws -> MTLRenderPipelineState {
|
||||||
|
let pipeDescription = MTLRenderPipelineDescriptor()
|
||||||
|
pipeDescription.vertexFunction = self.shader.vertexProgram
|
||||||
|
pipeDescription.fragmentFunction = self.shader.fragmentProgram
|
||||||
|
pipeDescription.colorAttachments[0].pixelFormat = self.colorFormat
|
||||||
|
self.blendFunc.setBlend(colorAttachment: &pipeDescription.colorAttachments[0])
|
||||||
|
pipeDescription.depthAttachmentPixelFormat = self.depthFormat
|
||||||
|
do {
|
||||||
|
return try device.makeRenderPipelineState(descriptor: pipeDescription)
|
||||||
|
} catch {
|
||||||
|
throw RendererError.initFailure("Failed to create pipeline state: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension BlendFunc {
|
||||||
|
func setBlend(colorAttachment: inout MTLRenderPipelineColorAttachmentDescriptor) {
|
||||||
|
switch self {
|
||||||
|
case .off:
|
||||||
|
colorAttachment.isBlendingEnabled = false
|
||||||
|
case .on(let srcFactor, let dstFactor, let equation):
|
||||||
|
colorAttachment.isBlendingEnabled = true
|
||||||
|
colorAttachment.rgbBlendOperation = .init(equation)
|
||||||
|
colorAttachment.alphaBlendOperation = .init(equation)
|
||||||
|
colorAttachment.sourceRGBBlendFactor = .init(srcFactor)
|
||||||
|
colorAttachment.sourceAlphaBlendFactor = .init(srcFactor)
|
||||||
|
colorAttachment.destinationRGBBlendFactor = .init(dstFactor)
|
||||||
|
colorAttachment.destinationAlphaBlendFactor = .init(dstFactor)
|
||||||
|
case .separate(let srcColor, let srcAlpha, let dstColor, let dstAlpha, let equColor, let equAlpha):
|
||||||
|
colorAttachment.isBlendingEnabled = true
|
||||||
|
colorAttachment.rgbBlendOperation = .init(equColor)
|
||||||
|
colorAttachment.alphaBlendOperation = .init(equAlpha)
|
||||||
|
colorAttachment.sourceRGBBlendFactor = .init(srcColor)
|
||||||
|
colorAttachment.sourceAlphaBlendFactor = .init(srcAlpha)
|
||||||
|
colorAttachment.destinationRGBBlendFactor = .init(dstColor)
|
||||||
|
colorAttachment.destinationAlphaBlendFactor = .init(dstAlpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension MTLBlendOperation {
|
||||||
|
init(_ equation: BlendFuncEquation) {
|
||||||
|
self = switch equation {
|
||||||
|
case .add: .add
|
||||||
|
case .subtract: .subtract
|
||||||
|
case .reverseSubtract: .reverseSubtract
|
||||||
|
case .min: .min
|
||||||
|
case .max: .max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate extension MTLBlendFactor {
|
||||||
|
init(_ source: BlendFuncSourceFactor) {
|
||||||
|
self = switch source {
|
||||||
|
case .zero: .zero
|
||||||
|
case .one: .one
|
||||||
|
case .srcColor: .sourceColor
|
||||||
|
case .oneMinusSrcColor: .oneMinusSourceColor
|
||||||
|
case .srcAlpha: .sourceAlpha
|
||||||
|
case .oneMinusSrcAlpha: .oneMinusSourceAlpha
|
||||||
|
case .dstColor: .destinationColor
|
||||||
|
case .oneMinusDstColor: .oneMinusDestinationColor
|
||||||
|
case .dstAlpha: .destinationAlpha
|
||||||
|
case .oneMinusDstAlpha: .oneMinusDestinationAlpha
|
||||||
|
case .srcAlphaSaturate: .sourceAlphaSaturated
|
||||||
|
/*
|
||||||
|
case .constantColor: .blendColor
|
||||||
|
case .oneMinusConstantColor: .oneMinusBlendColor
|
||||||
|
case .constantAlpha: .blendAlpha
|
||||||
|
case .oneMinusConstantAlpha: .oneMinusBlendAlpha
|
||||||
|
*/
|
||||||
|
case .src1Color: .source1Color
|
||||||
|
case .oneMinusSrc1Color: .oneMinusSource1Color
|
||||||
|
case .src1Alpha: .source1Alpha
|
||||||
|
case .oneMinusSrc1Alpha: .oneMinusSource1Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ destination: BlendFuncDestinationFactor) {
|
||||||
|
self = switch destination {
|
||||||
|
case .zero: .zero
|
||||||
|
case .one: .one
|
||||||
|
case .srcColor: .sourceColor
|
||||||
|
case .oneMinusSrcColor: .oneMinusSourceColor
|
||||||
|
case .srcAlpha: .sourceAlpha
|
||||||
|
case .oneMinusSrcAlpha: .oneMinusSourceAlpha
|
||||||
|
case .dstColor: .destinationColor
|
||||||
|
case .oneMinusDstColor: .oneMinusDestinationColor
|
||||||
|
case .dstAlpha: .destinationAlpha
|
||||||
|
case .oneMinusDstAlpha: .oneMinusDestinationAlpha
|
||||||
|
/*
|
||||||
|
case .constantColor: .blendColor
|
||||||
|
case .oneMinusConstantColor: .oneMinusBlendColor
|
||||||
|
case .constantAlpha: .blendAlpha
|
||||||
|
case .oneMinusConstantAlpha: .oneMinusBlendAlpha
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum RendererError: Error {
|
enum RendererError: Error {
|
||||||
case initFailure(_ message: String)
|
case initFailure(_ message: String)
|
||||||
case loadFailure(_ message: String)
|
case loadFailure(_ message: String)
|
||||||
|
Loading…
Reference in New Issue
Block a user