diff --git a/flake.lock b/flake.lock index d8c38ed..18e0a87 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,28 @@ "type": "github" } }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1785090369, + "narHash": "sha256-m0pDuRJG7EDo9ri+4Ksu83VsI+PlxNC9lNBfydejce4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "624af665418d3c65d544145b4d34ad696439570e", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "fenix": "fenix", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable" } }, "rust-analyzer-src": { diff --git a/flake.nix b/flake.nix index 4e50d25..461afa6 100644 --- a/flake.nix +++ b/flake.nix @@ -7,14 +7,15 @@ url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; }; - outputs = { self, nixpkgs, flake-utils, fenix }: + outputs = { self, nixpkgs, nixpkgs-unstable, flake-utils, fenix }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; toolchain = fenix.packages.${system}.fromToolchainFile { file = ./rust-toolchain.toml; - sha256 = "sha256-gh/xTkxKHL4eiRXzWv8KP7vfjSk61Iq48x47BEDFgfk="; + sha256 = "sha256-A1abGIbOtcBSdrUMhDGrER3pRM1hQP4fp9gh3Y4PKc8="; }; in { @@ -24,7 +25,7 @@ toolchain clang llvmPackages.bintools - typst + nixpkgs-unstable.legacyPackages.${pkgs.system}.typst ]; LIBCLANG_PATH = pkgs.lib.makeLibraryPath [ pkgs.llvmPackages_latest.libclang.lib ]; RUSTFLAGS = (builtins.map (a: ''-L ${a}/lib'') []); diff --git a/static/infinite-vision/post.typ b/static/infinite-vision/post.typ deleted file mode 100644 index d055b30..0000000 --- a/static/infinite-vision/post.typ +++ /dev/null @@ -1,55 +0,0 @@ -#let post_slug = "dynamic-vision-shader" -#let post_preview_image = "image2.png" -#let post_summary = "Using prompt injection to extract Gemini's system prompt" -#let post_date = "2026-07-28" - -= Dynamic Vision - -== The problem -In 3d, objects occlude your vision, and you cannot see behind them. This isn't really implemented in most 2D games as it can be disorienting, confusing, and therefore unnecessary. - -For Orebit, disorientation is a mechanic. Careless piloting in unfamiliar territory may cost the player a collision with the terrain or hostiles. Lack of spatial awareness can make it hard to detect your neighbors, which is great for hiding from potential enemies. As much of the players time is underground, the claustrophobia induced by occluding vision is also useful. With this in mind, it adds to the immersion to not know what lies around corners, and dynamic 2D vision is necessary. - - -== Solution 1 -Lighting mechanics can simulate dynamic vision, using darkness to represent hidden areas. First, the whole world needs to be dark. Then, emit light where the player should be able to see. Objects with light occluders on cast shadows, which effectively blocks vision behind objects like terrain. - -In Godot, this is implemented by applying a black CanvasModulate node to a scene to create darkness. Objects with collision polygons can reuse their polygon in a light occluder. PointLights using a circular texture can emit light around the character. -// #image("assets/image.png") - -== Solution 1 Flaws -The world in Orebit is massive, which becomes a problem when trying to light the entire area. The radius of the circular texture used must be at least as large as the farthest point visible, which can be tens of thousands of pixels away. This means a texture tens of thousands of pixels squared (for the x and y axis) must be used, which severely impacts performance even on high end hardware. - -The solution to this is to scale up a lower resolution light texture, but this has its own issues. The texture is made of discrete pixels, each one black or white if it is occluded or not occluded. As these pixels scale the shadow edges go from smooth, high resolution edges to pixelated, jagged edges. The other problem with scaling light maps is that light clips through objects close to the light source. - - -== Solution 2 -Another way to understand the problem is to think of every vision occluding polygon as a set of vision occluding line segments ($overline(A B)$). Visible or not, each line segment occludes a specific shape: a quad (a shape with four vertices) composed of one point on each end of the line segment, and another point at each line end projected away from the character. - - -#import "@preview/cetz:0.5.2" -#import "@preview/cetz-plot:0.1.4" - -#cetz.canvas({ - import cetz.draw: * - - line((0, 0), (2, 1), (3, 4), (2, 5), (1, 4), (0, 4), (-1, 2), (0, 0)) -}) - - -$ -p_1 &= A\ -p_2 &= A + d tan^(-1)(A-C)\ -p_3 &= B\ -p_4 &= B + d tan^(-1)(A-C) -$ - - - -Solution 2 can be implemented in Godot using ImmediateMesh's for each line in the polygon, with two triangles composing each quad for each line in the occluders polygon. - -(Solution 1 overlaid on solution 2) -// #image("assets/image2.png") - - - diff --git a/static/infinite-vision/assets/image.png b/static/posts/dynamic-vision/image.png similarity index 100% rename from static/infinite-vision/assets/image.png rename to static/posts/dynamic-vision/image.png diff --git a/static/infinite-vision/assets/image2.png b/static/posts/dynamic-vision/image2.png similarity index 100% rename from static/infinite-vision/assets/image2.png rename to static/posts/dynamic-vision/image2.png diff --git a/static/posts/dynamic-vision/post.typ b/static/posts/dynamic-vision/post.typ new file mode 100644 index 0000000..1ba2d66 --- /dev/null +++ b/static/posts/dynamic-vision/post.typ @@ -0,0 +1,126 @@ +#let post_slug = "dynamic-vision-shader" +#let post_preview_image = "image2.png" +#let post_summary = "Using prompt injection to extract Gemini's system prompt" +#let post_date = "2026-07-28" + + +#import "@preview/cetz:0.5.2" +#import "@preview/cetz-plot:0.1.4" + + +#let stroke_color = white +#let canvas_style = ( + circle: (fill: stroke_color), + stroke: stroke_color, +) + + += Dynamic Vision + +== The problem +In 3d, objects occlude your vision, and you cannot see behind them. This isn't really implemented in most 2D games as it can be disorienting, confusing, and therefore unnecessary. + +For Orebit, disorientation is a mechanic. Careless piloting in unfamiliar territory may cost the player a collision with the terrain or hostiles. Lack of spatial awareness can make it hard to detect your neighbors, which is great for hiding from potential enemies. As much of the players time is underground, the claustrophobia induced by occluding vision is also useful. With this in mind, it adds to the immersion to not know what lies around corners, and dynamic 2D vision is necessary. + + +== Solution 1 +Lighting mechanics can simulate dynamic vision, using darkness to represent hidden areas. First, the whole world needs to be dark. Then, emit light where the player should be able to see. Objects with light occluders on cast shadows, which effectively blocks vision behind objects like terrain. + +In Godot, this is implemented by applying a black CanvasModulate node to a scene to create darkness. Objects with collision polygons can reuse their polygon in a light occluder. PointLights using a circular texture can emit light around the character. +#image("/static/posts/dynamic-vision/image.png") + +== Solution 1 Flaws +The world in Orebit is massive, which becomes a problem when trying to light the entire area. The radius of the circular texture used must be at least as large as the farthest point visible, which can be tens of thousands of pixels away. This means a texture tens of thousands of pixels squared (for the x and y axis) must be used, which severely impacts performance even on high end hardware. + +The solution to this is to scale up a lower resolution light texture, but this has its own issues. The texture is made of discrete pixels, each one black or white if it is occluded or not occluded. As these pixels scale the shadow edges go from smooth, high resolution edges to pixelated, jagged edges. The other problem with scaling light maps is that light clips through objects close to the light source. + + +== Solution 2 +Another way to understand the problem is to think of every vision-occluding polygon as a set of vision-occluding line segments, ie $overline(A B)$. + +#set text(fill: stroke_color) +#html.frame( + align(center, + cetz.canvas({ + import cetz.draw: * + set-style(..canvas_style) + circle((0, 6), radius: (0.1), name: "C") + line((1, 4), "C", (2, 5), stroke: (dash: "dashed")) + line((0, 2), (2, 2), (3, 4), (2, 5), (1, 4), (0, 4), (-1, 3), (0, 2)) + line((2,5), (1, 4), name: "AB", stroke: (paint: blue, thickness: 4pt)) + content("AB", $ overline(A B) $, anchor: "south-east", padding: .2) +}))) +#set text(fill: black) + +Each line segment occludes a quad (a shape with four vertices) composed of one point on each end of the line segment, and another point at each line end projected away from the character. + +$ +d & >> 1\ +p_1 &= A\ +p_2 &= C + d (A-C)\ +p_3 &= C + d (B-C)\ +p_4 &= B\ +$ + + +#set text(fill: stroke_color) +#html.frame( + align(center, + cetz.canvas({ + import cetz.draw: * + set-style(..canvas_style) + + circle((0, 6), radius: (0.1), name: "C") + content("C", $ C $, anchor: "east", padding: .2) + + circle((2, 5), radius: (0.1), name: "A") + content("A", $ p_1 $, anchor: "south-west", padding: .2) + + circle((1, 4), radius: (0.1), name: "B") + content("B", $ p_4 $, anchor: "north-east", padding: .2) + + circle((6, 3), radius: (0.1), name: "p2") + content("p2", $ p_2 $, anchor: "west", padding: .2) + + circle((3, 0), radius: (0.1), name: "p3") + content("p3", $ p_3 $, anchor: "north", padding: .2) + + line((0, 2), (2, 2), (3, 4), (2, 5), (1, 4), (0, 4), (-1, 3), (0, 2)) + // circle((2, 5), radius: (0.08), fill: black, name: "p1") + line((2, 5), (6, 3), (3, 0), (1, 4), + fill: rgb(50, 50, 255, 100), + stroke: 0pt + ) + line("A", "C", "B", stroke: (dash: "dashed")) + // content((0, 2), $ p_1 $, anchor: "south", padding: .2) +}))) +#set text(fill: black) + + + + + +Solution 2 can be implemented in Godot using ImmediateMesh's for each line in the polygon, with two triangles composing each quad for each line in the occluders polygon. Setting vertex color allows the shader to differentiate which vertices to project away from the character. + +To update the mesh each frame, a shader on the ImmediateMesh can take the line segment and live character position. Despite running on thousands of line segments, the ImmediateMesh and gpu-shader exist to efficiently solve problems like this. + +```glsl +shader_type canvas_item; +# use the global coordinate system +render_mode world_vertex_coords; + +# updated each physics frame +uniform vec2 character_position; + +void vertex() { + if (COLOR.r > 0.5) { + VERTEX = VERTEX + (1e6 * (VERTEX - character_position)); + } +} +``` + +(Solution 1 overlaid on solution 2) +#image("/static/posts/dynamic-vision/image2.png") + + + diff --git a/static/style.css b/static/style.css index 7a33762..5c2a27f 100644 --- a/static/style.css +++ b/static/style.css @@ -1,5 +1,5 @@ @font-face { - font-family: 'Ethnocentric'; + /* font-family: 'Ethnocentric'; */ src: url('/static/ethnocentric.otf') format('opentype'); font-weight: normal; font-style: normal; @@ -12,6 +12,15 @@ text-align: center; } +.post a { + text-decoration: underline; + color:orangered; +} + +.post * { + text-align: left; +} + #navbar { display: flex; background: #000; @@ -73,6 +82,21 @@ a { text-decoration: none; } +math * { + font-family: serif; +} + +.multiline-equation { + text-align: center; + margin-left: 50%; + margin-right: 50%; +} + +svg { + margin: auto; +} + + .hover-underline { position: relative; display: inline-block; @@ -114,15 +138,6 @@ a { margin-top: 3.5rem; } -.post a { - text-decoration: underline; - color:orangered; -} - -.post * { - text-align: left; -} - pre { overflow-x: auto; background-color: #000;