diff --git a/Sources/Voxelotl/CMakeLists.txt b/Sources/Voxelotl/CMakeLists.txt index 8dfaf8f..a641177 100644 --- a/Sources/Voxelotl/CMakeLists.txt +++ b/Sources/Voxelotl/CMakeLists.txt @@ -1,11 +1,13 @@ -add_executable(Voxelotl MACOSX_BUNDLE +set(SOURCES # Resources Assets.xcassets test.png + wireshark.png # Shaders shadertypes.h shader.metal + shader2D.metal # Common utility library Common/ConcurrentDictionary.swift @@ -48,6 +50,8 @@ add_executable(Voxelotl MACOSX_BUNDLE Renderer/ModelBatch.swift Renderer/BlendMode.swift Renderer/BlendFunc.swift + Renderer/Sprite.swift + Renderer/SpriteBatch.swift Renderer/ChunkRenderer.swift Renderer/Metal/BlendFuncExtension.swift Renderer/Metal/ColorExtension.swift @@ -55,6 +59,8 @@ add_executable(Voxelotl MACOSX_BUNDLE Renderer/Metal/PipelineOptions.swift Renderer/Metal/Shader.swift Renderer/Metal/RendererMesh.swift + Renderer/Metal/RendererDynamicMesh.swift + Renderer/Metal/RendererTexture2D.swift Renderer/RendererError.swift Renderer/Renderer.swift @@ -80,6 +86,7 @@ add_executable(Voxelotl MACOSX_BUNDLE Camera.swift Player.swift Game.swift + SpriteTestGame.swift # Core application classes GameDelegate.swift @@ -90,12 +97,16 @@ add_executable(Voxelotl MACOSX_BUNDLE main.m ) -set_source_files_properties( - shader.metal PROPERTIES - LANGUAGE METAL - COMPILE_OPTIONS "-I${PROJECT_SOURCE_DIR}" -) +foreach (SOURCE IN LISTS SOURCES) + if (SOURCE MATCHES "\\.metal$") + set_source_files_properties( + "${SOURCE}" PROPERTIES + LANGUAGE METAL + COMPILE_OPTIONS "-I${PROJECT_SOURCE_DIR}") + endif() +endforeach() +add_executable(Voxelotl MACOSX_BUNDLE ${SOURCES}) target_include_directories(Voxelotl PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(Voxelotl PRIVATE SDLSwift) target_compile_definitions(Voxelotl PRIVATE $<$:DEBUG>) @@ -140,7 +151,11 @@ endif() set_source_files_properties(Assets.xcassets PROPERTIES MACOSX_PACKAGE_LOCATION Resources) set_source_files_properties(module.modulemap PROPERTIES MACOSX_PACKAGE_LOCATION Modules) -set_source_files_properties(test.png PROPERTIES MACOSX_PACKAGE_LOCATION Resources) +foreach (RESOURCE IN LISTS SOURCES) + if (RESOURCE MATCHES "\\.png$") + set_source_files_properties("${RESOURCE}" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + endif() +endforeach() #TODO: should use TREE mode as documented in https://cmake.org/cmake/help/latest/command/source_group.html source_group("Resources" FILES Assets.xcassets test.png) diff --git a/Sources/Voxelotl/GameDelegate.swift b/Sources/Voxelotl/GameDelegate.swift index 61fdcf2..69197f9 100644 --- a/Sources/Voxelotl/GameDelegate.swift +++ b/Sources/Voxelotl/GameDelegate.swift @@ -22,3 +22,9 @@ extension Duration { Double(components.seconds) + Double(components.attoseconds) * 1e-18 } } + +extension Float { + public init(_ value: Duration) { + self = Float(value.asFloat) + } +} diff --git a/Sources/Voxelotl/Program.swift b/Sources/Voxelotl/Program.swift index 576e156..c0dc6b8 100644 --- a/Sources/Voxelotl/Program.swift +++ b/Sources/Voxelotl/Program.swift @@ -5,7 +5,7 @@ import Foundation Thread.current.qualityOfService = .userInteractive let app = Application( - delegate: Game(), + delegate: SpriteTestGame(), configuration: ApplicationConfiguration( frame: Size(1280, 720), title: "Voxelotl Demo", diff --git a/Sources/Voxelotl/Renderer/Mesh.swift b/Sources/Voxelotl/Renderer/Mesh.swift index 0964d28..0ceff9c 100644 --- a/Sources/Voxelotl/Renderer/Mesh.swift +++ b/Sources/Voxelotl/Renderer/Mesh.swift @@ -21,3 +21,9 @@ public struct VertexPositionNormalColorTexcoord: Vertex { var color: SIMD4 var texCoord: SIMD2 } + +public struct VertexPosition2DTexcoordColor: Vertex { + var position: SIMD2 + var texCoord: SIMD2 + var color: SIMD4 +} diff --git a/Sources/Voxelotl/Renderer/Metal/RendererDynamicMesh.swift b/Sources/Voxelotl/Renderer/Metal/RendererDynamicMesh.swift new file mode 100644 index 0000000..ac8ee42 --- /dev/null +++ b/Sources/Voxelotl/Renderer/Metal/RendererDynamicMesh.swift @@ -0,0 +1,70 @@ +import Metal + +public struct RendererDynamicMesh { + private weak var _renderer: Renderer! + internal let _vertBufs: [MTLBuffer], _idxBufs: [MTLBuffer] + private var _numVertices: Int = 0, _numIndices: Int = 0 + + public let vertexCapacity: Int, indexCapacity: Int + public var vertexCount: Int { self._numVertices } + public var indexCount: Int { self._numIndices } + + init(renderer: Renderer, _ vertBufs: [MTLBuffer], _ idxBufs: [MTLBuffer]) { + self._renderer = renderer + self._vertBufs = vertBufs + self._idxBufs = idxBufs + self.vertexCapacity = self._vertBufs.map { $0.length }.min()! / MemoryLayout.stride + self.indexCapacity = self._idxBufs.map { $0.length }.min()! / MemoryLayout.stride + } + + public mutating func clear() { + self._numVertices = 0 + self._numIndices = 0 + } + + + public mutating func insert(vertices: [VertexType]) { + self.insert(vertices: vertices[...]) + } + + public mutating func insert(vertices: ArraySlice) { + assert(self._numVertices + vertices.count < self.vertexCapacity) + + let vertexBuffer: MTLBuffer = self._vertBufs[self._renderer.currentFrame] + vertexBuffer.contents().withMemoryRebound(to: VertexType.self, capacity: self.vertexCapacity) { vertexData in + for i in 0...stride + vertexBuffer.didModifyRange(stride * self._numVertices...stride + indexBuffer.didModifyRange(stride * self._numIndices.. + + internal init(metalTexture: MTLTexture, size: Size) { + self._textureBuffer = metalTexture + self.size = size + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs._textureBuffer.gpuResourceID._impl == rhs._textureBuffer.gpuResourceID._impl && lhs.size == rhs.size + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self._textureBuffer.hash) + } +} + diff --git a/Sources/Voxelotl/Renderer/Renderer.swift b/Sources/Voxelotl/Renderer/Renderer.swift index 28782a5..cd4dc08 100644 --- a/Sources/Voxelotl/Renderer/Renderer.swift +++ b/Sources/Voxelotl/Renderer/Renderer.swift @@ -19,7 +19,7 @@ public class Renderer { private var _defaultShader: Shader, _shader2D: Shader private let passDescription = MTLRenderPassDescriptor() private var _psos: [PipelineOptions: MTLRenderPipelineState] - private var depthStencilState: MTLDepthStencilState + private var _depthStencilEnabled: MTLDepthStencilState, _depthStencilDisabled: MTLDepthStencilState private let _defaultStorageMode: MTLResourceOptions private var depthTextures: [MTLTexture] @@ -28,11 +28,14 @@ public class Renderer { private var _encoder: MTLRenderCommandEncoder! = nil - private var defaultTexture: MTLTexture - private var cubeTexture: MTLTexture? = nil + private var defaultTexture: RendererTexture2D + private var cubeTexture: RendererTexture2D? = nil private let inFlightSemaphore = DispatchSemaphore(value: numFramesInFlight) - private var currentFrame = 0 + private var _currentFrame = 0 + + internal var currentFrame: Int { self._currentFrame } + internal var isManagedStorage: Bool { self._defaultStorageMode == .storageModeManaged } var frame: Rect { .init(origin: .zero, size: self.backBufferSize) } var aspectRatio: Float { self._aspectRatio } @@ -101,15 +104,16 @@ public class Renderer { return depthStencilTexture } - //self._instances = [MTLBuffer?](repeating: nil, count: numFramesInFlight) - let stencilDepthDescription = MTLDepthStencilDescriptor() stencilDepthDescription.depthCompareFunction = .less // OpenGL default stencilDepthDescription.isDepthWriteEnabled = true - guard let depthStencilState = device.makeDepthStencilState(descriptor: stencilDepthDescription) else { + guard let depthStencilEnabled = device.makeDepthStencilState(descriptor: stencilDepthDescription), + let depthStencilDisabled = device.makeDepthStencilState(descriptor: MTLDepthStencilDescriptor()) + else { throw RendererError.initFailure("Failed to create depth stencil state") } - self.depthStencilState = depthStencilState + self._depthStencilEnabled = depthStencilEnabled + self._depthStencilDisabled = depthStencilDisabled // Create shader library & grab functions do { @@ -226,9 +230,51 @@ public class Renderer { } } + internal func createDynamicMesh( + vertexCapacity: Int, indexCapacity: Int + ) -> RendererDynamicMesh? { + let vertexBuffers: [MTLBuffer], indexBuffers: [MTLBuffer] + do { + let byteCapacity = MemoryLayout.stride * vertexCapacity + vertexBuffers = try Self.createDynamicBuffer(self.device, capacity: byteCapacity, self._defaultStorageMode) + } catch { + printErr("Failed to create vertex buffer") + return nil + } + do { + let byteCapacity = MemoryLayout.stride * indexCapacity + indexBuffers = try Self.createDynamicBuffer(self.device, capacity: byteCapacity, self._defaultStorageMode) + } catch { + printErr("Failed to create index buffer") + return nil + } + return .init(renderer: self, vertexBuffers, indexBuffers) + } + + private static func createDynamicBuffer(_ device: MTLDevice, capacity: Int, _ transitoryOpt: MTLResourceOptions + ) throws -> [MTLBuffer] { + try autoreleasepool { + try (0.. RendererTexture2D? { + do { + return try Self.loadTexture(self.device, self.queue, resourcePath: path, self._defaultStorageMode) + } catch { + printErr(error) + return nil + } + } + static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, resourcePath path: String, _ transitoryOpt: MTLResourceOptions - ) throws -> MTLTexture { + ) throws -> RendererTexture2D { do { return try loadTexture(device, queue, url: Bundle.main.getResource(path), transitoryOpt) } catch ContentError.resourceNotFound(let message) { @@ -238,7 +284,7 @@ public class Renderer { static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, url imageUrl: URL, _ transitoryOpt: MTLResourceOptions - ) throws -> MTLTexture { + ) throws -> RendererTexture2D { do { return try loadTexture(device, queue, image2D: try NSImageLoader.open(url: imageUrl), transitoryOpt) } catch ImageLoaderError.openFailed(let message) { @@ -248,7 +294,7 @@ public class Renderer { static func loadTexture(_ device: MTLDevice, _ queue: MTLCommandQueue, image2D image: Image2D, _ transitoryOpt: MTLResourceOptions - ) throws -> MTLTexture { + ) throws -> RendererTexture2D { try autoreleasepool { let texDesc = MTLTextureDescriptor() texDesc.width = image.width @@ -292,7 +338,7 @@ public class Renderer { } cmdBuffer.commit() - return newTexture + return .init(metalTexture: newTexture, size: .init(image.width, image.height)) } } @@ -348,7 +394,7 @@ public class Renderer { passDescription.colorAttachments[0].clearColor = MTLClearColor(self._clearColor) passDescription.colorAttachments[0].texture = rt.texture - passDescription.depthAttachment.texture = self.depthTextures[self.currentFrame] + passDescription.depthAttachment.texture = self.depthTextures[self._currentFrame] // Lock the semaphore here if too many frames are "in flight" _ = inFlightSemaphore.wait(timeout: .distantFuture) @@ -366,8 +412,7 @@ public class Renderer { encoder.setFrontFacing(.counterClockwise) // OpenGL default encoder.setViewport(Self.makeViewport(rect: self.frame)) - encoder.setDepthStencilState(depthStencilState) - encoder.setFragmentTexture(cubeTexture ?? defaultTexture, index: 0) + encoder.setFragmentTexture(cubeTexture?._textureBuffer ?? defaultTexture._textureBuffer, index: 0) self._encoder = encoder frameFunc(self) @@ -377,9 +422,9 @@ public class Renderer { commandBuf.present(rt) commandBuf.commit() - self.currentFrame &+= 1 - if self.currentFrame == numFramesInFlight { - self.currentFrame = 0 + self._currentFrame &+= 1 + if self._currentFrame == numFramesInFlight { + self._currentFrame = 0 } } } @@ -388,6 +433,10 @@ public class Renderer { return ModelBatch(self) } + func createSpriteBatch() -> SpriteBatch { + return SpriteBatch(self) + } + internal func setupBatch(environment: Environment, camera: Camera) { assert(self._encoder != nil, "setupBatch can't be called outside of a frame being rendered") @@ -404,6 +453,7 @@ public class Renderer { self._cameraPos = camera.position self._directionalDir = simd_normalize(environment.lightDirection) + self._encoder.setDepthStencilState(self._depthStencilEnabled) self._encoder.setCullMode(.init(environment.cullFace)) // Ideal as long as our uniforms total 4 KB or less @@ -412,6 +462,50 @@ public class Renderer { index: VertexShaderInputIdx.uniforms.rawValue) } + internal func setupBatch(blendMode: BlendMode, frame: Rect) { + 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._shader2D, blendFunc: blendMode.function)) + } catch { + printErr(error) + } + + self._encoder.setDepthStencilState(self._depthStencilDisabled) + self._encoder.setCullMode(.none) + + var uniforms = Shader2DUniforms(projection: .orthographic( + left: frame.left, right: frame.right, + bottom: frame.down, top: frame.up, + near: 1, far: -1)) + + // Ideal as long as our uniforms total 4 KB or less + self._encoder.setVertexBytes(&uniforms, + length: MemoryLayout.stride, + index: VertexShaderInputIdx.uniforms.rawValue) + } + + internal func submit( + mesh: RendererDynamicMesh, + texture: RendererTexture2D?, + offset: Int, count: Int + ) { + assert(self._encoder != nil, "submit can't be called outside of a frame being rendered") + + self._encoder.setFragmentTexture(texture?._textureBuffer ?? defaultTexture._textureBuffer, index: 0) + self._encoder.setVertexBuffer(mesh._vertBufs[self._currentFrame], + offset: 0, + index: VertexShaderInputIdx.vertices.rawValue) + self._encoder.drawIndexedPrimitives( + type: .triangle, + indexCount: count, + indexType: .uint16, // Careful! + indexBuffer: mesh._idxBufs[self._currentFrame], + indexBufferOffset: MemoryLayout.stride * offset) + } + internal func submit(mesh: RendererMesh, instance: ModelBatch.Instance, material: Material) { assert(self._encoder != nil, "submit can't be called outside of a frame being rendered") var instanceData = VertexShaderInstance( @@ -452,16 +546,16 @@ public class Renderer { let instancesBytes = numInstances * MemoryLayout.stride // (Re)create instance buffer if needed - if self._instances[self.currentFrame] == nil || instancesBytes > self._instances[self.currentFrame]!.length { + if self._instances[self._currentFrame] == nil || instancesBytes > self._instances[self._currentFrame]!.length { guard let instanceBuffer = self.device.makeBuffer( length: instancesBytes, options: self._defaultStorageMode) else { fatalError("Failed to (re)create instance buffer") } - self._instances[self.currentFrame] = instanceBuffer + self._instances[self._currentFrame] = instanceBuffer } - let instanceBuffer = self._instances[self.currentFrame]! + let instanceBuffer = self._instances[self._currentFrame]! // Convert & upload instance data to the GPU //FIXME: currently will misbehave if batch is called more than once diff --git a/Sources/Voxelotl/Renderer/Sprite.swift b/Sources/Voxelotl/Renderer/Sprite.swift new file mode 100644 index 0000000..d3a5653 --- /dev/null +++ b/Sources/Voxelotl/Renderer/Sprite.swift @@ -0,0 +1,22 @@ +public struct Sprite { + public struct Flip: OptionSet { + public let rawValue: UInt16 + public init(rawValue: UInt16) { + self.rawValue = rawValue + } + + public static let none: Self = Self(rawValue: 0) + public static let x: Self = Self(rawValue: 1 << 0) + public static let y: Self = Self(rawValue: 1 << 1) + } + + var texture: RendererTexture2D + var position: SIMD2 + var scale: SIMD2 + var origin: SIMD2 + var shear: SIMD2 = .zero + var angle: Float + var depth: Int + var flip: Flip + var color: Color +} diff --git a/Sources/Voxelotl/Renderer/SpriteBatch.swift b/Sources/Voxelotl/Renderer/SpriteBatch.swift new file mode 100644 index 0000000..ef22ea4 --- /dev/null +++ b/Sources/Voxelotl/Renderer/SpriteBatch.swift @@ -0,0 +1,324 @@ +import simd + +public typealias Affine2D = simd_float2x2 + +public struct SpriteBatch { + public typealias VertexType = VertexPosition2DTexcoordColor + public typealias IndexType = UInt16 + + private weak var _renderer: Renderer! + private var _active = ActiveState.inactive + private var _blendMode = BlendMode.none + + private var _mesh: RendererDynamicMesh + private var _instances = [SpriteInstance]() + + public var viewport: Rect? = nil + + internal init(_ renderer: Renderer) { + self._renderer = renderer + self._mesh = renderer.createDynamicMesh(vertexCapacity: 4096, indexCapacity: 4096)! + } + + //MARK: - Public API + + //TODO: Sort + //FIXME: currently will misbehave if begin is called more than once per frame + public mutating func begin(blendMode: BlendMode = .normal) { + assert(self._active == .inactive, "call to SpriteBatch.begin without first calling end") + self._blendMode = blendMode + self._active = .begin + self._mesh.clear() + } + + public mutating func end() { + assert(self._active != .inactive, "call to SpriteBatch.end without first calling begin") + if !self._instances.isEmpty { + self.flush() + } + self._active = .inactive + } + + public mutating func draw(_ sprite: Sprite) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + fatalError("TODO") + } + + public mutating func draw(_ texture: RendererTexture2D, position: SIMD2) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + self.drawQuad(texture, position: position, size: Size(texture.size)) + } + + public mutating func draw(_ texture: RendererTexture2D, position: SIMD2, + scale: SIMD2, + angle: Float = 0.0, origin: SIMD2 = .zero, + flip: Sprite.Flip = .none, + color: Color = .white + //depth: Int = 0 + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let size = Size(texture.size) + let color = color.linear + if angle != 0 { + let bias = SIMD2(origin) / SIMD2(size) + self.drawQuad(texture, position: position, angle: angle, size: size * scale, offset: bias, color: color) + } else { + self.drawQuad(texture, position: position - origin, size: size * scale, color: color) + } + } + + public mutating func draw(_ texture: RendererTexture2D, position: SIMD2, + scale: Float = 1.0, + angle: Float = 0.0, origin: SIMD2 = .zero, + flip: Sprite.Flip = .none, + color: Color = .white + //depth: Int = 0 + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let size = Size(texture.size) + let color = color.linear + if angle != 0 { + let bias = SIMD2(origin) / SIMD2(size) + self.drawQuad(texture, position: position, angle: angle, size: size * Size(scalar: scale), offset: bias, color: color) + } else { + self.drawQuad(texture, position: position - origin, size: size * scale, color: color) + } + } + + public mutating func draw(_ texture: RendererTexture2D, destination: Rect?) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let rect = destination ?? self.viewport ?? Rect(self._renderer.frame) + self.drawQuad(texture, + p00: SIMD2(rect.left, rect.up), p10: SIMD2(rect.right, rect.up), + p01: SIMD2(rect.left, rect.down), p11: SIMD2(rect.right, rect.down)) + } + + public mutating func draw(_ texture: RendererTexture2D, transform: simd_float3x3) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let w = Float(texture.size.w), h = Float(texture.size.h) + self.drawQuad(texture, + p00: (transform * .init(0, 0, 1)).xy, + p10: (transform * .init(w, 0, 1)).xy, + p01: (transform * .init(0, h, 1)).xy, + p11: (transform * .init(w, h, 1)).xy) + } + + public mutating func draw(_ texture: RendererTexture2D, transform: simd_float3x3, + flip: Sprite.Flip = .none, + color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let w = Float(texture.size.w), h = Float(texture.size.h) + self.drawQuad(texture, + p00: (transform * .init(0, 0, 1)).xy, + p10: (transform * .init(w, 0, 1)).xy, + p01: (transform * .init(0, h, 1)).xy, + p11: (transform * .init(w, h, 1)).xy, + flip: flip, color: color.linear) + } + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, position: SIMD2) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let size = source.size + self.drawQuad(texture, source, + p00: .init(position.x, position.y), + p10: .init(position.x + size.w, position.y), + p01: .init(position.x, position.y + size.h), + p11: position + SIMD2(size)) + } + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, position: SIMD2, + scale: SIMD2, color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let size = source.size * scale + self.drawQuad(texture, source, + p00: .init(position.x, position.y), + p10: .init(position.x + size.w, position.y), + p01: .init(position.x, position.y + size.h), + p11: position + SIMD2(size), + color: color.linear) + } + public mutating func draw(_ texture: RendererTexture2D, source: Rect, position: SIMD2, + scale: Float = 1.0, color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let size = source.size * scale + self.drawQuad(texture, source, + p00: .init(position.x, position.y), + p10: .init(position.x + size.w, position.y), + p01: .init(position.x, position.y + size.h), + p11: position + SIMD2(size), + color: color.linear) + } + + //TODO: Everything + //public mutating func draw(_ texture: RendererTexture2D, source: Rect, position: SIMD2, scale: SIMD2, angle: Float = 0.0, origin: Point = .zero, flip: Sprite.Flip = .none, color: Color = .white, depth: Int = 0) { + //public mutating func draw(_ texture: RendererTexture2D, source: Rect, position: SIMD2, scale: Float = 1.0, angle: Float = 0.0, origin: Point = .zero, flip: Sprite.Flip = .none, color: Color = .white, depth: Int = 0) { + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, destination: Rect?) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let dst = destination ?? self.viewport ?? Rect(self._renderer.frame) + self.drawQuad(texture, source, + p00: SIMD2(dst.left, dst.up), p10: SIMD2(dst.right, dst.up), + p01: SIMD2(dst.left, dst.down), p11: SIMD2(dst.right, dst.down)) + } + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, destination: Rect?, + color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let dst = destination ?? self.viewport ?? Rect(self._renderer.frame) + self.drawQuad(texture, source, + p00: SIMD2(dst.left, dst.up), p10: SIMD2(dst.right, dst.up), + p01: SIMD2(dst.left, dst.down), p11: SIMD2(dst.right, dst.down), + color: color.linear) + } + + //TODO: Destination with rotation + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, transform: simd_float3x3) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let w = source.size.w, h = source.size.h + self.drawQuad(texture, source, + p00: (transform * .init(0, 0, 1)).xy, + p10: (transform * .init(w, 0, 1)).xy, + p01: (transform * .init(0, h, 1)).xy, + p11: (transform * .init(w, h, 1)).xy) + } + + public mutating func draw(_ texture: RendererTexture2D, source: Rect, transform: simd_float3x3, + flip: Sprite.Flip = .none, color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let w = source.size.w, h = source.size.h + self.drawQuad(texture, source, + p00: (transform * .init(0, 0, 1)).xy, + p10: (transform * .init(w, 0, 1)).xy, + p01: (transform * .init(0, h, 1)).xy, + p11: (transform * .init(w, h, 1)).xy, + color: color.linear) + } + + public mutating func draw(_ texture: RendererTexture2D, vertices: [VertexType]) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let base = self._mesh.vertexCount + self._mesh.insert(vertices: vertices) + self._mesh.insert(indices: (0..) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let base = self._mesh.vertexCount + self._mesh.insert(vertices: vertices) + self._mesh.insert(indices: (0..) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let base = self._mesh.vertexCount + self._mesh.insert(vertices: mesh.vertices) + self._mesh.insert(indices: mesh.indices, baseVertex: base) + self._instances.append(.init(texture: texture, size: UInt16(mesh.indices.count))) + } + + //MARK: - Private implementation + + private mutating func flush() { + assert(self._instances.count > 0) + + if self._active == .begin { + self._renderer.setupBatch(blendMode: self._blendMode, frame: self.viewport ?? .init(self._renderer.frame)) + self._active = .active + } + + var base = 0, offset = 0 + var prevTexture: RendererTexture2D! = nil + for instance in self._instances { + if prevTexture != nil && prevTexture != instance.texture { + self._renderer.submit(mesh: self._mesh, texture: prevTexture, offset: base, count: offset - base) + base = offset + } + offset += Int(instance.size) + prevTexture = instance.texture + } + self._renderer.submit(mesh: self._mesh, texture: prevTexture, offset: base, count: offset - base) + + self._instances.removeAll(keepingCapacity: true) + } + + private mutating func drawQuad(_ texture: RendererTexture2D, + position: SIMD2, size: Size, color: Color = .white + ) { + self.drawQuad(texture, + p00: position, + p10: .init(position.x + size.w, position.y), + p01: .init(position.x, position.y + size.h), + p11: .init(position.x + size.w, position.y + size.h), color: color) + } + + private mutating func drawQuad(_ texture: RendererTexture2D, + position: SIMD2, angle: Float, size: Size, offset bias: SIMD2, color: Color = .white + ) { + let (tc, ts) = (cos(angle), sin(angle)) + let rotate = matrix_float2x2( + .init( tc, ts), + .init(-ts, tc)) + let right = SIMD2(size.w, 0) * rotate + let down = SIMD2(0, size.h) * rotate + self.drawQuad(texture, + p00: position - right * bias.x - down * bias.y, + p10: position + right * (1 - bias.x) - down * bias.y, + p01: position - right * bias.x + down * (1 - bias.y), + p11: position + right * (1 - bias.x) + down * (1 - bias.y), color: color) + } + + private mutating func drawQuad(_ texture: RendererTexture2D, + p00: SIMD2, p10: SIMD2, p01: SIMD2, p11: SIMD2, + flip: Sprite.Flip, color: Color = .white + ) { + let flipX = flip.contains(.x), flipY = flip.contains(.y) + self.drawQuad(texture, p00: p00, p10: p10, p01: p01, p11: p11, + t00: .init(flipX ? 1 : 0, flipY ? 0 : 1), + t10: .init(flipX ? 0 : 1, flipY ? 0 : 1), + t01: .init(flipX ? 1 : 0, flipY ? 1 : 0), + t11: .init(flipX ? 0 : 1, flipY ? 1 : 0), + color: color) + } + + private mutating func drawQuad(_ texture: RendererTexture2D, _ source: Rect, + p00: SIMD2, p10: SIMD2, p01: SIMD2, p11: SIMD2, + color: Color = .white + ) { + let invSize = 1 / Size(texture.size) + let st = Extent(source) * invSize + self.drawQuad(texture, p00: p00, p10: p10, p01: p01, p11: p11, + t00: SIMD2(st.left, st.top), t10: SIMD2(st.right, st.top), + t01: SIMD2(st.left, st.bottom), t11: SIMD2(st.right, st.bottom), color: color) + } + + private mutating func drawQuad(_ texture: RendererTexture2D, + p00: SIMD2, p10: SIMD2, p01: SIMD2, p11: SIMD2, + t00: SIMD2 = SIMD2(0, 1), t10: SIMD2 = SIMD2(1, 1), + t01: SIMD2 = SIMD2(0, 0), t11: SIMD2 = SIMD2(1, 0), + color: Color = .white + ) { + let color = SIMD4(color) + let base = self._mesh.vertexCount + self._mesh.insert(vertices: zip([ p00, p01, p10, p11 ], [ t00, t01, t10, t11 ]) + .map { .init(position: $0, texCoord: $1, color: color) }) + self._mesh.insert(indices: [ 0, 1, 2, 2, 1, 3 ], baseVertex: base) + self._instances.append(.init(texture: texture, size: 6)) + } + + internal struct SpriteInstance { + let texture: RendererTexture2D + let size: IndexType + } + + internal enum ActiveState { + case inactive, begin, active + } +} diff --git a/Sources/Voxelotl/SpriteTestGame.swift b/Sources/Voxelotl/SpriteTestGame.swift new file mode 100644 index 0000000..d1c5f17 --- /dev/null +++ b/Sources/Voxelotl/SpriteTestGame.swift @@ -0,0 +1,106 @@ +import Foundation +import simd + +internal class SpriteTestGame: GameDelegate { + private var spriteBatch: SpriteBatch! + private var player = TestPlayer(position: .one * 10) + private var texture: RendererTexture2D! + private var wireShark: RendererTexture2D! + + private static let levelWidth = 40, levelHeight = 23 + private var level: [UInt8] + + init() { + self.level = .init(repeating: 0, count: Self.levelWidth * Self.levelHeight) + for i in 0..(renderer.frame.size)) * 0.01), + destination: nil, + color: .init(renderer.clearColor).setAlpha(0.7)) + + // Draw level + let scale: Float = 64 + for y in 0..(renderer.frame.size)) + mpos *= SIMD2(self.spriteBatch.viewport!.size) + } + let inter = 0.5 + sin(player.rotate * 10) * 0.5 + let color = Color.green.mix(.white, 0.3) + let mesh = Mesh.init(vertices: [ + .init(position: mpos, texCoord: .zero, color: .one), + .init(position: mpos + .init(50, 0) + .init(-50, 50) * inter, texCoord: .zero, color: SIMD4(color)), + .init(position: mpos + .init(0, 50) + .init( 50, -50) * inter, texCoord: .zero, color: SIMD4(color)), + .init(position: mpos + .init(80, 80), texCoord: .zero, color: .zero) + ], indices: [ 0, 1, 2, 1, 2, 3 ]) + if Mouse.down(.left) { + self.spriteBatch.draw(self.texture, mesh: mesh) + } else { + self.spriteBatch.draw(self.texture, vertices: mesh.vertices[..<3]) + } + self.spriteBatch.end() + } +} + +fileprivate struct TestPlayer { + var position: SIMD2 + var rotate: Float = 0 +} + +fileprivate extension Color { + func setAlpha(_ newAlpha: T) -> Self { + return .init(r: r, g: g, b: b, a: newAlpha) + } +} diff --git a/Sources/Voxelotl/shader2D.metal b/Sources/Voxelotl/shader2D.metal new file mode 100644 index 0000000..7220617 --- /dev/null +++ b/Sources/Voxelotl/shader2D.metal @@ -0,0 +1,28 @@ +#include "shadertypes.h" +#include + + +struct FragmentInput { + float4 position [[position]]; + float2 texCoord; + half4 color; +}; + +vertex FragmentInput vertex2DMain(uint vertexID [[vertex_id]], + device const Vertex2D* vtx [[buffer(VertexShaderInputIdxVertices)]], + constant Shader2DUniforms& u [[buffer(VertexShaderInputIdxUniforms)]] +) { + FragmentInput out; + out.position = u.projection * float4(vtx[vertexID].position, 0.0, 1.0); + out.texCoord = vtx[vertexID].texCoord; + out.color = half4(vtx[vertexID].color); + return out; +} + +fragment half4 fragment2DMain(FragmentInput in [[stage_in]], + metal::texture2d texture [[texture(0)]] +) { + constexpr metal::sampler sampler(metal::address::repeat, metal::filter::linear); + half4 texel = texture.sample(sampler, in.texCoord); + return texel * in.color; +} diff --git a/Sources/Voxelotl/shadertypes.h b/Sources/Voxelotl/shadertypes.h index c6041d4..9541dab 100644 --- a/Sources/Voxelotl/shadertypes.h +++ b/Sources/Voxelotl/shadertypes.h @@ -45,4 +45,16 @@ typedef struct { float specularIntensity; } FragmentShaderUniforms; +#pragma mark - UI & 2D Shader + +typedef struct { + vector_float2 position; + vector_float2 texCoord; + vector_float4 color; +} Vertex2D; + +typedef struct { + matrix_float4x4 projection; +} Shader2DUniforms; + #endif//SHADERTYPES_H diff --git a/Sources/Voxelotl/wireshark.png b/Sources/Voxelotl/wireshark.png new file mode 100644 index 0000000..f22f50c Binary files /dev/null and b/Sources/Voxelotl/wireshark.png differ