diff --git a/.agents/design/README.md b/.agents/design/README.md new file mode 100644 index 0000000..5c4fc09 --- /dev/null +++ b/.agents/design/README.md @@ -0,0 +1,32 @@ +# Blog design — agent rules + +Design and content rules for agents writing tinqs/blog posts. + +## Voice + +- **Confident, shipped.** Every feature is presented as built, complete, and working. Never "we plan to," "we're working on," "next we'll." +- **No evolution narrative.** Don't tell the "first we tried X, then we fixed Y" story. Present the final design as if it was the plan from day one. +- **External audience.** Readers are game developers and technical audiences outside Tinqs. No internal project names, no team org charts, no "Ozan decided" or "Uygar built." + +## What to never mention + +- **Asset pack names or vendors.** Polyperfect, Low Poly Ultimate Pack, Quaternius, Kevin Iglesias, etc. Say "our animal models" or nothing at all. +- **Unity import details.** FBX source format, `.anim` files, `.meta` clip ranges, `isleborn/` paths. +- **Internal tooling specifics.** `migrate_animals.py` internals, Blender pipeline details, repo paths. +- **Things that failed or were removed.** Failed migrations, broken assets, animals we deleted, bugs we shipped then fixed. If you must mention a bug, frame it as a design insight learned during development — never "we shipped this broken." +- **Roadmaps, tiers, trade-offs, future plans.** The post describes what exists. No "Tier A/B/C," no "recommended build order," no "what's next." + +## Structure + +- **Title:** technical, specific, bold. "How We Made 1,000 Animals Animate Without a Single Skeleton" not "Crowd Animation Update." +- **Opening:** state the problem (what stock Godot can't do), state our solution, give the numbers. +- **Body:** architecture, shader code, data flow, VRAM math, benchmarks. Ground everything in numbers. +- **Closing:** where to get it, what it drives (Ariki), related posts. No roadmap. + +## Numbers rule + +Every claim about performance, VRAM, or scale must cite a measured number. "60 FPS at 1,000 agents on M1 Pro" not "great performance." "1.6 MB per type" not "tiny VRAM." + +## Code + +Shader code and architecture diagrams are encouraged — this is a technical blog. But keep code blocks focused on the key insight, not the whole file. diff --git a/gpu-skinned-herds.html b/gpu-skinned-herds.html index 6c99e2a..2449704 100644 --- a/gpu-skinned-herds.html +++ b/gpu-skinned-herds.html @@ -357,7 +357,7 @@ NORMAL = normalize((skin * vec4(NORMAL, 0.0)).xyz);

What's driving it

In Ariki, the sim tracks animal migration across a 12km archipelago. AnimalHerdRenderer.cs groups sim ViewerState.animals by type, feeds world positions and yaw rotations to skinned_herd.gd — the reusable per-type herd backend. The herd bakes the palette once at setup, then set_positions() updates transforms each sim tick. set_clip_for_state() switches the active clip block in the custom data when the sim FSM changes state. set_speed_scale() adjusts the per-instance playback rate to match ground speed — feet stay planted.

The sim owns all behavior — 30 data-driven animals with per-animal senses, diet, combat stats, and FSM states (graze, drink, sleep, hunt, flee, scavenge, die). The client just renders. This is the same code in single-player and multiplayer — the sim is the host.

-

Bird flocks use the same system. BirdFlock.cs runs boid flocking on top of skinned_herd, sharing the palette with synchronized phases (airborne flapping in unison is intentional). 25 bird species migrated from the Low Poly Bird Ultimate Pack, each a single draw call.

+

Bird flocks use the same system. BirdFlock.cs runs boid flocking on top of skinned_herd, sharing the palette with synchronized phases (airborne flapping in unison is intentional). 25 bird species, each a single draw call.

Per-instance custom data means a walking Boar, a running Boar, an idle Boar, and an attacking Boar all share the same baked palette — they just point at different rows. The renderer groups by type, not by state. One palette, one draw call, any number of states.

Two bugs we shipped and fixed

The module had data-plane doctests from day one — round-trip pose get/set, dirty tracking, size clamping, AABB, column-major layout. All green. Then we put it on screen and two things were wrong.

@@ -369,7 +369,7 @@ NORMAL = normalize((skin * vec4(NORMAL, 0.0)).xyz);

Engine version: 4.6.5.

No C# wrapper is generated — instantiate from GDScript via ClassDB.instantiate() and call the bound methods. The binding surface is small and stable. See ariki-game/scenes/animals/skinned_herd.gd for the reference backend.

The production pipeline

-

The migrate_animals.py tool converts polyperfect FBX packs to game-ready GLBs — imports, cleans hierarchy, rebuilds named NLA clips from frame ranges, strips duplicate meshes, bakes into the flat assets/models/glbs/ directory. Each animal gets a catalog entry in animals_catalog.json with clip metadata, default state mapping, and an animSpeedRef for foot-sync.

+

The migrate_animals.py tool converts source FBX files to game-ready GLBs — imports, cleans hierarchy, rebuilds named NLA clips from frame ranges, strips duplicate meshes, bakes into the flat assets/models/glbs/ directory. Each animal gets a catalog entry in animals_catalog.json with clip metadata, default state mapping, and an animSpeedRef for foot-sync.

At runtime, AnimalHerdRenderer spawns one skinned_herd per animal type. The herd bakes the palette from the catalog GLB's clips. AnimalAnimationLogic maps sim FSM states to clip keywords (attack → "attack"/"bite", flee → "run"/"gallop", wander → "walk"). The renderer lerps positions between sim ticks for smooth motion and writes per-instance custom data each frame. Zero per-frame CPU on the animation path.

Where we stand vs the industry

The bone-matrix palette technique is the same architecture used by Assassin's Creed Unity, Total War: Warhammer, and Hitman for their crowd systems. We're using the same core idea, in a Godot fork, with smaller VRAM — our low-poly animals keep textures tiny.

diff --git a/posts/gpu-skinned-herds.md b/posts/gpu-skinned-herds.md index 143f23a..4d35a67 100644 --- a/posts/gpu-skinned-herds.md +++ b/posts/gpu-skinned-herds.md @@ -125,7 +125,7 @@ In [Ariki](https://www.arikigame.com), the sim tracks animal migration across a The sim owns all behavior — 30 data-driven animals with per-animal senses, diet, combat stats, and FSM states (graze, drink, sleep, hunt, flee, scavenge, die). The client just renders. This is the same code in single-player and multiplayer — the sim is the host. -Bird flocks use the same system. `BirdFlock.cs` runs boid flocking on top of `skinned_herd`, sharing the palette with synchronized phases (airborne flapping in unison is intentional). 25 bird species migrated from the Low Poly Bird Ultimate Pack, each a single draw call. +Bird flocks use the same system. `BirdFlock.cs` runs boid flocking on top of `skinned_herd`, sharing the palette with synchronized phases (airborne flapping in unison is intentional). 25 bird species, each a single draw call. Per-instance custom data means a walking Boar, a running Boar, an idle Boar, and an attacking Boar all share the same baked palette — they just point at different rows. The renderer groups by type, not by state. One palette, one draw call, any number of states. @@ -149,7 +149,7 @@ No C# wrapper is generated — instantiate from GDScript via `ClassDB.instantiat ## The production pipeline -The `migrate_animals.py` tool converts polyperfect FBX packs to game-ready GLBs — imports, cleans hierarchy, rebuilds named NLA clips from frame ranges, strips duplicate meshes, bakes into the flat `assets/models/glbs/` directory. Each animal gets a catalog entry in `animals_catalog.json` with clip metadata, default state mapping, and an `animSpeedRef` for foot-sync. +The `migrate_animals.py` tool converts source FBX files to game-ready GLBs — imports, cleans hierarchy, rebuilds named NLA clips from frame ranges, strips duplicate meshes, bakes into the flat `assets/models/glbs/` directory. Each animal gets a catalog entry in `animals_catalog.json` with clip metadata, default state mapping, and an `animSpeedRef` for foot-sync. At runtime, `AnimalHerdRenderer` spawns one `skinned_herd` per animal type. The herd bakes the palette from the catalog GLB's clips. `AnimalAnimationLogic` maps sim FSM states to clip keywords (attack → "attack"/"bite", flee → "run"/"gallop", wander → "walk"). The renderer lerps positions between sim ticks for smooth motion and writes per-instance custom data each frame. Zero per-frame CPU on the animation path.