Repository navigation
examples: COND-07 condenser — the built-in PixelWater effect on real artwork #730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c853a71
bead7f4
8cde0cb
9962591
cf3ad28
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .labelle/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| # COND-07 condenser — contained water on reference-resolution artwork | ||
|
|
||
| Two condenser units show the engine's built-in `PixelWater` component, using one | ||
| reservoir prefab with independent fill levels. Drops create localized fading | ||
| ripples. Continuous waves and reflection drift are disabled. Mist is stationary | ||
| and baked into the machine artwork. | ||
|
|
||
|  | ||
|
|
||
| ## Run | ||
|
|
||
| Use CLI 1.67.0 or newer: | ||
|
|
||
| ```sh | ||
| labelle run | ||
| ``` | ||
|
|
||
| The example uses the in-tree assembler from this branch, core 1.32.0, engine | ||
| 2.22.0, gfx 1.36.0 and BGFX 0.20.0. A monorepo assembler can discover sibling | ||
| package checkouts instead of these pins; read its package-resolution output. | ||
|
|
||
| ## Pixel density | ||
|
|
||
| The original 620x330 cropped reference is kept in | ||
| `assets/reference/condenser-detail.gif`. The wider room image is not included. | ||
| Source: https://raw.githubusercontent.com/labelle-toolkit/labelle-bgfx/6a1ce3a24f42adf75b41dec81ec33921b6a27918/docs/issue-references/condenser-100/condenser-detail.gif | ||
|
|
||
| The old asset pipeline reduced the art to 103x55 and enlarged it sixfold. This | ||
| lost grain and irregular small edges because the painting has no uniform native | ||
| pixel grid. The new pipeline preserves source-resolution pixels and renders | ||
| sprites at scale 1. Two left border columns are cropped to keep the existing | ||
| 618x330 unit layout and drop alignment; the two-unit window is 1236x330. | ||
|
|
||
| A temporal median across the reference's 72 frames removes moving drops without | ||
| spatial downsampling. Static mist remains in this plate. The derivation asserts | ||
| that the layered composition reconstructs the median plate pixel-for-pixel. | ||
| This is preservation of the static plate, not an assertion that the reference's | ||
| animation or inferred occluder silhouettes were recovered exactly. | ||
|
|
||
| ## Artwork versus water grid | ||
|
|
||
| Artwork resolution and effect resolution are independent: | ||
|
|
||
| | Quantity | Texture/screen pixels | Water/effect coordinates | | ||
| |---|---|---| | ||
| | Unit canvas | 618x330 | 103x55 | | ||
| | Basin origin | 24, 288 | 4, 48 | | ||
| | Basin size | 558x36 | 93x6 | | ||
| | Drop sprite | 6x30 at scale 1 | 1x5 | | ||
| | Impact X positions within basin | 126/168/258/306/390 | 21/28/43/51/65 | | ||
|
|
||
| The shader still uses `logical_size: [93, 6]`, `grid_pixels: 1`. Its impacts remain | ||
| six screen pixels per effect cell while the underlying artwork keeps its finer | ||
| detail. Both the drawn reservoir texture and its standalone mask cover the same | ||
| 558x36 rectangle. Mask and reflection are standalone `.image` resources; drawn | ||
| sprites use an atlas. | ||
|
|
||
| Unit A is 5/6 full; unit B is half full. The six-row effect grid has limited fill | ||
| resolution. Drops use the live fill level to find the contact plane and call | ||
| `game.addWaterRipple` once per impact. The foreground cooler and frame cover | ||
| water and drops in front-to-back layer order. | ||
|
|
||
| ## Rebuild assets | ||
|
|
||
| Install Pillow and numpy, then from this example directory: | ||
|
|
||
| ```sh | ||
| python3 tools/derive_layers.py | ||
| python3 tools/pack_atlas.py | ||
| ``` | ||
|
|
||
| `derive_layers.py` creates the full-resolution layers and verifies their static | ||
| composition. `pack_atlas.py` crops the basin textures and rebuilds the atlas. | ||
| The reflection is authored from the coil band; hidden water under the cooler is | ||
| reconstructed. Rectangular occlusion boundaries are inherited from the asset | ||
| investigation in labelle-bgfx#106. They are assumptions, not original layered art. | ||
|
|
||
| ## Validation | ||
|
|
||
| The updated example builds and runs on Windows/Vulkan. Logs confirm both live | ||
| water instances at 0.8333 and 0.5, the water program initializes, and drops emit | ||
| ripples. `preview.png` is an engine screenshot of this full-resolution revision. | ||
| The earlier branch's Metal verification used the older downsampled assets. | ||
|
|
||
| `CONDENSER_WATER_OFF=1` disables water shading for a static-art control. Example: | ||
|
|
||
| ```sh | ||
| labelle run --timeout=5s --screenshot=condenser.png --after=2s | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a reader copies this advertised static-art control command, it never sets Useful? React with 👍 / 👎. |
||
| ``` | ||
|
|
||
| The main authoring files are `prefabs/reservoir.jsonc`, `scenes/main.jsonc`, | ||
| `scripts/playing/10_drops.zig` and `scripts/playing/30_water_probe.zig`. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| { | ||
| "frames": { | ||
| "machine_interior.png": { | ||
| "frame": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "rotated": false, | ||
| "trimmed": false, | ||
| "spriteSourceSize": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "sourceSize": { | ||
| "w": 618, | ||
| "h": 330 | ||
| } | ||
| }, | ||
| "cooler_foreground.png": { | ||
| "frame": { | ||
| "x": 0, | ||
| "y": 334, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "rotated": false, | ||
| "trimmed": false, | ||
| "spriteSourceSize": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "sourceSize": { | ||
| "w": 618, | ||
| "h": 330 | ||
| } | ||
| }, | ||
| "frame_foreground.png": { | ||
| "frame": { | ||
| "x": 0, | ||
| "y": 668, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "rotated": false, | ||
| "trimmed": false, | ||
| "spriteSourceSize": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 618, | ||
| "h": 330 | ||
| }, | ||
| "sourceSize": { | ||
| "w": 618, | ||
| "h": 330 | ||
| } | ||
| }, | ||
| "drop.png": { | ||
| "frame": { | ||
| "x": 0, | ||
| "y": 1002, | ||
| "w": 6, | ||
| "h": 30 | ||
| }, | ||
| "rotated": false, | ||
| "trimmed": false, | ||
| "spriteSourceSize": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 6, | ||
| "h": 30 | ||
| }, | ||
| "sourceSize": { | ||
| "w": 6, | ||
| "h": 30 | ||
| } | ||
| }, | ||
| "reservoir_art.png": { | ||
| "frame": { | ||
| "x": 0, | ||
| "y": 1036, | ||
| "w": 558, | ||
| "h": 36 | ||
| }, | ||
| "rotated": false, | ||
| "trimmed": false, | ||
| "spriteSourceSize": { | ||
| "x": 0, | ||
| "y": 0, | ||
| "w": 558, | ||
| "h": 36 | ||
| }, | ||
| "sourceSize": { | ||
| "w": 558, | ||
| "h": 36 | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| //! One condensation drop: it falls under gravity from the machine's coils, | ||
| //! contacts the reservoir's CURRENT water surface, emits exactly ONE ripple, | ||
| //! and waits before the next release. | ||
| //! | ||
| //! Geometry uses the original 103x55 EFFECT coordinate system; the script | ||
| //! multiplies by six for screen positions. Static artwork is independently | ||
| //! stored at reference resolution and drawn at scale 1. | ||
| pub const Drop = struct { | ||
| pub const save = @import("labelle-core").Saveable(.transient, @This(), .{}); | ||
| /// The reservoir (`Reservoir.unit`) this drop falls into. | ||
| unit: u32 = 0, | ||
| /// Impact X in RESERVOIR-LOCAL native px — what `addWaterRipple` takes. | ||
| /// The five measured emitters are local x 21, 28, 43, 51, 65. | ||
| local_x: f32 = 0, | ||
| /// Native canvas Y the drop is released from (the reference releases at | ||
| /// native y 2..5). | ||
| release_y: f32 = 2, | ||
| /// Downward acceleration in native px/s^2. The reference's fall is | ||
| /// accelerating, not grid-stepped. | ||
| gravity: f32 = 230, | ||
| /// Seconds between one impact and the next release. | ||
| interval: f32 = 1.6, | ||
| /// Start-of-run offset so the five emitters do not drip in unison. | ||
| phase: f32 = 0, | ||
|
|
||
| // ── Runtime ────────────────────────────────────────────────────── | ||
| /// Seconds since this drop was released (negative while waiting). | ||
| t: f32 = 0, | ||
| /// Set the instant the drop crosses the surface, so exactly one ripple | ||
| /// is emitted per fall no matter how many frames the crossing spans. | ||
| rippled: bool = false, | ||
| /// Whether `t` has been seeded from `phase` yet. | ||
| started: bool = false, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| //! Marker identifying one condenser unit's reservoir, so the drop script can | ||
| //! pair a falling drop with the `PixelWater` entity it should ripple. | ||
| //! | ||
| //! Deliberately NOT a copy of any water state: the authoritative fill level | ||
| //! lives on the built-in `PixelWater` component and is read back through | ||
| //! `game.pixelWater(entity)`, which is what makes the drops land on the | ||
| //! CURRENT surface rather than on a number the script happens to remember. | ||
| pub const Reservoir = struct { | ||
| pub const save = @import("labelle-core").Saveable(.transient, @This(), .{}); | ||
| /// Which condenser unit this basin belongs to (0 = left, 1 = right). | ||
| unit: u32 = 0, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // labelle.lock — resolved dependency versions | ||
| // Generated by labelle-cli. Commit this file to git. | ||
| .{ | ||
| .cli_version = "1.67.0", | ||
| .resolved = .{ | ||
| .core = .{ .version = "1.32.0" }, | ||
| .engine = .{ .version = "2.22.0" }, | ||
| .gfx = .{ .version = "1.36.0" }, | ||
| .labelle = .{ .version = "1.67.0" }, | ||
| .ecs = .{ .name = "zig_ecs" }, | ||
| }, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| // The COND-07 reservoir — a game-root prefab whose whole point is the | ||
| // built-in `PixelWater` block below. | ||
| // | ||
| // A prefab file is a flat components block (same shape as | ||
| // `examples/fantasy-dungeon/plugin/packs/props/prefabs/torch.jsonc`). The | ||
| // scene instantiates it with `{ "prefab": "reservoir", ... }` and patches | ||
| // individual fields at the instance site; unmentioned fields are inherited | ||
| // (engine RFC-OVERRIDES-MERGE-RULES, deep merge). | ||
| { | ||
| // Placed by the scene — (4, 48) native px within each condenser unit, | ||
| // times the x6 enlargement. | ||
| "Position": { "x": 24, "y": 288 }, | ||
|
|
||
| // The water sprite's OWN texture: the authored static reservoir art, | ||
| // cropped to the logical rectangle. `fs_pixel_water` samples it as | ||
| // `s_tex` and composites the water OVER it, so it is also exactly what | ||
| // you see if the water program ever degrades — which is why this | ||
| // example verifies the effect by DIFFING FRAMES, not by eyeballing a | ||
| // screenshot. | ||
| "Sprite": { | ||
| "sprite_name": "reservoir_art.png", | ||
| "pivot": "top_left", | ||
| "layer": "water", | ||
| "scale_x": 1.0, | ||
| "scale_y": 1.0 | ||
| }, | ||
|
|
||
| // ── The built-in engine component (engine v2.22.0) ──────────────── | ||
| // Authored here, in the prefab's JSON config. The JSONC apply path | ||
| // routes it through `game.addPixelWater`, which validates every field | ||
| // (a bad value reports `field '<name>' <reason>` instead of a GPU-side | ||
| // mystery), acquires the mask/reflection in the AssetCatalog and | ||
| // creates the gfx water instance. | ||
| "PixelWater": { | ||
| // AssetCatalog `.image` keys (declared in project.labelle), each | ||
| // spanning exactly the logical rectangle. | ||
| "mask": "reservoir_mask", | ||
| "reflection": "reservoir_reflection", | ||
|
|
||
| // MEASURED reservoir rect: x 4, y 48, w 93, h 6 on the 103x55 | ||
| // native canvas. `grid_pixels` is 1 because a native art pixel IS | ||
| // an effect cell here — the reference's "6x6" is the x6 screen | ||
| // enlargement, not the cell size. | ||
| "logical_size": [93, 6], | ||
| "grid_pixels": 1, | ||
|
|
||
| // Default fill. The basin is only 6 native rows tall, so the level | ||
| // has 6 quantisation steps; 5/6 keeps one dry row of headroom so a | ||
| // +-1 px wave/ripple crest is actually visible (at level 1.0 the | ||
| // shader clamps the displaced surface to the top edge and ripples | ||
| // would be invisible). Overridden per instance in the scene. | ||
| "water_level": 0.8333, | ||
|
|
||
| // Measured sRGB palette (assets branch README, "Water-specific"). | ||
| // The engine keeps these as authored hex and converts to linear | ||
| // exactly once, at the gfx seam. | ||
| "deep_color": "#0F1719", | ||
| "surface_color": "#7AA5BB", | ||
| "highlight_color": "#425F6C", | ||
|
|
||
| // Contained water rests between impacts: no travelling base wave. | ||
| "wave_amplitude_pixels": 0.0, | ||
| "wave_period_seconds": 3.0, | ||
| "waves_enabled": false, | ||
|
|
||
| // Keep the supplied reflection fixed; only drop ripples move water. | ||
| "reflection_opacity": 0.35, | ||
| "distortion_pixels": 0.0, | ||
|
|
||
| // Drop impacts: one ripple per drop, from `game.addWaterRipple`. | ||
| "ripple_duration_seconds": 0.9, | ||
| "ripple_radius_pixels": 14.0, | ||
| "ripple_strength_pixels": 1.0 | ||
| }, | ||
|
|
||
| // Marker so the drop script can find its reservoir (unit 0 = left | ||
| // condenser). Overridden to 1 on the second instance. | ||
| "Reservoir": { "unit": 0 } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: labelle-toolkit/labelle-assembler
Length of output: 3474
🏁 Script executed:
Repository: labelle-toolkit/labelle-assembler
Length of output: 8155
Set
CONDENSER_WATER_OFF=1for the control capture.30_water_probe.zigtreats the missing variable as false and only sets water levels to0.0when the variable is enabled. The current command therefore does not capture the static-art control. Use the syntax for the target shell:CONDENSER_WATER_OFF=1 labelle run ...in POSIX shells,set CONDENSER_WATER_OFF=1 && labelle run ...incmd.exe, or$env:CONDENSER_WATER_OFF='1'; labelle run ...in PowerShell.🤖 Prompt for AI Agents