From b661791c7383fc226d26d754649867e87861c12a Mon Sep 17 00:00:00 2001 From: prescientmoon <git@moonythm.dev> Date: Thu, 5 Jun 2025 04:27:32 +0200 Subject: [PATCH] odin(sdl-opengl-rendering): implement JFA --- odin/sdl-opengl-rendering/README.md | 4 +- odin/sdl-opengl-rendering/flake.lock | 17 + odin/sdl-opengl-rendering/flake.nix | 12 + odin/sdl-opengl-rendering/src/external.odin | 4 + odin/sdl-opengl-rendering/src/main.odin | 175 +++++----- odin/sdl-opengl-rendering/src/sdl.odin | 56 ++- .../src/shaders/jfa-seed.frag.glsl | 16 + .../src/shaders/jfa.frag.glsl | 39 +++ .../src/shaders/jfa.vert.glsl | 14 + .../src/shaders/{frag.glsl => sdf.frag.glsl} | 8 +- .../src/shaders/{vert.glsl => sdf.vert.glsl} | 7 +- odin/sdl-opengl-rendering/src/shape.odin | 326 ++++++++++++++---- 12 files changed, 509 insertions(+), 169 deletions(-) create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl create mode 100644 odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl rename odin/sdl-opengl-rendering/src/shaders/{frag.glsl => sdf.frag.glsl} (86%) rename odin/sdl-opengl-rendering/src/shaders/{vert.glsl => sdf.vert.glsl} (57%) diff --git a/odin/sdl-opengl-rendering/README.md b/odin/sdl-opengl-rendering/README.md index f99c5cd..279d398 100644 --- a/odin/sdl-opengl-rendering/README.md +++ b/odin/sdl-opengl-rendering/README.md @@ -6,9 +6,9 @@ Methods to convert: [x] unset_clip_rect [x] clear_background [x] draw_rectangle -[ ] draw_rectangle_lines +[x] draw_rectangle_lines [x] draw_circle -[ ] draw_circle_lines +[x] draw_circle_lines [x] draw_line [ ] draw_triangle [ ] draw_circle_arc_lines_impl diff --git a/odin/sdl-opengl-rendering/flake.lock b/odin/sdl-opengl-rendering/flake.lock index 45c1a6f..78b730a 100644 --- a/odin/sdl-opengl-rendering/flake.lock +++ b/odin/sdl-opengl-rendering/flake.lock @@ -18,6 +18,22 @@ "type": "github" } }, + "glsl_analyzer": { + "flake": false, + "locked": { + "lastModified": 1749076974, + "narHash": "sha256-KnJPO3eVDfaHDwDrE1s0YvkVxpjMXtG8JlhqUe3G+30=", + "owner": "starlitcanopy", + "repo": "glsl_analyzer", + "rev": "0209e194a53370416d4f02487031740fc1ff913a", + "type": "github" + }, + "original": { + "owner": "starlitcanopy", + "repo": "glsl_analyzer", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1748693115, @@ -86,6 +102,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "glsl_analyzer": "glsl_analyzer", "nixpkgs": "nixpkgs", "odin": "odin", "ols": "ols" diff --git a/odin/sdl-opengl-rendering/flake.nix b/odin/sdl-opengl-rendering/flake.nix index fa10e25..9118c7d 100644 --- a/odin/sdl-opengl-rendering/flake.nix +++ b/odin/sdl-opengl-rendering/flake.nix @@ -11,6 +11,9 @@ ols.inputs.nixpkgs.follows = "nixpkgs"; ols.inputs.flake-utils.follows = "flake-utils"; ols.inputs.odin.follows = "odin"; + + glsl_analyzer.url = "github:starlitcanopy/glsl_analyzer"; + glsl_analyzer.flake = false; }; outputs = @@ -26,6 +29,14 @@ ]; }; + glsl_analyzer = pkgs.glsl_analyzer.overrideAttrs (_: { + src = inputs.glsl_analyzer; + + nativeBuildInputs = [ + pkgs.zig_0_14.hook + ]; + }); + inherit (pkgs) lib; in { @@ -42,6 +53,7 @@ pkgs.seer # Debugger GUI pkgs.valgrind # Detect memory leaks pkgs.renderdoc # Graphics debugger + glsl_analyzer # GLSL language server ]; buildInputs = [ diff --git a/odin/sdl-opengl-rendering/src/external.odin b/odin/sdl-opengl-rendering/src/external.odin index e501ba5..0b97717 100644 --- a/odin/sdl-opengl-rendering/src/external.odin +++ b/odin/sdl-opengl-rendering/src/external.odin @@ -1,3 +1,5 @@ +// This file contains types/constants/functions that are already implemented +// in my proper projects. package visuals ℝ :: f32 @@ -7,6 +9,7 @@ package visuals Mat3 :: matrix[3, 3]ℝ Mat4 :: matrix[4, 4]ℝ Color :: [4]ℝ +Affine2 :: matrix[2, 3]ℝ AABB :: struct { top_left: ℝ², @@ -29,6 +32,7 @@ vec2_perp :: proc(v: ℝ²) -> ℝ² { return {-v.y, v.x} } +@(private = "file") g_state: State g_renderer_state :: proc() -> ^State { return &g_state diff --git a/odin/sdl-opengl-rendering/src/main.odin b/odin/sdl-opengl-rendering/src/main.odin index 27b20d7..432b3fa 100644 --- a/odin/sdl-opengl-rendering/src/main.odin +++ b/odin/sdl-opengl-rendering/src/main.odin @@ -11,91 +11,96 @@ render :: proc() { state := g_renderer_state() state.tick += 1 - dims := screen_dimensions() - center := dims / 2 - draw_rect(ℝ²{30, 20}, ℝ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4}) - draw_rect( - 10, - center + center * math.sin(f32(state.tick) / 60), - Shape_Options{fill = {0, 1, 0, 1}, z = 0.6}, - ) - draw_rect( - ℝ²{1000, 800}, - center / 3, - Shape_Options{fill = {0, 1, 1, 1}, z = 0.5, stroke = {0, 0.5, 0.5, 1}, stroke_width = 3}, - ) + // dims := screen_dimensions() + // center := dims / 2 + // draw_rect(ℝ²{30, 20}, ℝ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4}) + // draw_rect( + // 10, + // center + center * math.sin(f32(state.tick) / 60), + // Shape_Options{fill = {0, 1, 0, 1}, z = 0.6}, + // ) + // draw_rect( + // ℝ²{1000, 800}, + // center / 3, + // Shape_Options{fill = {0, 1, 1, 1}, z = 0.5, stroke = {0, 0.5, 0.5, 1}, stroke_width = 3}, + // ) + // + // // count := math.pow(2, math.mod(ℝ(state.tick) / 30, 7)) + // count := ℝ(32) + // for x in ℝ(0) ..< count { + // for y in ℝ(0) ..< count { + // i := x * count + y + // pos := + // ℝ²{x + math.sin_f32(2 * math.π * (ℝ(state.tick) + i) / 60), y} / ℝ(count) + // color := Color{(pos.x + 1) / 2, (pos.y + 1) / 2, 1, 1} + // + // r := dims.x / ℝ(count) / 4 + // pos = pos * dims + // // pos.y = 2 * center.y - pos.y + // opts := Shape_Options { + // fill = color, + // z = 0.3, + // } + // opts.stroke.a = 1 + // opts.stroke.rgb = 1 - opts.fill.rgb + // opts.stroke_width = 1 + // + // if x > y { + // draw_rect(pos, 2 * r, opts) + // } else { + // draw_circle(pos + r, r, opts) + // } + // } + // } + // + // rect := □{center / 2, center / 2 + center * math.sin(f32(state.tick) / 120)} + // draw_rect(rect, Shape_Options{fill = {1, 0.6, 0.85, 0.3}}) + // + // // set_clip_rect(rect) + // draw_circle( + // center + center * {-0.25, -0.3}, + // 450, + // Shape_Options { + // fill = {0, 0, 0.5, 0.75}, + // z = -0.1, + // stroke = Color{0.7, 0.85, 1, 1}, + // stroke_width = 1, + // }, + // ) + // + // draw_line( + // Line{ℝ²{750, 200}, ℝ²{1800, 1600}, 10}, + // Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, + // rounded = false, + // ) + // + // draw_line( + // Line{ℝ²{750, 1000}, ℝ²{1200, 1000}, 5}, + // Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, + // ) + // + // draw_line( + // Line{ℝ²{1200, 1000}, ℝ²{300, 450}, 5}, + // Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, + // ) + // + // draw_line( + // Line{ℝ²{230, 1000}, ℝ²{1700, 350}, 20}, + // Shape_Options { + // fill = Color{1, 0, 1, 0.3}, + // z = -0.7, + // stroke_width = 3, + // stroke = Color{0.9, 0.68, 0.8, 1}, + // }, + // ) + // + // render_queue() - count := math.pow(2, math.mod(ℝ(state.tick) / 30, 10)) - for x in ℝ(0) ..< count { - for y in ℝ(0) ..< count { - i := x * count + y - pos := - ℝ²{x + math.sin_f32(2 * math.π * (ℝ(state.tick) + i) / 60), y} / ℝ(count) - color := Color{(pos.x + 1) / 2, (pos.y + 1) / 2, 1, 1} - - r := dims.x / ℝ(count) / 4 - pos = pos * dims - // pos.y = 2 * center.y - pos.y - opts := Shape_Options { - fill = color, - z = 0.3, - } - opts.stroke.a = 1 - opts.stroke.rgb = 1 - opts.fill.rgb - opts.stroke_width = 1 - - if x > y { - draw_rect(pos, 2 * r, opts) - } else { - draw_circle(pos + r, r, opts) - } - } - } - - rect := □{center / 2, center / 2 + center * math.sin(f32(state.tick) / 120)} - draw_rect(rect, Shape_Options{fill = {1, 0.6, 0.85, 0.3}}) - - // set_clip_rect(rect) - draw_circle( - center + center * {-0.25, -0.3}, - 450, - Shape_Options { - fill = {0, 0, 0.5, 0.75}, - z = -0.1, - stroke = Color{0.7, 0.85, 1, 1}, - stroke_width = 1, - }, - ) - - draw_line( - Line{ℝ²{750, 200}, ℝ²{1800, 1600}, 10}, - Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, - rounded = false, - ) - - draw_line( - Line{ℝ²{750, 1000}, ℝ²{1200, 1000}, 5}, - Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, - ) - - draw_line( - Line{ℝ²{1200, 1000}, ℝ²{300, 450}, 5}, - Shape_Options{fill = Color{1, 1, 1, 1}, z = -0.5}, - ) - - draw_line( - Line{ℝ²{230, 1000}, ℝ²{1700, 350}, 20}, - Shape_Options { - fill = Color{1, 0, 1, 0.3}, - z = -0.7, - stroke_width = 3, - stroke = Color{0.9, 0.68, 0.8, 1}, - }, - ) - - // unset_clip_rect() - - render_queue() + draw_rect(ℝ²{100, 100}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}}) + draw_rect(ℝ²{100, 400}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}}) + draw_rect(ℝ²{400, 100}, ℝ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}}) + draw_circle(ℝ²{1200, 350}, 200, Shape_Options{fill = {1, 0, 0, 1}}) + jfa() } // }}} // {{{ Main @@ -130,6 +135,8 @@ main :: proc() { switch ([^]u8)(event.text.text)[0] { case 'w': g_renderer_state().wireframe = !g_renderer_state().wireframe + case 'p': + g_renderer_state().pass = Render_Pass((u8(g_renderer_state().pass) + 1) % 3) } } } diff --git a/odin/sdl-opengl-rendering/src/sdl.odin b/odin/sdl-opengl-rendering/src/sdl.odin index 5af22f4..fee33d2 100644 --- a/odin/sdl-opengl-rendering/src/sdl.odin +++ b/odin/sdl-opengl-rendering/src/sdl.odin @@ -7,6 +7,13 @@ import "core:log" import "vendor:OpenGL" import "vendor:sdl3" +// For debugging purposes, we allow drawing up to any given pass +Render_Pass :: enum { + SDF, + JFA_Seed, + JFA, +} + State :: struct { window: ^sdl3.Window, @@ -19,31 +26,32 @@ State :: struct { // GPU data rect_mesh: Mesh, - ubo_globals: UBO, + ubos: [UBO_ID]UBO, + framebuffers: [Framebuffer_ID]FBO, + instance_buffers: [Instance_Param_Buf]u32, // Programs rect_program: Program, circle_program: Program, line_program: Program, rounded_line_program: Program, + jfa_program: Program, + jfa_seed_program: Program, // Instance buffers (CPU) - buf_matrices: [INSTANCES]Mat3, + buf_matrices: [INSTANCES]Affine2, buf_colors: [INSTANCES]Color, buf_lines: [INSTANCES][2]ℝ², buf_floats: [INSTANCES]ℝ, buf_vecs: [INSTANCES]ℝ², - // Instance buffers (GPU) - instance_buffers: [Instance_Param_Buf]u32, - // Flags tick: u32, wireframe: bool, + pass: Render_Pass, globals: Global_Uniforms, } - // {{{ Screen dimensions screen_dimensions :: proc() -> ℝ² { w, h: i32 @@ -149,7 +157,7 @@ sdl_init :: proc() -> (ok: bool) { (gl_ctx != nil) or_return OpenGL.load_up_to(GL_MAJOR, GL_MINOR, sdl3.gl_set_proc_address) - OpenGL.ClearColor(0, 0, 0, 1) + OpenGL.ClearColor(0, 0, 0, 0) OpenGL.Enable(OpenGL.DEPTH_TEST) OpenGL.Enable(OpenGL.BLEND) OpenGL.BlendFunc(OpenGL.SRC_ALPHA, OpenGL.ONE_MINUS_SRC_ALPHA) @@ -158,56 +166,64 @@ sdl_init :: proc() -> (ok: bool) { sdl_on_resize(screen_dimensions()) // }}} // {{{ Initialize GPU buffers & programs - // Initialize GPU buffers + // Initialize instance buffers OpenGL.GenBuffers(len(state.instance_buffers), ([^]u32)(&state.instance_buffers)) + // Initialize UBOs + OpenGL.GenBuffers(len(state.ubos), ([^]u32)(&state.ubos)) + for ubo, i in state.ubos { + OpenGL.BindBufferBase(OpenGL.UNIFORM_BUFFER, UBO_ID_BINDING[i], ubo) + } + // Initialize meshes state.rect_mesh = create_mesh({{0, 0}, {1, 0}, {1, 1}, {0, 1}}, {0, 1, 2, 3}) // Initialize programs state.rect_program = gen_program( { + template = .SDF, sdf_name = "sdf_rect", sdf_args = {"v_center", "v_dimensions"}, params = {{buf = .Center, name = "center"}, {buf = .Dimensions, name = "dimensions"}}, - id = 0, }, ) or_return state.circle_program = gen_program( { + template = .SDF, sdf_name = "sdf_circle", sdf_args = {"v_center", "v_radius"}, params = {{buf = .Center, name = "center"}, {buf = .Radius, name = "radius"}}, - id = 1, }, ) or_return state.rounded_line_program = gen_program( { + template = .SDF, sdf_name = "sdf_line", sdf_args = {"v_line", "v_thickness"}, params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}}, - id = 2, }, ) or_return state.rounded_line_program = gen_program( { + template = .SDF, sdf_name = "sdf_rounded_line", sdf_args = {"v_line", "v_thickness"}, params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}}, - id = 3, }, ) or_return - state.ubo_globals = create_ubo_globals() + state.jfa_seed_program = gen_program({template = .JFA_Seed}) or_return + state.jfa_program = gen_program({template = .JFA}) or_return // }}} state.q_rects = make([dynamic]Shape(□)) state.q_circles = make([dynamic]Shape(Circle2)) state.q_lines = make([dynamic]Shape(Line)) state.q_rounded_lines = make([dynamic]Shape(Rounded_Line)) + state.pass = .JFA return true } @@ -216,6 +232,12 @@ sdl_init :: proc() -> (ok: bool) { sdl_close :: proc() { state := g_renderer_state() + // TODO: perhaps perform some cleanup here? + + for fb in state.framebuffers { + destroy_framebuffer(fb) + } + _ = sdl3.StopTextInput(state.window) sdl3.DestroyWindow(state.window) sdl3.Quit() @@ -223,6 +245,7 @@ sdl_close :: proc() { // }}} // {{{ Resize sdl_on_resize :: proc(dims: ℝ²) { + state := g_renderer_state() OpenGL.Viewport(0, 0, i32(dims.x), i32(dims.y)) // odinfmt: disable @@ -234,6 +257,11 @@ sdl_on_resize :: proc(dims: ℝ²) { } // odinfmt: enable - g_renderer_state().globals.viewport_matrix = m + state.globals.viewport_matrix = m + + for &fb in state.framebuffers { + destroy_framebuffer(fb) + fb = create_framebuffer(dims) + } } // }}} diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl new file mode 100644 index 0000000..e22e413 --- /dev/null +++ b/odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl @@ -0,0 +1,16 @@ +#header + +out vec4 FragColor; +in vec2 v_uv; + +uniform sampler2D input_texture; +layout(std140, binding = 1) uniform Jfa { + float u_offset; + vec2 u_resolution; +}; + +void main() { + vec4 sample_value = texture(input_texture, v_uv); + + FragColor = vec4(v_uv * sample_value.a, 0, 1); +} diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl new file mode 100644 index 0000000..e7bd63a --- /dev/null +++ b/odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl @@ -0,0 +1,39 @@ +#header + +out vec4 FragColor; +in vec2 v_uv; + +uniform sampler2D input_texture; +layout(std140, binding = 1) uniform Jfa { + float u_offset; + vec2 u_resolution; +}; + +void main() { + vec4 nearest_seed = vec4(-2.0); + float nearest_dist = 999999999.9; + + for (float y = -1.0; y <= 1.0; y += 1.0) { + for (float x = -1.0; x <= 1.0; x += 1.0) { + vec2 sampleUV = v_uv + u_offset * vec2(x, y) / u_resolution; + + if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) { + continue; + } + + vec4 sample_value = texture(input_texture, sampleUV); + + if (sample_value.xy != vec2(0.0)) { + vec2 diff = (sample_value.xy - v_uv) * u_resolution; + float dist = dot(diff, diff); + + if (dist < nearest_dist) { + nearest_dist = dist; + nearest_seed = sample_value; + } + } + } + } + + FragColor = nearest_seed; +} diff --git a/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl b/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl new file mode 100644 index 0000000..c59a1e4 --- /dev/null +++ b/odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl @@ -0,0 +1,14 @@ +#header + +layout(location = 0) in vec2 a_pos; +out vec2 v_uv; + +layout(std140, binding = 1) uniform Jfa { + float u_offset; + vec2 u_resolution; +}; + +void main() { + v_uv = a_pos; + gl_Position = vec4(a_pos * 2 - 1, 0, 1); +} diff --git a/odin/sdl-opengl-rendering/src/shaders/frag.glsl b/odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl similarity index 86% rename from odin/sdl-opengl-rendering/src/shaders/frag.glsl rename to odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl index 174d65b..9e4c41f 100644 --- a/odin/sdl-opengl-rendering/src/shaders/frag.glsl +++ b/odin/sdl-opengl-rendering/src/shaders/sdf.frag.glsl @@ -1,9 +1,9 @@ #header out vec4 FragColor; -in vec2 v_pos; +in vec2 v_pos; -layout(std140) uniform Globals { +layout(std140, binding = 0) uniform Globals { mat4 u_viewport_matrix; float u_aa_width; }; @@ -33,8 +33,8 @@ float sdf_rounded_line(vec2[2] line, float thickness, vec2 p) { void main() { // This function gets auto-generated to call the right sdf - float dist = sdf(v_pos); - float alpha = smoothstep(u_aa_width, -u_aa_width, dist); + float dist = sdf(v_pos); + float alpha = smoothstep(u_aa_width, -u_aa_width, dist); float s_alpha = smoothstep(u_aa_width, -u_aa_width, abs(dist) - v_stroke_width); if (alpha < 0.001 && s_alpha < 0.001) discard; diff --git a/odin/sdl-opengl-rendering/src/shaders/vert.glsl b/odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl similarity index 57% rename from odin/sdl-opengl-rendering/src/shaders/vert.glsl rename to odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl index 79bf4fa..2e21d3e 100644 --- a/odin/sdl-opengl-rendering/src/shaders/vert.glsl +++ b/odin/sdl-opengl-rendering/src/shaders/sdf.vert.glsl @@ -1,6 +1,6 @@ #header -layout (location = 0) in vec2 a_pos; +layout(location = 0) in vec2 a_pos; out vec2 v_pos; layout(std140, binding = 0) uniform Globals { @@ -11,10 +11,9 @@ layout(std140, binding = 0) uniform Globals { #toplevelExtra void main() { - vec4 pos = u_viewport_matrix * vec4(i_model_matrix * vec3(a_pos, 1), 1); + v_pos = i_model_matrix * vec3(a_pos, 1); + vec4 pos = u_viewport_matrix * vec4(v_pos, i_z_offset, 1); gl_Position = vec4(pos.xyz, 1); - v_pos = (i_model_matrix * vec3(a_pos, 1)).xy; - #mainExtra } diff --git a/odin/sdl-opengl-rendering/src/shape.odin b/odin/sdl-opengl-rendering/src/shape.odin index f52149b..0676846 100644 --- a/odin/sdl-opengl-rendering/src/shape.odin +++ b/odin/sdl-opengl-rendering/src/shape.odin @@ -2,6 +2,7 @@ package visuals import "core:fmt" import "core:log" +import "core:math" import "core:math/linalg" import "core:slice" import "core:strings" @@ -12,7 +13,7 @@ Shape_Options :: struct { z: ℝ, fill: Color, stroke: Color, - stroke_width: f32, + stroke_width: ℝ, } Shape :: struct(T: typeid) { @@ -29,14 +30,14 @@ Line :: struct { Rounded_Line :: distinct Line // }}} // {{{ Shape -> Transform -to_transform_rect :: proc(rect: Shape(□)) -> (mat: Mat3) { +to_transform_rect :: proc(rect: Shape(□)) -> (mat: Affine2) { mat[0, 0] = rect.dimensions.x + rect.stroke_width * 2 mat[1, 1] = rect.dimensions.y + rect.stroke_width * 2 mat[2].xy = rect.top_left - rect.stroke_width return mat } -to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Mat3) { +to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Affine2) { r := circle.radius + circle.stroke_width mat[0, 0] = r * 2 mat[1, 1] = r * 2 @@ -44,7 +45,7 @@ to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Mat3) { return mat } -to_transform_line :: proc(line: Line) -> (mat: Mat3) { +to_transform_line :: proc(line: Line) -> (mat: Affine2) { dir := line.to - line.from mat[0].xy = dir / 2 mat[1].xy = vec2_perp(linalg.normalize0(dir)) * line.thickness @@ -52,7 +53,7 @@ to_transform_line :: proc(line: Line) -> (mat: Mat3) { return mat } -to_transform_rounded_line :: proc(line: Shape(Rounded_Line)) -> (mat: Mat3) { +to_transform_rounded_line :: proc(line: Shape(Rounded_Line)) -> (mat: Affine2) { dir := line.to - line.from len := linalg.length(dir) // TODO: return if this is close to 0 @@ -119,22 +120,39 @@ draw_line :: proc { } // }}} -// {{{ GPU data types +// {{{ Uniforms UBO :: u32 -UBO_GLOBALS_BINDING :: 0 -VERTEX_POS_LOCATION :: 0 -INSTANCES :: 1024 // The number of instances to allocate space for +UBO_ID :: enum { + Globals, + JFA, +} + +@(rodata) +UBO_ID_BINDING: [UBO_ID]u32 = { + .Globals = 0, + .JFA = 1, +} Global_Uniforms :: struct { viewport_matrix: Mat4, aaWidth: f32, } +JFA_Uniforms :: struct { + offset: f32, + _: f32, // padding + resolution: ℝ², +} +// }}} +// {{{ VBOS / Programs +VERTEX_POS_LOCATION :: 0 +INSTANCES :: 1024 // The number of instances to allocate space for Instance_Param_Buf :: enum { Fill, Stroke, Stroke_Width, + Z_Offset, Model_Mat, Dimensions, Center, @@ -148,12 +166,13 @@ INSTANCE_PARAM_LOCATIONS: [Instance_Param_Buf]u32 = { .Fill = 1, .Stroke = 2, .Stroke_Width = 3, - .Model_Mat = 4, - .Dimensions = 8, - .Center = 7, - .Radius = 8, - .Line = 7, - .Thickness = 9, + .Z_Offset = 4, + .Model_Mat = 5, + .Center = 8, + .Dimensions = 9, + .Radius = 9, + .Line = 8, + .Thickness = 10, } @(rodata) @@ -161,7 +180,8 @@ INSTANCE_PARAM_TYPE: [Instance_Param_Buf]string = { .Fill = "vec4", .Stroke = "vec4", .Stroke_Width = "float", - .Model_Mat = "mat3", + .Z_Offset = "float", + .Model_Mat = "mat3x2", .Dimensions = "vec2", .Center = "vec2", .Radius = "float", @@ -174,7 +194,8 @@ INSTANCE_PARAM_DIMS: [Instance_Param_Buf][2]i32 = { // (rows, cols) .Fill = {4, 1}, .Stroke = {4, 1}, .Stroke_Width = {1, 1}, - .Model_Mat = {3, 3}, + .Z_Offset = {1, 1}, + .Model_Mat = {2, 3}, .Dimensions = {2, 1}, .Center = {2, 1}, .Radius = {1, 1}, @@ -184,29 +205,18 @@ INSTANCE_PARAM_DIMS: [Instance_Param_Buf][2]i32 = { // (rows, cols) // Contains geometry data a shader can run on Mesh :: struct { - // Geometry data vertex_ind_buffer: u32, vertex_pos_buffer: u32, index_count: ℕ, } -// Contains data required to run a gpu program on some mesh +// Contains data required to run a GPU program on some mesh Program :: struct { program: u32, vao: u32, } // }}} -// {{{ Create UBO -create_ubo_globals :: proc() -> UBO { - id := gen_buffer() - - set_buffer(id, &Global_Uniforms{}, buffer = .Uniform) - OpenGL.BindBufferBase(OpenGL.UNIFORM_BUFFER, UBO_GLOBALS_BINDING, id) - - return id -} -// }}} -// {{{ Create mesh +// {{{ Meshes create_mesh :: proc(vertices: []ℝ², indices: []u32) -> (out: Mesh) { out.index_count = len(indices) @@ -219,7 +229,93 @@ create_mesh :: proc(vertices: []ℝ², indices: []u32) -> (out: Mesh) { return out } // }}} -// {{{ Shader processing +// {{{ Frame-buffers +FBO :: struct { + fbo: u32, + tex_color: u32, + tex_depth_stencil: u32, +} + +Framebuffer_ID :: enum { + JFA_1, + JFA_2, +} + +// TODO: perhaps create/destroy these in bulk +create_framebuffer :: proc(dims: ℝ²) -> (out: FBO) { + OpenGL.GenFramebuffers(1, &out.fbo) + OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, out.fbo) + defer OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, 0) + + OpenGL.GenTextures(1, &out.tex_color) + OpenGL.BindTexture(OpenGL.TEXTURE_2D, out.tex_color) + + OpenGL.TexImage2D( + OpenGL.TEXTURE_2D, + 0, // mipmap level of detail + OpenGL.RGBA, // internal format + i32(dims.x), + i32(dims.y), + 0, // border: must be 0 + OpenGL.RGBA, // format + OpenGL.UNSIGNED_BYTE, // pixel data type + nil, // data pointer + ) + + OpenGL.TexParameteri(OpenGL.TEXTURE_2D, OpenGL.TEXTURE_MIN_FILTER, OpenGL.LINEAR) + OpenGL.TexParameteri(OpenGL.TEXTURE_2D, OpenGL.TEXTURE_MAG_FILTER, OpenGL.LINEAR) + OpenGL.FramebufferTexture2D( + OpenGL.FRAMEBUFFER, + OpenGL.COLOR_ATTACHMENT0, + OpenGL.TEXTURE_2D, + out.tex_color, + 0, // mipmap level of detail + ) + + // TODO: we do not sample those, so we should generate renderbuffers instead + OpenGL.GenTextures(1, &out.tex_depth_stencil) + OpenGL.BindTexture(OpenGL.TEXTURE_2D, out.tex_depth_stencil) + + OpenGL.TexImage2D( + OpenGL.TEXTURE_2D, + 0, // mipmap level of detail + OpenGL.DEPTH24_STENCIL8, // internal format + i32(dims.x), + i32(dims.y), + 0, // border: must be 0 + OpenGL.DEPTH_STENCIL, // format + OpenGL.UNSIGNED_INT_24_8, // pixel data type + nil, // data pointer + ) + + OpenGL.FramebufferTexture2D( + OpenGL.FRAMEBUFFER, + OpenGL.DEPTH_STENCIL, + OpenGL.TEXTURE_2D, + out.tex_depth_stencil, + 0, // mipmap level of detail + ) + + OpenGL.BindTexture(OpenGL.TEXTURE_2D, 0) + + log.assert( + OpenGL.CheckFramebufferStatus(OpenGL.FRAMEBUFFER) == OpenGL.FRAMEBUFFER_COMPLETE, + "Failed to initialize framebuffer", + ) + + return out +} + +destroy_framebuffer :: proc(fbo: FBO) { + textures: []u32 = {fbo.tex_color, fbo.tex_depth_stencil} + OpenGL.DeleteTextures(2, raw_data(textures)) + + fbo := fbo.fbo + OpenGL.DeleteFramebuffers(1, &fbo) +} +// }}} +// {{{ Shaders +@(private = "file") Shader_Gen :: struct { template: string, header: string, @@ -227,6 +323,7 @@ Shader_Gen :: struct { main_extra: string, } +@(private = "file") process_shader :: proc(gen: Shader_Gen) -> string { s := gen.template s, _ = strings.replace_all(s, "#header\n", gen.header, context.temp_allocator) @@ -244,40 +341,74 @@ Instance_Param_Gen :: struct { vert_only: bool, } -Shader_Opts :: struct { - params: []Instance_Param_Gen, - - // The name of the toplevel sdf function to be used for rendering - sdf_name: string, - - // Additional arguments to pass to the sdf function declared above. - // These args get passed *befor#* the position vector. - sdf_args: []string, - - // File ID to display in error messages - id: u32, +Program_Template :: enum { + SDF, + JFA, + JFA_Seed, } +Shader_Opts :: struct { + params: []Instance_Param_Gen, + template: Program_Template, + + // SDF only parameters: + // - The name of the top-level SDF function to be used for rendering + sdf_name: string, + + // - Additional arguments to pass to the SDF function declared above. These + // arguments get passed *before* the position vector + sdf_args: []string, +} gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) { + // File ID to display in GLSL error messages + @(static) next_shader_id := 0 + id := next_shader_id + next_shader_id += 1 + + @(static) + @(rodata) + VERT_TEMPLATE: [Program_Template]string = { + .SDF = #load("./shaders/sdf.vert.glsl"), + .JFA = #load("./shaders/jfa.vert.glsl"), + .JFA_Seed = #load("./shaders/jfa.vert.glsl"), + } + + @(static) + @(rodata) + FRAG_TEMPLATE: [Program_Template]string = { + .SDF = #load("./shaders/sdf.frag.glsl"), + .JFA = #load("./shaders/jfa.frag.glsl"), + .JFA_Seed = #load("./shaders/jfa-seed.frag.glsl"), + } + // Instance parameters passed to every shape - COMMON_INSTANCE_PARAMS: []Instance_Param_Gen : { - {buf = .Fill, name = "fill"}, - {buf = .Stroke, name = "stroke"}, - {buf = .Stroke_Width, name = "stroke_width"}, - {buf = .Model_Mat, name = "model_matrix", vert_only = true}, + @(static) + @(rodata) + COMMON_INSTANCE_PARAMS: [Program_Template][]Instance_Param_Gen = { + .SDF = { + {buf = .Fill, name = "fill"}, + {buf = .Stroke, name = "stroke"}, + {buf = .Stroke_Width, name = "stroke_width"}, + {buf = .Z_Offset, name = "z_offset", vert_only = true}, + {buf = .Model_Mat, name = "model_matrix", vert_only = true}, + }, + .JFA = {}, + .JFA_Seed = {}, } opts := opts - opts.params = slice.concatenate([][]Instance_Param_Gen{COMMON_INSTANCE_PARAMS, opts.params}) + opts.params = slice.concatenate( + [][]Instance_Param_Gen{COMMON_INSTANCE_PARAMS[opts.template], opts.params}, + ) v_shader, f_shader: string // {{{ Vertex shader generation { gen: Shader_Gen = { - template = #load("./shaders/vert.glsl"), - header = fmt.tprintfln("#version 430\n#line 1 %v", opts.id), + template = VERT_TEMPLATE[opts.template], + header = fmt.tprintfln("#version 430\n#line 1 %v", id), } toplevel_extra, main_extra: strings.Builder @@ -326,8 +457,8 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) { // {{{ Fragment shader generation { gen: Shader_Gen = { - template = #load("./shaders/frag.glsl"), - header = fmt.tprintfln("#version 430\n#line 1 %v", opts.id), + template = FRAG_TEMPLATE[opts.template], + header = fmt.tprintfln("#version 430\n#line 1 %v", id), } toplevel_extra: strings.Builder @@ -348,12 +479,16 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) { ) } - strings.write_rune(&toplevel_extra, '\n') - fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {") - fmt.sbprintf(&toplevel_extra, " return %v(", opts.sdf_name) - for a in opts.sdf_args do fmt.sbprintf(&toplevel_extra, "%v, ", a) - fmt.sbprintfln(&toplevel_extra, "p);") - fmt.sbprintln(&toplevel_extra, "}") + if opts.template == .SDF { + log.assert(len(opts.sdf_name) > 0, "Empty SDF function name") + + strings.write_rune(&toplevel_extra, '\n') + fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {") + fmt.sbprintf(&toplevel_extra, " return %v(", opts.sdf_name) + for a in opts.sdf_args do fmt.sbprintf(&toplevel_extra, "%v, ", a) + fmt.sbprintfln(&toplevel_extra, "p);") + fmt.sbprintln(&toplevel_extra, "}") + } gen.toplevel_extra = strings.to_string(toplevel_extra) @@ -397,6 +532,7 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) { // }}} // {{{ Render the entire queue +@(private = "file") render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($T)) { OpenGL.UseProgram(program.program) OpenGL.BindVertexArray(program.vao) @@ -410,12 +546,13 @@ render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($ for shape, i in slice { state.buf_matrices[i] = to_transform(shape) - state.buf_matrices[i][2, 2] = shape.z + state.buf_floats[i] = shape.z state.buf_colors[i] = shape.fill } set_buffer(state.instance_buffers[.Model_Mat], &state.buf_matrices) set_buffer(state.instance_buffers[.Fill], &state.buf_colors) + set_buffer(state.instance_buffers[.Z_Offset], &state.buf_floats) for shape, i in slice { state.buf_colors[i] = shape.stroke @@ -468,7 +605,7 @@ render_queue :: proc() { state := g_renderer_state() // Update uniform data - set_buffer(state.ubo_globals, &state.globals, buffer = .Uniform) + set_buffer(state.ubos[.Globals], &state.globals, buffer = .Uniform) // Toggle the wireframe OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, state.wireframe ? OpenGL.LINE : OpenGL.FILL) @@ -481,3 +618,70 @@ render_queue :: proc() { OpenGL.UseProgram(0) } // }}} +// {{{ JFA +jfa :: proc() { + state := g_renderer_state() + dims := screen_dimensions() + + jfa1 := &state.framebuffers[.JFA_1] + jfa2 := &state.framebuffers[.JFA_2] + + OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, state.pass == .SDF ? 0 : jfa1.fbo) + clear_screen() + render_queue() + if state.pass == .SDF do return + + OpenGL.BindFramebuffer(OpenGL.FRAMEBUFFER, state.pass == .JFA_Seed ? 0 : jfa2.fbo) + clear_screen() + + OpenGL.UseProgram(state.jfa_seed_program.program) + OpenGL.BindVertexArray(state.jfa_seed_program.vao) + OpenGL.ActiveTexture(OpenGL.TEXTURE0) + OpenGL.BindTexture(OpenGL.TEXTURE_2D, jfa1.tex_color) + OpenGL.Uniform1i(OpenGL.GetUniformLocation(state.jfa_seed_program.program, "input_texture"), 0) + OpenGL.DrawElements( + OpenGL.TRIANGLE_FAN, + i32(state.rect_mesh.index_count), + OpenGL.UNSIGNED_INT, + nil, + ) + if state.pass == .JFA_Seed do return + + OpenGL.UseProgram(state.jfa_program.program) + OpenGL.BindVertexArray(state.jfa_program.vao) + OpenGL.ActiveTexture(OpenGL.TEXTURE0) + OpenGL.Uniform1i(OpenGL.GetUniformLocation(state.jfa_program.program, "input_texture"), 0) + + input_fbo := jfa2 + output_fbo := jfa1 + + uniforms: JFA_Uniforms = { + resolution = dims, + } + + passes := math.ceil(math.log2(math.max(dims.x, dims.y))) + + for i in 0 ..< passes { + OpenGL.BindFramebuffer( + OpenGL.FRAMEBUFFER, + state.pass == .JFA && i == passes - 1 ? 0 : output_fbo.fbo, + ) + clear_screen() + + uniforms.offset = math.pow(2, passes - i - 1) + set_buffer(state.ubos[.JFA], &uniforms, buffer = .Uniform) + + OpenGL.BindTexture(OpenGL.TEXTURE_2D, input_fbo.tex_color) + OpenGL.DrawElements( + OpenGL.TRIANGLE_FAN, + i32(state.rect_mesh.index_count), + OpenGL.UNSIGNED_INT, + nil, + ) + + temp := input_fbo + input_fbo = output_fbo + output_fbo = temp + } +} +// }}}