Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion art/stages/hidden-garden/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation with its figures removed. One chunk, `hg-0-0` (2160×720): the bench where Dexter sits at the start (x ≈ 190–320), the stone path and the timber jetty (path top y = 441), the end of the jetty where Dexter points (x ≈ 1290–1323), and one wrong patch in the pond's reflection below it – a rusty girder with a ladder that isn't there. Clean only.

**Layers** ([layers.json](layers.json), `python scripts/split_layers.py art/stages/hidden-garden`): [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). Everything above the path is one rear layer at scroll 1.0, and the foreground also scrolls at 1.0: rectangles and colours would tear the big tree or the reeds at another speed, so sky/far/mid parallax waits for hand-drawn masks.
**Layers** (#142): the model repainted the chunk as a flat-colour depth key, and the masks come from it ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll slower than the path (0.1, 0.25, 0.6, docs/04), and their plates are filled where the nearer layers hide them, so nothing tears open when the camera moves. The foreground scrolls at 1.2. [00 sky](layers/hg-0-0.00-sky.filled.png) · [01 far](layers/hg-0-0.01-far.filled.png) · [02 mid](layers/hg-0-0.02-mid.filled.png) · [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). The parallax at three camera positions: [parallax.png](images/hg-0-0-parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).

| | |
| --- | --- |
Expand Down
4 changes: 2 additions & 2 deletions art/stages/hidden-garden/boards.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Hidden Garden – stage chunks",
"readme": "The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation with its figures removed. One chunk, `hg-0-0` (2160×720): the bench where Dexter sits at the start (x ≈ 190–320), the stone path and the timber jetty (path top y = 441), the end of the jetty where Dexter points (x ≈ 1290–1323), and one wrong patch in the pond's reflection below it – a rusty girder with a ladder that isn't there. Clean only.\n\n**Layers** ([layers.json](layers.json), `python scripts/split_layers.py art/stages/hidden-garden`): [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). Everything above the path is one rear layer at scroll 1.0, and the foreground also scrolls at 1.0: rectangles and colours would tear the big tree or the reeds at another speed, so sky/far/mid parallax waits for hand-drawn masks.",
"readme": "The prologue, drawn from the approved [Hidden Garden concept](../../locations/hidden-garden-clean.md) as a flat side elevation with its figures removed. One chunk, `hg-0-0` (2160×720): the bench where Dexter sits at the start (x ≈ 190–320), the stone path and the timber jetty (path top y = 441), the end of the jetty where Dexter points (x ≈ 1290–1323), and one wrong patch in the pond's reflection below it – a rusty girder with a ladder that isn't there. Clean only.\n\n**Layers** (#142): the model repainted the chunk as a flat-colour depth key, and the masks come from it ([depth_layers.py](../../../scripts/depth_layers.py), [layers.json](layers.json)). Sky, far and mid scroll slower than the path (0.1, 0.25, 0.6, docs/04), and their plates are filled where the nearer layers hide them, so nothing tears open when the camera moves. The foreground scrolls at 1.2. [00 sky](layers/hg-0-0.00-sky.filled.png) · [01 far](layers/hg-0-0.01-far.filled.png) · [02 mid](layers/hg-0-0.02-mid.filled.png) · [03 rear](layers/hg-0-0.03-rear.png) · [04 water](layers/hg-0-0.04-water.png) · [05 play](layers/hg-0-0.05-play.png) · [07 foreground](layers/hg-0-0.07-foreground.png). The parallax at three camera positions: [parallax.png](images/hg-0-0-parallax.png) (`python scripts/compare.py parallax art/stages/hidden-garden`).",
"model": "gpt-image-2.5-sunburst",
"size": "2160x720",
"style": "FORMAT: a STAGE CONCEPT for a 2D side-scroller explored in all four directions - one wide 2:1 image in strict side view, drawn like an architect's ELEVATION: every path, wall, bed and building runs PARALLEL to the screen from left to right; depth comes only from layers stacked behind each other, never from things receding toward a vanishing point; no path, corridor or bed leads into the distance. The hero is small, about one ninth of the image height. Build real level structure into the scenery: a main route plus at least one higher branch and one lower branch or underground pocket, smooth hills and slopes (some steep enough to slide down), ledges with grabbable edges, something to climb (ladder, vines, scaffold, stairs), a dead end with a small reward glint. DEPTH in clear layers: hazy far background, mid-ground, the play layer, and a dark, out-of-focus foreground of leaves and grass partly covering the bottom edge and corners as if the camera sits behind foliage; reflections where there is water; a few falling leaves. LOOK: detailed hi-bit pixel art, fine 2-pixel grain, the warm palette and light of the style master. TONE: secret horror - calm and pretty first. No text, no logos, no UI.",
Expand All @@ -24,4 +24,4 @@
]
}
]
}
}
9 changes: 6 additions & 3 deletions art/stages/hidden-garden/export.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
{
"note": "Layers from split_layers.py (layers.json). Everything scrolls at 1.0 until sky/far/mid get hand-drawn masks.",
"note": "Layers from split_layers.py (layers.json) and depth_layers.py (#142). Sky, far and mid are drawn from their filled plates: the art's own pixels plus what the nearer layers hide, so they can scroll slower (docs/04) without holes. At rest the stack still rebuilds the art exactly (--verify).",
"chunks": {"hg-0-0": {"x": 0, "y": 0}},
"layers": {
"00-sky": {"file": "layers/{name}.00-sky.filled.png", "z": 0, "scroll": 0.1},
"01-far": {"file": "layers/{name}.01-far.filled.png", "z": 1, "scroll": 0.25},
"02-mid": {"file": "layers/{name}.02-mid.filled.png", "z": 2, "scroll": 0.6},
"03-rear": {"file": "layers/{name}.03-rear.png", "z": 3, "scroll": 1.0},
"04-water": {"file": "layers/{name}.04-water.png", "z": 4, "scroll": 1.0},
"05-play": {"file": "layers/{name}.05-play.png", "z": 5, "scroll": 1.0},
"07-foreground": {"file": "layers/{name}.07-foreground.png", "z": 7, "scroll": 1.0}
"07-foreground": {"file": "layers/{name}.07-foreground.png", "z": 7, "scroll": 1.2}
},
"states": {"clean": ["03-rear", "04-water", "05-play", "07-foreground"]},
"states": {"clean": ["00-sky", "01-far", "02-mid", "03-rear", "04-water", "05-play", "07-foreground"]},
"composed": {"clean": "images/{name}.png"}
}
2 changes: 1 addition & 1 deletion art/stages/hidden-garden/hg-0-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ Wide side-scrolling stage chunk at 3:1, drawn as an architect's elevation with e
- [hidden-garden-clean.png](../../locations/images/hidden-garden-clean.png)
- [style-master-preview.png](../../direction/images/style-master-preview.png)

Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-0-0.png` · rebuild: `python scripts/make_boards.py draw --config art/stages/hidden-garden/boards.json --redo hg-0-0`
Model `gpt-image-2.5-sunburst` · pixel factor 1 · source image `images/hg-0-0.png` · rebuild: `python scripts/make_boards.py draw hg-0-0 --redo --config art/stages/hidden-garden/boards.json`
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 13 additions & 1 deletion art/stages/hidden-garden/layers.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
{
"note": "Rules run in order; the last rule that claims a pixel wins. Everything above the path stays one rear layer (scroll 1.0): rectangles would tear the big tree from its trunk at another scroll speed, so sky/far/mid parallax waits for hand-drawn masks.",
"note": "Rules run in order; the last rule that claims a pixel wins. Sky, far and mid come from masks of the model's depth key (scripts/depth_layers.py, #142); the exact rectangles and colours for water, path and foreground follow and win.",
"chunks": {
"hg-0-0": {
"states": {
"clean": "images/hg-0-0.png"
},
"default": "03-rear",
"rules": [
{
"layer": "00-sky",
"alpha": "masks/hg-0-0.00-sky.png"
},
{
"layer": "01-far",
"alpha": "masks/hg-0-0.01-far.png"
},
{
"layer": "02-mid",
"alpha": "masks/hg-0-0.02-mid.png"
},
{
"layer": "04-water",
"rect": [
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified art/stages/hidden-garden/layers/hg-0-0.03-rear.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions docs/09-ART-PIPELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ What Codex's runs proved on Rusty Creek, generalised:
6. **Transition chunks** join different spaces – surface to underground, outside to the Mensa inside. They are generated with *both* neighbours as context and blend the two layer stacks over their height.
7. **Clean and anomaly share everything they can.** The anomaly state replaces only the layers that change (on Rusty Creek, only the structure layers). Masters are rebuilt from the plates and verified pixel-exact (`creek_layers.py`).

## Parallax layers from a depth key

Painted chunks are one picture. To give them depth without hand masks or redrawing each layer (which drifts), the model repaints the finished chunk as a **depth key**: the same shapes, one flat colour per depth band. The masks come from that key, and a second pass paints what each slow layer has behind the faster ones ([#142](https://github.com/THD-AI/WinCS/issues/142), first used on the Hidden Garden).

```mermaid
flowchart LR
A["Composed chunk<br/>images/&lt;chunk&gt;.png"] --> K["depth_layers.py key<br/>sunburst repaints it:<br/>sky cyan · far blue · mid green<br/>rear yellow · play red<br/>water navy · foreground magenta"]
K --> M["depth_layers.py masks<br/>snap to the 7 colours · vote specks away<br/>sky edge decided by the real pixel<br/>→ masks/&lt;chunk&gt;.00-sky / 01-far / 02-mid"]
M --> S["split_layers.py<br/>masks first, then the exact rules<br/>for path, water, foreground<br/>every pixel in one layer"]
S --> F["depth_layers.py fill<br/>mask = nearer layers below the skyline<br/>the model paints only that layer<br/>→ layers/&lt;chunk&gt;.0x.filled.png"]
F --> E["export.json · scroll 0.1 / 0.25 / 0.6 / 1.0 / 1.2"]
E --> V["export_stage.py --verify (exact at rest)<br/>compare.py parallax (three camera positions)"]
```

- **The key only says which band, never which pixel.** It drifts a few pixels at fine edges, so near the sky the real pixel decides: open sky is ≈ (40–60, 120–140, 240) and hazy far treetops ≈ (80, 120, 180), so "sky" means blue above 200. Path, water and foreground keep their exact rectangles and colour rules, which run after the masks and win.
- **Fills never change the picture at rest.** A fill is kept only under the nearer layers, and the layer's own pixels are copied from the art, so `--verify` still rebuilds the chunk exactly. The camera only ever reveals painted scenery.
- **Several chunks:** a plate at scroll *s* belongs at `chunk.x × 2160 × s`; lane B's loader places it at `chunk.x × 2160` today (commented on [#116](https://github.com/THD-AI/WinCS/pull/116)).

## Prompting lessons (23 Oct, all runs)

| What we tried | What happened | Rule now |
Expand Down
42 changes: 42 additions & 0 deletions scripts/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
with a 1 ft ruler (13 px, docs/04) and the documented height of each thing marked;
--atlas-dir tmp/atlas reads a review export instead of the game's atlases

python scripts/compare.py parallax art/stages/hidden-garden --state clean --at 0 440 880 --out …
the region's layers from export.json, drawn as the game draws them at each camera x (a layer at
scroll s sits at chunk.x * 2160 * s and moves s px per camera px), one 1280x720 view per row

Paths are relative to the repository. Images and crops are in their own pixels; `--width` sets the common width.
"""
import argparse
Expand Down Expand Up @@ -103,6 +107,36 @@ def lineup(specs: list[str], zoom: int = 3) -> tuple[Image.Image, list[str]]:
return out, report


VIEW = (1280, 720)


def parallax(region: Path, state: str, cameras: list[int]) -> Image.Image:
"""The stage at each camera x, every layer offset by its scroll factor; one view per row."""
config = json.loads((region / "export.json").read_text(encoding="utf-8"))
w, h = 2160, 720
rows = []
for cam in cameras:
view = Image.new("RGBA", VIEW, (0, 0, 0, 255))
for layer_id in config["states"][state]:
layer = config["layers"][layer_id]
for name, chunk in config["chunks"].items():
path = region / layer["file"].format(name=chunk.get("name", name))
if not path.exists():
continue
s = layer["scroll"]
x = round(chunk["x"] * w * s - cam * s)
plate = Image.open(path).convert("RGBA")
if x < 0: # alpha_composite needs a non-negative destination: crop the plate instead
plate, x = plate.crop((-x, 0, plate.width, plate.height)), 0
if x < VIEW[0] and plate.width:
view.alpha_composite(plate, (x, chunk["y"] * h))
rows.append(view)
out = Image.new("RGB", (VIEW[0], (VIEW[1] + 8) * len(rows) - 8), GRID)
for i, view in enumerate(rows):
out.paste(view.convert("RGB"), (0, i * (VIEW[1] + 8)))
return out


def main() -> None:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="mode", required=True)
Expand All @@ -116,12 +150,20 @@ def main() -> None:
two.add_argument("frames", nargs="+", help="atlas:frame, e.g. hero-move:walk-00")
two.add_argument("--out", type=Path, required=True)
two.add_argument("--atlas-dir", type=Path, help="read atlases from here (an export to tmp/ before approval)")
three = sub.add_parser("parallax", help="a stage region at several camera positions")
three.add_argument("region", type=Path)
three.add_argument("--state", default="clean")
three.add_argument("--at", type=int, nargs="+", default=[0, 440, 880], help="camera x positions")
three.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
if getattr(args, "atlas_dir", None):
global SPRITES
SPRITES = args.atlas_dir if args.atlas_dir.is_absolute() else ROOT / args.atlas_dir
if args.mode == "images":
out = images(args.ref, args.ours, args.width, args.grid)
elif args.mode == "parallax":
out = parallax(args.region if args.region.is_absolute() else ROOT / args.region, args.state, args.at)
out = out.quantize(256, dither=Image.Dither.NONE) # a review image, not art: keep it small in Git
else:
out, report = lineup(args.frames)
print("\n".join(report))
Expand Down
Loading
Loading