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] 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

View file

@ -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"

View file

@ -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 = [

View file

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

View file

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

View file

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

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

View file

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

View file

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