475 lines
14 KiB
Zig
475 lines
14 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 Line = Math.Line;
|
|
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;
|
|
|
|
const GraphicsFrame = @import("./frame.zig").Graphics;
|
|
|
|
// 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,
|
|
};
|
|
|
|
pub const Command = union(enum) {
|
|
pub const DrawRectangle = struct {
|
|
rect: Rect,
|
|
color: Vec4,
|
|
sprite: ?Sprite = null,
|
|
rotation: f32 = 0,
|
|
origin: Vec2 = .init(0, 0),
|
|
uv_flip_diagonal: bool = false,
|
|
uv_flip_horizontal: bool = false,
|
|
uv_flip_vertical: bool = false
|
|
};
|
|
|
|
pub const DrawCircle = struct {
|
|
center: Vec2,
|
|
radius: f32,
|
|
color: Vec4,
|
|
};
|
|
|
|
set_scissor: Rect,
|
|
draw_rectangle: DrawRectangle,
|
|
draw_line: struct { pos1: Vec2, pos2: Vec2, color: Vec4, width: f32 },
|
|
draw_text: struct {
|
|
pos: Vec2,
|
|
text: []const u8,
|
|
font: Font.Id,
|
|
size: f32,
|
|
color: Vec4,
|
|
},
|
|
push_transformation: struct { translation: Vec2, scale: Vec2 },
|
|
pop_transformation: void,
|
|
draw_circle: DrawCircle
|
|
};
|
|
|
|
const Texture = struct {
|
|
image: sg.Image,
|
|
view: sg.View,
|
|
info: Info,
|
|
|
|
const Info = struct {
|
|
width: u32,
|
|
height: u32,
|
|
};
|
|
|
|
const Id = enum(u32) { _ };
|
|
const Data = struct { width: u32, height: u32, rgba: [*]u8 };
|
|
};
|
|
pub const TextureId = Texture.Id;
|
|
pub const TextureInfo = Texture.Info;
|
|
|
|
pub const Sprite = struct {
|
|
texture: TextureId,
|
|
uv: Rect,
|
|
|
|
pub fn flipHorizontal(self: Sprite) Sprite {
|
|
var uv = self.uv;
|
|
uv.pos.x += uv.size.x;
|
|
uv.size.x *= -1;
|
|
|
|
return Sprite{
|
|
.texture = self.texture,
|
|
.uv = uv
|
|
};
|
|
}
|
|
|
|
pub fn getSize(self: Sprite) Vec2 {
|
|
const texture_info = getTextureInfo(self.texture);
|
|
const texture_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height);
|
|
return self.uv.size.multiply(texture_size);
|
|
}
|
|
};
|
|
|
|
var gpa: std.mem.Allocator = undefined;
|
|
|
|
var main_pipeline: sgl.Pipeline = .{};
|
|
var linear_sampler: sg.Sampler = .{};
|
|
var nearest_sampler: sg.Sampler = .{};
|
|
var font_context: fontstash.Context = undefined;
|
|
var textures: std.ArrayList(Texture) = .empty;
|
|
|
|
var scale_stack_buffer: [32]Vec2 = undefined;
|
|
var scale_stack: std.ArrayList(Vec2) = .empty;
|
|
|
|
pub fn init(options: Options) !void {
|
|
gpa = options.allocator;
|
|
|
|
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 {
|
|
textures.deinit(gpa);
|
|
imgui.shutdown();
|
|
font_context.deinit();
|
|
sgl.shutdown();
|
|
sg.shutdown();
|
|
}
|
|
|
|
pub fn drawCommand(command: Command) void {
|
|
switch (command) {
|
|
.push_transformation => |opts| {
|
|
pushTransform(opts.translation, opts.scale);
|
|
// font_resolution_scale = font_resolution_scale.multiply(opts.scale);
|
|
},
|
|
.pop_transformation => {
|
|
popTransform();
|
|
},
|
|
.draw_rectangle => |opts| {
|
|
drawRectangle(opts);
|
|
},
|
|
.set_scissor => |opts| {
|
|
sgl.scissorRectf(opts.pos.x, opts.pos.y, opts.size.x, opts.size.y, true);
|
|
},
|
|
.draw_line => |opts| {
|
|
drawLine(opts.pos1, opts.pos2, opts.color, opts.width);
|
|
},
|
|
.draw_text => |opts| {
|
|
const font_resolution_scale = scale_stack.getLast();
|
|
|
|
sgl.pushMatrix();
|
|
defer sgl.popMatrix();
|
|
|
|
sgl.scale(1 / font_resolution_scale.x, 1 / font_resolution_scale.y, 1);
|
|
|
|
font_context.setFont(opts.font);
|
|
font_context.setSize(opts.size * font_resolution_scale.y);
|
|
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(opts.pos.x * font_resolution_scale.x, opts.pos.y * font_resolution_scale.y, opts.text);
|
|
},
|
|
.draw_circle => |opts| {
|
|
sgl.beginTriangleStrip();
|
|
defer sgl.end();
|
|
|
|
const angle_step: f32 = std.math.pi * 2.0 / 32.0;
|
|
|
|
sgl.c4f(opts.color.x, opts.color.y, opts.color.z, opts.color.w);
|
|
|
|
sgl.v2f(opts.center.x, opts.center.y);
|
|
var angle: f32 = 0;
|
|
while (angle < std.math.pi * 2) {
|
|
const point_on_circle = Vec2.initAngle(angle).multiplyScalar(opts.radius).add(opts.center);
|
|
sgl.v2f(point_on_circle.x, point_on_circle.y);
|
|
sgl.v2f(opts.center.x, opts.center.y);
|
|
|
|
angle += angle_step;
|
|
}
|
|
sgl.v2f(opts.center.x + opts.radius, opts.center.y);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn drawCommands(commands: []const Command) void {
|
|
for (commands) |command| {
|
|
drawCommand(command);
|
|
}
|
|
}
|
|
|
|
pub fn beginFrame() void {
|
|
const zone = tracy.initZone(@src(), .{});
|
|
defer zone.deinit();
|
|
|
|
imgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), .delta_time = sapp.frameDuration(), .dpi_scale = sapp.dpiScale() });
|
|
|
|
scale_stack = .initBuffer(&scale_stack_buffer);
|
|
scale_stack.appendAssumeCapacity(.init(1, 1));
|
|
|
|
font_context.clearState();
|
|
sgl.defaults();
|
|
sgl.matrixModeProjection();
|
|
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
|
|
sgl.loadPipeline(main_pipeline);
|
|
}
|
|
|
|
pub fn endFrame(clear_color: Vec4) void {
|
|
const zone = tracy.initZone(@src(), .{});
|
|
defer zone.deinit();
|
|
|
|
var pass_action: sg.PassAction = .{};
|
|
|
|
pass_action.colors[0] = sg.ColorAttachmentAction{ .load_action = .CLEAR, .clear_value = .{ .r = clear_color.x, .g = clear_color.y, .b = clear_color.z, .a = clear_color.w } };
|
|
|
|
font_context.flush();
|
|
|
|
{
|
|
sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() });
|
|
defer sg.endPass();
|
|
|
|
sgl.draw();
|
|
|
|
imgui.render();
|
|
}
|
|
sg.commit();
|
|
}
|
|
|
|
fn pushTransform(translation: Vec2, scale: Vec2) void {
|
|
sgl.pushMatrix();
|
|
sgl.translate(translation.x, translation.y, 0);
|
|
sgl.scale(scale.x, scale.y, 1);
|
|
|
|
scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale));
|
|
}
|
|
|
|
fn popTransform() void {
|
|
sgl.popMatrix();
|
|
_ = scale_stack.pop().?;
|
|
}
|
|
|
|
const Vertex = struct { pos: Vec2, uv: Vec2 };
|
|
|
|
fn drawQuad(quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
|
|
sgl.enableTexture();
|
|
defer sgl.disableTexture();
|
|
|
|
const view = textures.items[@intFromEnum(texture_id)].view;
|
|
// TODO: Make sampler configurable
|
|
sgl.texture(view, nearest_sampler);
|
|
|
|
sgl.beginQuads();
|
|
defer sgl.end();
|
|
|
|
sgl.c4f(color.x, color.y, color.z, color.w);
|
|
for (quad) |vertex| {
|
|
const pos = vertex.pos;
|
|
const uv = vertex.uv;
|
|
sgl.v2fT2f(pos.x, pos.y, uv.x, uv.y);
|
|
}
|
|
}
|
|
|
|
fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void {
|
|
sgl.beginQuads();
|
|
defer sgl.end();
|
|
|
|
sgl.c4f(color.x, color.y, color.z, color.w);
|
|
for (quad) |pos| {
|
|
sgl.v2f(pos.x, pos.y);
|
|
}
|
|
}
|
|
|
|
fn drawRectangle(opts: Command.DrawRectangle) void {
|
|
const pos = opts.rect.pos;
|
|
const size = opts.rect.size;
|
|
|
|
const top_left = Vec2.init(0, 0).rotateAround(opts.rotation, opts.origin);
|
|
const top_right = Vec2.init(size.x, 0).rotateAround(opts.rotation, opts.origin);
|
|
const bottom_right = size.rotateAround(opts.rotation, opts.origin);
|
|
const bottom_left = Vec2.init(0, size.y).rotateAround(opts.rotation, opts.origin);
|
|
|
|
if (opts.sprite) |sprite| {
|
|
const uv = sprite.uv;
|
|
var uv_top_left = Vec2.init(uv.left(), uv.top());
|
|
var uv_top_right = Vec2.init(uv.right(), uv.top());
|
|
var uv_bottom_right = Vec2.init(uv.right(), uv.bottom());
|
|
var uv_bottom_left = Vec2.init(uv.left(), uv.bottom());
|
|
|
|
if (opts.uv_flip_diagonal) {
|
|
std.mem.swap(Vec2, &uv_bottom_left, &uv_top_right);
|
|
}
|
|
if (opts.uv_flip_horizontal) {
|
|
std.mem.swap(Vec2, &uv_top_left, &uv_top_right);
|
|
std.mem.swap(Vec2, &uv_bottom_left, &uv_bottom_right);
|
|
}
|
|
if (opts.uv_flip_vertical) {
|
|
std.mem.swap(Vec2, &uv_top_left, &uv_bottom_left);
|
|
std.mem.swap(Vec2, &uv_top_right, &uv_bottom_right);
|
|
}
|
|
|
|
const quad = [4]Vertex{
|
|
.{
|
|
.pos = pos.add(top_left),
|
|
.uv = uv_top_left
|
|
},
|
|
.{
|
|
.pos = pos.add(top_right),
|
|
.uv = uv_top_right,
|
|
},
|
|
.{
|
|
.pos = pos.add(bottom_right),
|
|
.uv = uv_bottom_right
|
|
},
|
|
.{
|
|
.pos = pos.add(bottom_left),
|
|
.uv = uv_bottom_left
|
|
}
|
|
};
|
|
drawQuad(quad, opts.color, sprite.texture);
|
|
} else {
|
|
const quad = .{ pos.add(top_left), pos.add(top_right), pos.add(bottom_right), pos.add(bottom_left) };
|
|
drawQuadNoUVs(quad, opts.color);
|
|
}
|
|
}
|
|
|
|
fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
|
|
const line = Line{
|
|
.p0 = from,
|
|
.p1 = to,
|
|
};
|
|
|
|
const quad = line.getQuad(width);
|
|
|
|
drawQuadNoUVs(
|
|
.{
|
|
quad.top_right,
|
|
quad.top_left,
|
|
quad.bottom_left,
|
|
quad.bottom_right
|
|
},
|
|
color
|
|
);
|
|
}
|
|
|
|
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
|
|
return try font_context.addFont(name, data);
|
|
}
|
|
|
|
fn makeView(image: sg.Image) !sg.View {
|
|
const image_view = sg.makeView(.{ .texture = .{ .image = image } });
|
|
if (image_view.id == sg.invalid_id) {
|
|
return error.InvalidView;
|
|
}
|
|
return image_view;
|
|
}
|
|
|
|
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
|
if (mipmaps.len == 0) {
|
|
return error.NoMipMaps;
|
|
}
|
|
|
|
var data: sg.ImageData = .{};
|
|
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
|
|
|
|
for (mipmaps) |mipmap| {
|
|
try mip_levels.appendBounded(.{ .ptr = mipmap.rgba, .size = mipmap.width * mipmap.height * 4 });
|
|
}
|
|
|
|
const image = sg.makeImage(.{ .width = @intCast(mipmaps[0].width), .height = @intCast(mipmaps[0].height), .pixel_format = .RGBA8, .usage = .{ .immutable = true }, .num_mipmaps = @intCast(mip_levels.items.len), .data = data });
|
|
if (image.id == sg.invalid_id) {
|
|
return error.InvalidImage;
|
|
}
|
|
|
|
return image;
|
|
}
|
|
|
|
pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
|
|
const image = try makeImageWithMipMaps(mipmaps);
|
|
errdefer sg.deallocImage(image);
|
|
|
|
const view = try makeView(image);
|
|
errdefer sg.deallocView(view);
|
|
|
|
assert(mipmaps.len > 0);
|
|
const index = textures.items.len;
|
|
try textures.append(gpa, .{ .image = image, .view = view, .info = .{
|
|
.width = mipmaps[0].width,
|
|
.height = mipmaps[0].height,
|
|
} });
|
|
|
|
return @enumFromInt(index);
|
|
}
|
|
|
|
pub fn getTextureInfo(id: TextureId) TextureInfo {
|
|
const texture = textures.items[@intFromEnum(id)];
|
|
return texture.info;
|
|
}
|
|
|
|
pub fn getTextureSize(id: TextureId) Vec2 {
|
|
const texture_info = getTextureInfo(id);
|
|
return Vec2.initFromInt(u32, texture_info.width, texture_info.height);
|
|
}
|