sokoban-v1/src/engine/graphics.zig
2026-01-04 21:52:32 +02:00

311 lines
8.6 KiB
Zig

const sokol = @import("sokol");
const sg = sokol.gfx;
const sglue = sokol.glue;
const slog = sokol.log;
const sapp = sokol.app;
const simgui = sokol.imgui;
const sgl = sokol.gl;
const Math = @import("./math.zig");
const Vec2 = Math.Vec2;
const Vec4 = Math.Vec4;
const rgb = Math.rgb;
const Rect = Math.Rect;
const std = @import("std");
const log = std.log.scoped(.graphics);
const assert = std.debug.assert;
const imgui = @import("imgui.zig");
const tracy = @import("tracy");
const fontstash = @import("./fontstash/root.zig");
pub const Font = fontstash.Font;
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug.
// From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
// Maybe a patch for sokol could be made?
// More info:
// * https://github.com/libsdl-org/SDL/issues/11618
// * https://github.com/nimgl/nimgl/issues/59
const Options = struct {
const ImguiFont = struct {
ttf_data: []const u8,
size: f32 = 16
};
allocator: std.mem.Allocator,
logger: sg.Logger = .{},
imgui_font: ?ImguiFont = null
};
const DrawFrame = struct {
screen_size: Vec2 = Vec2.zero,
bg_color: Vec4 = rgb(0, 0, 0),
scissor_stack_buffer: [32]Rect = undefined,
scissor_stack: std.ArrayListUnmanaged(Rect) = .empty,
fn init(self: *DrawFrame) void {
self.* = DrawFrame{
.scissor_stack = .initBuffer(&self.scissor_stack_buffer)
};
}
};
var draw_frame: DrawFrame = undefined;
var main_pipeline: sgl.Pipeline = .{};
var linear_sampler: sg.Sampler = .{};
var nearest_sampler: sg.Sampler = .{};
var font_context: fontstash.Context = undefined;
pub var font_resolution_scale: f32 = 1;
pub fn init(options: Options) !void {
draw_frame.init();
sg.setup(.{
.logger = options.logger,
.environment = sglue.environment(),
});
sgl.setup(.{
.logger = .{
.func = options.logger.func,
.user_data = options.logger.user_data
}
});
main_pipeline = sgl.makePipeline(.{
.colors = init: {
var colors: [8]sg.ColorTargetState = @splat(.{});
colors[0] = .{
.blend = .{
.enabled = true,
.src_factor_rgb = .SRC_ALPHA,
.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
.op_rgb = .ADD,
.src_factor_alpha = .ONE,
.dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
.op_alpha = .ADD,
},
};
break :init colors;
},
});
imgui.setup(options.allocator, .{
.logger = .{
.func = options.logger.func,
.user_data = options.logger.user_data
},
.no_default_font = options.imgui_font != null,
// TODO: Figure out a way to make imgui play nicely with UI
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
// Otherwise our own cursor should be used.
.disable_set_mouse_cursor = true
});
if (options.imgui_font) |imgui_font| {
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
}
linear_sampler = sg.makeSampler(.{
.min_filter = .LINEAR,
.mag_filter = .LINEAR,
.mipmap_filter = .LINEAR,
.label = "linear-sampler",
});
nearest_sampler = sg.makeSampler(.{
.min_filter = .NEAREST,
.mag_filter = .NEAREST,
.mipmap_filter = .NEAREST,
.label = "nearest-sampler",
});
const dpi_scale = sapp.dpiScale();
const atlas_size = 512;
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
font_context = try fontstash.Context.init(.{
.width = @intCast(atlas_dim),
.height = @intCast(atlas_dim),
});
}
pub fn deinit() void {
imgui.shutdown();
font_context.deinit();
sgl.shutdown();
sg.shutdown();
}
pub fn beginFrame() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
draw_frame.init();
draw_frame.screen_size = Vec2.init(sapp.widthf(), sapp.heightf());
draw_frame.scissor_stack.appendAssumeCapacity(Rect.init(0, 0, sapp.widthf(), sapp.heightf()));
imgui.newFrame(.{
.width = @intFromFloat(draw_frame.screen_size.x),
.height = @intFromFloat(draw_frame.screen_size.y),
.delta_time = sapp.frameDuration(),
.dpi_scale = sapp.dpiScale()
});
font_context.clearState();
sgl.defaults();
sgl.matrixModeProjection();
sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1);
sgl.loadPipeline(main_pipeline);
}
pub fn endFrame() void {
const zone = tracy.initZone(@src(), .{ });
defer zone.deinit();
assert(draw_frame.scissor_stack.items.len == 1);
var pass_action: sg.PassAction = .{};
pass_action.colors[0] = sg.ColorAttachmentAction{
.load_action = .CLEAR,
.clear_value = .{
.r = draw_frame.bg_color.x,
.g = draw_frame.bg_color.y,
.b = draw_frame.bg_color.z,
.a = draw_frame.bg_color.w
}
};
font_context.flush();
{
sg.beginPass(.{
.action = pass_action,
.swapchain = sglue.swapchain()
});
defer sg.endPass();
sgl.draw();
imgui.render();
}
sg.commit();
}
pub fn pushTransform(translation: Vec2, scale: f32) void {
sgl.pushMatrix();
sgl.translate(translation.x, translation.y, 0);
sgl.scale(scale, scale, 1);
}
pub fn popTransform() void {
sgl.popMatrix();
}
pub fn drawQuad(quad: [4]Vec2, color: Vec4) void {
sgl.beginQuads();
defer sgl.end();
sgl.c4f(color.x, color.y, color.z, color.w);
for (quad) |position| {
sgl.v2f(position.x, position.y);
}
}
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void {
drawQuad(
.{
// Top left
pos,
// Top right
pos.add(.{ .x = size.x, .y = 0 }),
// Bottom right
pos.add(size),
// Bottom left
pos.add(.{ .x = 0, .y = size.y }),
},
color
);
}
pub fn drawTriangle(tri: [3]Vec2, color: Vec4) void {
sgl.beginTriangles();
defer sgl.end();
sgl.c4f(color.x, color.y, color.z, color.w);
for (tri) |position| {
sgl.v2f(position.x, position.y);
}
}
pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const step = to.sub(from).normalized().multiplyScalar(width/2);
const top_left = from.add(step.rotateLeft90());
const bottom_left = from.add(step.rotateRight90());
const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90());
drawQuad(
.{ top_right, top_left, bottom_left, bottom_right },
color
);
}
pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
// TODO: Don't use line segments
drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
}
pub fn pushScissor(rect: Rect) void {
draw_frame.scissor_stack.appendAssumeCapacity(rect);
sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true);
}
pub fn popScissor() void {
_ = draw_frame.scissor_stack.pop().?;
const rect = draw_frame.scissor_stack.getLast();
sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true);
}
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
return try font_context.addFont(name, data);
}
pub const DrawTextOptions = struct {
font: Font.Id,
size: f32 = 16,
color: Vec4 = rgb(255, 255, 255),
};
pub fn drawText(position: Vec2, text: []const u8, opts: DrawTextOptions) void {
pushTransform(.{ .x = 0, .y = 0}, 1/font_resolution_scale);
defer popTransform();
font_context.setFont(opts.font);
font_context.setSize(opts.size * font_resolution_scale);
font_context.setAlign(.{ .x = .left, .y = .top });
font_context.setSpacing(0);
const r: u8 = @intFromFloat(opts.color.x * 255);
const g: u8 = @intFromFloat(opts.color.y * 255);
const b: u8 = @intFromFloat(opts.color.z * 255);
const a: u8 = @intFromFloat(opts.color.w * 255);
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
font_context.setColor(color);
font_context.drawText(
position.x * font_resolution_scale,
position.y * font_resolution_scale,
text
);
}