initial sprite batch implementation & testbed

This commit is contained in:
2024-09-13 18:59:14 +10:00
parent c0de651947
commit 79013c24c4
13 changed files with 732 additions and 29 deletions

View File

@ -21,3 +21,9 @@ public struct VertexPositionNormalColorTexcoord: Vertex {
var color: SIMD4<Float>
var texCoord: SIMD2<Float>
}
public struct VertexPosition2DTexcoordColor: Vertex {
var position: SIMD2<Float>
var texCoord: SIMD2<Float>
var color: SIMD4<Float>
}

View File

@ -0,0 +1,70 @@
import Metal
public struct RendererDynamicMesh<VertexType: Vertex, IndexType: UnsignedInteger> {
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<VertexType>.stride
self.indexCapacity = self._idxBufs.map { $0.length }.min()! / MemoryLayout<IndexType>.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<VertexType>) {
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..<vertices.count {
vertexData[self._numVertices + i] = vertices[i]
}
}
#if os(macOS)
if self._renderer.isManagedStorage {
let stride = MemoryLayout<VertexType>.stride
vertexBuffer.didModifyRange(stride * self._numVertices..<stride * vertices.count)
}
#endif
self._numVertices += vertices.count
}
public mutating func insert(indices: [IndexType], baseVertex: Int = 0) {
assert(self._numIndices + indices.count < self.indexCapacity)
let indexBuffer: MTLBuffer = self._idxBufs[self._renderer.currentFrame]
let base = IndexType(baseVertex)
indexBuffer.contents().withMemoryRebound(to: IndexType.self, capacity: self.indexCapacity) { indexData in
for i in 0..<indices.count {
indexData[self._numIndices + i] = base + indices[i]
}
}
#if os(macOS)
if self._renderer.isManagedStorage {
let stride = MemoryLayout<VertexType>.stride
indexBuffer.didModifyRange(stride * self._numIndices..<stride * indices.count)
}
#endif
self._numIndices += indices.count
}
}

View File

@ -0,0 +1,20 @@
import Metal
public struct RendererTexture2D: Hashable {
internal let _textureBuffer: MTLTexture
public let size: Size<Int>
internal init(metalTexture: MTLTexture, size: Size<Int>) {
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)
}
}

View File

@ -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<Int> { .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<VertexType: Vertex, IndexType: UnsignedInteger>(
vertexCapacity: Int, indexCapacity: Int
) -> RendererDynamicMesh<VertexType, IndexType>? {
let vertexBuffers: [MTLBuffer], indexBuffers: [MTLBuffer]
do {
let byteCapacity = MemoryLayout<VertexType>.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<IndexType>.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..<numFramesInFlight).map { _ in
guard let buffer = device.makeBuffer(length: capacity, options: transitoryOpt) else {
throw RendererError.initFailure("Failed to create buffer")
}
return buffer
}
}
}
public func loadTexture(resourcePath path: String) -> 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<Float>) {
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<Shader2DUniforms>.stride,
index: VertexShaderInputIdx.uniforms.rawValue)
}
internal func submit(
mesh: RendererDynamicMesh<SpriteBatch.VertexType, SpriteBatch.IndexType>,
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<SpriteBatch.IndexType>.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<VertexShaderInstance>.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

View File

@ -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<Float>
var scale: SIMD2<Float>
var origin: SIMD2<Float>
var shear: SIMD2<Float> = .zero
var angle: Float
var depth: Int
var flip: Flip
var color: Color<Float>
}

View File

@ -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<VertexType, IndexType>
private var _instances = [SpriteInstance]()
public var viewport: Rect<Float>? = 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<Float>) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
self.drawQuad(texture, position: position, size: Size<Float>(texture.size))
}
public mutating func draw(_ texture: RendererTexture2D, position: SIMD2<Float>,
scale: SIMD2<Float>,
angle: Float = 0.0, origin: SIMD2<Float> = .zero,
flip: Sprite.Flip = .none,
color: Color<Float> = .white
//depth: Int = 0
) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
let size = Size<Float>(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<Float>,
scale: Float = 1.0,
angle: Float = 0.0, origin: SIMD2<Float> = .zero,
flip: Sprite.Flip = .none,
color: Color<Float> = .white
//depth: Int = 0
) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
let size = Size<Float>(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<Float>?) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
let rect = destination ?? self.viewport ?? Rect<Float>(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<Float> = .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<Float>, position: SIMD2<Float>) {
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<Float>, position: SIMD2<Float>,
scale: SIMD2<Float>, color: Color<Float> = .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<Float>, position: SIMD2<Float>,
scale: Float = 1.0, color: Color<Float> = .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<Float>, position: SIMD2<Float>, scale: SIMD2<Float>, angle: Float = 0.0, origin: Point<Int> = .zero, flip: Sprite.Flip = .none, color: Color<Float> = .white, depth: Int = 0) {
//public mutating func draw(_ texture: RendererTexture2D, source: Rect<Float>, position: SIMD2<Float>, scale: Float = 1.0, angle: Float = 0.0, origin: Point<Int> = .zero, flip: Sprite.Flip = .none, color: Color<Float> = .white, depth: Int = 0) {
public mutating func draw(_ texture: RendererTexture2D, source: Rect<Float>, destination: Rect<Float>?) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
let dst = destination ?? self.viewport ?? Rect<Float>(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<Float>, destination: Rect<Float>?,
color: Color<Float> = .white
) {
assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin")
let dst = destination ?? self.viewport ?? Rect<Float>(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<Float>, 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<Float>, transform: simd_float3x3,
flip: Sprite.Flip = .none, color: Color<Float> = .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..<vertices.count).map(IndexType.init), baseVertex: base)
self._instances.append(.init(texture: texture, size: UInt16(vertices.count)))
}
public mutating func draw(_ texture: RendererTexture2D, vertices: ArraySlice<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..<vertices.count).map(IndexType.init), baseVertex: base)
self._instances.append(.init(texture: texture, size: UInt16(vertices.count)))
}
public mutating func draw(_ texture: RendererTexture2D, mesh: Mesh<VertexType, IndexType>) {
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<Float>, size: Size<Float>, color: Color<Float> = .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<Float>, angle: Float, size: Size<Float>, offset bias: SIMD2<Float>, color: Color<Float> = .white
) {
let (tc, ts) = (cos(angle), sin(angle))
let rotate = matrix_float2x2(
.init( tc, ts),
.init(-ts, tc))
let right = SIMD2<Float>(size.w, 0) * rotate
let down = SIMD2<Float>(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<Float>, p10: SIMD2<Float>, p01: SIMD2<Float>, p11: SIMD2<Float>,
flip: Sprite.Flip, color: Color<Float> = .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<Float>,
p00: SIMD2<Float>, p10: SIMD2<Float>, p01: SIMD2<Float>, p11: SIMD2<Float>,
color: Color<Float> = .white
) {
let invSize = 1 / Size<Float>(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<Float>, p10: SIMD2<Float>, p01: SIMD2<Float>, p11: SIMD2<Float>,
t00: SIMD2<Float> = SIMD2(0, 1), t10: SIMD2<Float> = SIMD2(1, 1),
t01: SIMD2<Float> = SIMD2(0, 0), t11: SIMD2<Float> = SIMD2(1, 0),
color: Color<Float> = .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
}
}