examples: import mach-examples@20ceb359231ff284cf343dddba8cf25112ffe717
Helps hexops/mach#1165 Signed-off-by: Stephen Gutekanst <stephen@hexops.com>
This commit is contained in:
parent
f25f435275
commit
0a8e22bb49
19 changed files with 3147 additions and 0 deletions
162
examples/custom-renderer/Game.zig
Normal file
162
examples/custom-renderer/Game.zig
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const ecs = mach.ecs;
|
||||||
|
const core = mach.core;
|
||||||
|
const math = mach.math;
|
||||||
|
const Renderer = @import("Renderer.zig");
|
||||||
|
|
||||||
|
const vec3 = math.vec3;
|
||||||
|
const vec2 = math.vec2;
|
||||||
|
const Vec2 = math.Vec2;
|
||||||
|
const Vec3 = math.Vec3;
|
||||||
|
|
||||||
|
timer: mach.Timer,
|
||||||
|
player: ecs.EntityID,
|
||||||
|
direction: Vec2 = vec2(0, 0),
|
||||||
|
spawning: bool = false,
|
||||||
|
spawn_timer: mach.Timer,
|
||||||
|
|
||||||
|
pub const components = struct {
|
||||||
|
pub const follower = void;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Each module must have a globally unique name declared, it is impossible to use two modules with
|
||||||
|
// the same name in a program. To avoid name conflicts, we follow naming conventions:
|
||||||
|
//
|
||||||
|
// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it
|
||||||
|
// provides.
|
||||||
|
// 2. Single-word names like `.renderer`, `.game`, etc. are reserved for the application itself.
|
||||||
|
// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui`
|
||||||
|
// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally
|
||||||
|
// unique.
|
||||||
|
//
|
||||||
|
pub const name = .game;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
renderer: *Renderer.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// The Mach .core is where we set window options, etc.
|
||||||
|
core.setTitle("Hello, ECS!");
|
||||||
|
|
||||||
|
// We can create entities, and set components on them. Note that components live in a module
|
||||||
|
// namespace, e.g. the `.renderer` module could have a 3D `.location` component with a different
|
||||||
|
// type than the `.physics2d` module's `.location` component if you desire.
|
||||||
|
|
||||||
|
const player = try engine.newEntity();
|
||||||
|
try renderer.set(player, .location, vec3(0, 0, 0));
|
||||||
|
try renderer.set(player, .scale, 1.0);
|
||||||
|
|
||||||
|
game.state = .{
|
||||||
|
.timer = try mach.Timer.start(),
|
||||||
|
.spawn_timer = try mach.Timer.start(),
|
||||||
|
.player = player,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
renderer: *Renderer.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
||||||
|
var iter = core.pollEvents();
|
||||||
|
var direction = game.state.direction;
|
||||||
|
var spawning = game.state.spawning;
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] -= 1,
|
||||||
|
.right => direction.v[0] += 1,
|
||||||
|
.up => direction.v[1] += 1,
|
||||||
|
.down => direction.v[1] -= 1,
|
||||||
|
.space => spawning = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.key_release => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] += 1,
|
||||||
|
.right => direction.v[0] -= 1,
|
||||||
|
.up => direction.v[1] -= 1,
|
||||||
|
.down => direction.v[1] += 1,
|
||||||
|
.space => spawning = false,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.close => try engine.send(.exit, .{}),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game.state.direction = direction;
|
||||||
|
game.state.spawning = spawning;
|
||||||
|
|
||||||
|
var player_pos = renderer.get(game.state.player, .location).?;
|
||||||
|
if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) {
|
||||||
|
for (0..10) |_| {
|
||||||
|
// Spawn a new follower entity
|
||||||
|
_ = game.state.spawn_timer.lap();
|
||||||
|
const new_entity = try engine.newEntity();
|
||||||
|
try game.set(new_entity, .follower, {});
|
||||||
|
try renderer.set(new_entity, .location, player_pos);
|
||||||
|
try renderer.set(new_entity, .scale, 1.0 / 6.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||||
|
const delta_time = game.state.timer.lap();
|
||||||
|
|
||||||
|
// Move following entities closer to us.
|
||||||
|
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .game = &.{.follower} },
|
||||||
|
} });
|
||||||
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
|
const locations = archetype.slice(.renderer, .location);
|
||||||
|
for (ids, locations) |id, location| {
|
||||||
|
// Avoid other follower entities by moving away from them if they are close to us.
|
||||||
|
const close_dist = 1.0 / 15.0;
|
||||||
|
var avoidance = Vec3.splat(0);
|
||||||
|
var avoidance_div: f32 = 1.0;
|
||||||
|
var archetypes_iter_2 = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .game = &.{.follower} },
|
||||||
|
} });
|
||||||
|
while (archetypes_iter_2.next()) |archetype_2| {
|
||||||
|
const other_ids = archetype_2.slice(.entity, .id);
|
||||||
|
const other_locations = archetype_2.slice(.renderer, .location);
|
||||||
|
for (other_ids, other_locations) |other_id, other_location| {
|
||||||
|
if (id == other_id) continue;
|
||||||
|
if (location.dist(&other_location) < close_dist) {
|
||||||
|
avoidance = avoidance.sub(&location.dir(&other_location, 0.0000001));
|
||||||
|
avoidance_div += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Avoid the player
|
||||||
|
var avoid_player_multiplier: f32 = 1.0;
|
||||||
|
if (location.dist(&player_pos) < close_dist * 6.0) {
|
||||||
|
avoidance = avoidance.sub(&location.dir(&player_pos, 0.0000001));
|
||||||
|
avoidance_div += 1.0;
|
||||||
|
avoid_player_multiplier = 4.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move away from things we want to avoid
|
||||||
|
const move_speed = 1.0 * delta_time;
|
||||||
|
var new_location = location.add(&avoidance.divScalar(avoidance_div).mulScalar(move_speed * avoid_player_multiplier));
|
||||||
|
|
||||||
|
// Move towards the center
|
||||||
|
new_location = new_location.lerp(&vec3(0, 0, 0), move_speed / avoidance_div);
|
||||||
|
try renderer.set(id, .location, new_location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the player position, by moving in the direction the player wants to go
|
||||||
|
// by the speed amount.
|
||||||
|
const speed = 1.0;
|
||||||
|
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||||
|
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||||
|
try renderer.set(game.state.player, .location, player_pos);
|
||||||
|
}
|
||||||
166
examples/custom-renderer/Renderer.zig
Normal file
166
examples/custom-renderer/Renderer.zig
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const mach = @import("mach");
|
||||||
|
const core = mach.core;
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const math = mach.math;
|
||||||
|
|
||||||
|
const Vec3 = math.Vec3;
|
||||||
|
|
||||||
|
const num_bind_groups = 1024 * 32;
|
||||||
|
|
||||||
|
// uniform bind group offset must be 256-byte aligned
|
||||||
|
const uniform_offset = 256;
|
||||||
|
|
||||||
|
pipeline: *gpu.RenderPipeline,
|
||||||
|
queue: *gpu.Queue,
|
||||||
|
bind_groups: [num_bind_groups]*gpu.BindGroup,
|
||||||
|
uniform_buffer: *gpu.Buffer,
|
||||||
|
|
||||||
|
pub const name = .renderer;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
pub const components = struct {
|
||||||
|
pub const location = Vec3;
|
||||||
|
pub const rotation = Vec3;
|
||||||
|
pub const scale = f32;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: this shouldn't be a packed struct, it should be extern.
|
||||||
|
const UniformBufferObject = packed struct {
|
||||||
|
offset: Vec3.Vector,
|
||||||
|
scale: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
renderer: *Mod,
|
||||||
|
) !void {
|
||||||
|
const device = engine.state.device;
|
||||||
|
const shader_module = device.createShaderModuleWGSL("shader.wgsl", @embedFile("shader.wgsl"));
|
||||||
|
|
||||||
|
// Fragment state
|
||||||
|
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 uniform_buffer = device.createBuffer(&.{
|
||||||
|
.usage = .{ .copy_dst = true, .uniform = true },
|
||||||
|
.size = @sizeOf(UniformBufferObject) * uniform_offset * num_bind_groups,
|
||||||
|
.mapped_at_creation = .false,
|
||||||
|
});
|
||||||
|
const bind_group_layout_entry = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
|
||||||
|
const bind_group_layout = device.createBindGroupLayout(
|
||||||
|
&gpu.BindGroupLayout.Descriptor.init(.{
|
||||||
|
.entries = &.{bind_group_layout_entry},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
var bind_groups: [num_bind_groups]*gpu.BindGroup = undefined;
|
||||||
|
for (bind_groups, 0..) |_, i| {
|
||||||
|
bind_groups[i] = device.createBindGroup(
|
||||||
|
&gpu.BindGroup.Descriptor.init(.{
|
||||||
|
.layout = bind_group_layout,
|
||||||
|
.entries = &.{
|
||||||
|
gpu.BindGroup.Entry.buffer(0, uniform_buffer, uniform_offset * i, @sizeOf(UniformBufferObject)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.state = .{
|
||||||
|
.pipeline = device.createRenderPipeline(&pipeline_descriptor),
|
||||||
|
.queue = device.getQueue(),
|
||||||
|
.bind_groups = bind_groups,
|
||||||
|
.uniform_buffer = uniform_buffer,
|
||||||
|
};
|
||||||
|
shader_module.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(
|
||||||
|
renderer: *Mod,
|
||||||
|
) !void {
|
||||||
|
renderer.state.pipeline.release();
|
||||||
|
renderer.state.queue.release();
|
||||||
|
for (renderer.state.bind_groups) |bind_group| bind_group.release();
|
||||||
|
renderer.state.uniform_buffer.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
renderer: *Mod,
|
||||||
|
) !void {
|
||||||
|
const device = engine.state.device;
|
||||||
|
|
||||||
|
// Begin our render pass
|
||||||
|
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||||
|
const color_attachment = gpu.RenderPassColorAttachment{
|
||||||
|
.view = back_buffer_view,
|
||||||
|
.clear_value = std.mem.zeroes(gpu.Color),
|
||||||
|
.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
|
||||||
|
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .renderer = &.{ .location, .scale } },
|
||||||
|
} });
|
||||||
|
var num_entities: usize = 0;
|
||||||
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
|
const locations = archetype.slice(.renderer, .location);
|
||||||
|
const scales = archetype.slice(.renderer, .scale);
|
||||||
|
for (ids, locations, scales) |id, location, scale| {
|
||||||
|
_ = id;
|
||||||
|
|
||||||
|
const ubo = UniformBufferObject{
|
||||||
|
.offset = location.v,
|
||||||
|
.scale = scale,
|
||||||
|
};
|
||||||
|
encoder.writeBuffer(renderer.state.uniform_buffer, uniform_offset * num_entities, &[_]UniformBufferObject{ubo});
|
||||||
|
num_entities += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = encoder.beginRenderPass(&render_pass_info);
|
||||||
|
for (renderer.state.bind_groups[0..num_entities]) |bind_group| {
|
||||||
|
pass.setPipeline(renderer.state.pipeline);
|
||||||
|
pass.setBindGroup(0, bind_group, &.{0});
|
||||||
|
pass.draw(3, 1, 0, 0);
|
||||||
|
}
|
||||||
|
pass.end();
|
||||||
|
pass.release();
|
||||||
|
|
||||||
|
var command = encoder.finish(null);
|
||||||
|
encoder.release();
|
||||||
|
|
||||||
|
renderer.state.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||||
|
command.release();
|
||||||
|
core.swap_chain.present();
|
||||||
|
back_buffer_view.release();
|
||||||
|
}
|
||||||
15
examples/custom-renderer/main.zig
Normal file
15
examples/custom-renderer/main.zig
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Experimental ECS app example. Not yet ready for actual use.
|
||||||
|
const mach = @import("mach");
|
||||||
|
|
||||||
|
const Renderer = @import("Renderer.zig");
|
||||||
|
const Game = @import("Game.zig");
|
||||||
|
|
||||||
|
// The list of modules to be used in our application. Our game itself is implemented in our own
|
||||||
|
// module called Game.
|
||||||
|
pub const modules = .{
|
||||||
|
mach.Engine,
|
||||||
|
Renderer,
|
||||||
|
Game,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const App = mach.App;
|
||||||
22
examples/custom-renderer/shader.wgsl
Normal file
22
examples/custom-renderer/shader.wgsl
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
struct Uniform {
|
||||||
|
pos: vec3<f32>,
|
||||||
|
scale: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
@group(0) @binding(0) var<uniform> in : Uniform;
|
||||||
|
|
||||||
|
@vertex fn vertex_main(
|
||||||
|
@builtin(vertex_index) VertexIndex : u32
|
||||||
|
) -> @builtin(position) vec4<f32> {
|
||||||
|
var positions = array<vec2<f32>, 3>(
|
||||||
|
vec2<f32>( 0.0, 0.1),
|
||||||
|
vec2<f32>(-0.1, -0.1),
|
||||||
|
vec2<f32>( 0.1, -0.1)
|
||||||
|
);
|
||||||
|
var pos = positions[VertexIndex];
|
||||||
|
return vec4<f32>((pos*in.scale)+in.pos.xy, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment fn frag_main() -> @location(0) vec4<f32> {
|
||||||
|
return vec4<f32>(1.0, 0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
150
examples/gkurve/draw.zig
Normal file
150
examples/gkurve/draw.zig
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const mach = @import("mach");
|
||||||
|
const App = @import("main.zig").App;
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const math = mach.math;
|
||||||
|
const AtlasUV = mach.gfx.Atlas.Region.UV;
|
||||||
|
|
||||||
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
const vec3 = math.vec3;
|
||||||
|
const Vec2 = @Vector(2, f32);
|
||||||
|
|
||||||
|
pub const Vertex = struct {
|
||||||
|
pos: @Vector(4, f32),
|
||||||
|
uv: Vec2,
|
||||||
|
};
|
||||||
|
const VERTEX_ATTRIBUTES = [_]gpu.VertexAttribute{
|
||||||
|
.{ .format = .float32x4, .offset = @offsetOf(Vertex, "pos"), .shader_location = 0 },
|
||||||
|
.{ .format = .float32x2, .offset = @offsetOf(Vertex, "uv"), .shader_location = 1 },
|
||||||
|
};
|
||||||
|
pub const VERTEX_BUFFER_LAYOUT = gpu.VertexBufferLayout{
|
||||||
|
.array_stride = @sizeOf(Vertex),
|
||||||
|
.step_mode = .vertex,
|
||||||
|
.attribute_count = VERTEX_ATTRIBUTES.len,
|
||||||
|
.attributes = &VERTEX_ATTRIBUTES,
|
||||||
|
};
|
||||||
|
pub const VertexUniform = Mat4x4;
|
||||||
|
|
||||||
|
const GkurveType = enum(u32) {
|
||||||
|
quadratic_convex = 0,
|
||||||
|
semicircle_convex = 1,
|
||||||
|
quadratic_concave = 2,
|
||||||
|
semicircle_concave = 3,
|
||||||
|
triangle = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const FragUniform = struct {
|
||||||
|
type: GkurveType = .triangle,
|
||||||
|
// Padding for struct alignment to 16 bytes (minimum in WebGPU uniform).
|
||||||
|
padding: @Vector(3, f32) = undefined,
|
||||||
|
blend_color: @Vector(4, f32) = @Vector(4, f32){ 1, 1, 1, 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn equilateralTriangle(app: *App, position: Vec2, scale: f32, uniform: FragUniform, uv: AtlasUV, height_scale: f32) !void {
|
||||||
|
const triangle_height = scale * @sqrt(0.75) * height_scale;
|
||||||
|
|
||||||
|
try app.vertices.appendSlice(&[3]Vertex{
|
||||||
|
.{
|
||||||
|
.pos = .{ position[0] + scale / 2, position[1] + triangle_height, 0, 1 },
|
||||||
|
.uv = .{ uv.x + uv.width * 0.5, uv.y + uv.height },
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.pos = .{ position[0], position[1], 0, 1 },
|
||||||
|
.uv = .{ uv.x, uv.y },
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.pos = .{ position[0] + scale, position[1], 0, 1 },
|
||||||
|
.uv = .{ uv.x + uv.width, uv.y },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try app.fragment_uniform_list.append(uniform);
|
||||||
|
|
||||||
|
app.update_vertex_buffer = true;
|
||||||
|
app.update_frag_uniform_buffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn quad(app: *App, position: Vec2, scale: Vec2, uniform: FragUniform, uv: AtlasUV) !void {
|
||||||
|
const bottom_left_uv = Vec2{ uv.x, uv.y };
|
||||||
|
const bottom_right_uv = Vec2{ uv.x + uv.width, uv.y };
|
||||||
|
const top_left_uv = Vec2{ uv.x, uv.y + uv.height };
|
||||||
|
const top_right_uv = Vec2{ uv.x + uv.width, uv.y + uv.height };
|
||||||
|
|
||||||
|
try app.vertices.appendSlice(&[6]Vertex{
|
||||||
|
.{ .pos = .{ position[0], position[1] + scale[1], 0, 1 }, .uv = top_left_uv },
|
||||||
|
.{ .pos = .{ position[0], position[1], 0, 1 }, .uv = bottom_left_uv },
|
||||||
|
.{ .pos = .{ position[0] + scale[0], position[1], 0, 1 }, .uv = bottom_right_uv },
|
||||||
|
|
||||||
|
.{ .pos = .{ position[0] + scale[0], position[1] + scale[1], 0, 1 }, .uv = top_right_uv },
|
||||||
|
.{ .pos = .{ position[0], position[1] + scale[1], 0, 1 }, .uv = top_left_uv },
|
||||||
|
.{ .pos = .{ position[0] + scale[0], position[1], 0, 1 }, .uv = bottom_right_uv },
|
||||||
|
});
|
||||||
|
|
||||||
|
try app.fragment_uniform_list.appendSlice(&.{ uniform, uniform });
|
||||||
|
|
||||||
|
app.update_vertex_buffer = true;
|
||||||
|
app.update_frag_uniform_buffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn circle(app: *App, position: Vec2, radius: f32, blend_color: @Vector(4, f32), uv: AtlasUV) !void {
|
||||||
|
const low_mid = Vertex{
|
||||||
|
.pos = .{ position[0], position[1] - (radius * 2.0), 0, 1 },
|
||||||
|
.uv = .{ uv.x + uv.width * 0.5, uv.y },
|
||||||
|
};
|
||||||
|
const high_mid = Vertex{
|
||||||
|
.pos = .{ position[0], position[1] + (radius * 2.0), 0, 1 },
|
||||||
|
.uv = .{ uv.x + uv.width * 0.5, uv.y + uv.height },
|
||||||
|
};
|
||||||
|
|
||||||
|
const mid_left = Vertex{
|
||||||
|
.pos = .{ position[0] - radius, position[1], 0, 1 },
|
||||||
|
.uv = .{ uv.x, uv.y + uv.height * 0.5 },
|
||||||
|
};
|
||||||
|
const mid_right = Vertex{
|
||||||
|
.pos = .{ position[0] + radius, position[1], 0, 1 },
|
||||||
|
.uv = .{ uv.x + uv.width, uv.y + uv.height * 0.5 },
|
||||||
|
};
|
||||||
|
|
||||||
|
try app.vertices.appendSlice(&[_]Vertex{
|
||||||
|
high_mid,
|
||||||
|
mid_left,
|
||||||
|
mid_right,
|
||||||
|
|
||||||
|
low_mid,
|
||||||
|
mid_left,
|
||||||
|
mid_right,
|
||||||
|
});
|
||||||
|
|
||||||
|
try app.fragment_uniform_list.appendSlice(&[_]FragUniform{
|
||||||
|
.{
|
||||||
|
.type = .semicircle_convex,
|
||||||
|
.blend_color = blend_color,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.type = .semicircle_convex,
|
||||||
|
.blend_color = blend_color,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.update_vertex_buffer = true;
|
||||||
|
app.update_frag_uniform_buffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getVertexUniformBufferObject() !VertexUniform {
|
||||||
|
// Note: We use window width/height here, not framebuffer width/height.
|
||||||
|
// On e.g. macOS, window size may be 640x480 while framebuffer size may be
|
||||||
|
// 1280x960 (subpixels.) Doing this lets us use a pixel, not subpixel,
|
||||||
|
// coordinate system.
|
||||||
|
const window_size = mach.core.size();
|
||||||
|
const proj = Mat4x4.projection2D(.{
|
||||||
|
.left = 0,
|
||||||
|
.right = @floatFromInt(window_size.width),
|
||||||
|
.bottom = 0,
|
||||||
|
.top = @floatFromInt(window_size.height),
|
||||||
|
.near = -0.1,
|
||||||
|
.far = 100,
|
||||||
|
});
|
||||||
|
const mvp = proj.mul(&Mat4x4.translate(vec3(-1, -1, 0)));
|
||||||
|
return mvp;
|
||||||
|
}
|
||||||
199
examples/gkurve/frag.wgsl
Executable file
199
examples/gkurve/frag.wgsl
Executable file
|
|
@ -0,0 +1,199 @@
|
||||||
|
struct FragUniform {
|
||||||
|
type_: u32,
|
||||||
|
padding: vec3<f32>,
|
||||||
|
blend_color: vec4<f32>,
|
||||||
|
}
|
||||||
|
@binding(1) @group(0) var<storage> ubos: array<FragUniform>;
|
||||||
|
@binding(2) @group(0) var mySampler: sampler;
|
||||||
|
@binding(3) @group(0) var myTexture: texture_2d<f32>;
|
||||||
|
|
||||||
|
const wireframe = false;
|
||||||
|
const antialiased = true;
|
||||||
|
const aa_px = 1.0; // pixels to consume for AA
|
||||||
|
const dist_scale_px = 300.0; // TODO: do not hard code
|
||||||
|
|
||||||
|
@fragment fn main(
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
@interpolate(linear) @location(1) bary_in: vec2<f32>,
|
||||||
|
@interpolate(flat) @location(2) triangle_index: u32,
|
||||||
|
) -> @location(0) vec4<f32> {
|
||||||
|
// Example 1: Visualize barycentric coordinates:
|
||||||
|
// let bary = bary_in;
|
||||||
|
// return vec4<f32>(bary.x, bary.y, 0.0, 1.0);
|
||||||
|
// return vec4<f32>(0.0, bary.x, 0.0, 1.0); // [1.0 (bottom-left vertex), 0.0 (bottom-right vertex)]
|
||||||
|
// return vec4<f32>(0.0, bary.y, 0.0, 1.0); // [1.0 (bottom-left vertex), 0.0 (top-right face)]
|
||||||
|
|
||||||
|
// Example 2: Very simple quadratic bezier
|
||||||
|
// let bary = bary_in;
|
||||||
|
// if (bary.x * bary.x - bary.y) > 0 {
|
||||||
|
// discard;
|
||||||
|
// }
|
||||||
|
// return vec4<f32>(0.0, 1.0, 0.0, 1.0);
|
||||||
|
|
||||||
|
// Example 3: Render gkurve primitives
|
||||||
|
let inversion = select( 1.0, -1.0, ubos[triangle_index].type_ == 0u || ubos[triangle_index].type_ == 1u);
|
||||||
|
// Texture uvs
|
||||||
|
var correct_uv = uv;
|
||||||
|
correct_uv.y = 1.0 - correct_uv.y;
|
||||||
|
var color = textureSample(myTexture, mySampler, correct_uv) * ubos[triangle_index].blend_color;
|
||||||
|
|
||||||
|
// Curve rendering
|
||||||
|
let border_color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
||||||
|
let border_px = 30.0;
|
||||||
|
let is_semicircle = ubos[triangle_index].type_ == 1u || ubos[triangle_index].type_ == 3u;
|
||||||
|
var result = select(
|
||||||
|
curveColor(bary_in, border_px, border_color, color, inversion, is_semicircle),
|
||||||
|
color,
|
||||||
|
ubos[triangle_index].type_ == 4u, // triangle rendering
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wireframe rendering
|
||||||
|
let wireframe_px = 1.0;
|
||||||
|
let wireframe_color = vec4<f32>(0.5, 0.5, 0.5, 1.0);
|
||||||
|
if (wireframe) {
|
||||||
|
result = wireframeColor(bary_in, wireframe_px, wireframe_color, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.a == 0.0) { discard; }
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Performs alpha 'over' blending between two premultiplied-alpha colors.
|
||||||
|
fn alphaOver(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {
|
||||||
|
return a + (b * (1.0 - a.a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates signed distance to a quadratic bézier curve using barycentric coordinates.
|
||||||
|
fn distanceToQuadratic(bary: vec2<f32>) -> f32 {
|
||||||
|
// Gradients
|
||||||
|
let px = dpdx(bary.xy);
|
||||||
|
let py = dpdy(bary.xy);
|
||||||
|
|
||||||
|
// Chain rule
|
||||||
|
let fx = (2.0 * bary.x) * px.x - px.y;
|
||||||
|
let fy = (2.0 * bary.x) * py.x - py.y;
|
||||||
|
|
||||||
|
return (bary.x * bary.x - bary.y) / sqrt(fx * fx + fy * fy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates signed distance to a semicircle using barycentric coordinates.
|
||||||
|
fn distanceToSemicircle(bary: vec2<f32>) -> f32 {
|
||||||
|
let x = abs(((bary.x - 0.5) * 2.0)); // [0.0 left, 1.0 center, 0.0 right]
|
||||||
|
let y = ((bary.x-bary.y) * 4.0); // [2.0 bottom, 0.0 top]
|
||||||
|
let c = x*x + y*y;
|
||||||
|
|
||||||
|
// Gradients
|
||||||
|
let px = dpdx(bary.xy);
|
||||||
|
let py = dpdy(bary.xy);
|
||||||
|
|
||||||
|
// Chain rule
|
||||||
|
let fx = c * px.x - px.y;
|
||||||
|
let fy = c * py.x - py.y;
|
||||||
|
|
||||||
|
let d = (1.0 - (x*x + y*y)) - 0.2;
|
||||||
|
return (-d / 6.0) / sqrt(fx * fx + fy * fy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates signed distance to the wireframe (i.e. faces) of the triangle using barycentric
|
||||||
|
// coordinates.
|
||||||
|
fn distanceToWireframe(bary: vec2<f32>) -> f32 {
|
||||||
|
let normal = vec3<f32>(
|
||||||
|
bary.y, // distance to right face
|
||||||
|
(bary.x - bary.y) * 2.0, // distance to bottom face
|
||||||
|
1.0 - (((bary.x - bary.y)) + bary.x), // distance to left face
|
||||||
|
);
|
||||||
|
let fw = sqrt(dpdx(normal)*dpdx(normal) + dpdy(normal)*dpdy(normal));
|
||||||
|
let d = normal / fw;
|
||||||
|
return min(min(d.x, d.y), d.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the color of the wireframe, taking into account antialiasing and alpha blending with
|
||||||
|
// the desired background blend color.
|
||||||
|
fn wireframeColor(bary: vec2<f32>, px: f32, color: vec4<f32>, blend_color: vec4<f32>) -> vec4<f32> {
|
||||||
|
let dist = distanceToWireframe(bary);
|
||||||
|
if (antialiased) {
|
||||||
|
let outer = dist;
|
||||||
|
let inner = (px + (aa_px * 2.0)) - dist;
|
||||||
|
let in_wireframe = outer >= 0.0 && inner >= 0.0;
|
||||||
|
if (in_wireframe) {
|
||||||
|
// Note: If this is the outer edge of the wireframe, we do not want to perform alpha
|
||||||
|
// blending with the background blend color, since it is an antialiased edge and should
|
||||||
|
// be transparent. However, if it is the internal edge of the wireframe, we do want to
|
||||||
|
// perform alpha blending as it should be an overlay, not transparent.
|
||||||
|
let is_outer_edge = outer < inner;
|
||||||
|
if (is_outer_edge) {
|
||||||
|
let alpha = smoothstep(0.0, 1.0, outer*(1.0 / aa_px));
|
||||||
|
return vec4<f32>((color.rgb/color.a)*alpha, alpha);
|
||||||
|
} else {
|
||||||
|
let aa_inner = inner - aa_px;
|
||||||
|
let alpha = smoothstep(0.0, 1.0, aa_inner*(1.0 / aa_px));
|
||||||
|
let wireframe_color = vec4<f32>((color.rgb/color.a)*alpha, alpha);
|
||||||
|
return alphaOver(wireframe_color, blend_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blend_color;
|
||||||
|
} else {
|
||||||
|
// If we're at the edge use the wireframe color, otherwise use the background blend_color.
|
||||||
|
return select(blend_color, color, (px - dist) >= 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculates the color for a curve, taking into account antialiasing and alpha blending with
|
||||||
|
// the desired background blend color.
|
||||||
|
//
|
||||||
|
// inversion: concave (-1.0) or convex (1.0)
|
||||||
|
// is_semicircle: quadratic bezier (false) or semicircle (true)
|
||||||
|
fn curveColor(
|
||||||
|
bary: vec2<f32>,
|
||||||
|
border_px: f32,
|
||||||
|
border_color: vec4<f32>,
|
||||||
|
blend_color: vec4<f32>,
|
||||||
|
inversion: f32,
|
||||||
|
is_semicircle: bool,
|
||||||
|
) -> vec4<f32> {
|
||||||
|
let dist = select(
|
||||||
|
distanceToQuadratic(bary),
|
||||||
|
distanceToSemicircle(bary),
|
||||||
|
is_semicircle,
|
||||||
|
) * inversion;
|
||||||
|
let is_inverted = (inversion + 1.0) / 2.0; // 1.0 if inverted, 0.0 otherwise
|
||||||
|
|
||||||
|
if (antialiased) {
|
||||||
|
let outer = dist + ((border_px + (aa_px * 2.0)) * is_inverted); // bottom
|
||||||
|
let inner = ((border_px + (aa_px * 2.0)) * (1.0-is_inverted)) - dist; // top
|
||||||
|
let in_border = outer >= 0.0 && inner >= 0.0;
|
||||||
|
if (in_border) {
|
||||||
|
// Note: If this is the outer edge of the curve, we do not want to perform alpha
|
||||||
|
// blending with the background blend color, since it is an antialiased edge and should
|
||||||
|
// be transparent. However, if it is the internal edge of the curve, we do want to
|
||||||
|
// perform alpha blending as it should be an overlay, not transparent.
|
||||||
|
let is_outer_edge = outer < inner;
|
||||||
|
if (is_outer_edge) {
|
||||||
|
let aa_outer = outer - (aa_px * is_inverted);
|
||||||
|
let alpha = smoothstep(0.0, 1.0, aa_outer*(1.0 / aa_px));
|
||||||
|
return vec4<f32>((border_color.rgb/border_color.a)*alpha, alpha);
|
||||||
|
} else {
|
||||||
|
let aa_inner = inner - (aa_px * (1.0 - is_inverted));
|
||||||
|
let alpha = smoothstep(0.0, 1.0, aa_inner*(1.0 / aa_px));
|
||||||
|
let new_border_color = vec4<f32>((border_color.rgb/border_color.a)*alpha, alpha);
|
||||||
|
return alphaOver(new_border_color, blend_color);
|
||||||
|
}
|
||||||
|
return border_color;
|
||||||
|
} else if (outer >= 0.0) {
|
||||||
|
return blend_color;
|
||||||
|
} else {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let outer = dist + (border_px * is_inverted);
|
||||||
|
let inner = (border_px * (1.0-is_inverted)) - dist;
|
||||||
|
let in_border = outer >= 0.0 && inner >= 0.0;
|
||||||
|
if (in_border) {
|
||||||
|
return border_color;
|
||||||
|
} else if (outer >= 0.0) {
|
||||||
|
return blend_color;
|
||||||
|
} else {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
146
examples/gkurve/label.zig
Normal file
146
examples/gkurve/label.zig
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
//! At the moment we use only rgba32, but maybe it could be useful to use also other types
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const ft = @import("freetype");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
const Atlas = mach.gfx.Atlas;
|
||||||
|
const AtlasErr = Atlas.Error;
|
||||||
|
const AtlasUV = Atlas.Region.UV;
|
||||||
|
const App = @import("main.zig").App;
|
||||||
|
const draw = @import("draw.zig");
|
||||||
|
|
||||||
|
pub const Label = @This();
|
||||||
|
|
||||||
|
const Vec2 = @Vector(2, f32);
|
||||||
|
const Vec4 = @Vector(4, f32);
|
||||||
|
|
||||||
|
const GlyphInfo = struct {
|
||||||
|
uv_data: AtlasUV,
|
||||||
|
metrics: ft.GlyphMetrics,
|
||||||
|
};
|
||||||
|
|
||||||
|
face: ft.Face,
|
||||||
|
size: i32,
|
||||||
|
char_map: std.AutoHashMap(u21, GlyphInfo),
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
const WriterContext = struct {
|
||||||
|
label: *Label,
|
||||||
|
app: *App,
|
||||||
|
position: Vec2,
|
||||||
|
text_color: Vec4,
|
||||||
|
};
|
||||||
|
const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr;
|
||||||
|
const Writer = std.io.Writer(WriterContext, WriterError, write);
|
||||||
|
|
||||||
|
pub fn writer(label: *Label, app: *App, position: Vec2, text_color: Vec4) Writer {
|
||||||
|
return Writer{
|
||||||
|
.context = .{
|
||||||
|
.label = label,
|
||||||
|
.app = app,
|
||||||
|
.position = position,
|
||||||
|
.text_color = text_color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(lib: ft.Library, font_path: [*:0]const u8, face_index: i32, char_size: i32, allocator: std.mem.Allocator) !Label {
|
||||||
|
return Label{
|
||||||
|
.face = try lib.createFace(font_path, face_index),
|
||||||
|
.size = char_size,
|
||||||
|
.char_map = std.AutoHashMap(u21, GlyphInfo).init(allocator),
|
||||||
|
.allocator = allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(label: *Label) void {
|
||||||
|
label.face.deinit();
|
||||||
|
label.char_map.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize {
|
||||||
|
var offset = Vec2{ 0, 0 };
|
||||||
|
var j: usize = 0;
|
||||||
|
while (j < bytes.len) {
|
||||||
|
const len = std.unicode.utf8ByteSequenceLength(bytes[j]) catch unreachable;
|
||||||
|
const char = std.unicode.utf8Decode(bytes[j..(j + len)]) catch unreachable;
|
||||||
|
j += len;
|
||||||
|
switch (char) {
|
||||||
|
'\n' => {
|
||||||
|
offset[0] = 0;
|
||||||
|
offset[1] -= @as(f32, @floatFromInt(ctx.label.face.size().metrics().height >> 6));
|
||||||
|
},
|
||||||
|
' ' => {
|
||||||
|
const v = try ctx.label.char_map.getOrPut(char);
|
||||||
|
if (!v.found_existing) {
|
||||||
|
try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0);
|
||||||
|
try ctx.label.face.loadChar(char, .{ .render = true });
|
||||||
|
const glyph = ctx.label.face.glyph();
|
||||||
|
v.value_ptr.* = GlyphInfo{
|
||||||
|
.uv_data = undefined,
|
||||||
|
.metrics = glyph.metrics(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
offset[0] += @as(f32, @floatFromInt(v.value_ptr.metrics.horiAdvance >> 6));
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
const v = try ctx.label.char_map.getOrPut(char);
|
||||||
|
if (!v.found_existing) {
|
||||||
|
try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0);
|
||||||
|
try ctx.label.face.loadChar(char, .{ .render = true });
|
||||||
|
const glyph = ctx.label.face.glyph();
|
||||||
|
const glyph_bitmap = glyph.bitmap();
|
||||||
|
const glyph_width = glyph_bitmap.width();
|
||||||
|
const glyph_height = glyph_bitmap.rows();
|
||||||
|
|
||||||
|
// Add 1 pixel padding to texture to avoid bleeding over other textures
|
||||||
|
const glyph_data = try ctx.label.allocator.alloc(zigimg.color.Rgba32, (glyph_width + 2) * (glyph_height + 2));
|
||||||
|
defer ctx.label.allocator.free(glyph_data);
|
||||||
|
const glyph_buffer = glyph_bitmap.buffer().?;
|
||||||
|
for (glyph_data, 0..) |*data, i| {
|
||||||
|
const x = i % (glyph_width + 2);
|
||||||
|
const y = i / (glyph_width + 2);
|
||||||
|
|
||||||
|
// zig fmt: off
|
||||||
|
const glyph_col =
|
||||||
|
if (x == 0 or x == (glyph_width + 1) or y == 0 or y == (glyph_height + 1))
|
||||||
|
0
|
||||||
|
else
|
||||||
|
glyph_buffer[(y - 1) * glyph_width + (x - 1)];
|
||||||
|
// zig fmt: on
|
||||||
|
|
||||||
|
data.* = zigimg.color.Rgba32.initRgb(glyph_col, glyph_col, glyph_col);
|
||||||
|
}
|
||||||
|
var glyph_atlas_region = try ctx.app.texture_atlas_data.reserve(ctx.label.allocator, glyph_width + 2, glyph_height + 2);
|
||||||
|
ctx.app.texture_atlas_data.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]);
|
||||||
|
|
||||||
|
glyph_atlas_region.x += 1;
|
||||||
|
glyph_atlas_region.y += 1;
|
||||||
|
glyph_atlas_region.width -= 2;
|
||||||
|
glyph_atlas_region.height -= 2;
|
||||||
|
|
||||||
|
v.value_ptr.* = GlyphInfo{
|
||||||
|
.uv_data = glyph_atlas_region.calculateUV(ctx.app.texture_atlas_data.size),
|
||||||
|
.metrics = glyph.metrics(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try draw.quad(
|
||||||
|
ctx.app,
|
||||||
|
ctx.position + offset + Vec2{ @as(f32, @floatFromInt(v.value_ptr.metrics.horiBearingX >> 6)), @as(f32, @floatFromInt((v.value_ptr.metrics.horiBearingY - v.value_ptr.metrics.height) >> 6)) },
|
||||||
|
.{ @as(f32, @floatFromInt(v.value_ptr.metrics.width >> 6)), @as(f32, @floatFromInt(v.value_ptr.metrics.height >> 6)) },
|
||||||
|
.{ .blend_color = ctx.text_color },
|
||||||
|
v.value_ptr.uv_data,
|
||||||
|
);
|
||||||
|
offset[0] += @as(f32, @floatFromInt(v.value_ptr.metrics.horiAdvance >> 6));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(label: *Label, app: *App, comptime fmt: []const u8, args: anytype, position: Vec2, text_color: Vec4) !void {
|
||||||
|
const w = writer(label, app, position, text_color);
|
||||||
|
try w.print(fmt, args);
|
||||||
|
}
|
||||||
339
examples/gkurve/main.zig
Normal file
339
examples/gkurve/main.zig
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
// TODO:
|
||||||
|
// - handle textures better with texture atlas
|
||||||
|
// - handle adding and removing triangles and quads better
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const core = mach.core;
|
||||||
|
const ft = @import("freetype");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
const assets = @import("assets");
|
||||||
|
const draw = @import("draw.zig");
|
||||||
|
const Label = @import("label.zig");
|
||||||
|
const ResizableLabel = @import("resizable_label.zig");
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const Atlas = mach.gfx.Atlas;
|
||||||
|
|
||||||
|
pub const App = @This();
|
||||||
|
|
||||||
|
pipeline: *gpu.RenderPipeline,
|
||||||
|
vertex_buffer: *gpu.Buffer,
|
||||||
|
vertices: std.ArrayList(draw.Vertex),
|
||||||
|
update_vertex_buffer: bool,
|
||||||
|
vertex_uniform_buffer: *gpu.Buffer,
|
||||||
|
update_vertex_uniform_buffer: bool,
|
||||||
|
frag_uniform_buffer: *gpu.Buffer,
|
||||||
|
fragment_uniform_list: std.ArrayList(draw.FragUniform),
|
||||||
|
update_frag_uniform_buffer: bool,
|
||||||
|
bind_group: *gpu.BindGroup,
|
||||||
|
texture_atlas_data: Atlas,
|
||||||
|
|
||||||
|
pub fn init(app: *App) !void {
|
||||||
|
try core.init(.{});
|
||||||
|
|
||||||
|
// TODO: Refactor texture atlas size number
|
||||||
|
app.texture_atlas_data = try Atlas.init(
|
||||||
|
core.allocator,
|
||||||
|
1280,
|
||||||
|
.rgba,
|
||||||
|
);
|
||||||
|
const atlas_size = gpu.Extent3D{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size };
|
||||||
|
|
||||||
|
const texture = core.device.createTexture(&.{
|
||||||
|
.size = atlas_size,
|
||||||
|
.format = .rgba8_unorm,
|
||||||
|
.usage = .{
|
||||||
|
.texture_binding = true,
|
||||||
|
.copy_dst = true,
|
||||||
|
.render_attachment = true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data_layout = gpu.Texture.DataLayout{
|
||||||
|
.bytes_per_row = @as(u32, @intCast(atlas_size.width * 4)),
|
||||||
|
.rows_per_image = @as(u32, @intCast(atlas_size.height)),
|
||||||
|
};
|
||||||
|
|
||||||
|
var img = try zigimg.Image.fromMemory(core.allocator, assets.gotta_go_fast_png);
|
||||||
|
defer img.deinit();
|
||||||
|
|
||||||
|
const atlas_img_region = try app.texture_atlas_data.reserve(core.allocator, @as(u32, @truncate(img.width)), @as(u32, @truncate(img.height)));
|
||||||
|
const img_uv_data = atlas_img_region.calculateUV(app.texture_atlas_data.size);
|
||||||
|
|
||||||
|
switch (img.pixels) {
|
||||||
|
.rgba32 => |pixels| app.texture_atlas_data.set(
|
||||||
|
atlas_img_region,
|
||||||
|
@as([*]const u8, @ptrCast(pixels.ptr))[0 .. pixels.len * 4],
|
||||||
|
),
|
||||||
|
.rgb24 => |pixels| {
|
||||||
|
const data = try rgb24ToRgba32(core.allocator, pixels);
|
||||||
|
defer data.deinit(core.allocator);
|
||||||
|
app.texture_atlas_data.set(
|
||||||
|
atlas_img_region,
|
||||||
|
@as([*]const u8, @ptrCast(data.rgba32.ptr))[0 .. data.rgba32.len * 4],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
else => @panic("unsupported image color format"),
|
||||||
|
}
|
||||||
|
|
||||||
|
const white_tex_scale = 80;
|
||||||
|
var atlas_white_region = try app.texture_atlas_data.reserve(core.allocator, white_tex_scale, white_tex_scale);
|
||||||
|
atlas_white_region.x += 1;
|
||||||
|
atlas_white_region.y += 1;
|
||||||
|
atlas_white_region.width -= 2;
|
||||||
|
atlas_white_region.height -= 2;
|
||||||
|
const white_texture_uv_data = atlas_white_region.calculateUV(app.texture_atlas_data.size);
|
||||||
|
const white_tex_data = try core.allocator.alloc(zigimg.color.Rgba32, white_tex_scale * white_tex_scale);
|
||||||
|
defer core.allocator.free(white_tex_data);
|
||||||
|
@memset(white_tex_data, zigimg.color.Rgba32.initRgb(0xff, 0xff, 0xff));
|
||||||
|
app.texture_atlas_data.set(atlas_white_region, @as([*]const u8, @ptrCast(white_tex_data.ptr))[0 .. white_tex_data.len * 4]);
|
||||||
|
|
||||||
|
app.vertices = try std.ArrayList(draw.Vertex).initCapacity(core.allocator, 9);
|
||||||
|
app.fragment_uniform_list = try std.ArrayList(draw.FragUniform).initCapacity(core.allocator, 3);
|
||||||
|
|
||||||
|
// Quick test for using freetype
|
||||||
|
const lib = try ft.Library.init();
|
||||||
|
defer lib.deinit();
|
||||||
|
|
||||||
|
const DemoMode = enum {
|
||||||
|
gkurves,
|
||||||
|
bitmap_text, // TODO: broken
|
||||||
|
text,
|
||||||
|
quad,
|
||||||
|
circle,
|
||||||
|
};
|
||||||
|
const demo_mode: DemoMode = .gkurves;
|
||||||
|
|
||||||
|
core.queue.writeTexture(
|
||||||
|
&.{ .texture = texture },
|
||||||
|
&data_layout,
|
||||||
|
&.{ .width = app.texture_atlas_data.size, .height = app.texture_atlas_data.size },
|
||||||
|
app.texture_atlas_data.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
const wsize = core.size();
|
||||||
|
const window_width = @as(f32, @floatFromInt(wsize.width));
|
||||||
|
const window_height = @as(f32, @floatFromInt(wsize.height));
|
||||||
|
const triangle_scale = 250;
|
||||||
|
switch (demo_mode) {
|
||||||
|
.gkurves => {
|
||||||
|
try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 1.9 }, triangle_scale, .{}, img_uv_data, 1.0);
|
||||||
|
try draw.equilateralTriangle(app, .{ window_width / 2, window_height / 1.9 - triangle_scale }, triangle_scale, .{ .type = .quadratic_concave }, img_uv_data, 1.0);
|
||||||
|
try draw.equilateralTriangle(app, .{ window_width / 2 - triangle_scale, window_height / 1.9 }, triangle_scale, .{ .type = .quadratic_convex }, white_texture_uv_data, 1.0);
|
||||||
|
try draw.equilateralTriangle(app, .{ window_width / 2 - triangle_scale, window_height / 1.9 - triangle_scale }, triangle_scale, .{ .type = .quadratic_convex }, white_texture_uv_data, 0.5);
|
||||||
|
},
|
||||||
|
else => @panic("disabled for now"),
|
||||||
|
// TODO: disabled for now because these rely on a Label API that expects a font filepath,
|
||||||
|
// rather than bytes. This gkurve example / experiment test bed should probably be moved
|
||||||
|
// elsewhere anyway.
|
||||||
|
//
|
||||||
|
// .bitmap_text => {
|
||||||
|
// // const character = "Gotta-go-fast!\n0123456789\n~!@#$%^&*()_+è\n:\"<>?`-=[];',./";
|
||||||
|
// // const character = "ABCDEFGHIJ\nKLMNOPQRST\nUVWXYZ";
|
||||||
|
// const size_multiplier = 5;
|
||||||
|
// var label = try Label.init(lib, assets.roboto_medium_ttf.path, 0, 110 * size_multiplier, core.allocator);
|
||||||
|
// defer label.deinit();
|
||||||
|
// try label.print(app, "All your game's bases are belong to us èçòà", .{}, @Vector(2, f32){ 0, 420 }, @Vector(4, f32){ 1, 1, 1, 1 });
|
||||||
|
// try label.print(app, "wow!", .{}, @Vector(2, f32){ 70 * size_multiplier, 70 }, @Vector(4, f32){ 1, 1, 1, 1 });
|
||||||
|
// },
|
||||||
|
// .text => {
|
||||||
|
// const character = "Gotta-go-fast!\n0123456789\n~!@#$%^&*()_+è\n:\"<>?`-=[];',./";
|
||||||
|
// const size_multiplier = 5;
|
||||||
|
|
||||||
|
// var resizable_label: ResizableLabel = undefined;
|
||||||
|
// try resizable_label.init(lib, assets.roboto_medium_ttf.path, 0, core.allocator, white_texture_uv_data);
|
||||||
|
// defer resizable_label.deinit();
|
||||||
|
// try resizable_label.print(app, character, .{}, @Vector(4, f32){ 20, 300, 0, 0 }, @Vector(4, f32){ 1, 1, 1, 1 }, 20 * size_multiplier);
|
||||||
|
// // try resizable_label.print(app, "@", .{}, @Vector(4, f32){ 20, 150, 0, 0 }, @Vector(4, f32){ 1, 1, 1, 1 }, 130 * size_multiplier);
|
||||||
|
// },
|
||||||
|
.quad => {
|
||||||
|
try draw.quad(app, .{ 0, 0 }, .{ 480, 480 }, .{}, .{ .x = 0, .y = 0, .width = 1, .height = 1 });
|
||||||
|
},
|
||||||
|
.circle => {
|
||||||
|
try draw.circle(app, .{ window_width / 2, window_height / 2 }, window_height / 2 - 10, .{ 0, 0.5, 0.75, 1.0 }, white_texture_uv_data);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const vs_module = core.device.createShaderModuleWGSL("vert", @embedFile("vert.wgsl"));
|
||||||
|
const fs_module = core.device.createShaderModuleWGSL("frag", @embedFile("frag.wgsl"));
|
||||||
|
|
||||||
|
const blend = gpu.BlendState{
|
||||||
|
.color = .{
|
||||||
|
.operation = .add,
|
||||||
|
.src_factor = .src_alpha,
|
||||||
|
.dst_factor = .one_minus_src_alpha,
|
||||||
|
},
|
||||||
|
.alpha = .{
|
||||||
|
.operation = .add,
|
||||||
|
.src_factor = .one,
|
||||||
|
.dst_factor = .zero,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const color_target = gpu.ColorTargetState{
|
||||||
|
.format = core.descriptor.format,
|
||||||
|
.blend = &blend,
|
||||||
|
.write_mask = gpu.ColorWriteMaskFlags.all,
|
||||||
|
};
|
||||||
|
const fragment = gpu.FragmentState.init(.{
|
||||||
|
.module = fs_module,
|
||||||
|
.entry_point = "main",
|
||||||
|
.targets = &.{color_target},
|
||||||
|
});
|
||||||
|
|
||||||
|
const vbgle = gpu.BindGroupLayout.Entry.buffer(0, .{ .vertex = true }, .uniform, true, 0);
|
||||||
|
const fbgle = gpu.BindGroupLayout.Entry.buffer(1, .{ .fragment = true }, .read_only_storage, true, 0);
|
||||||
|
const sbgle = gpu.BindGroupLayout.Entry.sampler(2, .{ .fragment = true }, .filtering);
|
||||||
|
const tbgle = gpu.BindGroupLayout.Entry.texture(3, .{ .fragment = true }, .float, .dimension_2d, false);
|
||||||
|
const bgl = core.device.createBindGroupLayout(
|
||||||
|
&gpu.BindGroupLayout.Descriptor.init(.{
|
||||||
|
.entries = &.{ vbgle, fbgle, sbgle, tbgle },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const bind_group_layouts = [_]*gpu.BindGroupLayout{bgl};
|
||||||
|
const pipeline_layout = core.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.init(.{
|
||||||
|
.module = vs_module,
|
||||||
|
.entry_point = "main",
|
||||||
|
.buffers = &.{draw.VERTEX_BUFFER_LAYOUT},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const vertex_buffer = core.device.createBuffer(&.{
|
||||||
|
.usage = .{ .copy_dst = true, .vertex = true },
|
||||||
|
.size = @sizeOf(draw.Vertex) * app.vertices.items.len,
|
||||||
|
.mapped_at_creation = .false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const vertex_uniform_buffer = core.device.createBuffer(&.{
|
||||||
|
.usage = .{ .copy_dst = true, .uniform = true },
|
||||||
|
.size = @sizeOf(draw.VertexUniform),
|
||||||
|
.mapped_at_creation = .false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const frag_uniform_buffer = core.device.createBuffer(&.{
|
||||||
|
.usage = .{ .copy_dst = true, .storage = true },
|
||||||
|
.size = @sizeOf(draw.FragUniform) * app.fragment_uniform_list.items.len,
|
||||||
|
.mapped_at_creation = .false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const sampler = core.device.createSampler(&.{
|
||||||
|
// .mag_filter = .linear,
|
||||||
|
// .min_filter = .linear,
|
||||||
|
});
|
||||||
|
|
||||||
|
std.debug.assert((app.vertices.items.len / 3) == app.fragment_uniform_list.items.len);
|
||||||
|
const bind_group = core.device.createBindGroup(
|
||||||
|
&gpu.BindGroup.Descriptor.init(.{
|
||||||
|
.layout = bgl,
|
||||||
|
.entries = &.{
|
||||||
|
gpu.BindGroup.Entry.buffer(0, vertex_uniform_buffer, 0, @sizeOf(draw.VertexUniform)),
|
||||||
|
gpu.BindGroup.Entry.buffer(1, frag_uniform_buffer, 0, @sizeOf(draw.FragUniform) * app.fragment_uniform_list.items.len),
|
||||||
|
gpu.BindGroup.Entry.sampler(2, sampler),
|
||||||
|
gpu.BindGroup.Entry.textureView(3, texture.createView(&gpu.TextureView.Descriptor{ .dimension = .dimension_2d })),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.pipeline = core.device.createRenderPipeline(&pipeline_descriptor);
|
||||||
|
app.vertex_buffer = vertex_buffer;
|
||||||
|
app.vertex_uniform_buffer = vertex_uniform_buffer;
|
||||||
|
app.frag_uniform_buffer = frag_uniform_buffer;
|
||||||
|
app.bind_group = bind_group;
|
||||||
|
app.update_vertex_buffer = true;
|
||||||
|
app.update_vertex_uniform_buffer = true;
|
||||||
|
app.update_frag_uniform_buffer = true;
|
||||||
|
|
||||||
|
vs_module.release();
|
||||||
|
fs_module.release();
|
||||||
|
pipeline_layout.release();
|
||||||
|
bgl.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(app: *App) void {
|
||||||
|
defer core.deinit();
|
||||||
|
|
||||||
|
app.vertex_buffer.release();
|
||||||
|
app.vertex_uniform_buffer.release();
|
||||||
|
app.frag_uniform_buffer.release();
|
||||||
|
app.bind_group.release();
|
||||||
|
app.vertices.deinit();
|
||||||
|
app.fragment_uniform_list.deinit();
|
||||||
|
app.texture_atlas_data.deinit(core.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(app: *App) !bool {
|
||||||
|
var iter = core.pollEvents();
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
if (ev.key == .space) return true;
|
||||||
|
},
|
||||||
|
.framebuffer_resize => {
|
||||||
|
app.update_vertex_uniform_buffer = true;
|
||||||
|
},
|
||||||
|
.close => return true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back_buffer_view = core.swap_chain.getCurrentTextureView().?;
|
||||||
|
const color_attachment = gpu.RenderPassColorAttachment{
|
||||||
|
.view = back_buffer_view,
|
||||||
|
.clear_value = std.mem.zeroes(gpu.Color),
|
||||||
|
.load_op = .clear,
|
||||||
|
.store_op = .store,
|
||||||
|
};
|
||||||
|
|
||||||
|
const encoder = core.device.createCommandEncoder(null);
|
||||||
|
const render_pass_info = gpu.RenderPassDescriptor.init(.{
|
||||||
|
.color_attachments = &.{color_attachment},
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
if (app.update_vertex_buffer) {
|
||||||
|
encoder.writeBuffer(app.vertex_buffer, 0, app.vertices.items);
|
||||||
|
app.update_vertex_buffer = false;
|
||||||
|
}
|
||||||
|
if (app.update_frag_uniform_buffer) {
|
||||||
|
encoder.writeBuffer(app.frag_uniform_buffer, 0, app.fragment_uniform_list.items);
|
||||||
|
app.update_frag_uniform_buffer = false;
|
||||||
|
}
|
||||||
|
if (app.update_vertex_uniform_buffer) {
|
||||||
|
encoder.writeBuffer(app.vertex_uniform_buffer, 0, &[_]draw.VertexUniform{try draw.getVertexUniformBufferObject()});
|
||||||
|
app.update_vertex_uniform_buffer = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass = encoder.beginRenderPass(&render_pass_info);
|
||||||
|
pass.setPipeline(app.pipeline);
|
||||||
|
pass.setVertexBuffer(0, app.vertex_buffer, 0, @sizeOf(draw.Vertex) * app.vertices.items.len);
|
||||||
|
pass.setBindGroup(0, app.bind_group, &.{ 0, 0 });
|
||||||
|
pass.draw(@as(u32, @truncate(app.vertices.items.len)), 1, 0, 0);
|
||||||
|
pass.end();
|
||||||
|
pass.release();
|
||||||
|
|
||||||
|
var command = encoder.finish(null);
|
||||||
|
encoder.release();
|
||||||
|
|
||||||
|
core.queue.submit(&[_]*gpu.CommandBuffer{command});
|
||||||
|
command.release();
|
||||||
|
core.swap_chain.present();
|
||||||
|
back_buffer_view.release();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
|
||||||
|
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < in.len) : (i += 1) {
|
||||||
|
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
555
examples/gkurve/resizable_label.zig
Normal file
555
examples/gkurve/resizable_label.zig
Normal file
|
|
@ -0,0 +1,555 @@
|
||||||
|
//! TODO: Refactor the API, maybe use a handle that contains the lib and other things and controls init and deinit of ft.Lib and other things
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const ft = @import("freetype");
|
||||||
|
const App = @import("main.zig").App;
|
||||||
|
const Vertex = @import("draw.zig").Vertex;
|
||||||
|
const math = mach.math;
|
||||||
|
const earcut = mach.earcut;
|
||||||
|
const Atlas = mach.gfx.Atlas;
|
||||||
|
const AtlasErr = Atlas.Error;
|
||||||
|
const AtlasUV = Atlas.Region.UV;
|
||||||
|
|
||||||
|
// If true, show the filled triangles green, the concave beziers blue and the convex ones red
|
||||||
|
const debug_colors = false;
|
||||||
|
|
||||||
|
pub const ResizableLabel = @This();
|
||||||
|
|
||||||
|
const Vec2 = @Vector(2, f32);
|
||||||
|
const Vec4 = @Vector(4, f32);
|
||||||
|
const VertexList = std.ArrayList(Vertex);
|
||||||
|
|
||||||
|
// All the data that a single character needs to be rendered
|
||||||
|
// TODO: hori/vert advance, write file format
|
||||||
|
const CharVertices = struct {
|
||||||
|
filled_vertices: VertexList,
|
||||||
|
filled_vertices_indices: std.ArrayList(u16),
|
||||||
|
// Concave vertices belong to the filled_vertices list, so just index them
|
||||||
|
concave_vertices: std.ArrayList(u16),
|
||||||
|
// The point outside of the convex bezier, doesn't belong to the filled vertices,
|
||||||
|
// But the other two points do, so put those in the indices
|
||||||
|
convex_vertices: VertexList,
|
||||||
|
convex_vertices_indices: std.ArrayList(u16),
|
||||||
|
|
||||||
|
fn deinit(self: CharVertices) void {
|
||||||
|
self.filled_vertices.deinit();
|
||||||
|
self.filled_vertices_indices.deinit();
|
||||||
|
self.concave_vertices.deinit();
|
||||||
|
self.convex_vertices.deinit();
|
||||||
|
self.convex_vertices_indices.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
face: ft.Face,
|
||||||
|
char_map: std.AutoHashMap(u21, CharVertices),
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
tessellator: earcut.Processor(f32),
|
||||||
|
white_texture: AtlasUV,
|
||||||
|
|
||||||
|
// The data that the write function needs
|
||||||
|
// TODO: move twxture here, don't limit to just white_texture
|
||||||
|
const WriterContext = struct {
|
||||||
|
label: *ResizableLabel,
|
||||||
|
app: *App,
|
||||||
|
position: Vec4,
|
||||||
|
text_color: Vec4,
|
||||||
|
text_size: u32,
|
||||||
|
};
|
||||||
|
const WriterError = ft.Error || std.mem.Allocator.Error || AtlasErr;
|
||||||
|
const Writer = std.io.Writer(WriterContext, WriterError, write);
|
||||||
|
|
||||||
|
pub fn writer(label: *ResizableLabel, app: *App, position: Vec4, text_color: Vec4, text_size: u32) Writer {
|
||||||
|
return Writer{
|
||||||
|
.context = .{
|
||||||
|
.label = label,
|
||||||
|
.app = app,
|
||||||
|
.position = position,
|
||||||
|
.text_color = text_color,
|
||||||
|
.text_size = text_size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(self: *ResizableLabel, lib: ft.Library, font_path: [*:0]const u8, face_index: i32, allocator: std.mem.Allocator, white_texture: AtlasUV) !void {
|
||||||
|
self.* = ResizableLabel{
|
||||||
|
.face = try lib.createFace(font_path, face_index),
|
||||||
|
.char_map = std.AutoHashMap(u21, CharVertices).init(allocator),
|
||||||
|
.allocator = allocator,
|
||||||
|
.tessellator = earcut.Processor(f32){},
|
||||||
|
.white_texture = white_texture,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(label: *ResizableLabel) void {
|
||||||
|
label.face.deinit();
|
||||||
|
label.tessellator.deinit(label.allocator);
|
||||||
|
|
||||||
|
var iter = label.char_map.valueIterator();
|
||||||
|
while (iter.next()) |ptr| {
|
||||||
|
ptr.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
label.char_map.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle offsets
|
||||||
|
// FIXME: many useless allocations for the arraylists
|
||||||
|
fn write(ctx: WriterContext, bytes: []const u8) WriterError!usize {
|
||||||
|
var offset = Vec4{ 0, 0, 0, 0 };
|
||||||
|
var c: usize = 0;
|
||||||
|
while (c < bytes.len) {
|
||||||
|
const len = std.unicode.utf8ByteSequenceLength(bytes[c]) catch unreachable;
|
||||||
|
const char = std.unicode.utf8Decode(bytes[c..(c + len)]) catch unreachable;
|
||||||
|
c += len;
|
||||||
|
switch (char) {
|
||||||
|
'\n' => {
|
||||||
|
offset[0] = 0;
|
||||||
|
offset[1] -= @as(f32, @floatFromInt(ctx.label.face.glyph().metrics().vertAdvance)) * (@as(f32, @floatFromInt(ctx.text_size)) / 1024);
|
||||||
|
},
|
||||||
|
' ' => {
|
||||||
|
@panic("TODO: Space character not implemented yet");
|
||||||
|
// const v = try ctx.label.char_map.getOrPut(char);
|
||||||
|
// if (!v.found_existing) {
|
||||||
|
// try ctx.label.face.setCharSize(ctx.label.size * 64, 0, 50, 0);
|
||||||
|
// try ctx.label.face.loadChar(char, .{ .render = true });
|
||||||
|
// const glyph = ctx.label.face.glyph;
|
||||||
|
// v.value_ptr.* = GlyphInfo{
|
||||||
|
// .uv_data = undefined,
|
||||||
|
// .metrics = glyph.metrics(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// offset[0] += @intToFloat(f32, v.value_ptr.metrics.horiAdvance >> 6);
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
const v = try ctx.label.char_map.getOrPut(char);
|
||||||
|
if (!v.found_existing) {
|
||||||
|
try ctx.label.face.loadChar(char, .{ .no_scale = true, .no_bitmap = true });
|
||||||
|
const glyph = ctx.label.face.glyph();
|
||||||
|
|
||||||
|
// Use a big scale and then scale to the actual text size
|
||||||
|
const multiplier = 1024 << 6;
|
||||||
|
const matrix = ft.Matrix{
|
||||||
|
.xx = 1 * multiplier,
|
||||||
|
.xy = 0 * multiplier,
|
||||||
|
.yx = 0 * multiplier,
|
||||||
|
.yy = 1 * multiplier,
|
||||||
|
};
|
||||||
|
glyph.outline().?.transform(matrix);
|
||||||
|
|
||||||
|
v.value_ptr.* = CharVertices{
|
||||||
|
.filled_vertices = VertexList.init(ctx.label.allocator),
|
||||||
|
.filled_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator),
|
||||||
|
.concave_vertices = std.ArrayList(u16).init(ctx.label.allocator),
|
||||||
|
.convex_vertices = VertexList.init(ctx.label.allocator),
|
||||||
|
.convex_vertices_indices = std.ArrayList(u16).init(ctx.label.allocator),
|
||||||
|
};
|
||||||
|
|
||||||
|
var outline_ctx = OutlineContext{
|
||||||
|
.outline_verts = std.ArrayList(std.ArrayList(Vec2)).init(ctx.label.allocator),
|
||||||
|
.inside_verts = std.ArrayList(Vec2).init(ctx.label.allocator),
|
||||||
|
.concave_vertices = std.ArrayList(Vec2).init(ctx.label.allocator),
|
||||||
|
.convex_vertices = std.ArrayList(Vec2).init(ctx.label.allocator),
|
||||||
|
};
|
||||||
|
defer outline_ctx.outline_verts.deinit();
|
||||||
|
defer {
|
||||||
|
for (outline_ctx.outline_verts.items) |*item| item.deinit();
|
||||||
|
}
|
||||||
|
defer outline_ctx.inside_verts.deinit();
|
||||||
|
defer outline_ctx.concave_vertices.deinit();
|
||||||
|
defer outline_ctx.convex_vertices.deinit();
|
||||||
|
|
||||||
|
const callbacks = ft.Outline.Funcs(*OutlineContext){
|
||||||
|
.move_to = moveToFunction,
|
||||||
|
.line_to = lineToFunction,
|
||||||
|
.conic_to = conicToFunction,
|
||||||
|
.cubic_to = cubicToFunction,
|
||||||
|
.shift = 0,
|
||||||
|
.delta = 0,
|
||||||
|
};
|
||||||
|
try ctx.label.face.glyph().outline().?.decompose(&outline_ctx, callbacks);
|
||||||
|
uniteOutsideAndInsideVertices(&outline_ctx);
|
||||||
|
|
||||||
|
// Tessellator.triangulatePolygons() doesn't seem to work, so just
|
||||||
|
// call triangulatePolygon() for each polygon, and put the results all
|
||||||
|
// in all_outlines and all_indices
|
||||||
|
var all_outlines = std.ArrayList(Vec2).init(ctx.label.allocator);
|
||||||
|
defer all_outlines.deinit();
|
||||||
|
var all_indices = std.ArrayList(u16).init(ctx.label.allocator);
|
||||||
|
defer all_indices.deinit();
|
||||||
|
var idx_offset: u16 = 0;
|
||||||
|
for (outline_ctx.outline_verts.items) |item| {
|
||||||
|
if (item.items.len == 0) continue;
|
||||||
|
// TODO(gkurve): don't discard this, make tessellator use Vec2 / avoid copy?
|
||||||
|
var polygon = std.ArrayListUnmanaged(f32){};
|
||||||
|
defer polygon.deinit(ctx.label.allocator);
|
||||||
|
if (ctx.label.face.glyph().outline().?.orientation() == .truetype) {
|
||||||
|
// TrueType orientation has clockwise contours, so reverse the list
|
||||||
|
// since we need CCW.
|
||||||
|
var i = item.items.len - 1;
|
||||||
|
while (i > 0) : (i -= 1) {
|
||||||
|
try polygon.append(ctx.label.allocator, item.items[i][0]);
|
||||||
|
try polygon.append(ctx.label.allocator, item.items[i][1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (item.items) |vert| {
|
||||||
|
try polygon.append(ctx.label.allocator, vert[0]);
|
||||||
|
try polygon.append(ctx.label.allocator, vert[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try ctx.label.tessellator.process(ctx.label.allocator, polygon.items, null, 2);
|
||||||
|
|
||||||
|
for (ctx.label.tessellator.triangles.items) |idx| {
|
||||||
|
try all_outlines.append(Vec2{ polygon.items[idx * 2], polygon.items[(idx * 2) + 1] });
|
||||||
|
try all_indices.append(@as(u16, @intCast((idx * 2) + idx_offset)));
|
||||||
|
}
|
||||||
|
idx_offset += @as(u16, @intCast(ctx.label.tessellator.triangles.items.len));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (all_outlines.items) |item| {
|
||||||
|
// FIXME: The uv_data is wrong, should be pushed up by the lowest a character can be
|
||||||
|
const vertex_uv = item / math.vec.splat(@Vector(2, f32), 1024 << 6);
|
||||||
|
const vertex_pos = Vec4{ item[0], item[1], 0, 1 };
|
||||||
|
try v.value_ptr.filled_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv });
|
||||||
|
}
|
||||||
|
try v.value_ptr.filled_vertices_indices.appendSlice(all_indices.items);
|
||||||
|
|
||||||
|
// TODO(gkurve): could more optimally find index (e.g. already know it from
|
||||||
|
// data structure, instead of finding equal point.)
|
||||||
|
for (outline_ctx.concave_vertices.items) |concave_control| {
|
||||||
|
for (all_outlines.items, 0..) |item, j| {
|
||||||
|
if (vec2Equal(item, concave_control)) {
|
||||||
|
try v.value_ptr.concave_vertices.append(@as(u16, @truncate(j)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std.debug.assert((outline_ctx.convex_vertices.items.len % 3) == 0);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < outline_ctx.convex_vertices.items.len) : (i += 3) {
|
||||||
|
const vert = outline_ctx.convex_vertices.items[i];
|
||||||
|
const vertex_uv = vert / math.vec.splat(@Vector(2, f32), 1024 << 6);
|
||||||
|
const vertex_pos = Vec4{ vert[0], vert[1], 0, 1 };
|
||||||
|
try v.value_ptr.convex_vertices.append(Vertex{ .pos = vertex_pos, .uv = vertex_uv });
|
||||||
|
|
||||||
|
var found: usize = 0;
|
||||||
|
for (all_outlines.items, 0..) |item, j| {
|
||||||
|
if (vec2Equal(item, outline_ctx.convex_vertices.items[i + 1])) {
|
||||||
|
try v.value_ptr.convex_vertices_indices.append(@as(u16, @truncate(j)));
|
||||||
|
found += 1;
|
||||||
|
}
|
||||||
|
if (vec2Equal(item, outline_ctx.convex_vertices.items[i + 2])) {
|
||||||
|
try v.value_ptr.convex_vertices_indices.append(@as(u16, @truncate(j)));
|
||||||
|
found += 1;
|
||||||
|
}
|
||||||
|
if (found == 2) break;
|
||||||
|
}
|
||||||
|
std.debug.assert(found == 2);
|
||||||
|
}
|
||||||
|
std.debug.assert(((v.value_ptr.convex_vertices.items.len + v.value_ptr.convex_vertices_indices.items.len) % 3) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the data and apply resizing of pos and uv
|
||||||
|
const filled_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.filled_vertices.items.len);
|
||||||
|
defer ctx.label.allocator.free(filled_vertices_after_offset);
|
||||||
|
for (filled_vertices_after_offset, 0..) |*vert, i| {
|
||||||
|
vert.* = v.value_ptr.filled_vertices.items[i];
|
||||||
|
vert.pos *= Vec4{ @as(f32, @floatFromInt(ctx.text_size)) / 1024, @as(f32, @floatFromInt(ctx.text_size)) / 1024, 0, 1 };
|
||||||
|
vert.pos += ctx.position + offset;
|
||||||
|
vert.uv = .{
|
||||||
|
vert.uv[0] * ctx.label.white_texture.width + ctx.label.white_texture.x,
|
||||||
|
vert.uv[1] * ctx.label.white_texture.height + ctx.label.white_texture.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try ctx.app.vertices.appendSlice(filled_vertices_after_offset);
|
||||||
|
|
||||||
|
if (debug_colors) {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = .{ 0, 1, 0, 1 } }, filled_vertices_after_offset.len / 3);
|
||||||
|
} else {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .blend_color = ctx.text_color }, filled_vertices_after_offset.len / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
var convex_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.convex_vertices.items.len + v.value_ptr.convex_vertices_indices.items.len);
|
||||||
|
defer ctx.label.allocator.free(convex_vertices_after_offset);
|
||||||
|
var j: u16 = 0;
|
||||||
|
var k: u16 = 0;
|
||||||
|
var convex_vertices_consumed: usize = 0;
|
||||||
|
while (j < convex_vertices_after_offset.len) : (j += 3) {
|
||||||
|
convex_vertices_after_offset[j] = v.value_ptr.convex_vertices.items[j / 3];
|
||||||
|
convex_vertices_consumed += 1;
|
||||||
|
|
||||||
|
convex_vertices_after_offset[j].pos *= Vec4{ @as(f32, @floatFromInt(ctx.text_size)) / 1024, @as(f32, @floatFromInt(ctx.text_size)) / 1024, 0, 1 };
|
||||||
|
convex_vertices_after_offset[j].pos += ctx.position + offset;
|
||||||
|
convex_vertices_after_offset[j].uv = .{
|
||||||
|
convex_vertices_after_offset[j].uv[0] * ctx.label.white_texture.width + ctx.label.white_texture.x,
|
||||||
|
convex_vertices_after_offset[j].uv[1] * ctx.label.white_texture.height + ctx.label.white_texture.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
convex_vertices_after_offset[j + 1] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k]];
|
||||||
|
convex_vertices_after_offset[j + 2] = filled_vertices_after_offset[v.value_ptr.convex_vertices_indices.items[k + 1]];
|
||||||
|
k += 2;
|
||||||
|
}
|
||||||
|
std.debug.assert(convex_vertices_consumed == v.value_ptr.convex_vertices.items.len);
|
||||||
|
try ctx.app.vertices.appendSlice(convex_vertices_after_offset);
|
||||||
|
|
||||||
|
if (debug_colors) {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_convex, .blend_color = .{ 1, 0, 0, 1 } }, convex_vertices_after_offset.len / 3);
|
||||||
|
} else {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_convex, .blend_color = ctx.text_color }, convex_vertices_after_offset.len / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const concave_vertices_after_offset = try ctx.label.allocator.alloc(Vertex, v.value_ptr.concave_vertices.items.len);
|
||||||
|
defer ctx.label.allocator.free(concave_vertices_after_offset);
|
||||||
|
for (concave_vertices_after_offset, 0..) |*vert, i| {
|
||||||
|
vert.* = filled_vertices_after_offset[v.value_ptr.concave_vertices.items[i]];
|
||||||
|
}
|
||||||
|
try ctx.app.vertices.appendSlice(concave_vertices_after_offset);
|
||||||
|
|
||||||
|
if (debug_colors) {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_concave, .blend_color = .{ 0, 0, 1, 1 } }, concave_vertices_after_offset.len / 3);
|
||||||
|
} else {
|
||||||
|
try ctx.app.fragment_uniform_list.appendNTimes(.{ .type = .quadratic_concave, .blend_color = ctx.text_color }, concave_vertices_after_offset.len / 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.app.update_vertex_buffer = true;
|
||||||
|
ctx.app.update_frag_uniform_buffer = true;
|
||||||
|
|
||||||
|
offset[0] += @as(f32, @floatFromInt(ctx.label.face.glyph().metrics().horiAdvance)) * (@as(f32, @floatFromInt(ctx.text_size)) / 1024);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bytes.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First move to initialize the outline, (first point),
|
||||||
|
// After many Q L or C, we will come back to the first point and then call M again if we need to hollow
|
||||||
|
// On the second M, we instead use an L to connect the first point to the start of the hollow path.
|
||||||
|
// We then follow like normal and at the end of the hollow path we use another L to close the path.
|
||||||
|
|
||||||
|
// This is basically how an o would be drawn, each ┌... character is a Vertex
|
||||||
|
// ┌--------┐
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | ┌----┐ |
|
||||||
|
// └-┘ | | Consider the vertices here and below to be at the same height, they are coincident
|
||||||
|
// ┌-┐ | |
|
||||||
|
// | └----┘ |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// | |
|
||||||
|
// └--------┘
|
||||||
|
|
||||||
|
const OutlineContext = struct {
|
||||||
|
/// There may be more than one polygon (for example with 'i' we have the polygon of the base and
|
||||||
|
/// another for the circle)
|
||||||
|
outline_verts: std.ArrayList(std.ArrayList(Vec2)),
|
||||||
|
|
||||||
|
/// The internal outline, used for carving the shape. For example in 'a', we would first get the
|
||||||
|
/// outline of the entire 'a', but if we stopped there, the center hole would be filled, so we
|
||||||
|
/// need another outline for carving the filled polygon.
|
||||||
|
inside_verts: std.ArrayList(Vec2),
|
||||||
|
|
||||||
|
/// For the concave (inner 'o') and convex (outer 'o') beziers
|
||||||
|
concave_vertices: std.ArrayList(Vec2),
|
||||||
|
convex_vertices: std.ArrayList(Vec2),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// If there are elements in inside_verts, unite them with the outline_verts, effectively carving
|
||||||
|
/// the shape
|
||||||
|
fn uniteOutsideAndInsideVertices(ctx: *OutlineContext) void {
|
||||||
|
if (ctx.inside_verts.items.len != 0) {
|
||||||
|
// Check which point of outline is closer to the first of inside
|
||||||
|
var last_outline = &ctx.outline_verts.items[ctx.outline_verts.items.len - 1];
|
||||||
|
if (last_outline.items.len == 0 and ctx.outline_verts.items.len >= 2) {
|
||||||
|
last_outline = &ctx.outline_verts.items[ctx.outline_verts.items.len - 2];
|
||||||
|
}
|
||||||
|
std.debug.assert(last_outline.items.len != 0);
|
||||||
|
const closest_to_inside: usize = blk: {
|
||||||
|
const first_point_inside = ctx.inside_verts.items[0];
|
||||||
|
var min = math.floatMax(f32);
|
||||||
|
var closest_index: usize = undefined;
|
||||||
|
|
||||||
|
for (last_outline.items, 0..) |item, i| {
|
||||||
|
const dist = @reduce(.Add, (item - first_point_inside) * (item - first_point_inside));
|
||||||
|
if (dist < min) {
|
||||||
|
min = dist;
|
||||||
|
closest_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break :blk closest_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.inside_verts.append(last_outline.items[closest_to_inside]) catch unreachable;
|
||||||
|
last_outline.insertSlice(closest_to_inside + 1, ctx.inside_verts.items) catch unreachable;
|
||||||
|
ctx.inside_verts.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Return also allocation error
|
||||||
|
fn moveToFunction(ctx: *OutlineContext, _to: ft.Vector) ft.Error!void {
|
||||||
|
uniteOutsideAndInsideVertices(ctx);
|
||||||
|
|
||||||
|
const to = Vec2{ @as(f32, @floatFromInt(_to.x)), @as(f32, @floatFromInt(_to.y)) };
|
||||||
|
|
||||||
|
// To check wether a point is carving a polygon, use the point-in-polygon test to determine if
|
||||||
|
// we're inside or outside of the polygon.
|
||||||
|
const new_point_is_inside = pointInPolygon(to, ctx.outline_verts.items);
|
||||||
|
|
||||||
|
if (ctx.outline_verts.items.len == 0 or ctx.outline_verts.items[ctx.outline_verts.items.len - 1].items.len > 0) {
|
||||||
|
// The last polygon we were building is now finished.
|
||||||
|
const new_outline_list = std.ArrayList(Vec2).init(ctx.outline_verts.allocator);
|
||||||
|
ctx.outline_verts.append(new_outline_list) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new_point_is_inside) {
|
||||||
|
ctx.inside_verts.append(to) catch unreachable;
|
||||||
|
} else {
|
||||||
|
ctx.outline_verts.items[ctx.outline_verts.items.len - 1].append(to) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lineToFunction(ctx: *OutlineContext, to: ft.Vector) ft.Error!void {
|
||||||
|
// std.log.info("L {} {}", .{ to.x, to.y });
|
||||||
|
|
||||||
|
// If inside_verts is not empty, we need to fill it
|
||||||
|
if (ctx.inside_verts.items.len != 0) {
|
||||||
|
ctx.inside_verts.append(.{ @as(f32, @floatFromInt(to.x)), @as(f32, @floatFromInt(to.y)) }) catch unreachable;
|
||||||
|
} else {
|
||||||
|
// Otherwise append the new point to the last polygon
|
||||||
|
ctx.outline_verts.items[ctx.outline_verts.items.len - 1].append(.{ @as(f32, @floatFromInt(to.x)), @as(f32, @floatFromInt(to.y)) }) catch unreachable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called to indicate that a quadratic bezier curve occured between the previous point on the glyph
|
||||||
|
/// outline to the `_to` point on the path, with the specified `_control` quadratic bezier control
|
||||||
|
/// point.
|
||||||
|
fn conicToFunction(ctx: *OutlineContext, _control: ft.Vector, _to: ft.Vector) ft.Error!void {
|
||||||
|
// std.log.info("C {} {} {} {}", .{ control.x, control.y, to.x, to.y });
|
||||||
|
const control = Vec2{ @as(f32, @floatFromInt(_control.x)), @as(f32, @floatFromInt(_control.y)) };
|
||||||
|
const to = Vec2{ @as(f32, @floatFromInt(_to.x)), @as(f32, @floatFromInt(_to.y)) };
|
||||||
|
|
||||||
|
// If our last point was inside the glyph (e.g. the hole in the letter 'o') then this is a
|
||||||
|
// continuation of that path, and we should write this vertex to inside_verts. Otherwise we're
|
||||||
|
// on the outside and the vertex should go in outline_verts.
|
||||||
|
//
|
||||||
|
// We derive if we're on the inside or outside based on whether inside_verts has items in it,
|
||||||
|
// because only a lineTo callback can move us from the inside to the outside or vice-versa. A
|
||||||
|
// quadratic bezier would *always* be the continuation of an inside or outside path.
|
||||||
|
var verts_to_write = if (ctx.inside_verts.items.len != 0) &ctx.inside_verts else &ctx.outline_verts.items[ctx.outline_verts.items.len - 1];
|
||||||
|
const previous_point = verts_to_write.items[verts_to_write.items.len - 1];
|
||||||
|
|
||||||
|
var vertices = [_]Vec2{ control, to, previous_point };
|
||||||
|
|
||||||
|
const vec1 = control - previous_point;
|
||||||
|
const vec2 = to - control;
|
||||||
|
|
||||||
|
// CCW (convex) or CW (concave)?
|
||||||
|
if ((vec1[0] * vec2[1] - vec1[1] * vec2[0]) <= 0) {
|
||||||
|
// Convex
|
||||||
|
ctx.convex_vertices.appendSlice(&vertices) catch unreachable;
|
||||||
|
verts_to_write.append(to) catch unreachable;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concave
|
||||||
|
//
|
||||||
|
// In this case, we need to write a vertex (for the filled triangle) to the quadratic
|
||||||
|
// control point. However, since this is the concave case the control point could be outside
|
||||||
|
// the shape itself. We need to ensure it is not, otherwise the triangle would end up filling
|
||||||
|
// space outside the shape.
|
||||||
|
//
|
||||||
|
// Diagram: https://user-images.githubusercontent.com/3173176/189944586-bc1b109a-62c4-4ef5-a605-4c6a7e4a1abd.png
|
||||||
|
//
|
||||||
|
// To fix this, we must determine if the control point intersects with any of our outline
|
||||||
|
// segments. If it does, we use that intersection point as the vertex. Otherwise, it doesn't go
|
||||||
|
// past an outline segment and we can use the control point just fine.
|
||||||
|
var intersection: ?Vec2 = null;
|
||||||
|
for (ctx.outline_verts.items) |polygon| {
|
||||||
|
var i: usize = 1;
|
||||||
|
while (i < polygon.items.len) : (i += 1) {
|
||||||
|
const v1 = polygon.items[i - 1];
|
||||||
|
const v2 = polygon.items[i];
|
||||||
|
if (vec2Equal(v1, previous_point) or vec2Equal(v1, control) or vec2Equal(v1, to) or vec2Equal(v2, previous_point) or vec2Equal(v2, control) or vec2Equal(v2, to)) continue;
|
||||||
|
|
||||||
|
intersection = intersectLineSegments(v1, v2, previous_point, control);
|
||||||
|
if (intersection != null) break;
|
||||||
|
}
|
||||||
|
if (intersection != null) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intersection) |intersect| {
|
||||||
|
// TODO: properly scale control/intersection point a little bit towards the previous_point,
|
||||||
|
// so our tessellator doesn't get confused about it being exactly on the path.
|
||||||
|
//
|
||||||
|
// TODO(gkurve): Moving this control point changes the bezier shape (obviously) which means
|
||||||
|
// it is no longer true to the original shape. Need to fix this with some type of negative
|
||||||
|
// border on the gkurve primitive.
|
||||||
|
vertices[0] = Vec2{ intersect[0] * 0.99, intersect[1] * 0.99 };
|
||||||
|
}
|
||||||
|
ctx.concave_vertices.appendSlice(&vertices) catch unreachable;
|
||||||
|
verts_to_write.append(vertices[0]) catch unreachable;
|
||||||
|
verts_to_write.append(to) catch unreachable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doesn't seem to be used much
|
||||||
|
fn cubicToFunction(ctx: *OutlineContext, control_0: ft.Vector, control_1: ft.Vector, to: ft.Vector) ft.Error!void {
|
||||||
|
_ = ctx;
|
||||||
|
_ = control_0;
|
||||||
|
_ = control_1;
|
||||||
|
_ = to;
|
||||||
|
@panic("TODO: search how to approximate cubic bezier with quadratic ones");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(label: *ResizableLabel, app: *App, comptime fmt: []const u8, args: anytype, position: Vec4, text_color: Vec4, text_size: u32) !void {
|
||||||
|
const w = writer(label, app, position, text_color, text_size);
|
||||||
|
try w.print(fmt, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Intersects the line segments [p0, p1] and [p2, p3], returning the intersection point if any.
|
||||||
|
fn intersectLineSegments(p0: Vec2, p1: Vec2, p2: Vec2, p3: Vec2) ?Vec2 {
|
||||||
|
const s1 = Vec2{ p1[0] - p0[0], p1[1] - p0[1] };
|
||||||
|
const s2 = Vec2{ p3[0] - p2[0], p3[1] - p2[1] };
|
||||||
|
const s = (-s1[1] * (p0[0] - p2[0]) + s1[0] * (p0[1] - p2[1])) / (-s2[0] * s1[1] + s1[0] * s2[1]);
|
||||||
|
const t = (s2[0] * (p0[1] - p2[1]) - s2[1] * (p0[0] - p2[0])) / (-s2[0] * s1[1] + s1[0] * s2[1]);
|
||||||
|
|
||||||
|
if (s >= 0 and s <= 1 and t >= 0 and t <= 1) {
|
||||||
|
// Collision
|
||||||
|
return Vec2{ p0[0] + (t * s1[0]), p0[1] + (t * s1[1]) };
|
||||||
|
}
|
||||||
|
return null; // No collision
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersectRayToLineSegment(ray_origin: Vec2, ray_direction: Vec2, p1: Vec2, p2: Vec2) ?Vec2 {
|
||||||
|
return intersectLineSegments(ray_origin, ray_origin * (ray_direction * Vec2{ 10000000.0, 10000000.0 }), p1, p2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec2Equal(a: Vec2, b: Vec2) bool {
|
||||||
|
return a[0] == b[0] and a[1] == b[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vec2CrossProduct(a: Vec2, b: Vec2) f32 {
|
||||||
|
return (a[0] * b[1]) - (a[1] * b[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pointInPolygon(p: Vec2, polygon: []std.ArrayList(Vec2)) bool {
|
||||||
|
// Cast a ray to the right of the point and check
|
||||||
|
// when this ray intersects the edges of the polygons,
|
||||||
|
// if the number of intersections is odd -> inside,
|
||||||
|
// if it's even -> outside
|
||||||
|
var is_inside = false;
|
||||||
|
for (polygon) |contour| {
|
||||||
|
var i: usize = 1;
|
||||||
|
while (i < contour.items.len) : (i += 1) {
|
||||||
|
const v1 = contour.items[i - 1];
|
||||||
|
const v2 = contour.items[i];
|
||||||
|
|
||||||
|
if (intersectRayToLineSegment(p, Vec2{ 1, p[1] }, v1, v2)) |_| {
|
||||||
|
is_inside = !is_inside;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return is_inside;
|
||||||
|
}
|
||||||
314
examples/gkurve/tracy.zig
Normal file
314
examples/gkurve/tracy.zig
Normal file
|
|
@ -0,0 +1,314 @@
|
||||||
|
// Copied from zig/src/tracy.zig
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
// TODO: integrate with tracy?
|
||||||
|
// const build_options = @import("build_options");
|
||||||
|
|
||||||
|
// pub const enable = if (builtin.is_test) false else build_options.enable_tracy;
|
||||||
|
// pub const enable_allocation = enable and build_options.enable_tracy_allocation;
|
||||||
|
// pub const enable_callstack = enable and build_options.enable_tracy_callstack;
|
||||||
|
pub const enable = false;
|
||||||
|
pub const enable_allocation = enable and false;
|
||||||
|
pub const enable_callstack = enable and false;
|
||||||
|
|
||||||
|
// TODO: make this configurable
|
||||||
|
const callstack_depth = 10;
|
||||||
|
|
||||||
|
const ___tracy_c_zone_context = extern struct {
|
||||||
|
id: u32,
|
||||||
|
active: c_int,
|
||||||
|
|
||||||
|
pub inline fn end(self: @This()) void {
|
||||||
|
___tracy_emit_zone_end(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn addText(self: @This(), text: []const u8) void {
|
||||||
|
___tracy_emit_zone_text(self, text.ptr, text.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setName(self: @This(), name: []const u8) void {
|
||||||
|
___tracy_emit_zone_name(self, name.ptr, name.len);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setColor(self: @This(), color: u32) void {
|
||||||
|
___tracy_emit_zone_color(self, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setValue(self: @This(), value: u64) void {
|
||||||
|
___tracy_emit_zone_value(self, value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Ctx = if (enable) ___tracy_c_zone_context else struct {
|
||||||
|
pub inline fn end(self: @This()) void {
|
||||||
|
_ = self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn addText(self: @This(), text: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setName(self: @This(), name: []const u8) void {
|
||||||
|
_ = self;
|
||||||
|
_ = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setColor(self: @This(), color: u32) void {
|
||||||
|
_ = self;
|
||||||
|
_ = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn setValue(self: @This(), value: u64) void {
|
||||||
|
_ = self;
|
||||||
|
_ = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub inline fn trace(comptime src: std.builtin.SourceLocation) Ctx {
|
||||||
|
if (!enable) return .{};
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
return ___tracy_emit_zone_begin_callstack(&.{
|
||||||
|
.name = null,
|
||||||
|
.function = src.fn_name.ptr,
|
||||||
|
.file = src.file.ptr,
|
||||||
|
.line = src.line,
|
||||||
|
.color = 0,
|
||||||
|
}, callstack_depth, 1);
|
||||||
|
} else {
|
||||||
|
return ___tracy_emit_zone_begin(&.{
|
||||||
|
.name = null,
|
||||||
|
.function = src.fn_name.ptr,
|
||||||
|
.file = src.file.ptr,
|
||||||
|
.line = src.line,
|
||||||
|
.color = 0,
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn traceNamed(comptime src: std.builtin.SourceLocation, comptime name: [:0]const u8) Ctx {
|
||||||
|
if (!enable) return .{};
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
return ___tracy_emit_zone_begin_callstack(&.{
|
||||||
|
.name = name.ptr,
|
||||||
|
.function = src.fn_name.ptr,
|
||||||
|
.file = src.file.ptr,
|
||||||
|
.line = src.line,
|
||||||
|
.color = 0,
|
||||||
|
}, callstack_depth, 1);
|
||||||
|
} else {
|
||||||
|
return ___tracy_emit_zone_begin(&.{
|
||||||
|
.name = name.ptr,
|
||||||
|
.function = src.fn_name.ptr,
|
||||||
|
.file = src.file.ptr,
|
||||||
|
.line = src.line,
|
||||||
|
.color = 0,
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tracyAllocator(allocator: std.mem.Allocator) TracyAllocator(null) {
|
||||||
|
return TracyAllocator(null).init(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn TracyAllocator(comptime name: ?[:0]const u8) type {
|
||||||
|
return struct {
|
||||||
|
parent_allocator: std.mem.Allocator,
|
||||||
|
|
||||||
|
const Self = @This();
|
||||||
|
|
||||||
|
pub fn init(parent_allocator: std.mem.Allocator) Self {
|
||||||
|
return .{
|
||||||
|
.parent_allocator = parent_allocator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allocator(self: *Self) std.mem.Allocator {
|
||||||
|
return std.mem.Allocator.init(self, allocFn, resizeFn, freeFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allocFn(self: *Self, len: usize, ptr_align: u29, len_align: u29, ret_addr: usize) std.mem.Allocator.Error![]u8 {
|
||||||
|
const result = self.parent_allocator.rawAlloc(len, ptr_align, len_align, ret_addr);
|
||||||
|
if (result) |data| {
|
||||||
|
if (data.len != 0) {
|
||||||
|
if (name) |n| {
|
||||||
|
allocNamed(data.ptr, data.len, n);
|
||||||
|
} else {
|
||||||
|
alloc(data.ptr, data.len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else |_| {
|
||||||
|
messageColor("allocation failed", 0xFF0000);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resizeFn(self: *Self, buf: []u8, buf_align: u29, new_len: usize, len_align: u29, ret_addr: usize) ?usize {
|
||||||
|
if (self.parent_allocator.rawResize(buf, buf_align, new_len, len_align, ret_addr)) |resized_len| {
|
||||||
|
if (name) |n| {
|
||||||
|
freeNamed(buf.ptr, n);
|
||||||
|
allocNamed(buf.ptr, resized_len, n);
|
||||||
|
} else {
|
||||||
|
free(buf.ptr);
|
||||||
|
alloc(buf.ptr, resized_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resized_len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// during normal operation the compiler hits this case thousands of times due to this
|
||||||
|
// emitting messages for it is both slow and causes clutter
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn freeFn(self: *Self, buf: []u8, buf_align: u29, ret_addr: usize) void {
|
||||||
|
self.parent_allocator.rawFree(buf, buf_align, ret_addr);
|
||||||
|
// this condition is to handle free being called on an empty slice that was never even allocated
|
||||||
|
// example case: `std.process.getSelfExeSharedLibPaths` can return `&[_][:0]u8{}`
|
||||||
|
if (buf.len != 0) {
|
||||||
|
if (name) |n| {
|
||||||
|
freeNamed(buf.ptr, n);
|
||||||
|
} else {
|
||||||
|
free(buf.ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function only accepts comptime known strings, see `messageCopy` for runtime strings
|
||||||
|
pub inline fn message(comptime msg: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_messageL(msg.ptr, if (enable_callstack) callstack_depth else 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function only accepts comptime known strings, see `messageColorCopy` for runtime strings
|
||||||
|
pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_messageLC(msg.ptr, color, if (enable_callstack) callstack_depth else 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn messageCopy(msg: []const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_message(msg.ptr, msg.len, if (enable_callstack) callstack_depth else 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn messageColorCopy(msg: [:0]const u8, color: u32) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_messageC(msg.ptr, msg.len, color, if (enable_callstack) callstack_depth else 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn frameMark() void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_frame_mark(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn frameMarkNamed(comptime name: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_frame_mark(name.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub inline fn namedFrame(comptime name: [:0]const u8) Frame(name) {
|
||||||
|
frameMarkStart(name);
|
||||||
|
return .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn Frame(comptime name: [:0]const u8) type {
|
||||||
|
return struct {
|
||||||
|
pub fn end(_: @This()) void {
|
||||||
|
frameMarkEnd(name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn frameMarkStart(comptime name: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_frame_mark_start(name.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn frameMarkEnd(comptime name: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
___tracy_emit_frame_mark_end(name.ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn ___tracy_emit_frame_mark_start(name: [*:0]const u8) void;
|
||||||
|
extern fn ___tracy_emit_frame_mark_end(name: [*:0]const u8) void;
|
||||||
|
|
||||||
|
inline fn alloc(ptr: [*]u8, len: usize) void {
|
||||||
|
if (!enable) return;
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
___tracy_emit_memory_alloc_callstack(ptr, len, callstack_depth, 0);
|
||||||
|
} else {
|
||||||
|
___tracy_emit_memory_alloc(ptr, len, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn allocNamed(ptr: [*]u8, len: usize, comptime name: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
___tracy_emit_memory_alloc_callstack_named(ptr, len, callstack_depth, 0, name.ptr);
|
||||||
|
} else {
|
||||||
|
___tracy_emit_memory_alloc_named(ptr, len, 0, name.ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn free(ptr: [*]u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
___tracy_emit_memory_free_callstack(ptr, callstack_depth, 0);
|
||||||
|
} else {
|
||||||
|
___tracy_emit_memory_free(ptr, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn freeNamed(ptr: [*]u8, comptime name: [:0]const u8) void {
|
||||||
|
if (!enable) return;
|
||||||
|
|
||||||
|
if (enable_callstack) {
|
||||||
|
___tracy_emit_memory_free_callstack_named(ptr, callstack_depth, 0, name.ptr);
|
||||||
|
} else {
|
||||||
|
___tracy_emit_memory_free_named(ptr, 0, name.ptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern fn ___tracy_emit_zone_begin(
|
||||||
|
srcloc: *const ___tracy_source_location_data,
|
||||||
|
active: c_int,
|
||||||
|
) ___tracy_c_zone_context;
|
||||||
|
extern fn ___tracy_emit_zone_begin_callstack(
|
||||||
|
srcloc: *const ___tracy_source_location_data,
|
||||||
|
depth: c_int,
|
||||||
|
active: c_int,
|
||||||
|
) ___tracy_c_zone_context;
|
||||||
|
extern fn ___tracy_emit_zone_text(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void;
|
||||||
|
extern fn ___tracy_emit_zone_name(ctx: ___tracy_c_zone_context, txt: [*]const u8, size: usize) void;
|
||||||
|
extern fn ___tracy_emit_zone_color(ctx: ___tracy_c_zone_context, color: u32) void;
|
||||||
|
extern fn ___tracy_emit_zone_value(ctx: ___tracy_c_zone_context, value: u64) void;
|
||||||
|
extern fn ___tracy_emit_zone_end(ctx: ___tracy_c_zone_context) void;
|
||||||
|
extern fn ___tracy_emit_memory_alloc(ptr: *const anyopaque, size: usize, secure: c_int) void;
|
||||||
|
extern fn ___tracy_emit_memory_alloc_callstack(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int) void;
|
||||||
|
extern fn ___tracy_emit_memory_free(ptr: *const anyopaque, secure: c_int) void;
|
||||||
|
extern fn ___tracy_emit_memory_free_callstack(ptr: *const anyopaque, depth: c_int, secure: c_int) void;
|
||||||
|
extern fn ___tracy_emit_memory_alloc_named(ptr: *const anyopaque, size: usize, secure: c_int, name: [*:0]const u8) void;
|
||||||
|
extern fn ___tracy_emit_memory_alloc_callstack_named(ptr: *const anyopaque, size: usize, depth: c_int, secure: c_int, name: [*:0]const u8) void;
|
||||||
|
extern fn ___tracy_emit_memory_free_named(ptr: *const anyopaque, secure: c_int, name: [*:0]const u8) void;
|
||||||
|
extern fn ___tracy_emit_memory_free_callstack_named(ptr: *const anyopaque, depth: c_int, secure: c_int, name: [*:0]const u8) void;
|
||||||
|
extern fn ___tracy_emit_message(txt: [*]const u8, size: usize, callstack: c_int) void;
|
||||||
|
extern fn ___tracy_emit_messageL(txt: [*:0]const u8, callstack: c_int) void;
|
||||||
|
extern fn ___tracy_emit_messageC(txt: [*]const u8, size: usize, color: u32, callstack: c_int) void;
|
||||||
|
extern fn ___tracy_emit_messageLC(txt: [*:0]const u8, color: u32, callstack: c_int) void;
|
||||||
|
extern fn ___tracy_emit_frame_mark(name: ?[*:0]const u8) void;
|
||||||
|
|
||||||
|
const ___tracy_source_location_data = extern struct {
|
||||||
|
name: ?[*:0]const u8,
|
||||||
|
function: [*:0]const u8,
|
||||||
|
file: [*:0]const u8,
|
||||||
|
line: u32,
|
||||||
|
color: u32,
|
||||||
|
};
|
||||||
40
examples/gkurve/vert.wgsl
Normal file
40
examples/gkurve/vert.wgsl
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
struct VertexUniform {
|
||||||
|
matrix: mat4x4<f32>,
|
||||||
|
}
|
||||||
|
@binding(0) @group(0) var<uniform> ubo: VertexUniform;
|
||||||
|
|
||||||
|
struct VertexOut {
|
||||||
|
@builtin(position) position_clip: vec4<f32>,
|
||||||
|
@location(0) frag_uv: vec2<f32>,
|
||||||
|
@interpolate(linear) @location(1) frag_bary: vec2<f32>,
|
||||||
|
@interpolate(flat) @location(2) triangle_index: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex fn main(
|
||||||
|
@builtin(vertex_index) vertex_index: u32,
|
||||||
|
@location(0) position: vec4<f32>,
|
||||||
|
@location(1) uv: vec2<f32>,
|
||||||
|
) -> VertexOut {
|
||||||
|
var output : VertexOut;
|
||||||
|
output.position_clip = ubo.matrix * position;
|
||||||
|
output.frag_uv = uv;
|
||||||
|
|
||||||
|
// Generates [0.0, 0.0], [0.5, 0.0], [1.0, 1.0]
|
||||||
|
//
|
||||||
|
// Equal to:
|
||||||
|
//
|
||||||
|
// if ((vertex_index+1u) % 3u == 0u) {
|
||||||
|
// output.frag_bary = vec2<f32>(0.0, 0.0);
|
||||||
|
// } else if ((vertex_index+1u) % 3u == 1u) {
|
||||||
|
// output.frag_bary = vec2<f32>(0.5, 0.0);
|
||||||
|
// } else {
|
||||||
|
// output.frag_bary = vec2<f32>(1.0, 1.0);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
output.frag_bary = vec2<f32>(
|
||||||
|
f32((vertex_index+1u) % 3u) * 0.5,
|
||||||
|
1.0 - f32((((vertex_index + 3u) % 3u) + 1u) % 2u),
|
||||||
|
);
|
||||||
|
output.triangle_index = vertex_index / 3u;
|
||||||
|
return output;
|
||||||
|
}
|
||||||
211
examples/glyphs/Game.zig
Normal file
211
examples/glyphs/Game.zig
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const core = mach.core;
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const ecs = mach.ecs;
|
||||||
|
const Sprite = mach.gfx.Sprite;
|
||||||
|
const math = mach.math;
|
||||||
|
const vec2 = math.vec2;
|
||||||
|
const vec3 = math.vec3;
|
||||||
|
const Vec2 = math.Vec2;
|
||||||
|
const Vec3 = math.Vec3;
|
||||||
|
const Mat3x3 = math.Mat3x3;
|
||||||
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
|
||||||
|
const Text = @import("Text.zig");
|
||||||
|
|
||||||
|
timer: mach.Timer,
|
||||||
|
player: mach.ecs.EntityID,
|
||||||
|
direction: Vec2 = vec2(0, 0),
|
||||||
|
spawning: bool = false,
|
||||||
|
spawn_timer: mach.Timer,
|
||||||
|
fps_timer: mach.Timer,
|
||||||
|
frame_count: usize,
|
||||||
|
sprites: usize,
|
||||||
|
rand: std.rand.DefaultPrng,
|
||||||
|
time: f32,
|
||||||
|
|
||||||
|
const d0 = 0.000001;
|
||||||
|
|
||||||
|
// Each module must have a globally unique name declared, it is impossible to use two modules with
|
||||||
|
// the same name in a program. To avoid name conflicts, we follow naming conventions:
|
||||||
|
//
|
||||||
|
// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it
|
||||||
|
// provides.
|
||||||
|
// 2. Single-word names like `.game` are reserved for the application itself.
|
||||||
|
// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui`
|
||||||
|
// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally
|
||||||
|
// unique.
|
||||||
|
//
|
||||||
|
pub const name = .game;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
pub const Pipeline = enum(u32) {
|
||||||
|
default,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
sprite_mod: *Sprite.Mod,
|
||||||
|
text_mod: *Text.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// The Mach .core is where we set window options, etc.
|
||||||
|
core.setTitle("gfx.Sprite example");
|
||||||
|
|
||||||
|
// Initialize mach.gfx.Text module
|
||||||
|
try text_mod.send(.init, .{});
|
||||||
|
|
||||||
|
// Tell sprite_mod to use the texture
|
||||||
|
try sprite_mod.send(.init, .{});
|
||||||
|
try sprite_mod.send(.initPipeline, .{Sprite.PipelineOptions{
|
||||||
|
.pipeline = @intFromEnum(Pipeline.text),
|
||||||
|
.texture = text_mod.state.texture,
|
||||||
|
}});
|
||||||
|
|
||||||
|
// We can create entities, and set components on them. Note that components live in a module
|
||||||
|
// namespace, e.g. the `Sprite` module could have a 3D `.location` component with a different
|
||||||
|
// type than the `.physics2d` module's `.location` component if you desire.
|
||||||
|
|
||||||
|
const r = text_mod.state.regions.get('?').?;
|
||||||
|
const player = try engine.newEntity();
|
||||||
|
try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
|
||||||
|
try sprite_mod.set(player, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
|
||||||
|
try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
|
||||||
|
try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.text));
|
||||||
|
try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)});
|
||||||
|
|
||||||
|
game.state = .{
|
||||||
|
.timer = try mach.Timer.start(),
|
||||||
|
.spawn_timer = try mach.Timer.start(),
|
||||||
|
.player = player,
|
||||||
|
.fps_timer = try mach.Timer.start(),
|
||||||
|
.frame_count = 0,
|
||||||
|
.sprites = 0,
|
||||||
|
.rand = std.rand.DefaultPrng.init(1337),
|
||||||
|
.time = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
sprite_mod: *Sprite.Mod,
|
||||||
|
text_mod: *Text.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
||||||
|
var iter = core.pollEvents();
|
||||||
|
var direction = game.state.direction;
|
||||||
|
var spawning = game.state.spawning;
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] -= 1,
|
||||||
|
.right => direction.v[0] += 1,
|
||||||
|
.up => direction.v[1] += 1,
|
||||||
|
.down => direction.v[1] -= 1,
|
||||||
|
.space => spawning = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.key_release => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] += 1,
|
||||||
|
.right => direction.v[0] -= 1,
|
||||||
|
.up => direction.v[1] -= 1,
|
||||||
|
.down => direction.v[1] += 1,
|
||||||
|
.space => spawning = false,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.close => try engine.send(.exit, .{}),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game.state.direction = direction;
|
||||||
|
game.state.spawning = spawning;
|
||||||
|
|
||||||
|
var player_transform = sprite_mod.get(game.state.player, .transform).?;
|
||||||
|
var player_pos = player_transform.translation();
|
||||||
|
if (!spawning and game.state.spawn_timer.read() > 1.0 / 60.0) {
|
||||||
|
// Spawn new entities
|
||||||
|
_ = game.state.spawn_timer.lap();
|
||||||
|
for (0..50) |_| {
|
||||||
|
var new_pos = player_pos;
|
||||||
|
new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
|
||||||
|
const rand_index = game.state.rand.random().intRangeAtMost(usize, 0, text_mod.state.regions.count() - 1);
|
||||||
|
const r = text_mod.state.regions.entries.get(rand_index).value;
|
||||||
|
|
||||||
|
const new_entity = try engine.newEntity();
|
||||||
|
try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scaleScalar(0.3)));
|
||||||
|
try sprite_mod.set(new_entity, .size, vec2(@floatFromInt(r.width), @floatFromInt(r.height)));
|
||||||
|
try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(@floatFromInt(r.x), @floatFromInt(r.y))));
|
||||||
|
try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.text));
|
||||||
|
game.state.sprites += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||||
|
const delta_time = game.state.timer.lap();
|
||||||
|
|
||||||
|
// Animate entities
|
||||||
|
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .mach_gfx_sprite = &.{.transform} },
|
||||||
|
} });
|
||||||
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
|
const transforms = archetype.slice(.mach_gfx_sprite, .transform);
|
||||||
|
for (ids, transforms) |id, *old_transform| {
|
||||||
|
var location = old_transform.translation();
|
||||||
|
if (location.x() < -@as(f32, @floatFromInt(core.size().width)) / 1.5 or location.x() > @as(f32, @floatFromInt(core.size().width)) / 1.5 or location.y() < -@as(f32, @floatFromInt(core.size().height)) / 1.5 or location.y() > @as(f32, @floatFromInt(core.size().height)) / 1.5) {
|
||||||
|
try engine.entities.remove(id);
|
||||||
|
game.state.sprites -= 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var transform = Mat4x4.ident;
|
||||||
|
transform = transform.mul(&Mat4x4.scale(Vec3.splat(1.0 + (0.2 * delta_time))));
|
||||||
|
transform = transform.mul(&Mat4x4.translate(location));
|
||||||
|
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time));
|
||||||
|
transform = transform.mul(&Mat4x4.scale(Vec3.splat(@max(math.cos(game.state.time / 2.0), 0.2))));
|
||||||
|
|
||||||
|
// TODO: .set() API is substantially slower due to internals
|
||||||
|
// try sprite_mod.set(id, .transform, transform);
|
||||||
|
old_transform.* = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the player position, by moving in the direction the player wants to go
|
||||||
|
// by the speed amount.
|
||||||
|
const speed = 200.0;
|
||||||
|
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||||
|
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||||
|
player_transform = Mat4x4.translate(player_pos).mul(
|
||||||
|
&Mat4x4.scale(Vec3.splat(1.0)),
|
||||||
|
);
|
||||||
|
try sprite_mod.set(game.state.player, .transform, player_transform);
|
||||||
|
|
||||||
|
try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.text)});
|
||||||
|
|
||||||
|
// Perform pre-render work
|
||||||
|
try sprite_mod.send(.preRender, .{@intFromEnum(Pipeline.text)});
|
||||||
|
|
||||||
|
// Render a frame
|
||||||
|
try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
||||||
|
try sprite_mod.send(.render, .{@intFromEnum(Pipeline.text)});
|
||||||
|
try engine.send(.endPass, .{});
|
||||||
|
try engine.send(.present, .{}); // Present the frame
|
||||||
|
|
||||||
|
// Every second, update the window title with the FPS
|
||||||
|
if (game.state.fps_timer.read() >= 1.0) {
|
||||||
|
try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites });
|
||||||
|
game.state.fps_timer.reset();
|
||||||
|
game.state.frame_count = 0;
|
||||||
|
}
|
||||||
|
game.state.frame_count += 1;
|
||||||
|
game.state.time += delta_time;
|
||||||
|
}
|
||||||
123
examples/glyphs/Text.zig
Normal file
123
examples/glyphs/Text.zig
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
const mach = @import("mach");
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const ecs = mach.ecs;
|
||||||
|
const ft = @import("freetype");
|
||||||
|
const std = @import("std");
|
||||||
|
const assets = @import("assets");
|
||||||
|
|
||||||
|
pub const name = .game_text;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
const RegionMap = std.AutoArrayHashMapUnmanaged(u21, mach.gfx.Atlas.Region);
|
||||||
|
|
||||||
|
texture_atlas: mach.gfx.Atlas,
|
||||||
|
texture: *gpu.Texture,
|
||||||
|
ft: ft.Library,
|
||||||
|
face: ft.Face,
|
||||||
|
regions: RegionMap = .{},
|
||||||
|
|
||||||
|
pub fn deinit(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
text_mod: *Mod,
|
||||||
|
) !void {
|
||||||
|
text_mod.state.texture_atlas.deinit(engine.allocator);
|
||||||
|
text_mod.state.texture.release();
|
||||||
|
text_mod.state.face.deinit();
|
||||||
|
text_mod.state.ft.deinit();
|
||||||
|
text_mod.state.regions.deinit(engine.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const local = struct {
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
text_mod: *Mod,
|
||||||
|
) !void {
|
||||||
|
const device = engine.state.device;
|
||||||
|
|
||||||
|
// rgba32_pixels
|
||||||
|
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||||
|
|
||||||
|
// Create a GPU texture
|
||||||
|
const texture = device.createTexture(&.{
|
||||||
|
.size = img_size,
|
||||||
|
.format = .rgba8_unorm,
|
||||||
|
.usage = .{
|
||||||
|
.texture_binding = true,
|
||||||
|
.copy_dst = true,
|
||||||
|
.render_attachment = true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var s = &text_mod.state;
|
||||||
|
s.texture = texture;
|
||||||
|
s.texture_atlas = try mach.gfx.Atlas.init(
|
||||||
|
engine.allocator,
|
||||||
|
img_size.width,
|
||||||
|
.rgba,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: state fields' default values do not work
|
||||||
|
s.regions = .{};
|
||||||
|
|
||||||
|
s.ft = try ft.Library.init();
|
||||||
|
s.face = try s.ft.createFaceMemory(assets.roboto_medium_ttf, 0);
|
||||||
|
|
||||||
|
try text_mod.send(.prepare, .{&[_]u21{ '?', '!', 'a', 'b', '#', '@', '%', '$', '&', '^', '*', '+', '=', '<', '>', '/', ':', ';', 'Q', '~' }});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
text_mod: *Mod,
|
||||||
|
codepoints: []const u21,
|
||||||
|
) !void {
|
||||||
|
const device = engine.state.device;
|
||||||
|
const queue = device.getQueue();
|
||||||
|
var s = &text_mod.state;
|
||||||
|
|
||||||
|
for (codepoints) |codepoint| {
|
||||||
|
const font_size = 48 * 1;
|
||||||
|
try s.face.setCharSize(font_size * 64, 0, 50, 0);
|
||||||
|
try s.face.loadChar(codepoint, .{ .render = true });
|
||||||
|
const glyph = s.face.glyph();
|
||||||
|
const metrics = glyph.metrics();
|
||||||
|
|
||||||
|
const glyph_bitmap = glyph.bitmap();
|
||||||
|
const glyph_width = glyph_bitmap.width();
|
||||||
|
const glyph_height = glyph_bitmap.rows();
|
||||||
|
|
||||||
|
// Add 1 pixel padding to texture to avoid bleeding over other textures
|
||||||
|
const margin = 1;
|
||||||
|
const glyph_data = try engine.allocator.alloc([4]u8, (glyph_width + (margin * 2)) * (glyph_height + (margin * 2)));
|
||||||
|
defer engine.allocator.free(glyph_data);
|
||||||
|
const glyph_buffer = glyph_bitmap.buffer().?;
|
||||||
|
for (glyph_data, 0..) |*data, i| {
|
||||||
|
const x = i % (glyph_width + (margin * 2));
|
||||||
|
const y = i / (glyph_width + (margin * 2));
|
||||||
|
if (x < margin or x > (glyph_width + margin) or y < margin or y > (glyph_height + margin)) {
|
||||||
|
data.* = [4]u8{ 0, 0, 0, 0 };
|
||||||
|
} else {
|
||||||
|
const alpha = glyph_buffer[((y - margin) * glyph_width + (x - margin)) % glyph_buffer.len];
|
||||||
|
data.* = [4]u8{ 0, 0, 0, alpha };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var glyph_atlas_region = try s.texture_atlas.reserve(engine.allocator, glyph_width + (margin * 2), glyph_height + (margin * 2));
|
||||||
|
s.texture_atlas.set(glyph_atlas_region, @as([*]const u8, @ptrCast(glyph_data.ptr))[0 .. glyph_data.len * 4]);
|
||||||
|
|
||||||
|
glyph_atlas_region.x += margin;
|
||||||
|
glyph_atlas_region.y += margin;
|
||||||
|
glyph_atlas_region.width -= margin * 2;
|
||||||
|
glyph_atlas_region.height -= margin * 2;
|
||||||
|
|
||||||
|
try s.regions.put(engine.allocator, codepoint, glyph_atlas_region);
|
||||||
|
_ = metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rgba32_pixels
|
||||||
|
const img_size = gpu.Extent3D{ .width = 1024, .height = 1024 };
|
||||||
|
const data_layout = gpu.Texture.DataLayout{
|
||||||
|
.bytes_per_row = @as(u32, @intCast(img_size.width * 4)),
|
||||||
|
.rows_per_image = @as(u32, @intCast(img_size.height)),
|
||||||
|
};
|
||||||
|
queue.writeTexture(&.{ .texture = s.texture }, &data_layout, &img_size, s.texture_atlas.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
16
examples/glyphs/main.zig
Normal file
16
examples/glyphs/main.zig
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
// Experimental ECS app example. Not yet ready for actual use.
|
||||||
|
const mach = @import("mach");
|
||||||
|
|
||||||
|
const Game = @import("Game.zig");
|
||||||
|
const Text = @import("Text.zig");
|
||||||
|
|
||||||
|
// The list of modules to be used in our application. Our game itself is implemented in our own
|
||||||
|
// module called Game.
|
||||||
|
pub const modules = .{
|
||||||
|
mach.Engine,
|
||||||
|
mach.gfx.Sprite,
|
||||||
|
Text,
|
||||||
|
Game,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const App = mach.App;
|
||||||
241
examples/sprite/Game.zig
Normal file
241
examples/sprite/Game.zig
Normal file
|
|
@ -0,0 +1,241 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
const assets = @import("assets");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const core = mach.core;
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const ecs = mach.ecs;
|
||||||
|
const Sprite = mach.gfx.Sprite;
|
||||||
|
const math = mach.math;
|
||||||
|
|
||||||
|
const vec2 = math.vec2;
|
||||||
|
const vec3 = math.vec3;
|
||||||
|
const Vec2 = math.Vec2;
|
||||||
|
const Vec3 = math.Vec3;
|
||||||
|
const Mat3x3 = math.Mat3x3;
|
||||||
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
|
||||||
|
timer: mach.Timer,
|
||||||
|
player: mach.ecs.EntityID,
|
||||||
|
direction: Vec2 = vec2(0, 0),
|
||||||
|
spawning: bool = false,
|
||||||
|
spawn_timer: mach.Timer,
|
||||||
|
fps_timer: mach.Timer,
|
||||||
|
frame_count: usize,
|
||||||
|
sprites: usize,
|
||||||
|
rand: std.rand.DefaultPrng,
|
||||||
|
time: f32,
|
||||||
|
|
||||||
|
const d0 = 0.000001;
|
||||||
|
|
||||||
|
// Each module must have a globally unique name declared, it is impossible to use two modules with
|
||||||
|
// the same name in a program. To avoid name conflicts, we follow naming conventions:
|
||||||
|
//
|
||||||
|
// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it
|
||||||
|
// provides.
|
||||||
|
// 2. Single-word names like `.game` are reserved for the application itself.
|
||||||
|
// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui`
|
||||||
|
// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally
|
||||||
|
// unique.
|
||||||
|
//
|
||||||
|
pub const name = .game;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
pub const Pipeline = enum(u32) {
|
||||||
|
default,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
sprite_mod: *Sprite.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// The Mach .core is where we set window options, etc.
|
||||||
|
core.setTitle("gfx.Sprite example");
|
||||||
|
|
||||||
|
// We can create entities, and set components on them. Note that components live in a module
|
||||||
|
// namespace, e.g. the `.mach_gfx_sprite` module could have a 3D `.location` component with a different
|
||||||
|
// type than the `.physics2d` module's `.location` component if you desire.
|
||||||
|
|
||||||
|
const player = try engine.newEntity();
|
||||||
|
try sprite_mod.set(player, .transform, Mat4x4.translate(vec3(-0.02, 0, 0)));
|
||||||
|
try sprite_mod.set(player, .size, vec2(32, 32));
|
||||||
|
try sprite_mod.set(player, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
||||||
|
try sprite_mod.set(player, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
|
|
||||||
|
try sprite_mod.send(.init, .{});
|
||||||
|
try sprite_mod.send(.initPipeline, .{Sprite.PipelineOptions{
|
||||||
|
.pipeline = @intFromEnum(Pipeline.default),
|
||||||
|
.texture = try loadTexture(engine),
|
||||||
|
}});
|
||||||
|
try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
game.state = .{
|
||||||
|
.timer = try mach.Timer.start(),
|
||||||
|
.spawn_timer = try mach.Timer.start(),
|
||||||
|
.player = player,
|
||||||
|
.fps_timer = try mach.Timer.start(),
|
||||||
|
.frame_count = 0,
|
||||||
|
.sprites = 0,
|
||||||
|
.rand = std.rand.DefaultPrng.init(1337),
|
||||||
|
.time = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
sprite_mod: *Sprite.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
||||||
|
var iter = core.pollEvents();
|
||||||
|
var direction = game.state.direction;
|
||||||
|
var spawning = game.state.spawning;
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] -= 1,
|
||||||
|
.right => direction.v[0] += 1,
|
||||||
|
.up => direction.v[1] += 1,
|
||||||
|
.down => direction.v[1] -= 1,
|
||||||
|
.space => spawning = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.key_release => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] += 1,
|
||||||
|
.right => direction.v[0] -= 1,
|
||||||
|
.up => direction.v[1] -= 1,
|
||||||
|
.down => direction.v[1] += 1,
|
||||||
|
.space => spawning = false,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.close => try engine.send(.exit, .{}),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game.state.direction = direction;
|
||||||
|
game.state.spawning = spawning;
|
||||||
|
|
||||||
|
var player_transform = sprite_mod.get(game.state.player, .transform).?;
|
||||||
|
var player_pos = player_transform.translation();
|
||||||
|
if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) {
|
||||||
|
// Spawn new entities
|
||||||
|
_ = game.state.spawn_timer.lap();
|
||||||
|
for (0..100) |_| {
|
||||||
|
var new_pos = player_pos;
|
||||||
|
new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
|
||||||
|
const new_entity = try engine.newEntity();
|
||||||
|
try sprite_mod.set(new_entity, .transform, Mat4x4.translate(new_pos).mul(&Mat4x4.scale(Vec3.splat(0.3))));
|
||||||
|
try sprite_mod.set(new_entity, .size, vec2(32, 32));
|
||||||
|
try sprite_mod.set(new_entity, .uv_transform, Mat3x3.translate(vec2(0, 0)));
|
||||||
|
try sprite_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
|
game.state.sprites += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||||
|
const delta_time = game.state.timer.lap();
|
||||||
|
|
||||||
|
// Rotate entities
|
||||||
|
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .mach_gfx_sprite = &.{.transform} },
|
||||||
|
} });
|
||||||
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
|
const transforms = archetype.slice(.mach_gfx_sprite, .transform);
|
||||||
|
for (ids, transforms) |id, *old_transform| {
|
||||||
|
_ = id;
|
||||||
|
const location = old_transform.*.translation();
|
||||||
|
// var transform = old_transform.mul(&Mat4x4.translate(-location));
|
||||||
|
// transform = mat.rotateZ(0.3 * delta_time).mul(&transform);
|
||||||
|
// transform = transform.mul(&Mat4x4.translate(location));
|
||||||
|
var transform = Mat4x4.ident;
|
||||||
|
transform = transform.mul(&Mat4x4.translate(location));
|
||||||
|
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time));
|
||||||
|
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5)));
|
||||||
|
|
||||||
|
// TODO: .set() API is substantially slower due to internals
|
||||||
|
// try sprite_mod.set(id, .transform, transform);
|
||||||
|
old_transform.* = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the player position, by moving in the direction the player wants to go
|
||||||
|
// by the speed amount.
|
||||||
|
const speed = 200.0;
|
||||||
|
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||||
|
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||||
|
try sprite_mod.set(game.state.player, .transform, Mat4x4.translate(player_pos));
|
||||||
|
try sprite_mod.send(.updated, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
// Perform pre-render work
|
||||||
|
try sprite_mod.send(.preRender, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
// Render a frame
|
||||||
|
try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
||||||
|
try sprite_mod.send(.render, .{@intFromEnum(Pipeline.default)});
|
||||||
|
try engine.send(.endPass, .{});
|
||||||
|
try engine.send(.present, .{}); // Present the frame
|
||||||
|
|
||||||
|
// Every second, update the window title with the FPS
|
||||||
|
if (game.state.fps_timer.read() >= 1.0) {
|
||||||
|
try core.printTitle("gfx.Sprite example [ FPS: {d} ] [ Sprites: {d} ]", .{ game.state.frame_count, game.state.sprites });
|
||||||
|
game.state.fps_timer.reset();
|
||||||
|
game.state.frame_count = 0;
|
||||||
|
}
|
||||||
|
game.state.frame_count += 1;
|
||||||
|
game.state.time += delta_time;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move this helper into gfx module
|
||||||
|
fn loadTexture(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
) !*gpu.Texture {
|
||||||
|
const device = engine.state.device;
|
||||||
|
const queue = device.getQueue();
|
||||||
|
|
||||||
|
// Load the image from memory
|
||||||
|
var img = try zigimg.Image.fromMemory(engine.allocator, assets.sprites_sheet_png);
|
||||||
|
defer img.deinit();
|
||||||
|
const img_size = gpu.Extent3D{ .width = @as(u32, @intCast(img.width)), .height = @as(u32, @intCast(img.height)) };
|
||||||
|
|
||||||
|
// Create a GPU texture
|
||||||
|
const texture = device.createTexture(&.{
|
||||||
|
.size = img_size,
|
||||||
|
.format = .rgba8_unorm,
|
||||||
|
.usage = .{
|
||||||
|
.texture_binding = true,
|
||||||
|
.copy_dst = true,
|
||||||
|
.render_attachment = true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data_layout = gpu.Texture.DataLayout{
|
||||||
|
.bytes_per_row = @as(u32, @intCast(img.width * 4)),
|
||||||
|
.rows_per_image = @as(u32, @intCast(img.height)),
|
||||||
|
};
|
||||||
|
switch (img.pixels) {
|
||||||
|
.rgba32 => |pixels| queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, pixels),
|
||||||
|
.rgb24 => |pixels| {
|
||||||
|
const data = try rgb24ToRgba32(engine.allocator, pixels);
|
||||||
|
defer data.deinit(engine.allocator);
|
||||||
|
queue.writeTexture(&.{ .texture = texture }, &data_layout, &img_size, data.rgba32);
|
||||||
|
},
|
||||||
|
else => @panic("unsupported image color format"),
|
||||||
|
}
|
||||||
|
return texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb24ToRgba32(allocator: std.mem.Allocator, in: []zigimg.color.Rgb24) !zigimg.color.PixelStorage {
|
||||||
|
const out = try zigimg.color.PixelStorage.init(allocator, .rgba32, in.len);
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < in.len) : (i += 1) {
|
||||||
|
out.rgba32[i] = zigimg.color.Rgba32{ .r = in[i].r, .g = in[i].g, .b = in[i].b, .a = 255 };
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
14
examples/sprite/main.zig
Normal file
14
examples/sprite/main.zig
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Experimental ECS app example. Not yet ready for actual use.
|
||||||
|
const mach = @import("mach");
|
||||||
|
|
||||||
|
const Game = @import("Game.zig");
|
||||||
|
|
||||||
|
// The list of modules to be used in our application. Our game itself is implemented in our own
|
||||||
|
// module called Game.
|
||||||
|
pub const modules = .{
|
||||||
|
mach.Engine,
|
||||||
|
mach.gfx.Sprite,
|
||||||
|
Game,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const App = mach.App;
|
||||||
186
examples/sysaudio/main.zig
Normal file
186
examples/sysaudio/main.zig
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
// A simple tone engine.
|
||||||
|
//
|
||||||
|
// It renders 512 tones simultaneously, each with their own frequency and duration.
|
||||||
|
//
|
||||||
|
// `keyToFrequency` can be used to convert a keyboard key to a frequency, so that the
|
||||||
|
// keys asdfghj on your QWERTY keyboard will map to the notes C/D/E/F/G/A/B[4], the
|
||||||
|
// keys above qwertyu will map to C5 and the keys below zxcvbnm will map to C3.
|
||||||
|
//
|
||||||
|
// The duration is hard-coded to 1.5s. To prevent clicking, tones are faded in linearly over
|
||||||
|
// the first 1/64th duration of the tone. To provide a cool sustained effect, tones are faded
|
||||||
|
// out using 1-log10(x*10) (google it to see how it looks, it's strong for most of the duration of
|
||||||
|
// the note then fades out slowly.)
|
||||||
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
|
|
||||||
|
const mach = @import("mach");
|
||||||
|
const math = mach.math;
|
||||||
|
const sysaudio = mach.sysaudio;
|
||||||
|
|
||||||
|
pub const App = @This();
|
||||||
|
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
|
||||||
|
audio_ctx: sysaudio.Context,
|
||||||
|
player: sysaudio.Player,
|
||||||
|
playing: [512]Tone = std.mem.zeroes([512]Tone),
|
||||||
|
|
||||||
|
const Tone = struct {
|
||||||
|
frequency: f32,
|
||||||
|
sample_counter: usize,
|
||||||
|
duration: usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(app: *App) !void {
|
||||||
|
try mach.core.init(.{});
|
||||||
|
|
||||||
|
app.audio_ctx = try sysaudio.Context.init(null, gpa.allocator(), .{});
|
||||||
|
errdefer app.audio_ctx.deinit();
|
||||||
|
try app.audio_ctx.refresh();
|
||||||
|
|
||||||
|
const device = app.audio_ctx.defaultDevice(.playback) orelse return error.NoDeviceFound;
|
||||||
|
app.player = try app.audio_ctx.createPlayer(device, writeCallback, .{ .user_data = app });
|
||||||
|
try app.player.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(app: *App) void {
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
defer mach.core.deinit();
|
||||||
|
|
||||||
|
app.player.deinit();
|
||||||
|
app.audio_ctx.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(app: *App) !bool {
|
||||||
|
var iter = mach.core.pollEvents();
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
const vol = try app.player.volume();
|
||||||
|
switch (ev.key) {
|
||||||
|
.down => try app.player.setVolume(@max(0.0, vol - 0.1)),
|
||||||
|
.up => try app.player.setVolume(@min(1.0, vol + 0.1)),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
app.fillTone(keyToFrequency(ev.key));
|
||||||
|
},
|
||||||
|
.close => return true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (builtin.cpu.arch != .wasm32) {
|
||||||
|
const back_buffer_view = mach.core.swap_chain.getCurrentTextureView().?;
|
||||||
|
|
||||||
|
mach.core.swap_chain.present();
|
||||||
|
back_buffer_view.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn writeCallback(ctx: ?*anyopaque, output: []u8) void {
|
||||||
|
const app: *App = @as(*App, @ptrCast(@alignCast(ctx)));
|
||||||
|
|
||||||
|
// const seconds_per_frame = 1.0 / @as(f32, @floatFromInt(player.sampleRate()));
|
||||||
|
const frame_size = app.player.format().frameSize(@intCast(app.player.channels().len));
|
||||||
|
const frames = output.len / frame_size;
|
||||||
|
_ = frames;
|
||||||
|
|
||||||
|
var frame: usize = 0;
|
||||||
|
while (frame < output.len) : (frame += frame_size) {
|
||||||
|
// Calculate the audio sample we'll play on both channels for this frame
|
||||||
|
var sample: f32 = 0;
|
||||||
|
for (&app.playing) |*tone| {
|
||||||
|
if (tone.sample_counter >= tone.duration) continue;
|
||||||
|
|
||||||
|
tone.sample_counter += 1;
|
||||||
|
const sample_counter = @as(f32, @floatFromInt(tone.sample_counter));
|
||||||
|
const duration = @as(f32, @floatFromInt(tone.duration));
|
||||||
|
|
||||||
|
// The sine wave that plays the frequency.
|
||||||
|
const gain = 0.1;
|
||||||
|
const sine_wave = math.sin(tone.frequency * 2.0 * math.pi * sample_counter / @as(f32, @floatFromInt(app.player.sampleRate()))) * gain;
|
||||||
|
|
||||||
|
// A number ranging from 0.0 to 1.0 in the first 1/64th of the duration of the tone.
|
||||||
|
const fade_in = @min(sample_counter / (duration / 64.0), 1.0);
|
||||||
|
|
||||||
|
// A number ranging from 1.0 to 0.0 over half the duration of the tone.
|
||||||
|
const progression = sample_counter / duration; // 0.0 (tone start) to 1.0 (tone end)
|
||||||
|
const fade_out = 1.0 - math.clamp(math.log10(progression * 10.0), 0.0, 1.0);
|
||||||
|
|
||||||
|
// Mix this tone into the sample we'll actually play on e.g. the speakers, reducing
|
||||||
|
// sine wave intensity if we're fading in or out over the entire duration of the
|
||||||
|
// tone.
|
||||||
|
sample += sine_wave * fade_in * fade_out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert our float sample to the format the audio driver is working in
|
||||||
|
sysaudio.convertTo(
|
||||||
|
f32,
|
||||||
|
// Pass two samples (assume two channel audio)
|
||||||
|
// Note that in a real application this must match app.player.channels().len
|
||||||
|
&.{ sample, sample },
|
||||||
|
app.player.format(),
|
||||||
|
output[frame..][0..frame_size],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fillTone(app: *App, frequency: f32) void {
|
||||||
|
for (&app.playing) |*tone| {
|
||||||
|
if (tone.sample_counter >= tone.duration) {
|
||||||
|
tone.* = Tone{
|
||||||
|
.frequency = frequency,
|
||||||
|
.sample_counter = 0,
|
||||||
|
.duration = @as(usize, @intFromFloat(1.5 * @as(f32, @floatFromInt(app.player.sampleRate())))), // play the tone for 1.5s
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyToFrequency(key: mach.core.Key) f32 {
|
||||||
|
// The frequencies here just come from a piano frequencies chart. You can google for them.
|
||||||
|
return switch (key) {
|
||||||
|
// First row of piano keys, the highest.
|
||||||
|
.q => 523.25, // C5
|
||||||
|
.w => 587.33, // D5
|
||||||
|
.e => 659.26, // E5
|
||||||
|
.r => 698.46, // F5
|
||||||
|
.t => 783.99, // G5
|
||||||
|
.y => 880.0, // A5
|
||||||
|
.u => 987.77, // B5
|
||||||
|
.i => 1046.5, // C6
|
||||||
|
.o => 1174.7, // D6
|
||||||
|
.p => 1318.5, // E6
|
||||||
|
.left_bracket => 1396.9, // F6
|
||||||
|
.right_bracket => 1568.0, // G6
|
||||||
|
|
||||||
|
// Second row of piano keys, the middle.
|
||||||
|
.a => 261.63, // C4
|
||||||
|
.s => 293.67, // D4
|
||||||
|
.d => 329.63, // E4
|
||||||
|
.f => 349.23, // F4
|
||||||
|
.g => 392.0, // G4
|
||||||
|
.h => 440.0, // A4
|
||||||
|
.j => 493.88, // B4
|
||||||
|
.k => 523.25, // C5
|
||||||
|
.l => 587.33, // D5
|
||||||
|
.semicolon => 659.26, // E5
|
||||||
|
.apostrophe => 698.46, // F5
|
||||||
|
|
||||||
|
// Third row of piano keys, the lowest.
|
||||||
|
.z => 130.81, // C3
|
||||||
|
.x => 146.83, // D3
|
||||||
|
.c => 164.81, // E3
|
||||||
|
.v => 174.61, // F3
|
||||||
|
.b => 196.00, // G3
|
||||||
|
.n => 220.0, // A3
|
||||||
|
.m => 246.94, // B3
|
||||||
|
.comma => 261.63, // C4
|
||||||
|
.period => 293.67, // D4
|
||||||
|
.slash => 329.63, // E5
|
||||||
|
else => 0.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
234
examples/text/Game.zig
Normal file
234
examples/text/Game.zig
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const zigimg = @import("zigimg");
|
||||||
|
const assets = @import("assets");
|
||||||
|
const mach = @import("mach");
|
||||||
|
const core = mach.core;
|
||||||
|
const gfx = mach.gfx;
|
||||||
|
const gpu = mach.gpu;
|
||||||
|
const ecs = mach.ecs;
|
||||||
|
const Text = mach.gfx.Text;
|
||||||
|
const math = mach.math;
|
||||||
|
|
||||||
|
const vec2 = math.vec2;
|
||||||
|
const vec3 = math.vec3;
|
||||||
|
const vec4 = math.vec4;
|
||||||
|
const Vec2 = math.Vec2;
|
||||||
|
const Vec3 = math.Vec3;
|
||||||
|
const Mat3x3 = math.Mat3x3;
|
||||||
|
const Mat4x4 = math.Mat4x4;
|
||||||
|
|
||||||
|
timer: mach.Timer,
|
||||||
|
player: mach.ecs.EntityID,
|
||||||
|
direction: Vec2 = vec2(0, 0),
|
||||||
|
spawning: bool = false,
|
||||||
|
spawn_timer: mach.Timer,
|
||||||
|
fps_timer: mach.Timer,
|
||||||
|
frame_count: usize,
|
||||||
|
texts: usize,
|
||||||
|
rand: std.rand.DefaultPrng,
|
||||||
|
time: f32,
|
||||||
|
|
||||||
|
const d0 = 0.000001;
|
||||||
|
|
||||||
|
// Each module must have a globally unique name declared, it is impossible to use two modules with
|
||||||
|
// the same name in a program. To avoid name conflicts, we follow naming conventions:
|
||||||
|
//
|
||||||
|
// 1. `.mach` and the `.mach_foobar` namespace is reserved for Mach itself and the modules it
|
||||||
|
// provides.
|
||||||
|
// 2. Single-word names like `.game` are reserved for the application itself.
|
||||||
|
// 3. Libraries which provide modules MUST be prefixed with an "owner" name, e.g. `.ziglibs_imgui`
|
||||||
|
// instead of `.imgui`. We encourage using e.g. your GitHub name, as these must be globally
|
||||||
|
// unique.
|
||||||
|
//
|
||||||
|
pub const name = .game;
|
||||||
|
pub const Mod = mach.Mod(@This());
|
||||||
|
|
||||||
|
pub const Pipeline = enum(u32) {
|
||||||
|
default,
|
||||||
|
};
|
||||||
|
|
||||||
|
const upscale = 1.0;
|
||||||
|
|
||||||
|
pub fn init(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
text_mod: *Text.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// The Mach .core is where we set window options, etc.
|
||||||
|
core.setTitle("gfx.Text example");
|
||||||
|
|
||||||
|
// Create some text
|
||||||
|
const player = try engine.newEntity();
|
||||||
|
try text_mod.set(player, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
|
try text_mod.set(player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(vec3(0, 0, 0))));
|
||||||
|
const style1 = Text.Style{
|
||||||
|
.font_name = "Roboto Medium", // TODO
|
||||||
|
.font_size = 48 * gfx.px_per_pt, // 48pt
|
||||||
|
.font_weight = gfx.font_weight_normal,
|
||||||
|
.italic = false,
|
||||||
|
.color = vec4(0.6, 1.0, 0.6, 1.0),
|
||||||
|
};
|
||||||
|
var style2 = style1;
|
||||||
|
style2.italic = true;
|
||||||
|
var style3 = style1;
|
||||||
|
style3.font_weight = gfx.font_weight_bold;
|
||||||
|
try text_mod.set(player, .text, &.{
|
||||||
|
.{
|
||||||
|
.string = "Text but with spaces 😊\nand\n",
|
||||||
|
.style = &style1,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.string = "italics\nand\n",
|
||||||
|
.style = &style2,
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.string = "bold\nand\n",
|
||||||
|
.style = &style3,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try text_mod.send(.init, .{});
|
||||||
|
try text_mod.send(.initPipeline, .{Text.PipelineOptions{
|
||||||
|
.pipeline = @intFromEnum(Pipeline.default),
|
||||||
|
}});
|
||||||
|
try text_mod.send(.updated, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
game.state = .{
|
||||||
|
.timer = try mach.Timer.start(),
|
||||||
|
.spawn_timer = try mach.Timer.start(),
|
||||||
|
.player = player,
|
||||||
|
.fps_timer = try mach.Timer.start(),
|
||||||
|
.frame_count = 0,
|
||||||
|
.texts = 0,
|
||||||
|
.rand = std.rand.DefaultPrng.init(1337),
|
||||||
|
.time = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(engine: *mach.Engine.Mod) !void {
|
||||||
|
_ = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(
|
||||||
|
engine: *mach.Engine.Mod,
|
||||||
|
text_mod: *Text.Mod,
|
||||||
|
game: *Mod,
|
||||||
|
) !void {
|
||||||
|
// TODO(engine): event polling should occur in mach.Engine module and get fired as ECS events.
|
||||||
|
var iter = core.pollEvents();
|
||||||
|
var direction = game.state.direction;
|
||||||
|
var spawning = game.state.spawning;
|
||||||
|
while (iter.next()) |event| {
|
||||||
|
switch (event) {
|
||||||
|
.key_press => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] -= 1,
|
||||||
|
.right => direction.v[0] += 1,
|
||||||
|
.up => direction.v[1] += 1,
|
||||||
|
.down => direction.v[1] -= 1,
|
||||||
|
.space => spawning = true,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.key_release => |ev| {
|
||||||
|
switch (ev.key) {
|
||||||
|
.left => direction.v[0] += 1,
|
||||||
|
.right => direction.v[0] -= 1,
|
||||||
|
.up => direction.v[1] -= 1,
|
||||||
|
.down => direction.v[1] += 1,
|
||||||
|
.space => spawning = false,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.close => try engine.send(.exit, .{}),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
game.state.direction = direction;
|
||||||
|
game.state.spawning = spawning;
|
||||||
|
|
||||||
|
var player_transform = text_mod.get(game.state.player, .transform).?;
|
||||||
|
var player_pos = player_transform.translation().divScalar(upscale);
|
||||||
|
if (spawning and game.state.spawn_timer.read() > 1.0 / 60.0) {
|
||||||
|
// Spawn new entities
|
||||||
|
_ = game.state.spawn_timer.lap();
|
||||||
|
for (0..1) |_| {
|
||||||
|
var new_pos = player_pos;
|
||||||
|
new_pos.v[0] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
new_pos.v[1] += game.state.rand.random().floatNorm(f32) * 25;
|
||||||
|
|
||||||
|
const new_entity = try engine.newEntity();
|
||||||
|
try text_mod.set(new_entity, .pipeline, @intFromEnum(Pipeline.default));
|
||||||
|
try text_mod.set(new_entity, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(new_pos)));
|
||||||
|
|
||||||
|
const style1 = Text.Style{
|
||||||
|
.font_name = "Roboto Medium", // TODO
|
||||||
|
.font_size = 48 * gfx.px_per_pt, // 48pt
|
||||||
|
.font_weight = gfx.font_weight_normal,
|
||||||
|
.italic = false,
|
||||||
|
.color = vec4(0.6, 1.0, 0.6, 1.0),
|
||||||
|
};
|
||||||
|
try text_mod.set(new_entity, .text, &.{
|
||||||
|
.{
|
||||||
|
.string = "!$?😊",
|
||||||
|
.style = &style1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
game.state.texts += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiply by delta_time to ensure that movement is the same speed regardless of the frame rate.
|
||||||
|
const delta_time = game.state.timer.lap();
|
||||||
|
|
||||||
|
// Rotate entities
|
||||||
|
var archetypes_iter = engine.entities.query(.{ .all = &.{
|
||||||
|
.{ .mach_gfx_text = &.{.transform} },
|
||||||
|
} });
|
||||||
|
while (archetypes_iter.next()) |archetype| {
|
||||||
|
const ids = archetype.slice(.entity, .id);
|
||||||
|
const transforms = archetype.slice(.mach_gfx_text, .transform);
|
||||||
|
for (ids, transforms) |id, *old_transform| {
|
||||||
|
_ = id;
|
||||||
|
const location = old_transform.*.translation();
|
||||||
|
// var transform = old_transform.mul(&Mat4x4.translate(-location));
|
||||||
|
// transform = mat.rotateZ(0.3 * delta_time).mul(&transform);
|
||||||
|
// transform = transform.mul(&Mat4x4.translate(location));
|
||||||
|
var transform = Mat4x4.ident;
|
||||||
|
transform = transform.mul(&Mat4x4.translate(location));
|
||||||
|
transform = transform.mul(&Mat4x4.rotateZ(2 * math.pi * game.state.time));
|
||||||
|
transform = transform.mul(&Mat4x4.scaleScalar(@min(math.cos(game.state.time / 2.0), 0.5)));
|
||||||
|
|
||||||
|
// TODO: .set() API is substantially slower due to internals
|
||||||
|
// try text_mod.set(id, .transform, transform);
|
||||||
|
old_transform.* = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the player position, by moving in the direction the player wants to go
|
||||||
|
// by the speed amount.
|
||||||
|
const speed = 200.0 / upscale;
|
||||||
|
player_pos.v[0] += direction.x() * speed * delta_time;
|
||||||
|
player_pos.v[1] += direction.y() * speed * delta_time;
|
||||||
|
try text_mod.set(game.state.player, .transform, Mat4x4.scaleScalar(upscale).mul(&Mat4x4.translate(player_pos)));
|
||||||
|
try text_mod.send(.updated, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
// Perform pre-render work
|
||||||
|
try text_mod.send(.preRender, .{@intFromEnum(Pipeline.default)});
|
||||||
|
|
||||||
|
// Render a frame
|
||||||
|
try engine.send(.beginPass, .{gpu.Color{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }});
|
||||||
|
try text_mod.send(.render, .{@intFromEnum(Pipeline.default)});
|
||||||
|
try engine.send(.endPass, .{});
|
||||||
|
try engine.send(.present, .{}); // Present the frame
|
||||||
|
|
||||||
|
// Every second, update the window title with the FPS
|
||||||
|
if (game.state.fps_timer.read() >= 1.0) {
|
||||||
|
try core.printTitle("gfx.Text example [ FPS: {d} ] [ Texts: {d} ]", .{ game.state.frame_count, game.state.texts });
|
||||||
|
game.state.fps_timer.reset();
|
||||||
|
game.state.frame_count = 0;
|
||||||
|
}
|
||||||
|
game.state.frame_count += 1;
|
||||||
|
game.state.time += delta_time;
|
||||||
|
}
|
||||||
14
examples/text/main.zig
Normal file
14
examples/text/main.zig
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Experimental ECS app example. Not yet ready for actual use.
|
||||||
|
const mach = @import("mach");
|
||||||
|
|
||||||
|
const Game = @import("Game.zig");
|
||||||
|
|
||||||
|
// The list of modules to be used in our application. Our game itself is implemented in our own
|
||||||
|
// module called Game.
|
||||||
|
pub const modules = .{
|
||||||
|
mach.Engine,
|
||||||
|
mach.gfx.Text,
|
||||||
|
Game,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const App = mach.App;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue