mirror of
				https://github.com/GayPizzaSpecifications/voxelotl-engine.git
				synced 2025-11-04 02:59:37 +00:00 
			
		
		
		
	sRGB aware framebuffer & colour handling
This commit is contained in:
		@ -10,6 +10,7 @@ add_executable(Voxelotl MACOSX_BUNDLE
 | 
			
		||||
  Matrix4x4.swift
 | 
			
		||||
  Rectangle.swift
 | 
			
		||||
  AABB.swift
 | 
			
		||||
  Color.swift
 | 
			
		||||
 | 
			
		||||
  NSImageLoader.swift
 | 
			
		||||
  Renderer.swift
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										183
									
								
								Sources/Voxelotl/Color.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								Sources/Voxelotl/Color.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,183 @@
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
public struct Color<T: SIMDScalar>: Equatable {
 | 
			
		||||
  private var _values: SIMD4<T>
 | 
			
		||||
 | 
			
		||||
  internal var values: SIMD4<T> { self._values }
 | 
			
		||||
 | 
			
		||||
  public init(r newR: T, g newG: T, b newB: T, a newA: T) {
 | 
			
		||||
    self._values = .init(newR, newG, newB, newA)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @inline(__always) public var r: T { get { self._values.x } set(newR) { self._values.x = newR } }
 | 
			
		||||
  @inline(__always) public var g: T { get { self._values.y } set(newG) { self._values.y = newG } }
 | 
			
		||||
  @inline(__always) public var b: T { get { self._values.z } set(newB) { self._values.z = newB } }
 | 
			
		||||
  @inline(__always) public var a: T { get { self._values.w } set(newA) { self._values.w = newA } }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Sadly doesn't seem to be a better way to do this generically at the moment
 | 
			
		||||
public extension Color where T: AdditiveArithmetic {
 | 
			
		||||
  @inline(__always) static var zero: T { T.zero }
 | 
			
		||||
}
 | 
			
		||||
public extension Color where T: FixedWidthInteger {
 | 
			
		||||
  static var black: Self   { .init(r: zero, g: zero, b: zero, a: one) }
 | 
			
		||||
  static var white: Self   { .init(r:  one, g:  one, b:  one, a: one) }
 | 
			
		||||
  static var red: Self     { .init(r:  one, g: zero, b: zero, a: one) }
 | 
			
		||||
  static var green: Self   { .init(r: zero, g:  one, b: zero, a: one) }
 | 
			
		||||
  static var blue: Self    { .init(r: zero, g: zero, b:  one, a: one) }
 | 
			
		||||
  static var yellow: Self  { .init(r:  one, g:  one, b: zero, a: one) }
 | 
			
		||||
  static var magenta: Self { .init(r:  one, g: zero, b:  one, a: one) }
 | 
			
		||||
  static var cyan: Self    { .init(r: zero, g:  one, b:  one, a: one) }
 | 
			
		||||
}
 | 
			
		||||
public extension Color where T: BinaryFloatingPoint {
 | 
			
		||||
  static var black: Self   { .init(r: zero, g: zero, b: zero, a: one) }
 | 
			
		||||
  static var white: Self   { .init(r:  one, g:  one, b:  one, a: one) }
 | 
			
		||||
  static var red: Self     { .init(r:  one, g: zero, b: zero, a: one) }
 | 
			
		||||
  static var green: Self   { .init(r: zero, g:  one, b: zero, a: one) }
 | 
			
		||||
  static var blue: Self    { .init(r: zero, g: zero, b:  one, a: one) }
 | 
			
		||||
  static var yellow: Self  { .init(r:  one, g:  one, b: zero, a: one) }
 | 
			
		||||
  static var magenta: Self { .init(r:  one, g: zero, b:  one, a: one) }
 | 
			
		||||
  static var cyan: Self    { .init(r: zero, g:  one, b:  one, a: one) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension Color where T: FixedWidthInteger {
 | 
			
		||||
  @inline(__always) static var one: T { T.max }
 | 
			
		||||
 | 
			
		||||
  init(r newR: T, g newG: T, b newB: T) {
 | 
			
		||||
    self.init(r: newR, g: newG, b: newB, a: Self.one)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init<U: BinaryFloatingPoint>(_ other: Color<U>) {
 | 
			
		||||
    self.init(
 | 
			
		||||
      r: T((other.r * U(0xFF)).clamp(0, 0xFF)),
 | 
			
		||||
      g: T((other.g * U(0xFF)).clamp(0, 0xFF)),
 | 
			
		||||
      b: T((other.b * U(0xFF)).clamp(0, 0xFF)),
 | 
			
		||||
      a: T((other.a * U(0xFF)).clamp(0, 0xFF)))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func mix(_ other: Self, _ n: Float) -> Self {
 | 
			
		||||
    Self(Color<Float>(self).mix(Color<Float>(other), n.saturated))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var linear: Self { Self(Color<Float>(self).linear) }
 | 
			
		||||
  var sRGB: Self { Self(Color<Float>(self).sRGB) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension Color where T == UInt8 {
 | 
			
		||||
  init(rgba8888 c: UInt32) {
 | 
			
		||||
    self.init(
 | 
			
		||||
    r: UInt8((c & 0xFF000000) >> 24),
 | 
			
		||||
    g: UInt8((c & 0x00FF0000) >> 16),
 | 
			
		||||
    b: UInt8((c & 0x0000FF00) >>  8),
 | 
			
		||||
    a: UInt8((c & 0x000000FF) >>  0))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(rgb888 c: UInt32) {
 | 
			
		||||
    self.init(
 | 
			
		||||
      r: UInt8((c & 0xFF0000) >> 16),
 | 
			
		||||
      g: UInt8((c & 0x00FF00) >>  8),
 | 
			
		||||
      b: UInt8((c & 0x0000FF) >>  0))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var sRGB: Self {
 | 
			
		||||
    return Self(
 | 
			
		||||
      r: sRGB8FromLinear8Table[Int(r)],
 | 
			
		||||
      g: sRGB8FromLinear8Table[Int(g)],
 | 
			
		||||
      b: sRGB8FromLinear8Table[Int(b)],
 | 
			
		||||
      a: a)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fileprivate var sRGB8FromLinear8Table: [UInt8] = {
 | 
			
		||||
  (0..<0x100).map { i in
 | 
			
		||||
    var x = Double(i) / 0xFF
 | 
			
		||||
    if x < 0.0031308 {
 | 
			
		||||
      x *= 12.92
 | 
			
		||||
    } else {
 | 
			
		||||
      x = 1.055 * pow(abs(x), 1 / 2.4) - 0.055
 | 
			
		||||
    }
 | 
			
		||||
    x = floor((x * 0xFF) + 0.5)
 | 
			
		||||
    return UInt8(truncating: NSNumber(value: x))
 | 
			
		||||
  }
 | 
			
		||||
}()
 | 
			
		||||
 | 
			
		||||
public extension Color where T: BinaryFloatingPoint {
 | 
			
		||||
  @inline(__always) static var one: T { T(1) }
 | 
			
		||||
 | 
			
		||||
  init(r newR: T, g newG: T, b newB: T) {
 | 
			
		||||
    self.init(r: newR, g: newG, b: newB, a: Self.one)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init<U: BinaryInteger>(_ other: Color<U>) {
 | 
			
		||||
    let mul = 1 / T(0xFF)
 | 
			
		||||
    self.init(
 | 
			
		||||
      r: T(other.r) * mul,
 | 
			
		||||
      g: T(other.g) * mul,
 | 
			
		||||
      b: T(other.b) * mul,
 | 
			
		||||
      a: T(other.a) * mul)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(rgba8888 c: UInt32) {
 | 
			
		||||
    self.init(Color<UInt8>(rgba8888: c))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(rgb888 c: UInt32) {
 | 
			
		||||
    self.init(Color<UInt8>(rgb888: c))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  func mix(_ other: Self, _ n: T) -> Self{
 | 
			
		||||
    let x = n.saturated
 | 
			
		||||
    return .init(
 | 
			
		||||
      r: x.lerp(r, other.r),
 | 
			
		||||
      g: x.lerp(g, other.g),
 | 
			
		||||
      b: x.lerp(b, other.b),
 | 
			
		||||
      a: x.lerp(a, other.a))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var linear: Self {
 | 
			
		||||
    Self(
 | 
			
		||||
      r: linearFromSRGB(r),
 | 
			
		||||
      g: linearFromSRGB(g),
 | 
			
		||||
      b: linearFromSRGB(b),
 | 
			
		||||
      a: a)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var sRGB: Self {
 | 
			
		||||
    Self(
 | 
			
		||||
      r: sRGBFromLinear(r),
 | 
			
		||||
      g: sRGBFromLinear(g),
 | 
			
		||||
      b: sRGBFromLinear(b),
 | 
			
		||||
      a: a)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @inline(__always) fileprivate func linearFromSRGB(_ x: T) -> T {
 | 
			
		||||
    if x < 0.04045 {
 | 
			
		||||
      x / 12.92
 | 
			
		||||
    } else {
 | 
			
		||||
      T(pow((Double(x) + 0.055) / 1.055, 2.4))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @inline(__always) fileprivate func sRGBFromLinear(_ x: T) -> T {
 | 
			
		||||
    if x.isNaN || x <= 0 {
 | 
			
		||||
      0
 | 
			
		||||
    } else if x >= 1 {
 | 
			
		||||
      1
 | 
			
		||||
    } else if x < 0.0031308 {
 | 
			
		||||
      x * 12.92
 | 
			
		||||
    } else {
 | 
			
		||||
#if false
 | 
			
		||||
      // Approximation
 | 
			
		||||
      1.13005 * sqrt(abs(x - 0.00228)) - 0.13448 * x + 0.005719
 | 
			
		||||
#else
 | 
			
		||||
      T(1.055 * pow(Double(abs(x)), 1 / 2.4) - 0.055)
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public extension SIMD4 {
 | 
			
		||||
  init(_ other: Color<Scalar>) {
 | 
			
		||||
    self = other.values
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -6,5 +6,5 @@ public extension FloatingPoint {
 | 
			
		||||
  @inline(__always) func mlerp(_ a: Self, _ b: Self) -> Self { a + (b - a) * self }
 | 
			
		||||
 | 
			
		||||
  @inline(__always) func clamp(_ a: Self, _ b: Self) -> Self { min(max(self, a), b) }
 | 
			
		||||
  @inline(__always) func saturate() -> Self { self.clamp(0, 1) }
 | 
			
		||||
  @inline(__always) var saturated: Self { self.clamp(0, 1) }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,20 +2,20 @@ import simd
 | 
			
		||||
 | 
			
		||||
struct Box {
 | 
			
		||||
  var geometry: AABB
 | 
			
		||||
  var color: SIMD4<Float> = .one
 | 
			
		||||
  var color: Color<Float16> = .white
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Instance {
 | 
			
		||||
  let position: SIMD3<Float>
 | 
			
		||||
  let scale: SIMD3<Float>
 | 
			
		||||
  let rotation: simd_quatf
 | 
			
		||||
  let color: SIMD4<Float>
 | 
			
		||||
  let color: Color<Float16>
 | 
			
		||||
 | 
			
		||||
  init(
 | 
			
		||||
    position: SIMD3<Float> = .zero,
 | 
			
		||||
    scale: SIMD3<Float> = .one,
 | 
			
		||||
    rotation: simd_quatf = .identity,
 | 
			
		||||
    color: SIMD4<Float> = .one
 | 
			
		||||
    color: Color<Float16> = .white
 | 
			
		||||
  ) {
 | 
			
		||||
    self.position = position
 | 
			
		||||
    self.scale = scale
 | 
			
		||||
@ -25,13 +25,20 @@ struct Instance {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let boxes: [Box] = [
 | 
			
		||||
  Box(geometry: .fromUnitCube(position: .init(0, -1, 0) * 2, scale: .init(10, 0.1, 10) * 2)),
 | 
			
		||||
  Box(geometry: .fromUnitCube(position: .init(-2.5, 0, -3) * 2, scale: .init(repeating: 2)), color: .init(1, 0.5, 0.75, 1)),
 | 
			
		||||
  Box(geometry: .fromUnitCube(position: .init(-2.5, -0.5, -5) * 2, scale: .init(repeating: 2)), color: .init(0.75, 1, 1, 1))
 | 
			
		||||
  Box(geometry: .fromUnitCube(
 | 
			
		||||
    position: .init( 0,  -1,  0) * 2,
 | 
			
		||||
    scale:    .init(10, 0.1, 10) * 2)),
 | 
			
		||||
  Box(geometry: .fromUnitCube(
 | 
			
		||||
    position: .init(-2.5, 0, -3) * 2,
 | 
			
		||||
    scale:    .init(repeating: 2)),
 | 
			
		||||
    color:    .init(rgb888: 0xFF80BF).linear),
 | 
			
		||||
  Box(geometry: .fromUnitCube(
 | 
			
		||||
    position: .init(-2.5, -0.5, -5) * 2,
 | 
			
		||||
    scale:    .init(repeating: 2)),
 | 
			
		||||
    color:    .init(rgb888: 0xBFFFFF).linear)
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
class Game: GameDelegate {
 | 
			
		||||
 | 
			
		||||
  private var fpsCalculator = FPSCalculator()
 | 
			
		||||
  var camera = Camera(fov: 60, size: .one, range: 0.06...50)
 | 
			
		||||
  var player = Player()
 | 
			
		||||
@ -61,15 +68,15 @@ class Game: GameDelegate {
 | 
			
		||||
    var instances: [Instance] = boxes.map {
 | 
			
		||||
      Instance(
 | 
			
		||||
        position: $0.geometry.center,
 | 
			
		||||
        scale: $0.geometry.size * 0.5,
 | 
			
		||||
        color: $0.color)
 | 
			
		||||
        scale:    $0.geometry.size * 0.5,
 | 
			
		||||
        color:    $0.color)
 | 
			
		||||
    }
 | 
			
		||||
    instances.append(
 | 
			
		||||
      Instance(
 | 
			
		||||
        position: .init(0, sin(totalTime * 1.5) * 0.5, -2) * 2,
 | 
			
		||||
        scale: .init(repeating: 0.5),
 | 
			
		||||
        scale:    .init(repeating: 0.5),
 | 
			
		||||
        rotation: .init(angle: totalTime * 3.0, axis: .init(0, 1, 0)),
 | 
			
		||||
        color: .init(0.5, 0.5, 1, 1)))
 | 
			
		||||
        color:    .init(r: 0.5, g: 0.5, b: 1).linear))
 | 
			
		||||
    renderer.batch(instances: instances, camera: self.camera)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -41,7 +41,9 @@ fileprivate let cubeIndices: [UInt16] = [
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
fileprivate let numFramesInFlight: Int = 3
 | 
			
		||||
fileprivate let colorFormat: MTLPixelFormat = .bgra8Unorm_srgb
 | 
			
		||||
fileprivate let depthFormat: MTLPixelFormat = .depth16Unorm
 | 
			
		||||
fileprivate let clearColor: Color<Double> = .black.mix(.white, 0.1).linear
 | 
			
		||||
 | 
			
		||||
public class Renderer {
 | 
			
		||||
  private var device: MTLDevice
 | 
			
		||||
@ -86,7 +88,7 @@ public class Renderer {
 | 
			
		||||
    self.device = device
 | 
			
		||||
 | 
			
		||||
    layer.device = device
 | 
			
		||||
    layer.pixelFormat = MTLPixelFormat.bgra8Unorm
 | 
			
		||||
    layer.pixelFormat = colorFormat
 | 
			
		||||
 | 
			
		||||
    // Setup command queue
 | 
			
		||||
    guard let queue = device.makeCommandQueue() else {
 | 
			
		||||
@ -99,7 +101,7 @@ public class Renderer {
 | 
			
		||||
 | 
			
		||||
    passDescription.colorAttachments[0].loadAction  = .clear
 | 
			
		||||
    passDescription.colorAttachments[0].storeAction = .store
 | 
			
		||||
    passDescription.colorAttachments[0].clearColor  = MTLClearColorMake(0.1, 0.1, 0.1, 1.0)
 | 
			
		||||
    passDescription.colorAttachments[0].clearColor  = MTLClearColor(clearColor)
 | 
			
		||||
    passDescription.depthAttachment.loadAction  = .clear
 | 
			
		||||
    passDescription.depthAttachment.storeAction = .dontCare
 | 
			
		||||
    passDescription.depthAttachment.clearDepth  = 1.0
 | 
			
		||||
@ -349,11 +351,7 @@ public class Renderer {
 | 
			
		||||
          .translate(instance.position) *
 | 
			
		||||
          matrix_float4x4(instance.rotation) *
 | 
			
		||||
          .scale(instance.scale),
 | 
			
		||||
        color: .init(
 | 
			
		||||
          UInt8(truncating: NSNumber(value: instance.color.x * 0xFF)),
 | 
			
		||||
          UInt8(truncating: NSNumber(value: instance.color.y * 0xFF)),
 | 
			
		||||
          UInt8(truncating: NSNumber(value: instance.color.z * 0xFF)),
 | 
			
		||||
          UInt8(truncating: NSNumber(value: instance.color.w * 0xFF))))
 | 
			
		||||
        color: SIMD4(Color<UInt8>(instance.color)))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ideal as long as our uniforms total 4 KB or less
 | 
			
		||||
@ -374,6 +372,12 @@ public class Renderer {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
extension MTLClearColor {
 | 
			
		||||
  init(_ color: Color<Double>) {
 | 
			
		||||
    self.init(red: color.r, green: color.g, blue: color.b, alpha: color.a)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum RendererError: Error {
 | 
			
		||||
  case initFailure(_ message: String)
 | 
			
		||||
  case loadFailure(_ message: String)
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user