mirror of
https://github.com/GayPizzaSpecifications/voxelotl-engine.git
synced 2025-08-02 21:00:57 +00:00
initial sprite batch implementation & testbed
This commit is contained in:
parent
c0de651947
commit
79013c24c4
@ -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 $<$<CONFIG:Debug>: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)
|
||||
|
@ -22,3 +22,9 @@ extension Duration {
|
||||
Double(components.seconds) + Double(components.attoseconds) * 1e-18
|
||||
}
|
||||
}
|
||||
|
||||
extension Float {
|
||||
public init(_ value: Duration) {
|
||||
self = Float(value.asFloat)
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
}
|
||||
|
70
Sources/Voxelotl/Renderer/Metal/RendererDynamicMesh.swift
Normal file
70
Sources/Voxelotl/Renderer/Metal/RendererDynamicMesh.swift
Normal 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
|
||||
}
|
||||
}
|
20
Sources/Voxelotl/Renderer/Metal/RendererTexture2D.swift
Normal file
20
Sources/Voxelotl/Renderer/Metal/RendererTexture2D.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
22
Sources/Voxelotl/Renderer/Sprite.swift
Normal file
22
Sources/Voxelotl/Renderer/Sprite.swift
Normal 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>
|
||||
}
|
324
Sources/Voxelotl/Renderer/SpriteBatch.swift
Normal file
324
Sources/Voxelotl/Renderer/SpriteBatch.swift
Normal 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
|
||||
}
|
||||
}
|
106
Sources/Voxelotl/SpriteTestGame.swift
Normal file
106
Sources/Voxelotl/SpriteTestGame.swift
Normal file
@ -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..<Self.levelWidth { self.level[i + (Self.levelHeight - 1) * Self.levelWidth] = 1 }
|
||||
for i in 0..<Self.levelWidth { self.level[i + (Self.levelHeight - 2) * Self.levelWidth] = 1 }
|
||||
for i in 17...20 { self.level[10 + (i) * Self.levelWidth] = 1 }
|
||||
for i in 17...20 { self.level[14 + (i) * Self.levelWidth] = 1 }
|
||||
for i in 11...13 { self.level[i + (17) * Self.levelWidth] = 1 }
|
||||
}
|
||||
|
||||
func create(_ renderer: Renderer) {
|
||||
self.spriteBatch = renderer.createSpriteBatch()
|
||||
// Uncomment to squeesh
|
||||
//self.spriteBatch.viewport = .init(renderer.frame)
|
||||
renderer.clearColor = .init(hue: 301.2, saturation: 0.357, value: 0.488).linear // .magenta.mix(.white, 0.4).mix(.black, 0.8)
|
||||
self.texture = renderer.loadTexture(resourcePath: "test.png")
|
||||
self.wireShark = renderer.loadTexture(resourcePath: "wireshark.png")
|
||||
}
|
||||
|
||||
func update(_ time: GameTime) {
|
||||
if let pad = GameController.current?.state {
|
||||
self.player.position += pad.leftStick.radialDeadzone(min: 0.1, max: 1) * 1000 * Float(time.delta)
|
||||
}
|
||||
self.player.rotate += Float(time.delta)
|
||||
}
|
||||
|
||||
func draw(_ renderer: Renderer, _ time: GameTime) {
|
||||
self.spriteBatch.begin(blendMode: .premultiplied)
|
||||
|
||||
// Draw background
|
||||
self.spriteBatch.draw(self.texture,
|
||||
source: .init(
|
||||
origin: .init(scalar: fmod(player.rotate, 32)),
|
||||
size: (spriteBatch.viewport?.size ?? Size<Float>(renderer.frame.size)) * 0.01),
|
||||
destination: nil,
|
||||
color: .init(renderer.clearColor).setAlpha(0.7))
|
||||
|
||||
// Draw level
|
||||
let scale: Float = 64
|
||||
for y in 0..<Self.levelHeight {
|
||||
for x in 0..<Self.levelWidth {
|
||||
if level[x + Self.levelWidth * y] == 1 {
|
||||
self.spriteBatch.draw(self.texture, destination: .init(origin: Point(Point(x, y)) * scale, size: .init(scalar: scale)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw wireshark (controllable)
|
||||
self.spriteBatch.draw(self.wireShark,
|
||||
position: player.position,
|
||||
scale: .init(sin(player.rotate * 5), cos(player.rotate * 3)),
|
||||
angle: player.rotate, origin: .init(250, 275))
|
||||
|
||||
// Sliding door test
|
||||
self.spriteBatch.draw(self.texture, source: .init(
|
||||
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))
|
||||
|
||||
// Draw mouse cursor
|
||||
var mpos = Mouse.position
|
||||
if self.spriteBatch.viewport != nil {
|
||||
mpos /= SIMD2(Size<Float>(renderer.frame.size))
|
||||
mpos *= SIMD2(self.spriteBatch.viewport!.size)
|
||||
}
|
||||
let inter = 0.5 + sin(player.rotate * 10) * 0.5
|
||||
let color = Color<Float>.green.mix(.white, 0.3)
|
||||
let mesh = Mesh<VertexPosition2DTexcoordColor, UInt16>.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<Float>
|
||||
var rotate: Float = 0
|
||||
}
|
||||
|
||||
fileprivate extension Color {
|
||||
func setAlpha(_ newAlpha: T) -> Self {
|
||||
return .init(r: r, g: g, b: b, a: newAlpha)
|
||||
}
|
||||
}
|
28
Sources/Voxelotl/shader2D.metal
Normal file
28
Sources/Voxelotl/shader2D.metal
Normal file
@ -0,0 +1,28 @@
|
||||
#include "shadertypes.h"
|
||||
#include <metal_stdlib>
|
||||
|
||||
|
||||
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<half, metal::access::sample> texture [[texture(0)]]
|
||||
) {
|
||||
constexpr metal::sampler sampler(metal::address::repeat, metal::filter::linear);
|
||||
half4 texel = texture.sample(sampler, in.texCoord);
|
||||
return texel * in.color;
|
||||
}
|
@ -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
|
||||
|
BIN
Sources/Voxelotl/wireshark.png
Normal file
BIN
Sources/Voxelotl/wireshark.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
Loading…
Reference in New Issue
Block a user