mach: add gfx2d / Sprite2D ECS module
Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
03fe3d02a9
commit
8d2d31f6cb
3 changed files with 361 additions and 0 deletions
256
src/gfx2d/Sprite2D.zig
Normal file
256
src/gfx2d/Sprite2D.zig
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
const std = @import("std");
|
||||
const gpu = @import("mach").gpu;
|
||||
const ecs = @import("mach").ecs;
|
||||
|
||||
const math = @import("../math.zig");
|
||||
const mat = math.mat;
|
||||
const Vec2 = math.Vec2;
|
||||
const Vec3 = math.Vec3;
|
||||
const Mat3x3 = math.Mat3x3;
|
||||
const Mat4x4 = math.Mat4x4;
|
||||
|
||||
/// Public state
|
||||
texture: *gpu.Texture,
|
||||
|
||||
/// Internal state
|
||||
pipeline: *gpu.RenderPipeline,
|
||||
queue: *gpu.Queue,
|
||||
bind_group: *gpu.BindGroup,
|
||||
uniform_buffer: *gpu.Buffer,
|
||||
sprite_transforms: *gpu.Buffer,
|
||||
sprite_uv_transforms: *gpu.Buffer,
|
||||
sprite_sizes: *gpu.Buffer,
|
||||
texture_size: Vec2,
|
||||
|
||||
pub const name = .mach_sprite2d;
|
||||
|
||||
pub const components = .{
|
||||
// TODO: these cannot be doc comments /// because this is a tuple, not a struct. Maybe it should
|
||||
// be a struct with decls?
|
||||
|
||||
// The sprite model transformation matrix. A sprite is measured in pixel units, starting from
|
||||
// (0, 0) at the top-left corner and extending to the size of the sprite. By default, the world
|
||||
// origin (0, 0) lives at the center of the window.
|
||||
//
|
||||
// Example: in a 500px by 500px window, a sprite located at (0, 0) with size (250, 250) will
|
||||
// cover the top-right hand corner of the window.
|
||||
.transform = Mat4x4,
|
||||
|
||||
// UV coordinate transformation matrix describing top-left corner / origin of sprite, in pixels.
|
||||
.uv_transform = Mat3x3,
|
||||
|
||||
// The size of the sprite, in pixels.
|
||||
.size = Vec2,
|
||||
};
|
||||
|
||||
const Uniforms = packed struct {
|
||||
/// The view * orthographic projection matrix
|
||||
view_projection: Mat4x4,
|
||||
|
||||
/// Total size of the sprite texture in pixels
|
||||
texture_size: Vec2,
|
||||
};
|
||||
|
||||
pub fn machSprite2DInit(adapter: anytype) !void {
|
||||
var mach = adapter.mod(.mach);
|
||||
var sprite2d = adapter.mod(.mach_sprite2d);
|
||||
const core = mach.state().core;
|
||||
const device = mach.state().device;
|
||||
|
||||
const uniform_buffer = device.createBuffer(&.{
|
||||
.usage = .{ .copy_dst = true, .uniform = true },
|
||||
.size = @sizeOf(Uniforms),
|
||||
.mapped_at_creation = false,
|
||||
});
|
||||
|
||||
// Create a sampler with linear filtering for smooth interpolation.
|
||||
const queue = device.getQueue();
|
||||
const texture_sampler = device.createSampler(&.{
|
||||
.mag_filter = .linear,
|
||||
.min_filter = .linear,
|
||||
});
|
||||
|
||||
const sprite_buffer_cap = 1024 * 128; // TODO: allow user to specify preallocation
|
||||
const sprite_transforms = device.createBuffer(&.{
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(Mat4x4) * sprite_buffer_cap,
|
||||
.mapped_at_creation = false,
|
||||
});
|
||||
const sprite_uv_transforms = device.createBuffer(&.{
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(Mat3x3) * sprite_buffer_cap,
|
||||
.mapped_at_creation = false,
|
||||
});
|
||||
const sprite_sizes = device.createBuffer(&.{
|
||||
.usage = .{ .storage = true, .copy_dst = true },
|
||||
.size = @sizeOf(Vec2) * sprite_buffer_cap,
|
||||
.mapped_at_creation = false,
|
||||
});
|
||||
|
||||
const bind_group_layout = device.createBindGroupLayout(
|
||||
&gpu.BindGroupLayout.Descriptor.init(.{
|
||||
.entries = &.{
|
||||
gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0),
|
||||
gpu.BindGroupLayout.Entry.buffer(1, .{ .vertex = true }, .read_only_storage, true, 0),
|
||||
gpu.BindGroupLayout.Entry.buffer(2, .{ .vertex = true }, .read_only_storage, true, 0),
|
||||
gpu.BindGroupLayout.Entry.buffer(3, .{ .vertex = true }, .read_only_storage, true, 0),
|
||||
gpu.BindGroupLayout.Entry.sampler(4, .{ .fragment = true }, .filtering),
|
||||
gpu.BindGroupLayout.Entry.texture(5, .{ .fragment = true }, .float, .dimension_2d, false),
|
||||
},
|
||||
}),
|
||||
);
|
||||
var bind_group = device.createBindGroup(
|
||||
&gpu.BindGroup.Descriptor.init(.{
|
||||
.layout = bind_group_layout,
|
||||
.entries = &.{
|
||||
gpu.BindGroup.Entry.buffer(0, uniform_buffer, 0, @sizeOf(Uniforms)),
|
||||
gpu.BindGroup.Entry.buffer(1, sprite_transforms, 0, @sizeOf(Mat4x4) * sprite_buffer_cap),
|
||||
gpu.BindGroup.Entry.buffer(2, sprite_uv_transforms, 0, @sizeOf(Mat3x3) * sprite_buffer_cap),
|
||||
gpu.BindGroup.Entry.buffer(3, sprite_sizes, 0, @sizeOf(Vec2) * sprite_buffer_cap),
|
||||
gpu.BindGroup.Entry.sampler(4, texture_sampler),
|
||||
gpu.BindGroup.Entry.textureView(5, sprite2d.state().texture.createView(&gpu.TextureView.Descriptor{})),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||
const blend = gpu.BlendState{};
|
||||
const color_target = gpu.ColorTargetState{
|
||||
.format = core.descriptor().format,
|
||||
.blend = &blend,
|
||||
.write_mask = gpu.ColorWriteMaskFlags.all,
|
||||
};
|
||||
const fragment = gpu.FragmentState.init(.{
|
||||
.module = shader_module,
|
||||
.entry_point = "frag_main",
|
||||
.targets = &.{color_target},
|
||||
});
|
||||
|
||||
const bind_group_layouts = [_]*gpu.BindGroupLayout{bind_group_layout};
|
||||
const pipeline_layout = device.createPipelineLayout(&gpu.PipelineLayout.Descriptor.init(.{
|
||||
.bind_group_layouts = &bind_group_layouts,
|
||||
}));
|
||||
const pipeline_descriptor = gpu.RenderPipeline.Descriptor{
|
||||
.fragment = &fragment,
|
||||
.layout = pipeline_layout,
|
||||
.vertex = gpu.VertexState{
|
||||
.module = shader_module,
|
||||
.entry_point = "vertex_main",
|
||||
},
|
||||
};
|
||||
|
||||
sprite2d.initState(.{
|
||||
.pipeline = device.createRenderPipeline(&pipeline_descriptor),
|
||||
.queue = queue,
|
||||
.bind_group = bind_group,
|
||||
.uniform_buffer = uniform_buffer,
|
||||
.sprite_transforms = sprite_transforms,
|
||||
.sprite_uv_transforms = sprite_uv_transforms,
|
||||
.sprite_sizes = sprite_sizes,
|
||||
.texture_size = Vec2{
|
||||
@intToFloat(f32, sprite2d.state().texture.getWidth()),
|
||||
@intToFloat(f32, sprite2d.state().texture.getHeight()),
|
||||
},
|
||||
.texture = sprite2d.state().texture,
|
||||
});
|
||||
shader_module.release();
|
||||
}
|
||||
|
||||
pub fn deinit(adapter: anytype) !void {
|
||||
var sprite2d = adapter.mod(.mach_sprite2d);
|
||||
|
||||
sprite2d.state().texture.release();
|
||||
sprite2d.state().pipeline.release();
|
||||
sprite2d.state().queue.release();
|
||||
sprite2d.state().bind_group.release();
|
||||
sprite2d.state().uniform_buffer.release();
|
||||
sprite2d.state().sprite_transforms.release();
|
||||
sprite2d.state().sprite_uv_transforms.release();
|
||||
sprite2d.state().sprite_sizes.release();
|
||||
}
|
||||
|
||||
pub fn tick(adapter: anytype) !void {
|
||||
var mach = adapter.mod(.mach);
|
||||
var sprite2d = adapter.mod(.mach_sprite2d);
|
||||
const core = mach.state().core;
|
||||
const device = mach.state().device;
|
||||
|
||||
// Begin our render pass
|
||||
const back_buffer_view = core.swapChain().getCurrentTextureView();
|
||||
const color_attachment = gpu.RenderPassColorAttachment{
|
||||
.view = back_buffer_view,
|
||||
.clear_value = gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 },
|
||||
.load_op = .clear,
|
||||
.store_op = .store,
|
||||
};
|
||||
|
||||
const encoder = device.createCommandEncoder(null);
|
||||
const render_pass_info = gpu.RenderPassDescriptor.init(.{
|
||||
.color_attachments = &.{color_attachment},
|
||||
});
|
||||
|
||||
// Update uniform buffer
|
||||
const ortho = mat.ortho(
|
||||
-@intToFloat(f32, core.size().width) / 2,
|
||||
@intToFloat(f32, core.size().width) / 2,
|
||||
-@intToFloat(f32, core.size().height) / 2,
|
||||
@intToFloat(f32, core.size().height) / 2,
|
||||
-0.1,
|
||||
100000,
|
||||
);
|
||||
const uniforms = Uniforms{
|
||||
.view_projection = ortho,
|
||||
.texture_size = sprite2d.state().texture_size,
|
||||
};
|
||||
encoder.writeBuffer(sprite2d.state().uniform_buffer, 0, &[_]Uniforms{uniforms});
|
||||
|
||||
// Synchronize entity data into our GPU sprite buffer
|
||||
var archetypes_iter = adapter.entities.query(.{ .all = &.{
|
||||
.{ .mach_sprite2d = &.{
|
||||
.uv_transform,
|
||||
.transform,
|
||||
.size,
|
||||
} },
|
||||
} });
|
||||
|
||||
// TODO: eliminate these
|
||||
var sprite_transforms = try std.ArrayListUnmanaged(Mat4x4).initCapacity(adapter.allocator, 1000);
|
||||
defer sprite_transforms.deinit(adapter.allocator);
|
||||
var sprite_uv_transforms = try std.ArrayListUnmanaged(Mat3x3).initCapacity(adapter.allocator, 1000);
|
||||
defer sprite_uv_transforms.deinit(adapter.allocator);
|
||||
var sprite_sizes = try std.ArrayListUnmanaged(Vec2).initCapacity(adapter.allocator, 1000);
|
||||
defer sprite_sizes.deinit(adapter.allocator);
|
||||
while (archetypes_iter.next()) |archetype| {
|
||||
var transforms = archetype.slice(.mach_sprite2d, .transform);
|
||||
var uv_transforms = archetype.slice(.mach_sprite2d, .uv_transform);
|
||||
var sizes = archetype.slice(.mach_sprite2d, .size);
|
||||
for (transforms, uv_transforms, sizes) |transform, uv_transform, size| {
|
||||
try sprite_transforms.append(adapter.allocator, transform);
|
||||
try sprite_uv_transforms.append(adapter.allocator, uv_transform);
|
||||
try sprite_sizes.append(adapter.allocator, size);
|
||||
}
|
||||
}
|
||||
const total_vertices = @intCast(u32, sprite_sizes.items.len * 6);
|
||||
if (sprite_transforms.items.len > 0) {
|
||||
encoder.writeBuffer(sprite2d.state().sprite_transforms, 0, sprite_transforms.items);
|
||||
encoder.writeBuffer(sprite2d.state().sprite_uv_transforms, 0, sprite_uv_transforms.items);
|
||||
encoder.writeBuffer(sprite2d.state().sprite_sizes, 0, sprite_sizes.items);
|
||||
}
|
||||
|
||||
// Draw the sprite batch
|
||||
const pass = encoder.beginRenderPass(&render_pass_info);
|
||||
pass.setPipeline(sprite2d.state().pipeline);
|
||||
// TODO: remove dynamic offsets?
|
||||
pass.setBindGroup(0, sprite2d.state().bind_group, &.{ 0, 0, 0, 0 });
|
||||
pass.draw(total_vertices, 1, 0, 0);
|
||||
pass.end();
|
||||
pass.release();
|
||||
|
||||
var command = encoder.finish(null);
|
||||
encoder.release();
|
||||
|
||||
sprite2d.state().queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||
command.release();
|
||||
core.swapChain().present();
|
||||
back_buffer_view.release();
|
||||
}
|
||||
102
src/gfx2d/shader.wgsl
Normal file
102
src/gfx2d/shader.wgsl
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
//-----------------------------------------------------------------------------
|
||||
// Vertex shader
|
||||
//-----------------------------------------------------------------------------
|
||||
struct VertexOutput {
|
||||
// Vertex position
|
||||
@builtin(position) Position : vec4<f32>,
|
||||
|
||||
// UV coordinate
|
||||
@location(0) fragUV : vec2<f32>,
|
||||
};
|
||||
|
||||
// Our vertex shader will recieve these parameters
|
||||
struct Uniforms {
|
||||
// The view * orthographic projection matrix
|
||||
view_projection: mat4x4<f32>,
|
||||
|
||||
// Total size of the sprite texture in pixels
|
||||
texture_size: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> uniforms : Uniforms;
|
||||
|
||||
// Sprite model transformation matrices
|
||||
@group(0) @binding(1) var<storage, read> sprite_transforms: array<mat4x4<f32>>;
|
||||
|
||||
// Sprite UV coordinate transformation matrices. Sprite UV coordinates are (0, 0) at the top-left
|
||||
// corner, and in pixels.
|
||||
@group(0) @binding(2) var<storage, read> sprite_uv_transforms: array<mat3x3<f32>>;
|
||||
|
||||
// Sprite sizes, in pixels.
|
||||
@group(0) @binding(3) var<storage, read> sprite_sizes: array<vec2<f32>>;
|
||||
|
||||
@vertex
|
||||
fn vertex_main(
|
||||
@builtin(vertex_index) VertexIndex : u32
|
||||
) -> VertexOutput {
|
||||
// Our vertex shader will be called six times per sprite (2 triangles make up a sprite, so six
|
||||
// vertices.) The VertexIndex tells us which vertex we need to render, so we know e.g. vertices
|
||||
// 0-5 correspond to the first sprite, vertices 6-11 correspond to the second sprite, and so on.
|
||||
let sprite_transform = sprite_transforms[VertexIndex / 6];
|
||||
let sprite_uv_transform = sprite_uv_transforms[VertexIndex / 6];
|
||||
let sprite_size = sprite_sizes[VertexIndex / 6];
|
||||
|
||||
// Imagine the vertices and UV coordinates of a card. There are two triangles, the UV coordinates
|
||||
// describe the corresponding location of each vertex on the texture. We hard-code the vertex
|
||||
// positions and UV coordinates here:
|
||||
let positions = array<vec2<f32>, 6>(
|
||||
vec2<f32>(0, 0), // left, bottom
|
||||
vec2<f32>(0, 1), // left, top
|
||||
vec2<f32>(1, 0), // right, bottom
|
||||
vec2<f32>(1, 0), // right, bottom
|
||||
vec2<f32>(0, 1), // left, top
|
||||
vec2<f32>(1, 1), // right, top
|
||||
);
|
||||
let uvs = array<vec2<f32>, 6>(
|
||||
vec2<f32>(0, 1), // left, bottom
|
||||
vec2<f32>(0, 0), // left, top
|
||||
vec2<f32>(1, 1), // right, bottom
|
||||
vec2<f32>(1, 1), // right, bottom
|
||||
vec2<f32>(0, 0), // left, top
|
||||
vec2<f32>(1, 0), // right, top
|
||||
);
|
||||
|
||||
// Based on the vertex index, we determine which positions[n] and uvs[n] we need to use. Our
|
||||
// vertex shader is invoked 6 times per sprite, we need to produce the right vertex/uv coordinates
|
||||
// each time to produce a textured card.
|
||||
let pos_2d = positions[VertexIndex % 6];
|
||||
var uv = uvs[VertexIndex % 6];
|
||||
|
||||
// Currently, our pos_2d and uv coordinates describe a card that covers 1px by 1px; and the UV
|
||||
// coordinates describe using the entire texture. We alter the coordinates to describe the
|
||||
// desired sprite location, size, and apply a subset of the texture instead of the entire texture.
|
||||
var pos = vec4<f32>(pos_2d * sprite_size, 0, 1); // normalized -> pixels
|
||||
pos = sprite_transform * pos; // apply sprite transform (pixels)
|
||||
pos = uniforms.view_projection * pos; // pixels -> normalized
|
||||
|
||||
uv *= sprite_size; // normalized -> pixels
|
||||
uv = (sprite_uv_transform * vec3<f32>(uv.xy, 1)).xy; // apply sprite UV transform (pixels)
|
||||
uv /= uniforms.texture_size; // pixels -> normalized
|
||||
|
||||
var output : VertexOutput;
|
||||
output.Position = pos;
|
||||
output.fragUV = uv;
|
||||
return output;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Fragment shader
|
||||
//-----------------------------------------------------------------------------
|
||||
@group(0) @binding(4) var spriteSampler: sampler;
|
||||
@group(0) @binding(5) var spriteTexture: texture_2d<f32>;
|
||||
|
||||
@fragment
|
||||
fn frag_main(
|
||||
@location(0) fragUV: vec2<f32>
|
||||
) -> @location(0) vec4<f32> {
|
||||
var c = textureSample(spriteTexture, spriteSampler, fragUV);
|
||||
if (c.a <= 0.0) {
|
||||
discard;
|
||||
}
|
||||
return vec4<f32>(0.3, 0.2, 0.5, 1.0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue