Add zoom and panning via mouse
This commit is contained in:
parent
26383ed79e
commit
3e2e39100a
4 changed files with 112 additions and 14 deletions
|
|
@ -31,6 +31,11 @@ const BENCH_RAMP_AMOUNT: usize = 50_000; // entities added per ramp
|
||||||
const BENCH_EXIT_THRESHOLD_MS: f32 = 25.0; // exit when frame time exceeds this
|
const BENCH_EXIT_THRESHOLD_MS: f32 = 25.0; // exit when frame time exceeds this
|
||||||
const BENCH_EXIT_SUSTAIN: f32 = 1.0; // must stay above threshold for this long
|
const BENCH_EXIT_SUSTAIN: f32 = 1.0; // must stay above threshold for this long
|
||||||
|
|
||||||
|
// zoom settings
|
||||||
|
const ZOOM_MIN: f32 = 1.0;
|
||||||
|
const ZOOM_MAX: f32 = 10.0;
|
||||||
|
const ZOOM_SPEED: f32 = 0.1; // multiplier per scroll tick
|
||||||
|
|
||||||
const BenchmarkLogger = struct {
|
const BenchmarkLogger = struct {
|
||||||
file: ?std.fs.File,
|
file: ?std.fs.File,
|
||||||
last_logged_frame_ms: f32,
|
last_logged_frame_ms: f32,
|
||||||
|
|
@ -264,6 +269,11 @@ pub fn main() !void {
|
||||||
var rng = prng.random();
|
var rng = prng.random();
|
||||||
|
|
||||||
var paused = false;
|
var paused = false;
|
||||||
|
|
||||||
|
// camera state for zoom/pan
|
||||||
|
var zoom: f32 = 1.0;
|
||||||
|
var pan = @Vector(2, f32){ 0, 0 };
|
||||||
|
|
||||||
var logger = BenchmarkLogger.init();
|
var logger = BenchmarkLogger.init();
|
||||||
defer logger.deinit();
|
defer logger.deinit();
|
||||||
|
|
||||||
|
|
@ -316,6 +326,7 @@ pub fn main() !void {
|
||||||
} else {
|
} else {
|
||||||
// manual controls
|
// manual controls
|
||||||
handleInput(&entities, &rng, &paused);
|
handleInput(&entities, &rng, &paused);
|
||||||
|
handleCamera(&zoom, &pan);
|
||||||
}
|
}
|
||||||
|
|
||||||
// update
|
// update
|
||||||
|
|
@ -333,7 +344,7 @@ pub fn main() !void {
|
||||||
|
|
||||||
if (use_ssbo) {
|
if (use_ssbo) {
|
||||||
// SSBO instanced rendering path (12 bytes per entity)
|
// SSBO instanced rendering path (12 bytes per entity)
|
||||||
ssbo_renderer.?.render(&entities);
|
ssbo_renderer.?.render(&entities, zoom, pan);
|
||||||
} else if (use_instancing) {
|
} else if (use_instancing) {
|
||||||
// GPU instancing path (64 bytes per entity)
|
// GPU instancing path (64 bytes per entity)
|
||||||
const xforms = transforms.?;
|
const xforms = transforms.?;
|
||||||
|
|
@ -384,7 +395,7 @@ pub fn main() !void {
|
||||||
|
|
||||||
// metrics overlay (skip in bench mode for cleaner headless run)
|
// metrics overlay (skip in bench mode for cleaner headless run)
|
||||||
if (!bench_mode) {
|
if (!bench_mode) {
|
||||||
ui.drawMetrics(&entities, update_time_us, render_time_us, paused, ui_font);
|
ui.drawMetrics(&entities, update_time_us, render_time_us, paused, zoom, ui_font);
|
||||||
ui.drawMemory(entities.count, ui_font);
|
ui.drawMemory(entities.count, ui_font);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -456,3 +467,68 @@ fn handleInput(entities: *sandbox.Entities, rng: *std.Random, paused: *bool) voi
|
||||||
ui.show_ui = !ui.show_ui;
|
ui.show_ui = !ui.show_ui;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleCamera(zoom: *f32, pan: *@Vector(2, f32)) void {
|
||||||
|
const wheel = rl.getMouseWheelMove();
|
||||||
|
|
||||||
|
if (wheel != 0) {
|
||||||
|
const mouse_pos = rl.getMousePosition();
|
||||||
|
const old_zoom = zoom.*;
|
||||||
|
|
||||||
|
// calculate new zoom
|
||||||
|
const zoom_factor = if (wheel > 0) (1.0 + ZOOM_SPEED) else (1.0 / (1.0 + ZOOM_SPEED));
|
||||||
|
var new_zoom = old_zoom * zoom_factor;
|
||||||
|
new_zoom = std.math.clamp(new_zoom, ZOOM_MIN, ZOOM_MAX);
|
||||||
|
|
||||||
|
if (new_zoom != old_zoom) {
|
||||||
|
// zoom toward mouse cursor:
|
||||||
|
// keep the world point under the cursor stationary
|
||||||
|
// world_pos = (screen_pos / old_zoom) + old_pan
|
||||||
|
// new_pan = world_pos - (screen_pos / new_zoom)
|
||||||
|
const world_x = (mouse_pos.x / old_zoom) + pan.*[0];
|
||||||
|
const world_y = (mouse_pos.y / old_zoom) + pan.*[1];
|
||||||
|
pan.*[0] = world_x - (mouse_pos.x / new_zoom);
|
||||||
|
pan.*[1] = world_y - (mouse_pos.y / new_zoom);
|
||||||
|
zoom.* = new_zoom;
|
||||||
|
|
||||||
|
// clamp pan to bounds
|
||||||
|
clampPan(pan, zoom.*);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pan with any mouse button drag (only when zoomed in)
|
||||||
|
if (zoom.* > 1.0) {
|
||||||
|
const any_button = rl.isMouseButtonDown(.left) or
|
||||||
|
rl.isMouseButtonDown(.right) or
|
||||||
|
rl.isMouseButtonDown(.middle);
|
||||||
|
if (any_button) {
|
||||||
|
const delta = rl.getMouseDelta();
|
||||||
|
// drag down = view down, drag right = view right
|
||||||
|
pan.*[0] -= delta.x / zoom.*;
|
||||||
|
pan.*[1] += delta.y / zoom.*;
|
||||||
|
clampPan(pan, zoom.*);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset on Return
|
||||||
|
if (rl.isKeyPressed(.enter)) {
|
||||||
|
zoom.* = 1.0;
|
||||||
|
pan.* = @Vector(2, f32){ 0, 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clampPan(pan: *@Vector(2, f32), zoom: f32) void {
|
||||||
|
// when zoomed in, limit pan so viewport stays in simulation bounds
|
||||||
|
// visible area = screen_size / zoom
|
||||||
|
// max pan = world_size - visible_area
|
||||||
|
const screen_w: f32 = @floatFromInt(SCREEN_WIDTH);
|
||||||
|
const screen_h: f32 = @floatFromInt(SCREEN_HEIGHT);
|
||||||
|
const visible_w = screen_w / zoom;
|
||||||
|
const visible_h = screen_h / zoom;
|
||||||
|
|
||||||
|
const max_pan_x = @max(0, screen_w - visible_w);
|
||||||
|
const max_pan_y = @max(0, screen_h - visible_h);
|
||||||
|
|
||||||
|
pan.*[0] = std.math.clamp(pan.*[0], 0, max_pan_x);
|
||||||
|
pan.*[1] = std.math.clamp(pan.*[1], 0, max_pan_y);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ layout(std430, binding = 0) readonly buffer EntityData {
|
||||||
|
|
||||||
// screen size for NDC conversion
|
// screen size for NDC conversion
|
||||||
uniform vec2 screenSize;
|
uniform vec2 screenSize;
|
||||||
|
uniform float zoom;
|
||||||
|
uniform vec2 pan;
|
||||||
|
|
||||||
out vec2 fragTexCoord;
|
out vec2 fragTexCoord;
|
||||||
out vec3 fragColor;
|
out vec3 fragColor;
|
||||||
|
|
@ -25,13 +27,13 @@ void main() {
|
||||||
// get entity data from SSBO
|
// get entity data from SSBO
|
||||||
Entity e = entities[gl_InstanceID];
|
Entity e = entities[gl_InstanceID];
|
||||||
|
|
||||||
// convert entity position to NDC
|
// apply pan offset and zoom to convert to NDC
|
||||||
// entity coords are in screen pixels, convert to [-1, 1]
|
// pan is in screen pixels, zoom scales the view
|
||||||
float ndcX = (e.x / screenSize.x) * 2.0 - 1.0;
|
float ndcX = ((e.x - pan.x) * zoom / screenSize.x) * 2.0 - 1.0;
|
||||||
float ndcY = (e.y / screenSize.y) * 2.0 - 1.0;
|
float ndcY = ((e.y - pan.y) * zoom / screenSize.y) * 2.0 - 1.0;
|
||||||
|
|
||||||
// quad size in NDC (16 pixels)
|
// quad size in NDC (16 pixels, scaled by zoom)
|
||||||
float quadSizeNdc = 16.0 / screenSize.x;
|
float quadSizeNdc = (16.0 * zoom) / screenSize.x;
|
||||||
|
|
||||||
// offset by quad corner position
|
// offset by quad corner position
|
||||||
gl_Position = vec4(ndcX + position.x * quadSizeNdc,
|
gl_Position = vec4(ndcX + position.x * quadSizeNdc,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ pub const SsboRenderer = struct {
|
||||||
ssbo_id: u32,
|
ssbo_id: u32,
|
||||||
screen_size_loc: i32,
|
screen_size_loc: i32,
|
||||||
circle_texture_loc: i32,
|
circle_texture_loc: i32,
|
||||||
|
zoom_loc: i32,
|
||||||
|
pan_loc: i32,
|
||||||
circle_texture_id: u32,
|
circle_texture_id: u32,
|
||||||
gpu_buffer: []sandbox.GpuEntity,
|
gpu_buffer: []sandbox.GpuEntity,
|
||||||
|
|
||||||
|
|
@ -53,6 +55,8 @@ pub const SsboRenderer = struct {
|
||||||
// get uniform locations
|
// get uniform locations
|
||||||
const screen_size_loc = rl.gl.rlGetLocationUniform(shader_id, "screenSize");
|
const screen_size_loc = rl.gl.rlGetLocationUniform(shader_id, "screenSize");
|
||||||
const circle_texture_loc = rl.gl.rlGetLocationUniform(shader_id, "circleTexture");
|
const circle_texture_loc = rl.gl.rlGetLocationUniform(shader_id, "circleTexture");
|
||||||
|
const zoom_loc = rl.gl.rlGetLocationUniform(shader_id, "zoom");
|
||||||
|
const pan_loc = rl.gl.rlGetLocationUniform(shader_id, "pan");
|
||||||
|
|
||||||
if (screen_size_loc < 0) {
|
if (screen_size_loc < 0) {
|
||||||
std.debug.print("ssbo: warning - screenSize uniform not found\n", .{});
|
std.debug.print("ssbo: warning - screenSize uniform not found\n", .{});
|
||||||
|
|
@ -116,6 +120,8 @@ pub const SsboRenderer = struct {
|
||||||
.ssbo_id = ssbo_id,
|
.ssbo_id = ssbo_id,
|
||||||
.screen_size_loc = screen_size_loc,
|
.screen_size_loc = screen_size_loc,
|
||||||
.circle_texture_loc = circle_texture_loc,
|
.circle_texture_loc = circle_texture_loc,
|
||||||
|
.zoom_loc = zoom_loc,
|
||||||
|
.pan_loc = pan_loc,
|
||||||
.circle_texture_id = circle_texture.id,
|
.circle_texture_id = circle_texture.id,
|
||||||
.gpu_buffer = gpu_buffer,
|
.gpu_buffer = gpu_buffer,
|
||||||
};
|
};
|
||||||
|
|
@ -129,7 +135,7 @@ pub const SsboRenderer = struct {
|
||||||
std.heap.page_allocator.free(self.gpu_buffer);
|
std.heap.page_allocator.free(self.gpu_buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(self: *SsboRenderer, entities: *const sandbox.Entities) void {
|
pub fn render(self: *SsboRenderer, entities: *const sandbox.Entities, zoom: f32, pan: @Vector(2, f32)) void {
|
||||||
if (entities.count == 0) return;
|
if (entities.count == 0) return;
|
||||||
|
|
||||||
// flush raylib's internal render batch before our custom GL calls
|
// flush raylib's internal render batch before our custom GL calls
|
||||||
|
|
@ -155,6 +161,13 @@ pub const SsboRenderer = struct {
|
||||||
const screen_size = [2]f32{ @floatFromInt(SCREEN_WIDTH), @floatFromInt(SCREEN_HEIGHT) };
|
const screen_size = [2]f32{ @floatFromInt(SCREEN_WIDTH), @floatFromInt(SCREEN_HEIGHT) };
|
||||||
rl.gl.rlSetUniform(self.screen_size_loc, &screen_size, @intFromEnum(rl.gl.rlShaderUniformDataType.rl_shader_uniform_vec2), 1);
|
rl.gl.rlSetUniform(self.screen_size_loc, &screen_size, @intFromEnum(rl.gl.rlShaderUniformDataType.rl_shader_uniform_vec2), 1);
|
||||||
|
|
||||||
|
// set zoom uniform
|
||||||
|
rl.gl.rlSetUniform(self.zoom_loc, &zoom, @intFromEnum(rl.gl.rlShaderUniformDataType.rl_shader_uniform_float), 1);
|
||||||
|
|
||||||
|
// set pan uniform
|
||||||
|
const pan_arr = [2]f32{ pan[0], pan[1] };
|
||||||
|
rl.gl.rlSetUniform(self.pan_loc, &pan_arr, @intFromEnum(rl.gl.rlShaderUniformDataType.rl_shader_uniform_vec2), 1);
|
||||||
|
|
||||||
// bind texture
|
// bind texture
|
||||||
rl.gl.rlActiveTextureSlot(0);
|
rl.gl.rlActiveTextureSlot(0);
|
||||||
rl.gl.rlEnableTexture(self.circle_texture_id);
|
rl.gl.rlEnableTexture(self.circle_texture_id);
|
||||||
|
|
|
||||||
17
src/ui.zig
17
src/ui.zig
|
|
@ -31,7 +31,7 @@ pub var show_ui: bool = true;
|
||||||
// drawing functions
|
// drawing functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
pub fn drawMetrics(entities: *const sandbox.Entities, update_us: i64, render_us: i64, paused: bool, font: rl.Font) void {
|
pub fn drawMetrics(entities: *const sandbox.Entities, update_us: i64, render_us: i64, paused: bool, zoom: f32, font: rl.Font) void {
|
||||||
if (!show_ui) return;
|
if (!show_ui) return;
|
||||||
|
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [256]u8 = undefined;
|
||||||
|
|
@ -47,7 +47,7 @@ pub fn drawMetrics(entities: *const sandbox.Entities, update_us: i64, render_us:
|
||||||
// metrics box (below fps)
|
// metrics box (below fps)
|
||||||
const metrics_y: i32 = 5 + fps_box_height + 5;
|
const metrics_y: i32 = 5 + fps_box_height + 5;
|
||||||
var y: f32 = @as(f32, @floatFromInt(metrics_y)) + box_padding;
|
var y: f32 = @as(f32, @floatFromInt(metrics_y)) + box_padding;
|
||||||
const bg_height: i32 = if (paused) 130 else 100;
|
const bg_height: i32 = if (paused) 150 else 120;
|
||||||
rl.drawRectangle(5, metrics_y, 180, bg_height, box_bg);
|
rl.drawRectangle(5, metrics_y, 180, bg_height, box_bg);
|
||||||
|
|
||||||
// entity count
|
// entity count
|
||||||
|
|
@ -72,6 +72,11 @@ pub fn drawMetrics(entities: *const sandbox.Entities, update_us: i64, render_us:
|
||||||
rl.drawTextEx(font, render_text, .{ .x = padding, .y = y }, font_size, 0, text_color);
|
rl.drawTextEx(font, render_text, .{ .x = padding, .y = y }, font_size, 0, text_color);
|
||||||
y += line_height;
|
y += line_height;
|
||||||
|
|
||||||
|
// zoom level
|
||||||
|
const zoom_text = std.fmt.bufPrintZ(&buf, "zoom: {d:.1}x", .{zoom}) catch "?";
|
||||||
|
rl.drawTextEx(font, zoom_text, .{ .x = padding, .y = y }, font_size, 0, if (zoom > 1.0) highlight_color else text_color);
|
||||||
|
y += line_height;
|
||||||
|
|
||||||
// paused indicator
|
// paused indicator
|
||||||
if (paused) {
|
if (paused) {
|
||||||
y += line_height;
|
y += line_height;
|
||||||
|
|
@ -118,7 +123,7 @@ pub fn drawMemory(entity_count: usize, font: rl.Font) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drawControls(font: rl.Font, metrics_bottom: i32) void {
|
fn drawControls(font: rl.Font, metrics_bottom: i32) void {
|
||||||
const ctrl_box_height: i32 = @intFromFloat(small_line_height * 5 + box_padding * 2);
|
const ctrl_box_height: i32 = @intFromFloat(small_line_height * 7 + box_padding * 2);
|
||||||
const ctrl_box_y: i32 = metrics_bottom + 5;
|
const ctrl_box_y: i32 = metrics_bottom + 5;
|
||||||
rl.drawRectangle(5, ctrl_box_y, 175, ctrl_box_height, box_bg);
|
rl.drawRectangle(5, ctrl_box_y, 175, ctrl_box_height, box_bg);
|
||||||
|
|
||||||
|
|
@ -127,8 +132,10 @@ fn drawControls(font: rl.Font, metrics_bottom: i32) void {
|
||||||
const controls = [_][]const u8{
|
const controls = [_][]const u8{
|
||||||
"+/-: 10k entities",
|
"+/-: 10k entities",
|
||||||
"shift +/-: 50k",
|
"shift +/-: 50k",
|
||||||
"space: pause",
|
"scroll: zoom",
|
||||||
"r: reset",
|
"drag: pan (zoomed)",
|
||||||
|
"space: pause, r: reset",
|
||||||
|
"enter: reset zoom",
|
||||||
"tab: toggle ui",
|
"tab: toggle ui",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue