diff --git a/Sources/Voxelotl/Math/Point.swift b/Sources/Voxelotl/Math/Point.swift index b6690a9..8f6a5d5 100644 --- a/Sources/Voxelotl/Math/Point.swift +++ b/Sources/Voxelotl/Math/Point.swift @@ -13,6 +13,11 @@ public struct Point: Hashable { self.y = value } + public init(_ size: Size) { + self.x = size.w + self.y = size.h + } + @inline(__always) public static func == (lhs: Self, rhs: Self) -> Bool { lhs.x == rhs.x && lhs.y == rhs.y } @inline(__always) public static func != (lhs: Self, rhs: Self) -> Bool { lhs.x != rhs.x || lhs.y != rhs.y } } diff --git a/Sources/Voxelotl/Renderer/Sprite.swift b/Sources/Voxelotl/Renderer/Sprite.swift index d3a5653..6686fda 100644 --- a/Sources/Voxelotl/Renderer/Sprite.swift +++ b/Sources/Voxelotl/Renderer/Sprite.swift @@ -8,6 +8,7 @@ public struct Sprite { 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) + public static let diag: Self = Self(rawValue: 1 << 2) } var texture: RendererTexture2D diff --git a/Sources/Voxelotl/Renderer/SpriteBatch.swift b/Sources/Voxelotl/Renderer/SpriteBatch.swift index ef22ea4..3c72966 100644 --- a/Sources/Voxelotl/Renderer/SpriteBatch.swift +++ b/Sources/Voxelotl/Renderer/SpriteBatch.swift @@ -46,7 +46,7 @@ public struct SpriteBatch { 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)) + self.drawQuad(texture, .positions(position, Size(texture.size))) } public mutating func draw(_ texture: RendererTexture2D, position: SIMD2, @@ -58,12 +58,13 @@ public struct SpriteBatch { ) { assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") let size = Size(texture.size) + let texCoord = Quad.texcoords(flip) 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) + let bias = origin / SIMD2(size) + self.drawQuad(texture, .positions(position, size * scale, angle, bias), texCoord, color: color) } else { - self.drawQuad(texture, position: position - origin, size: size * scale, color: color) + self.drawQuad(texture, .positions(position - origin, size * scale), texCoord, color: color) } } @@ -76,128 +77,152 @@ public struct SpriteBatch { ) { assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") let size = Size(texture.size) + let texCoord = Quad.texcoords(flip) 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) + self.drawQuad(texture, .positions(position, size * scale, angle, bias), texCoord, color: color) } else { - self.drawQuad(texture, position: position - origin, size: size * scale, color: color) + self.drawQuad(texture, .positions(position, size * scale), texCoord, 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)) + self.drawQuad(texture, .positions(destination ?? self.viewport ?? Rect(self._renderer.frame))) + } + + public mutating func draw(_ texture: RendererTexture2D, destination: Rect?, + angle: Float = 0.0, center: Point? = .zero, flip: Sprite.Flip = .none, color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let dst = destination ?? self.viewport ?? Rect(self._renderer.frame) + if dst.size.w.isZero || dst.size.h.isZero { return } + let texCoord = Quad.texcoords(flip) + let color = color.linear + if angle != 0 { + let origin = SIMD2(center ?? Point(dst.size * 0.5)) + let bias = origin / SIMD2(dst.size) + self.drawQuad(texture, .positions(SIMD2(dst.origin) + origin, dst.size, angle, bias), texCoord, color: color) + } else { + self.drawQuad(texture, .positions(dst), texCoord, color: color) + } } 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) + self.drawQuad(texture, .positions(transform, Size(texture.size))) } public mutating func draw(_ texture: RendererTexture2D, transform: simd_float3x3, - flip: Sprite.Flip = .none, - color: Color = .white + 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) + .positions(transform, Size(texture.size)), + .texcoords(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)) + self.drawQuad(texture, .positions(position, source.size), .texcoords(texture.size, source)) } 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) + self.drawQuad(texture, + .positions(position, source.size * scale), + .texcoords(texture.size, source), 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) + self.drawQuad(texture, + .positions(position, source.size * scale), + .texcoords(texture.size, source), 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, + 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") + if source.size.w.isZero || source.size.h.isZero { return } + let texCoord = Quad.texcoords(texture.size, source, flip) + let color = color.linear + if angle != 0 { + let bias = origin / SIMD2(source.size) + self.drawQuad(texture, .positions(position, source.size * scale, angle, bias), texCoord, color: color) + } else { + self.drawQuad(texture, .positions(position - SIMD2(origin) * scale, source.size * scale), texCoord, color: color) + } + } + public mutating func draw(_ texture: RendererTexture2D, source: Rect, + 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") + if source.size.w.isZero || source.size.h.isZero { return } + let texCoord = Quad.texcoords(texture.size, source, flip) + let color = color.linear + if angle != 0 { + let bias = origin / SIMD2(source.size) + self.drawQuad(texture, .positions(position, source.size * scale, angle, bias), texCoord, color: color) + } else { + self.drawQuad(texture, .positions(position - SIMD2(origin) * scale, source.size * scale), texCoord, color: color) + } + } 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)) + self.drawQuad(texture, + .positions(destination ?? self.viewport ?? Rect(self._renderer.frame)), + .texcoords(texture.size, source)) } 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) + self.drawQuad(texture, + .positions(destination ?? self.viewport ?? Rect(self._renderer.frame)), + .texcoords(texture.size, source), color: color.linear) } - //TODO: Destination with rotation + public mutating func draw(_ texture: RendererTexture2D, source: Rect, destination: Rect?, + angle: Float = 0.0, center: Point? = .zero, flip: Sprite.Flip = .none, color: Color = .white + ) { + assert(self._active != .inactive, "call to SpriteBatch.draw without calling begin") + let dst = destination ?? self.viewport ?? Rect(self._renderer.frame) + if dst.size.w.isZero || dst.size.h.isZero { return } + let texCoord = Quad.texcoords(texture.size, source, flip) + let color = color.linear + if angle != 0 { + let origin = SIMD2(center ?? Point(dst.size * 0.5)) + let bias = origin / SIMD2(dst.size) + self.drawQuad(texture, .positions(SIMD2(dst.origin) + origin, dst.size, angle, bias), texCoord, color: color) + } else { + self.drawQuad(texture, .positions(dst), texCoord, color: color) + } + } 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) + self.drawQuad(texture, .positions(transform, source.size), .texcoords(texture.size, source)) } 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) + self.drawQuad(texture, .positions(transform, source.size), .texcoords(texture.size, source), color: color.linear) } public mutating func draw(_ texture: RendererTexture2D, vertices: [VertexType]) { @@ -249,65 +274,17 @@ public struct SpriteBatch { 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) + fileprivate struct Quad { + let v00: SIMD2, v01: SIMD2 + let v10: SIMD2, v11: SIMD2 } 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 + _ p: Quad, _ t: Quad = .texcoordsDefault, 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 ]) + self._mesh.insert(vertices: zip([ p.v00, p.v01, p.v10, p.v11 ], [ t.v00, t.v01, t.v10, t.v11 ]) .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)) @@ -322,3 +299,78 @@ public struct SpriteBatch { case inactive, begin, active } } + +fileprivate extension SpriteBatch.Quad { + static func positions(_ position: SIMD2, _ size: Size) -> Self { + .init( + v00: position, + v01: .init(position.x + size.w, position.y), + v10: .init(position.x, position.y + size.h), + v11: .init(position.x + size.w, position.y + size.h)) + } + + static func positions(_ position: SIMD2, _ size: Size, + _ angle: Float, _ offset: SIMD2 + ) -> Self { + 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 + return .init ( + v00: position - right * offset.x - down * offset.y, + v01: position + right * (1 - offset.x) - down * offset.y, + v10: position - right * offset.x + down * (1 - offset.y), + v11: position + right * (1 - offset.x) + down * (1 - offset.y)) + } + + static func positions(_ rect: Rect) -> Self { + .init( + v00: SIMD2(rect.left, rect.up), v01: SIMD2(rect.right, rect.up), + v10: SIMD2(rect.left, rect.down), v11: SIMD2(rect.right, rect.down)) + } + + static func positions(_ transform: simd_float3x3, _ size: Size) -> Self { + let w = size.w, h = size.h + return .init( + v00: (transform * .init(0, 0, 1)).xy, + v01: (transform * .init(w, 0, 1)).xy, + v10: (transform * .init(0, h, 1)).xy, + v11: (transform * .init(w, h, 1)).xy) + } + + static let texcoordsDefault = Self( + v00: SIMD2(0, 1), v01: SIMD2(1, 1), + v10: SIMD2(0, 0), v11: SIMD2(1, 0)) + + static func texcoords(_ flip: Sprite.Flip) -> Self { + let flipX = flip.contains(.x), flipY = flip.contains(.y) + //TODO: diag + return .init( + v00: .init(flipX ? 1 : 0, flipY ? 0 : 1), + v01: .init(flipX ? 0 : 1, flipY ? 0 : 1), + v10: .init(flipX ? 1 : 0, flipY ? 1 : 0), + v11: .init(flipX ? 0 : 1, flipY ? 1 : 0)) + } + + static func texcoords(_ texSize: Size, _ source: Rect) -> Self { + let invSize = 1 / Size(texSize) + let st = Extent(source) * invSize + return .init( + v00: SIMD2(st.left, st.top), v01: SIMD2(st.right, st.top), + v10: SIMD2(st.left, st.bottom), v11: SIMD2(st.right, st.bottom)) + } + + static func texcoords(_ texSize: Size, _ source: Rect, _ flip: Sprite.Flip) -> Self { + let flipX = flip.contains(.x), flipY = flip.contains(.y) + let invSize = 1 / Size(texSize) + let st = Extent(source) * invSize + //TODO: diag + return .init( + v00: .init(flipX ? st.right : st.left, flipY ? st.bottom : st.top), + v01: .init(flipX ? st.left : st.right, flipY ? st.bottom : st.top), + v10: .init(flipX ? st.right : st.left, flipY ? st.top : st.bottom), + v11: .init(flipX ? st.left : st.right, flipY ? st.top : st.bottom)) + } +} diff --git a/Sources/Voxelotl/SpriteTestGame.swift b/Sources/Voxelotl/SpriteTestGame.swift index d1c5f17..f781b46 100644 --- a/Sources/Voxelotl/SpriteTestGame.swift +++ b/Sources/Voxelotl/SpriteTestGame.swift @@ -44,6 +44,7 @@ internal class SpriteTestGame: GameDelegate { origin: .init(scalar: fmod(player.rotate, 32)), size: (spriteBatch.viewport?.size ?? Size(renderer.frame.size)) * 0.01), destination: nil, + angle: sin(player.rotate) * .pi * 0.1, center: nil, color: .init(renderer.clearColor).setAlpha(0.7)) // Draw level @@ -63,13 +64,13 @@ internal class SpriteTestGame: GameDelegate { angle: player.rotate, origin: .init(250, 275)) // Sliding door test - self.spriteBatch.draw(self.texture, source: .init( + let doorAngle = max(sin(player.rotate * 2.6) - 0.75, 0) * .pi + self.spriteBatch.draw(self.texture, source: Rect( origin: .init(4 + cos(player.rotate / 1.2) * 4, 0), size: .init(4 + cos(player.rotate / 1.3) * 4, 16)), - transform: .init( - .init( 24, 0, 0), - .init( 0, 12, 0), - .init(704, 1152, 1)), color: .red.mix(.white, 0.3)) + position: .init(704 + 24, 1152 + 12), scale: .init(24, 12), + angle: doorAngle, origin: SIMD2(repeating: 1), + flip: .none, color: .red.mix(.white, 0.3)) // Draw mouse cursor var mpos = Mouse.position