1
Fork 0

odin(sdl-opengl-rendering): implement JFA

This commit is contained in:
prescientmoon 2025-06-05 04:27:32 +02:00
parent 25be983558
commit b661791c73
Signed by: prescientmoon
SSH key fingerprint: SHA256:WFp/cO76nbarETAoQcQXuV+0h7XJsEsOCI0UsyPIy6U
12 changed files with 509 additions and 169 deletions

View file

@ -6,9 +6,9 @@ Methods to convert:
[x] unset_clip_rect [x] unset_clip_rect
[x] clear_background [x] clear_background
[x] draw_rectangle [x] draw_rectangle
[ ] draw_rectangle_lines [x] draw_rectangle_lines
[x] draw_circle [x] draw_circle
[ ] draw_circle_lines [x] draw_circle_lines
[x] draw_line [x] draw_line
[ ] draw_triangle [ ] draw_triangle
[ ] draw_circle_arc_lines_impl [ ] draw_circle_arc_lines_impl

View file

@ -18,6 +18,22 @@
"type": "github" "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": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1748693115, "lastModified": 1748693115,
@ -86,6 +102,7 @@
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"glsl_analyzer": "glsl_analyzer",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"odin": "odin", "odin": "odin",
"ols": "ols" "ols": "ols"

View file

@ -11,6 +11,9 @@
ols.inputs.nixpkgs.follows = "nixpkgs"; ols.inputs.nixpkgs.follows = "nixpkgs";
ols.inputs.flake-utils.follows = "flake-utils"; ols.inputs.flake-utils.follows = "flake-utils";
ols.inputs.odin.follows = "odin"; ols.inputs.odin.follows = "odin";
glsl_analyzer.url = "github:starlitcanopy/glsl_analyzer";
glsl_analyzer.flake = false;
}; };
outputs = outputs =
@ -26,6 +29,14 @@
]; ];
}; };
glsl_analyzer = pkgs.glsl_analyzer.overrideAttrs (_: {
src = inputs.glsl_analyzer;
nativeBuildInputs = [
pkgs.zig_0_14.hook
];
});
inherit (pkgs) lib; inherit (pkgs) lib;
in in
{ {
@ -42,6 +53,7 @@
pkgs.seer # Debugger GUI pkgs.seer # Debugger GUI
pkgs.valgrind # Detect memory leaks pkgs.valgrind # Detect memory leaks
pkgs.renderdoc # Graphics debugger pkgs.renderdoc # Graphics debugger
glsl_analyzer # GLSL language server
]; ];
buildInputs = [ buildInputs = [

View file

@ -1,3 +1,5 @@
// This file contains types/constants/functions that are already implemented
// in my proper projects.
package visuals package visuals
:: f32 :: f32
@ -7,6 +9,7 @@ package visuals
Mat3 :: matrix[3, 3] Mat3 :: matrix[3, 3]
Mat4 :: matrix[4, 4] Mat4 :: matrix[4, 4]
Color :: [4] Color :: [4]
Affine2 :: matrix[2, 3]
AABB :: struct { AABB :: struct {
top_left: ², top_left: ²,
@ -29,6 +32,7 @@ vec2_perp :: proc(v: ℝ²) -> ℝ² {
return {-v.y, v.x} return {-v.y, v.x}
} }
@(private = "file")
g_state: State g_state: State
g_renderer_state :: proc() -> ^State { g_renderer_state :: proc() -> ^State {
return &g_state return &g_state

View file

@ -11,91 +11,96 @@ render :: proc() {
state := g_renderer_state() state := g_renderer_state()
state.tick += 1 state.tick += 1
dims := screen_dimensions() // dims := screen_dimensions()
center := dims / 2 // center := dims / 2
draw_rect(²{30, 20}, ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4}) // draw_rect(²{30, 20}, ²{100, 200}, Shape_Options{fill = {1, 0, 0, 1}, z = 0.4})
draw_rect( // draw_rect(
10, // 10,
center + center * math.sin(f32(state.tick) / 60), // center + center * math.sin(f32(state.tick) / 60),
Shape_Options{fill = {0, 1, 0, 1}, z = 0.6}, // Shape_Options{fill = {0, 1, 0, 1}, z = 0.6},
) // )
draw_rect( // draw_rect(
²{1000, 800}, // ²{1000, 800},
center / 3, // center / 3,
Shape_Options{fill = {0, 1, 1, 1}, z = 0.5, stroke = {0, 0.5, 0.5, 1}, stroke_width = 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)) draw_rect(²{100, 100}, ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
for x in (0) ..< count { draw_rect(²{100, 400}, ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
for y in (0) ..< count { draw_rect(²{400, 100}, ²{100, 100}, Shape_Options{fill = {1, 0, 0, 1}})
i := x * count + y draw_circle(²{1200, 350}, 200, Shape_Options{fill = {1, 0, 0, 1}})
pos := jfa()
²{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()
} }
// }}} // }}}
// {{{ Main // {{{ Main
@ -130,6 +135,8 @@ main :: proc() {
switch ([^]u8)(event.text.text)[0] { switch ([^]u8)(event.text.text)[0] {
case 'w': case 'w':
g_renderer_state().wireframe = !g_renderer_state().wireframe g_renderer_state().wireframe = !g_renderer_state().wireframe
case 'p':
g_renderer_state().pass = Render_Pass((u8(g_renderer_state().pass) + 1) % 3)
} }
} }
} }

View file

@ -7,6 +7,13 @@ import "core:log"
import "vendor:OpenGL" import "vendor:OpenGL"
import "vendor:sdl3" import "vendor:sdl3"
// For debugging purposes, we allow drawing up to any given pass
Render_Pass :: enum {
SDF,
JFA_Seed,
JFA,
}
State :: struct { State :: struct {
window: ^sdl3.Window, window: ^sdl3.Window,
@ -19,31 +26,32 @@ State :: struct {
// GPU data // GPU data
rect_mesh: Mesh, rect_mesh: Mesh,
ubo_globals: UBO, ubos: [UBO_ID]UBO,
framebuffers: [Framebuffer_ID]FBO,
instance_buffers: [Instance_Param_Buf]u32,
// Programs // Programs
rect_program: Program, rect_program: Program,
circle_program: Program, circle_program: Program,
line_program: Program, line_program: Program,
rounded_line_program: Program, rounded_line_program: Program,
jfa_program: Program,
jfa_seed_program: Program,
// Instance buffers (CPU) // Instance buffers (CPU)
buf_matrices: [INSTANCES]Mat3, buf_matrices: [INSTANCES]Affine2,
buf_colors: [INSTANCES]Color, buf_colors: [INSTANCES]Color,
buf_lines: [INSTANCES][2]², buf_lines: [INSTANCES][2]²,
buf_floats: [INSTANCES], buf_floats: [INSTANCES],
buf_vecs: [INSTANCES]², buf_vecs: [INSTANCES]²,
// Instance buffers (GPU)
instance_buffers: [Instance_Param_Buf]u32,
// Flags // Flags
tick: u32, tick: u32,
wireframe: bool, wireframe: bool,
pass: Render_Pass,
globals: Global_Uniforms, globals: Global_Uniforms,
} }
// {{{ Screen dimensions // {{{ Screen dimensions
screen_dimensions :: proc() -> ² { screen_dimensions :: proc() -> ² {
w, h: i32 w, h: i32
@ -149,7 +157,7 @@ sdl_init :: proc() -> (ok: bool) {
(gl_ctx != nil) or_return (gl_ctx != nil) or_return
OpenGL.load_up_to(GL_MAJOR, GL_MINOR, sdl3.gl_set_proc_address) 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.DEPTH_TEST)
OpenGL.Enable(OpenGL.BLEND) OpenGL.Enable(OpenGL.BLEND)
OpenGL.BlendFunc(OpenGL.SRC_ALPHA, OpenGL.ONE_MINUS_SRC_ALPHA) OpenGL.BlendFunc(OpenGL.SRC_ALPHA, OpenGL.ONE_MINUS_SRC_ALPHA)
@ -158,56 +166,64 @@ sdl_init :: proc() -> (ok: bool) {
sdl_on_resize(screen_dimensions()) sdl_on_resize(screen_dimensions())
// }}} // }}}
// {{{ Initialize GPU buffers & programs // {{{ Initialize GPU buffers & programs
// Initialize GPU buffers // Initialize instance buffers
OpenGL.GenBuffers(len(state.instance_buffers), ([^]u32)(&state.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 // Initialize meshes
state.rect_mesh = create_mesh({{0, 0}, {1, 0}, {1, 1}, {0, 1}}, {0, 1, 2, 3}) state.rect_mesh = create_mesh({{0, 0}, {1, 0}, {1, 1}, {0, 1}}, {0, 1, 2, 3})
// Initialize programs // Initialize programs
state.rect_program = gen_program( state.rect_program = gen_program(
{ {
template = .SDF,
sdf_name = "sdf_rect", sdf_name = "sdf_rect",
sdf_args = {"v_center", "v_dimensions"}, sdf_args = {"v_center", "v_dimensions"},
params = {{buf = .Center, name = "center"}, {buf = .Dimensions, name = "dimensions"}}, params = {{buf = .Center, name = "center"}, {buf = .Dimensions, name = "dimensions"}},
id = 0,
}, },
) or_return ) or_return
state.circle_program = gen_program( state.circle_program = gen_program(
{ {
template = .SDF,
sdf_name = "sdf_circle", sdf_name = "sdf_circle",
sdf_args = {"v_center", "v_radius"}, sdf_args = {"v_center", "v_radius"},
params = {{buf = .Center, name = "center"}, {buf = .Radius, name = "radius"}}, params = {{buf = .Center, name = "center"}, {buf = .Radius, name = "radius"}},
id = 1,
}, },
) or_return ) or_return
state.rounded_line_program = gen_program( state.rounded_line_program = gen_program(
{ {
template = .SDF,
sdf_name = "sdf_line", sdf_name = "sdf_line",
sdf_args = {"v_line", "v_thickness"}, sdf_args = {"v_line", "v_thickness"},
params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}}, params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}},
id = 2,
}, },
) or_return ) or_return
state.rounded_line_program = gen_program( state.rounded_line_program = gen_program(
{ {
template = .SDF,
sdf_name = "sdf_rounded_line", sdf_name = "sdf_rounded_line",
sdf_args = {"v_line", "v_thickness"}, sdf_args = {"v_line", "v_thickness"},
params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}}, params = {{buf = .Line, name = "line"}, {buf = .Thickness, name = "thickness"}},
id = 3,
}, },
) or_return ) 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_rects = make([dynamic]Shape())
state.q_circles = make([dynamic]Shape(Circle2)) state.q_circles = make([dynamic]Shape(Circle2))
state.q_lines = make([dynamic]Shape(Line)) state.q_lines = make([dynamic]Shape(Line))
state.q_rounded_lines = make([dynamic]Shape(Rounded_Line)) state.q_rounded_lines = make([dynamic]Shape(Rounded_Line))
state.pass = .JFA
return true return true
} }
@ -216,6 +232,12 @@ sdl_init :: proc() -> (ok: bool) {
sdl_close :: proc() { sdl_close :: proc() {
state := g_renderer_state() state := g_renderer_state()
// TODO: perhaps perform some cleanup here?
for fb in state.framebuffers {
destroy_framebuffer(fb)
}
_ = sdl3.StopTextInput(state.window) _ = sdl3.StopTextInput(state.window)
sdl3.DestroyWindow(state.window) sdl3.DestroyWindow(state.window)
sdl3.Quit() sdl3.Quit()
@ -223,6 +245,7 @@ sdl_close :: proc() {
// }}} // }}}
// {{{ Resize // {{{ Resize
sdl_on_resize :: proc(dims: ²) { sdl_on_resize :: proc(dims: ²) {
state := g_renderer_state()
OpenGL.Viewport(0, 0, i32(dims.x), i32(dims.y)) OpenGL.Viewport(0, 0, i32(dims.x), i32(dims.y))
// odinfmt: disable // odinfmt: disable
@ -234,6 +257,11 @@ sdl_on_resize :: proc(dims: ℝ²) {
} }
// odinfmt: enable // 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)
}
} }
// }}} // }}}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}

View file

@ -1,9 +1,9 @@
#header #header
out vec4 FragColor; 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; mat4 u_viewport_matrix;
float u_aa_width; float u_aa_width;
}; };
@ -33,8 +33,8 @@ float sdf_rounded_line(vec2[2] line, float thickness, vec2 p) {
void main() { void main() {
// This function gets auto-generated to call the right sdf // This function gets auto-generated to call the right sdf
float dist = sdf(v_pos); float dist = sdf(v_pos);
float alpha = smoothstep(u_aa_width, -u_aa_width, dist); 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); float s_alpha = smoothstep(u_aa_width, -u_aa_width, abs(dist) - v_stroke_width);
if (alpha < 0.001 && s_alpha < 0.001) discard; if (alpha < 0.001 && s_alpha < 0.001) discard;

View file

@ -1,6 +1,6 @@
#header #header
layout (location = 0) in vec2 a_pos; layout(location = 0) in vec2 a_pos;
out vec2 v_pos; out vec2 v_pos;
layout(std140, binding = 0) uniform Globals { layout(std140, binding = 0) uniform Globals {
@ -11,10 +11,9 @@ layout(std140, binding = 0) uniform Globals {
#toplevelExtra #toplevelExtra
void main() { 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); gl_Position = vec4(pos.xyz, 1);
v_pos = (i_model_matrix * vec3(a_pos, 1)).xy;
#mainExtra #mainExtra
} }

View file

@ -2,6 +2,7 @@ package visuals
import "core:fmt" import "core:fmt"
import "core:log" import "core:log"
import "core:math"
import "core:math/linalg" import "core:math/linalg"
import "core:slice" import "core:slice"
import "core:strings" import "core:strings"
@ -12,7 +13,7 @@ Shape_Options :: struct {
z: , z: ,
fill: Color, fill: Color,
stroke: Color, stroke: Color,
stroke_width: f32, stroke_width: ,
} }
Shape :: struct(T: typeid) { Shape :: struct(T: typeid) {
@ -29,14 +30,14 @@ Line :: struct {
Rounded_Line :: distinct Line Rounded_Line :: distinct Line
// }}} // }}}
// {{{ Shape -> Transform // {{{ 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[0, 0] = rect.dimensions.x + rect.stroke_width * 2
mat[1, 1] = rect.dimensions.y + rect.stroke_width * 2 mat[1, 1] = rect.dimensions.y + rect.stroke_width * 2
mat[2].xy = rect.top_left - rect.stroke_width mat[2].xy = rect.top_left - rect.stroke_width
return mat 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 r := circle.radius + circle.stroke_width
mat[0, 0] = r * 2 mat[0, 0] = r * 2
mat[1, 1] = r * 2 mat[1, 1] = r * 2
@ -44,7 +45,7 @@ to_transform_circle :: proc(circle: Shape(Circle2)) -> (mat: Mat3) {
return mat return mat
} }
to_transform_line :: proc(line: Line) -> (mat: Mat3) { to_transform_line :: proc(line: Line) -> (mat: Affine2) {
dir := line.to - line.from dir := line.to - line.from
mat[0].xy = dir / 2 mat[0].xy = dir / 2
mat[1].xy = vec2_perp(linalg.normalize0(dir)) * line.thickness mat[1].xy = vec2_perp(linalg.normalize0(dir)) * line.thickness
@ -52,7 +53,7 @@ to_transform_line :: proc(line: Line) -> (mat: Mat3) {
return mat 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 dir := line.to - line.from
len := linalg.length(dir) // TODO: return if this is close to 0 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 :: 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 { Global_Uniforms :: struct {
viewport_matrix: Mat4, viewport_matrix: Mat4,
aaWidth: f32, 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 { Instance_Param_Buf :: enum {
Fill, Fill,
Stroke, Stroke,
Stroke_Width, Stroke_Width,
Z_Offset,
Model_Mat, Model_Mat,
Dimensions, Dimensions,
Center, Center,
@ -148,12 +166,13 @@ INSTANCE_PARAM_LOCATIONS: [Instance_Param_Buf]u32 = {
.Fill = 1, .Fill = 1,
.Stroke = 2, .Stroke = 2,
.Stroke_Width = 3, .Stroke_Width = 3,
.Model_Mat = 4, .Z_Offset = 4,
.Dimensions = 8, .Model_Mat = 5,
.Center = 7, .Center = 8,
.Radius = 8, .Dimensions = 9,
.Line = 7, .Radius = 9,
.Thickness = 9, .Line = 8,
.Thickness = 10,
} }
@(rodata) @(rodata)
@ -161,7 +180,8 @@ INSTANCE_PARAM_TYPE: [Instance_Param_Buf]string = {
.Fill = "vec4", .Fill = "vec4",
.Stroke = "vec4", .Stroke = "vec4",
.Stroke_Width = "float", .Stroke_Width = "float",
.Model_Mat = "mat3", .Z_Offset = "float",
.Model_Mat = "mat3x2",
.Dimensions = "vec2", .Dimensions = "vec2",
.Center = "vec2", .Center = "vec2",
.Radius = "float", .Radius = "float",
@ -174,7 +194,8 @@ INSTANCE_PARAM_DIMS: [Instance_Param_Buf][2]i32 = { // (rows, cols)
.Fill = {4, 1}, .Fill = {4, 1},
.Stroke = {4, 1}, .Stroke = {4, 1},
.Stroke_Width = {1, 1}, .Stroke_Width = {1, 1},
.Model_Mat = {3, 3}, .Z_Offset = {1, 1},
.Model_Mat = {2, 3},
.Dimensions = {2, 1}, .Dimensions = {2, 1},
.Center = {2, 1}, .Center = {2, 1},
.Radius = {1, 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 // Contains geometry data a shader can run on
Mesh :: struct { Mesh :: struct {
// Geometry data
vertex_ind_buffer: u32, vertex_ind_buffer: u32,
vertex_pos_buffer: u32, vertex_pos_buffer: u32,
index_count: , 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 :: struct {
program: u32, program: u32,
vao: u32, vao: u32,
} }
// }}} // }}}
// {{{ Create UBO // {{{ Meshes
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
create_mesh :: proc(vertices: []², indices: []u32) -> (out: Mesh) { create_mesh :: proc(vertices: []², indices: []u32) -> (out: Mesh) {
out.index_count = len(indices) out.index_count = len(indices)
@ -219,7 +229,93 @@ create_mesh :: proc(vertices: []ℝ², indices: []u32) -> (out: Mesh) {
return out 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 { Shader_Gen :: struct {
template: string, template: string,
header: string, header: string,
@ -227,6 +323,7 @@ Shader_Gen :: struct {
main_extra: string, main_extra: string,
} }
@(private = "file")
process_shader :: proc(gen: Shader_Gen) -> string { process_shader :: proc(gen: Shader_Gen) -> string {
s := gen.template s := gen.template
s, _ = strings.replace_all(s, "#header\n", gen.header, context.temp_allocator) s, _ = strings.replace_all(s, "#header\n", gen.header, context.temp_allocator)
@ -244,40 +341,74 @@ Instance_Param_Gen :: struct {
vert_only: bool, vert_only: bool,
} }
Shader_Opts :: struct { Program_Template :: enum {
params: []Instance_Param_Gen, SDF,
JFA,
// The name of the toplevel sdf function to be used for rendering JFA_Seed,
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,
} }
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) { 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 // Instance parameters passed to every shape
COMMON_INSTANCE_PARAMS: []Instance_Param_Gen : { @(static)
{buf = .Fill, name = "fill"}, @(rodata)
{buf = .Stroke, name = "stroke"}, COMMON_INSTANCE_PARAMS: [Program_Template][]Instance_Param_Gen = {
{buf = .Stroke_Width, name = "stroke_width"}, .SDF = {
{buf = .Model_Mat, name = "model_matrix", vert_only = true}, {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 := 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 v_shader, f_shader: string
// {{{ Vertex shader generation // {{{ Vertex shader generation
{ {
gen: Shader_Gen = { gen: Shader_Gen = {
template = #load("./shaders/vert.glsl"), template = VERT_TEMPLATE[opts.template],
header = fmt.tprintfln("#version 430\n#line 1 %v", opts.id), header = fmt.tprintfln("#version 430\n#line 1 %v", id),
} }
toplevel_extra, main_extra: strings.Builder toplevel_extra, main_extra: strings.Builder
@ -326,8 +457,8 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
// {{{ Fragment shader generation // {{{ Fragment shader generation
{ {
gen: Shader_Gen = { gen: Shader_Gen = {
template = #load("./shaders/frag.glsl"), template = FRAG_TEMPLATE[opts.template],
header = fmt.tprintfln("#version 430\n#line 1 %v", opts.id), header = fmt.tprintfln("#version 430\n#line 1 %v", id),
} }
toplevel_extra: strings.Builder toplevel_extra: strings.Builder
@ -348,12 +479,16 @@ gen_program :: proc(opts: Shader_Opts) -> (out: Program, ok: bool) {
) )
} }
strings.write_rune(&toplevel_extra, '\n') if opts.template == .SDF {
fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {") log.assert(len(opts.sdf_name) > 0, "Empty SDF function name")
fmt.sbprintf(&toplevel_extra, " return %v(", opts.sdf_name)
for a in opts.sdf_args do fmt.sbprintf(&toplevel_extra, "%v, ", a) strings.write_rune(&toplevel_extra, '\n')
fmt.sbprintfln(&toplevel_extra, "p);") fmt.sbprintln(&toplevel_extra, "float sdf(vec2 p) {")
fmt.sbprintln(&toplevel_extra, "}") 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) 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 // {{{ Render the entire queue
@(private = "file")
render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($T)) { render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($T)) {
OpenGL.UseProgram(program.program) OpenGL.UseProgram(program.program)
OpenGL.BindVertexArray(program.vao) OpenGL.BindVertexArray(program.vao)
@ -410,12 +546,13 @@ render_instanced :: proc(program: Program, mesh: Mesh, shapes: ^[dynamic]Shape($
for shape, i in slice { for shape, i in slice {
state.buf_matrices[i] = to_transform(shape) 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 state.buf_colors[i] = shape.fill
} }
set_buffer(state.instance_buffers[.Model_Mat], &state.buf_matrices) set_buffer(state.instance_buffers[.Model_Mat], &state.buf_matrices)
set_buffer(state.instance_buffers[.Fill], &state.buf_colors) set_buffer(state.instance_buffers[.Fill], &state.buf_colors)
set_buffer(state.instance_buffers[.Z_Offset], &state.buf_floats)
for shape, i in slice { for shape, i in slice {
state.buf_colors[i] = shape.stroke state.buf_colors[i] = shape.stroke
@ -468,7 +605,7 @@ render_queue :: proc() {
state := g_renderer_state() state := g_renderer_state()
// Update uniform data // Update uniform data
set_buffer(state.ubo_globals, &state.globals, buffer = .Uniform) set_buffer(state.ubos[.Globals], &state.globals, buffer = .Uniform)
// Toggle the wireframe // Toggle the wireframe
OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, state.wireframe ? OpenGL.LINE : OpenGL.FILL) OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, state.wireframe ? OpenGL.LINE : OpenGL.FILL)
@ -481,3 +618,70 @@ render_queue :: proc() {
OpenGL.UseProgram(0) 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
}
}
// }}}