diff --git a/odin/sdl-opengl-rendering/.envrc b/odin/sdl-opengl-rendering/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/odin/sdl-opengl-rendering/.envrc @@ -0,0 +1 @@ +use flake diff --git a/odin/sdl-opengl-rendering/.gitignore b/odin/sdl-opengl-rendering/.gitignore index a8a0dce..c136a62 100644 --- a/odin/sdl-opengl-rendering/.gitignore +++ b/odin/sdl-opengl-rendering/.gitignore @@ -1 +1,2 @@ *.bin +!.envrc diff --git a/odin/sdl-opengl-rendering/flake.lock b/odin/sdl-opengl-rendering/flake.lock index fcda77b..9a609a9 100644 --- a/odin/sdl-opengl-rendering/flake.lock +++ b/odin/sdl-opengl-rendering/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1738680400, - "narHash": "sha256-ooLh+XW8jfa+91F1nhf9OF7qhuA/y1ChLx6lXDNeY5U=", + "lastModified": 1748693115, + "narHash": "sha256-StSrWhklmDuXT93yc3GrTlb0cKSS0agTAxMGjLKAsY8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "799ba5bffed04ced7067a91798353d360788b30d", + "rev": "910796cabe436259a29a72e8d3f5e180fc6dfacc", "type": "github" }, "original": { @@ -34,10 +34,61 @@ "type": "github" } }, + "odin": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1747681085, + "narHash": "sha256-tG177/u/vDUnxXeIFjJWYTRSuvYRfSCDYv3tjgdf+Ec=", + "owner": "starlitcanopy", + "repo": "odin", + "rev": "612433442cc03297474a31d3d40fce74ce3f5331", + "type": "github" + }, + "original": { + "owner": "starlitcanopy", + "repo": "odin", + "type": "github" + } + }, + "ols": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ], + "odin": [ + "odin" + ] + }, + "locked": { + "lastModified": 1747682765, + "narHash": "sha256-uq7kh10bBIx/gnnYOUCO2YD5+4UaNeD1W6YSlrYtJyQ=", + "owner": "starlitcanopy", + "repo": "ols", + "rev": "81d3e8ecb1ade5c02c4769961027b3d86b7a6f94", + "type": "github" + }, + "original": { + "owner": "starlitcanopy", + "repo": "ols", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "odin": "odin", + "ols": "ols" } }, "systems": { diff --git a/odin/sdl-opengl-rendering/flake.nix b/odin/sdl-opengl-rendering/flake.nix index b358b50..18af8e8 100644 --- a/odin/sdl-opengl-rendering/flake.nix +++ b/odin/sdl-opengl-rendering/flake.nix @@ -2,74 +2,42 @@ inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; + + odin.url = "github:starlitcanopy/odin"; + odin.inputs.nixpkgs.follows = "nixpkgs"; + odin.inputs.flake-utils.follows = "flake-utils"; + + ols.url = "github:starlitcanopy/ols"; + ols.inputs.nixpkgs.follows = "nixpkgs"; + ols.inputs.flake-utils.follows = "flake-utils"; + ols.inputs.odin.follows = "odin"; }; outputs = inputs: - { - overlays.default = final: prev: { - # {{{ Odin - odin = prev.odin.overrideAttrs (_: { - version = "unstable-2025-03-28"; - src = final.fetchFromGitHub { - owner = "starlitcanopy"; - repo = "Odin"; - rev = "a1fc243f8df8b510e6de4f5e115fbfc09371cb9d"; - sha256 = "0fb8lk47nb7ln0skjn3lyfi499q3wlnzp6w3qc4wf4s5zj43d6zh"; - }; - }); - # }}} - # {{{ OLS - ols = prev.ols.overrideAttrs (_: { - version = "unstable-2025-03-12"; - src = final.fetchFromGitHub { - owner = "DanielGavin"; - repo = "ols"; - rev = "1e44e3d78ad8a74ef09c7f54a6f6d3f7df517f8e"; - sha256 = "16f7b8ijcaj5m2bdgbbl1q1mzgpgzzazrap2g17hkgy63aqq8qmf"; - }; - - installPhase = '' - runHook preInstall - - install -Dm755 ols odinfmt -t $out/bin/ - cp -r builtin $out/bin/ - wrapProgram $out/bin/ols --set-default ODIN_ROOT ${final.odin}/share - - runHook postInstall - ''; - }); - # }}} - }; - } - // inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) ( + inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) ( system: let - pkgs = inputs.nixpkgs.legacyPackages.${system}.extend inputs.self.overlays.default; - cross = import inputs.nixpkgs { - localSystem = system; - crossSystem.config = "x86_64-w64-mingw32"; + pkgs = import inputs.nixpkgs { + inherit system; + overlays = [ + inputs.odin.overlays.default + inputs.ols.overlays.default + ]; }; inherit (pkgs) lib; in { - packages = { - inherit (pkgs) odin; - gcc = cross.callPackage (import ./gcc.nix) { }; - libgl = cross.libGL; - }; - # {{{ Shell devShell = pkgs.mkShell rec { nativeBuildInputs = [ pkgs.pkg-config - pkgs.entr # File change detection pkgs.odin # Compiler pkgs.mold # Linker + pkgs.ols # Language server pkgs.just # Script runner pkgs.samply # Profiler - pkgs.ols # Language server pkgs.gdb # Debugger pkgs.seer # Debugger GUI pkgs.valgrind # Detect memory leaks diff --git a/odin/sdl-opengl-rendering/src/main.odin b/odin/sdl-opengl-rendering/src/main.odin index b4a7fa6..715fbf4 100644 --- a/odin/sdl-opengl-rendering/src/main.odin +++ b/odin/sdl-opengl-rendering/src/main.odin @@ -9,12 +9,16 @@ import "vendor:OpenGL" import "vendor:sdl3" State :: struct { - tick: u32, - window: ^sdl3.Window, - program: u32, - rect_vao: VAO, - circle_vao: VAO, - wireframe: bool, + tick: u32, + window: ^sdl3.Window, + rect_program: u32, + circle_program: u32, + line_program: u32, + rect_vao: VAO, + circle_vao: VAO, + wireframe: bool, + buf_matrices: [INSTANCES]Mat3, + buf_colors: [INSTANCES]Color, } // {{{ Initialization @@ -98,26 +102,23 @@ init :: proc() -> (state: State, ok: bool) { OpenGL.Enable(OpenGL.BLEND) OpenGL.BlendFunc(OpenGL.SRC_ALPHA, OpenGL.ONE_MINUS_SRC_ALPHA) - state.program = OpenGL.load_shaders_source( - #load("./vert.glsl"), - #load("./frag.glsl"), + state.rect_program = OpenGL.load_shaders_source( + #load("./shaders/vert.glsl"), + #load("./shaders/rect.frag.glsl"), + ) or_return + + state.circle_program = OpenGL.load_shaders_source( + #load("./shaders/vert.glsl"), + #load("./shaders/circle.frag.glsl"), + ) or_return + + state.line_program = OpenGL.load_shaders_source( + #load("./shaders/vert.glsl"), + #load("./shaders/line.frag.glsl"), ) or_return state.rect_vao = create_vao({{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}, {0, 1, 2, 3}) or_return - - CIRCLE_POINTS :: 127 - circle_vertices: [CIRCLE_POINTS + 1]ℝ² - circle_indices: [CIRCLE_POINTS + 2]u32 - - for i in 0 ..< CIRCLE_POINTS { - θ := ℝ(i) * 2 * math.π / CIRCLE_POINTS - circle_vertices[i + 1] = {math.cos(θ), math.sin(θ)} - circle_indices[i + 1] = u32(i + 1) - } - - circle_indices[CIRCLE_POINTS + 1] = circle_indices[1] - - state.circle_vao = create_vao(circle_vertices[:], circle_indices[:]) or_return + // state.circle_vao = create_vao({{-1, -1}, {1, -1}, {1, 1}, {-1, 1}}, {0, 1, 2, 3}) or_return init_command_queue() @@ -126,7 +127,9 @@ init :: proc() -> (state: State, ok: bool) { // }}} // {{{ Close close :: proc(state: State) { - OpenGL.DeleteProgram(state.program) + OpenGL.DeleteProgram(state.rect_program) + OpenGL.DeleteProgram(state.circle_program) + _ = sdl3.StopTextInput(state.window) sdl3.DestroyWindow(state.window) sdl3.Quit() @@ -136,16 +139,6 @@ close :: proc(state: State) { // {{{ Render render :: proc(state: ^State) { state.tick += 1 - OpenGL.Clear(OpenGL.COLOR_BUFFER_BIT | OpenGL.DEPTH_BUFFER_BIT) - - OpenGL.UseProgram(state.program) - defer OpenGL.UseProgram(0) - - if state.wireframe { - OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, OpenGL.LINE) - } else { - OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, OpenGL.FILL) - } draw_rect({-0.5, 0}, {0.75, 0.5}, {1, 0, 0, 1}, z = 0.5) draw_rect({0.5, 0.25}, {0.3, 0.5}, {0, 1, 0, 1}, z = -0.5) @@ -171,6 +164,9 @@ render :: proc(state: ^State) { } draw_circle({-0.25, -0.3}, 0.6, Color{0, 0, 0.5, 0.75}, z = -0.1) + draw_line({-0.25, 0}, {0.66, 0.4}, 0.01, Color{1, 1, 1, 1}, z = -0.5) + + clear_screen() render_queue(state) } // }}} diff --git a/odin/sdl-opengl-rendering/src/math.odin b/odin/sdl-opengl-rendering/src/math.odin new file mode 100644 index 0000000..ebe0c89 --- /dev/null +++ b/odin/sdl-opengl-rendering/src/math.odin @@ -0,0 +1,13 @@ +package visuals + +ℝ :: f32 +ℝ² :: [2]ℝ +ℝ³ :: [3]ℝ +ℕ :: uint +Mat3 :: matrix[3, 3]ℝ +Color :: [4]ℝ + +// Get a vector perpendicular to the given input +vec2_perp :: proc(v: ℝ²) -> ℝ² { + return {-v.y, v.x} +} diff --git a/odin/sdl-opengl-rendering/src/shaders/circle.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/circle.frag.glsl new file mode 100644 index 0000000..8614803 --- /dev/null +++ b/odin/sdl-opengl-rendering/src/shaders/circle.frag.glsl @@ -0,0 +1,13 @@ +#version 330 + +out vec4 FragColor; + +in vec4 vertexColor; +in vec2 vertexPos; + +void main() { + vec2 p = vertexPos; + float mask = p.x * p.x + p.y * p.y > 1 ? 0 : 1; + + FragColor = mask * vertexColor; +} diff --git a/odin/sdl-opengl-rendering/src/shaders/line.frag.glsl b/odin/sdl-opengl-rendering/src/shaders/line.frag.glsl new file mode 100644 index 0000000..65e4d01 --- /dev/null +++ b/odin/sdl-opengl-rendering/src/shaders/line.frag.glsl @@ -0,0 +1,18 @@ +#version 330 + +out vec4 FragColor; + +in vec4 vertexColor; +in vec2 vertexPos; + +float sdfLine(vec2 p) { + return abs(p.y) - 0.5; +} + +void main() { + float aWidth = 0.001; + float dist = sdfLine(vertexPos); + float alpha = smoothstep(aWidth, -aWidth, dist); + + FragColor = vec4(vertexColor.xyz, alpha * vertexColor.a); +} diff --git a/odin/sdl-opengl-rendering/src/frag.glsl b/odin/sdl-opengl-rendering/src/shaders/rect.frag.glsl similarity index 100% rename from odin/sdl-opengl-rendering/src/frag.glsl rename to odin/sdl-opengl-rendering/src/shaders/rect.frag.glsl diff --git a/odin/sdl-opengl-rendering/src/vert.glsl b/odin/sdl-opengl-rendering/src/shaders/vert.glsl similarity index 87% rename from odin/sdl-opengl-rendering/src/vert.glsl rename to odin/sdl-opengl-rendering/src/shaders/vert.glsl index a5761e3..65a450e 100644 --- a/odin/sdl-opengl-rendering/src/vert.glsl +++ b/odin/sdl-opengl-rendering/src/shaders/vert.glsl @@ -5,9 +5,11 @@ layout (location = 1) in vec4 instanceFill; layout (location = 2) in mat3 instanceMatrix; out vec4 vertexColor; +out vec2 vertexPos; void main() { vec3 pos = instanceMatrix * vec3(aPos.xy, 1); gl_Position = vec4(pos.xyz, 1); vertexColor = instanceFill; + vertexPos = aPos.xy; } diff --git a/odin/sdl-opengl-rendering/src/shape.odin b/odin/sdl-opengl-rendering/src/shape.odin index 34932e7..a08fce6 100644 --- a/odin/sdl-opengl-rendering/src/shape.odin +++ b/odin/sdl-opengl-rendering/src/shape.odin @@ -1,21 +1,15 @@ package visuals import "core:log" +import "core:math/linalg" import "vendor:OpenGL" -// {{{ Math constants -ℝ :: f32 -ℝ² :: [2]ℝ -ℝ³ :: [3]ℝ -ℕ :: uint -Mat3 :: matrix[3, 3]ℝ -Color :: [4]ℝ -// }}} // {{{ Command queues Shape :: struct { z: ℝ, fill: Color, } + Rect :: struct { using shape: Shape, top_left: ℝ², @@ -28,9 +22,17 @@ Circle :: struct { radius: ℝ, } +Line :: struct { + using shape: Shape, + from: ℝ², + to: ℝ², + thickness: ℝ, +} + Command_Queue :: struct { rects: [dynamic]Rect, circles: [dynamic]Circle, + lines: [dynamic]Line, } queue: ^Command_Queue @@ -38,7 +40,9 @@ queue: ^Command_Queue init_command_queue :: proc() { queue = new(Command_Queue) queue^ = Command_Queue { - rects = make([dynamic]Rect), + rects = make([dynamic]Rect), + circles = make([dynamic]Circle), + lines = make([dynamic]Line), } } @@ -71,6 +75,21 @@ draw_circle :: proc { draw_circle_struct, draw_circle_args, } + +draw_line_args :: proc(from, to: ℝ², thickness: ℝ, color: Color, z: ℝ = 0) { + draw_line_struct( + Line{from = from, to = to, thickness = thickness, shape = Shape{z = z, fill = color}}, + ) +} + +draw_line_struct :: proc(line: Line) { + append(&queue.lines, line) +} + +draw_line :: proc { + draw_line_struct, + draw_line_args, +} // }}} // {{{ VAO & consts @@ -151,12 +170,29 @@ create_vao :: proc(vertices: []ℝ², indices: []u32) -> (out: VAO, ok: bool) { return out, true } // }}} -// {{{ Set rect transforms -set_rect_transforms :: proc(vao: ^VAO, rects: []Rect) -> ℕ { - log.assert(len(rects) <= INSTANCES, "Attempting to send too many rects to the GPU") - matrices := new([INSTANCES]Mat3, context.temp_allocator) - fills := new([INSTANCES]Color, context.temp_allocator) +// {{{ Set transforms +// Commit the contents of `state.buf_matrices` and `state.buf_colors` to the +// GPU. +commit_buffers :: proc(state: ^State, vao: ^VAO) { + OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_mat_buffer) + OpenGL.BufferData( + OpenGL.ARRAY_BUFFER, + INSTANCES * size_of(Mat3), + &state.buf_matrices, + OpenGL.DYNAMIC_DRAW, + ) + OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_fill_buffer) + OpenGL.BufferData( + OpenGL.ARRAY_BUFFER, + INSTANCES * size_of(Color), + &state.buf_colors, + OpenGL.DYNAMIC_DRAW, + ) +} + +set_rect_transforms :: proc(state: ^State, vao: ^VAO, rects: []Rect) -> ℕ { + log.assert(len(rects) <= INSTANCES, "Attempting to send too many rects to the GPU") for rect, i in rects { // This matrix must transform the rect [-1, 1]² into the desired rect mat: Mat3 @@ -167,27 +203,15 @@ set_rect_transforms :: proc(vao: ^VAO, rects: []Rect) -> ℕ { mat[2].xy = center.xy mat[2].z = rect.z - matrices[i] = mat - fills[i] = rect.fill + state.buf_matrices[i] = mat + state.buf_colors[i] = rect.fill } - OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_mat_buffer) - OpenGL.BufferData( - OpenGL.ARRAY_BUFFER, - INSTANCES * size_of(Mat3), - matrices, - OpenGL.DYNAMIC_DRAW, - ) - OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_fill_buffer) - OpenGL.BufferData(OpenGL.ARRAY_BUFFER, INSTANCES * size_of(Color), fills, OpenGL.DYNAMIC_DRAW) - return len(rects) } -set_circle_transforms :: proc(vao: ^VAO, circles: []Circle) -> ℕ { +set_circle_transforms :: proc(state: ^State, vao: ^VAO, circles: []Circle) -> ℕ { log.assert(len(circles) <= INSTANCES, "Attempting to send too many circles to the GPU") - matrices := new([INSTANCES]Mat3, context.temp_allocator) - fills := new([INSTANCES]Color, context.temp_allocator) for circle, i in circles { mat: Mat3 @@ -197,22 +221,31 @@ set_circle_transforms :: proc(vao: ^VAO, circles: []Circle) -> ℕ { mat[2].xy = circle.center.xy mat[2].z = circle.z - matrices[i] = mat - fills[i] = circle.fill + state.buf_matrices[i] = mat + state.buf_colors[i] = circle.fill } - OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_mat_buffer) - OpenGL.BufferData( - OpenGL.ARRAY_BUFFER, - INSTANCES * size_of(Mat3), - matrices, - OpenGL.DYNAMIC_DRAW, - ) - OpenGL.BindBuffer(OpenGL.ARRAY_BUFFER, vao.instance_fill_buffer) - OpenGL.BufferData(OpenGL.ARRAY_BUFFER, INSTANCES * size_of(Color), fills, OpenGL.DYNAMIC_DRAW) - return len(circles) } + +set_line_transforms :: proc(state: ^State, vao: ^VAO, lines: []Line) -> ℕ { + log.assert(len(lines) <= INSTANCES, "Attempting to send too many lines to the GPU") + + for line, i in lines { + mat: Mat3 + + dir := line.to - line.from + mat[0].xy = dir / 2 + mat[1].xy = vec2_perp(linalg.normalize0(dir)) * line.thickness + mat[2].xy = (line.from + line.to) / 2 + mat[2].z = line.z + + state.buf_matrices[i] = mat + state.buf_colors[i] = line.fill + } + + return len(lines) +} // }}} // {{{ Render the entire queue draw_instances :: proc(vao: VAO, instances: ℕ) { @@ -226,26 +259,47 @@ draw_instances :: proc(vao: VAO, instances: ℕ) { ) } -render_queue :: proc(state: ^State) { - rect_steps := len(queue.rects) / INSTANCES +clear_screen :: proc() { + OpenGL.Clear(OpenGL.COLOR_BUFFER_BIT | OpenGL.DEPTH_BUFFER_BIT) +} +render_queue :: proc(state: ^State) { + if state.wireframe { + OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, OpenGL.LINE) + } else { + OpenGL.PolygonMode(OpenGL.FRONT_AND_BACK, OpenGL.FILL) + } + + OpenGL.UseProgram(state.rect_program) for i := 0; i < len(queue.rects); i += INSTANCES { slice := queue.rects[i:] if len(slice) > INSTANCES {slice = slice[:INSTANCES]} - instances := set_rect_transforms(&state.rect_vao, slice) + instances := set_rect_transforms(state, &state.rect_vao, slice) + commit_buffers(state, &state.rect_vao) draw_instances(state.rect_vao, instances) } - clear(&queue.rects) - circle_steps := len(queue.circles) / INSTANCES + OpenGL.UseProgram(state.circle_program) for i := 0; i < len(queue.circles); i += INSTANCES { slice := queue.circles[i:] if len(slice) > INSTANCES {slice = slice[:INSTANCES]} - instances := set_circle_transforms(&state.circle_vao, slice) - draw_instances(state.circle_vao, instances) + instances := set_circle_transforms(state, &state.rect_vao, slice) + commit_buffers(state, &state.rect_vao) + draw_instances(state.rect_vao, instances) } - clear(&queue.circles) + + OpenGL.UseProgram(state.line_program) + for i := 0; i < len(queue.lines); i += INSTANCES { + slice := queue.lines[i:] + if len(slice) > INSTANCES {slice = slice[:INSTANCES]} + instances := set_line_transforms(state, &state.rect_vao, slice) + commit_buffers(state, &state.rect_vao) + draw_instances(state.rect_vao, instances) + } + clear(&queue.lines) + + OpenGL.UseProgram(0) } // }}}