odin(sdl-opengl-rendering): implement JFA
This commit is contained in:
parent
25be983558
commit
b661791c73
12 changed files with 509 additions and 169 deletions
odin/sdl-opengl-rendering
|
@ -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
|
||||
|
|
17
odin/sdl-opengl-rendering/flake.lock
generated
17
odin/sdl-opengl-rendering/flake.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
|
16
odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl
Normal file
16
odin/sdl-opengl-rendering/src/shaders/jfa-seed.frag.glsl
Normal 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);
|
||||
}
|
39
odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl
Normal file
39
odin/sdl-opengl-rendering/src/shaders/jfa.frag.glsl
Normal 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;
|
||||
}
|
14
odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl
Normal file
14
odin/sdl-opengl-rendering/src/shaders/jfa.vert.glsl
Normal 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);
|
||||
}
|
|
@ -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;
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
// }}}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue