Compare commits
@@ -0,0 +1,326 @@
|
|||||||
|
---
|
||||||
|
name: character-pipeline
|
||||||
|
description: Import, rig, stylize and animate an anime-styled character into Papaya-Shooter as a selectable skin — keeping the model's own skeleton, artist weights and separate body/cloth/hair meshes, with cloth and hair driven by the spring solver. Use when adding a new playable character, re-importing an existing one, debugging skinning/cloth/hair/animation problems on a character, or changing the cel-shaded look. Triggers on "add a character", "import a skin", "new playable model", "skirt clipping", "hair flailing", "T-posing", "character looks squashed".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Character pipeline
|
||||||
|
|
||||||
|
Turns a source model into a playable, cel-shaded, cloth-simulated character skin.
|
||||||
|
|
||||||
|
The whole design follows one principle, which is also what the Hoyoverse-class
|
||||||
|
anime pipelines (Genshin / Star Rail / Zenless Zone Zero) are built on:
|
||||||
|
|
||||||
|
> **The character is not one object. It is a body, a set of garments, and hair —
|
||||||
|
> authored separately, rigged separately, and moved by different systems.**
|
||||||
|
> The body is skinned and animated. The garments and hair are bone chains that
|
||||||
|
> the animation never touches; physics moves them. Keeping those separate is
|
||||||
|
> what makes the result read as an anime character instead of a mannequin in a
|
||||||
|
> painted-on costume.
|
||||||
|
|
||||||
|
Everything below exists to protect that separation.
|
||||||
|
|
||||||
|
## The one rule
|
||||||
|
|
||||||
|
**If a model arrives with a skeleton, that skeleton ships.** Its bones, its
|
||||||
|
artist-painted weights, its per-part meshes and its skirt/hair chains all
|
||||||
|
survive. Only the ANIMATION is moved onto it.
|
||||||
|
|
||||||
|
The old route (`strip_rig.py` → `autorig.py` → `merge_animations.py`) solved a
|
||||||
|
bone-*naming* problem by destroying the asset — 18 meshes became 1, 21 skirt
|
||||||
|
bones and ~50 hair bones became 0, and 16% of vertices ended up pulled by both
|
||||||
|
legs. `tools/rig_map.py` solves naming properly now. **Never reach for
|
||||||
|
`strip_rig.py` or `--rebind`** unless the model genuinely has no skeleton at all.
|
||||||
|
|
||||||
|
## Doing it
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From an already-rigged local model (the normal case)
|
||||||
|
python tools/pipeline.py --input assets/characters/incoming/<name>.glb --name <name> --rigged
|
||||||
|
|
||||||
|
# From a Sketchfab UID (needs SKETCHFAB_API_TOKEN)
|
||||||
|
python tools/pipeline.py --uid <uid> --name <name>
|
||||||
|
|
||||||
|
# From an unrigged mesh — auto-rigs, and accepts the quality loss
|
||||||
|
python tools/pipeline.py --input <mesh.glb> --name <name>
|
||||||
|
|
||||||
|
# Rigged (or auto-rigged) but with NO skirt/hair bones — grow them, or the
|
||||||
|
# costume is welded solid and the spring solver has nothing to simulate
|
||||||
|
python tools/pipeline.py --input <model.glb> --name <name> --rigged --grow-cloth
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, once, so Godot sees the new files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . --import
|
||||||
|
```
|
||||||
|
|
||||||
|
The result is `assets/characters/skins/<name>.glb` + `<name>.rig.json`, and a
|
||||||
|
registry entry in `skins.json` that `SkinManager` picks up with no code change.
|
||||||
|
|
||||||
|
Blender is required (`BLENDER_PATH`, or auto-found under
|
||||||
|
`C:\Program Files\Blender Foundation`). Godot lives at
|
||||||
|
`C:\Program Files\Godot\Godot_v4.7-stable_win64_console.exe`.
|
||||||
|
|
||||||
|
## The stages, and what each one protects
|
||||||
|
|
||||||
|
| Stage | Where | Protects |
|
||||||
|
|---|---|---|
|
||||||
|
| Fix unlit/emissive materials | `tools/gltf_fix.py` | Textures surviving import at all |
|
||||||
|
| Resolve bone ROLES, not names | `tools/rig_map.py` | The model's own skeleton |
|
||||||
|
| Rebuild parenting | `retarget.py::rebuild_hierarchy` | Limbs/cloth following the hips |
|
||||||
|
| Grow cloth chains (opt-in) | `tools/cloth_bones.py` | A costume that has no bones being able to move at all |
|
||||||
|
| Subdivide cloth panels | `retarget.py::subdivide_cloth_panels` | A skirt being able to bend at all |
|
||||||
|
| Retarget clips as rest-relative deltas | `retarget.py::retarget_clip` | Limbs not being twisted by foreign bone roll |
|
||||||
|
| Leave cosmetic bones unkeyed | `export_optimize_animation_keep_anim_armature=False` | Physics owning the cloth |
|
||||||
|
| Classify every SURFACE | `tools/surface_map.py` | The runtime never re-guessing what a surface is |
|
||||||
|
| Write the rig sidecar | `retarget.py::describe_rig` | The runtime never re-guessing anatomy |
|
||||||
|
| Cel look, per surface class | `LevelMaterials.apply_character_look` | Hair not reading as a solid dark cap |
|
||||||
|
| Cloth + hair | `characters/spring_bones.gd` | Clothes reading as clothes |
|
||||||
|
| Per-character judgement calls | `characters/tuning_store.gd` | Art direction not becoming another constant |
|
||||||
|
|
||||||
|
## The surface table
|
||||||
|
|
||||||
|
The sidecar carries a `surfaces` list saying what each mesh surface IS — `body`,
|
||||||
|
`cloth`, `hair`, `accessory`, or `linework` (the model's own ink shell, which is
|
||||||
|
not a surface of the character at all). It is keyed on the MATERIAL name, because
|
||||||
|
every character in this game arrives with its meshes called `Object_7` through
|
||||||
|
`Object_32` while material names survive the glTF round trip intact.
|
||||||
|
|
||||||
|
`SkinSurfaces` reads it and `apply_character_look` acts on it: hair takes a much
|
||||||
|
thinner outline than the body, cloth a heavier one and a crisper terminator,
|
||||||
|
accessories the heaviest. `SkinnedPlayerModel.surfaces_of(cls)` answers the
|
||||||
|
question for anything else that needs it.
|
||||||
|
|
||||||
|
Backfill a character that predates it, without re-importing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
blender --background --python tools/surface_map.py -- \
|
||||||
|
assets/characters/skins/<name>.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
New imports get it from `describe_rig`, built from the same chains the solver
|
||||||
|
uses, so the surface table and the cloth solver can never disagree about which
|
||||||
|
bones are a skirt.
|
||||||
|
|
||||||
|
## Per-character judgement
|
||||||
|
|
||||||
|
Anything derivable from the skeleton is derived. What is left is genuinely an
|
||||||
|
artist's call, and it lives in layered JSON rather than in a constant:
|
||||||
|
|
||||||
|
| File | Scope | Class |
|
||||||
|
|---|---|---|
|
||||||
|
| `assets/characters/weapon_holds.json` | character + weapon | `WeaponHoldTuning` |
|
||||||
|
| `assets/characters/rig_anchors.json` | character | `RigAnchors` |
|
||||||
|
|
||||||
|
Both layer `defaults` → `skins.<skin>._all` → `skins.<skin>.<subject>` through
|
||||||
|
`TuningStore`. An absent file means "use what the code derives", so nothing here
|
||||||
|
is required for the game to run. Adding a knob is adding a row to a `KNOBS` spec
|
||||||
|
table — the lab builds its whole UI from those.
|
||||||
|
|
||||||
|
`RigAnchors` is where "the grip sits here in the palm" lives. A hand bone's
|
||||||
|
origin is the WRIST; how far down the palm a grip belongs depends on the
|
||||||
|
character's hand and cannot be derived. It defaults to identity, and identity is
|
||||||
|
exactly the derived mount. Do **not** put a fixed rotation on the weapon mount
|
||||||
|
instead — a bone attachment is expressed in the BONE's axes, no two rigs agree on
|
||||||
|
those, and that constant is why the hand mount points were once wrong on every
|
||||||
|
character.
|
||||||
|
|
||||||
|
## The rig lab
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --path . res://debug/rig_lab.tscn
|
||||||
|
```
|
||||||
|
|
||||||
|
Pick a character, a weapon, a pose or a single clip. Drag sliders for the HOLD
|
||||||
|
(character + weapon) and the ANCHORS (character), and save.
|
||||||
|
|
||||||
|
**The HOLD is per pose.** The runtime blends between exactly two holds, on
|
||||||
|
`ads`, so the lab offers two: low ready and aiming. Selecting a pose rebuilds
|
||||||
|
the hold sliders to that pose's — you never see a control belonging to the pose
|
||||||
|
you are not adjusting. Running and Crouched use the low-ready hold, and the
|
||||||
|
heading says so rather than letting someone tune "Running" and wonder why
|
||||||
|
standing still changed. `pitch` exists at low ready only: down the sights the
|
||||||
|
muzzle follows the camera, so there is nothing there to tune, and a slider that
|
||||||
|
does nothing is worse than a missing one.
|
||||||
|
|
||||||
|
**The wrists turn the HAND, not the gun.** The weapon is a child of a
|
||||||
|
BoneAttachment3D on the trigger hand, so the two are welded by construction: a
|
||||||
|
wrist rotation swings the barrel off the aim line and takes every control that
|
||||||
|
could correct it along with it, which made the knob useless for aligning a hand
|
||||||
|
to a gun. `ShooterPoseModifier.wrist_comp_r` is the exact counter-rotation in
|
||||||
|
the hand's local frame, and `SkinnedPlayerModel._hold_weapon_still` applies it.
|
||||||
|
To rotate the GUN inside the hand instead, use the grip rotation in ANCHORS.
|
||||||
|
|
||||||
|
Knobs that describe the WEAPON and the hands on it — where each hand sits along
|
||||||
|
it and off its barrel line, the finger curls, the weapon size — are shared,
|
||||||
|
because shouldering a gun does not move the hand along it. Both wrists take
|
||||||
|
pitch, yaw and roll in the gun's own frame, per pose. Click a surface class
|
||||||
|
to isolate it — that is how the classifier gets checked: click `hair` and
|
||||||
|
anything else still standing was misclassified.
|
||||||
|
|
||||||
|
**Drag the coloured markers.** They ARE the anchor points the hands are solved
|
||||||
|
onto — red the trigger grip, green the support hand, blue the buttstock — and
|
||||||
|
the one under the mouse swells and draws through the body so it can be grabbed
|
||||||
|
where the hands would otherwise hide it.
|
||||||
|
|
||||||
|
Dragging an anchor is not the same as any slider:
|
||||||
|
|
||||||
|
| | moves |
|
||||||
|
|---|---|
|
||||||
|
| `grip_offset` (ANCHORS) | the GUN, inside the fist |
|
||||||
|
| `gun_fore` / `gun_stock` (HOLD) | the hands ALONG the weapon's own axis |
|
||||||
|
| dragging a marker | the anchor itself, in three dimensions |
|
||||||
|
|
||||||
|
That distinction was the gap. `gun_fore` and `gun_stock` are distances along the
|
||||||
|
barrel, so the two hand anchors could slide up and down the gun and nowhere
|
||||||
|
else — no use for a handguard below the bore, an angled foregrip, or a pistol
|
||||||
|
whose grip is nowhere near its barrel line. A drag writes `grip_shift` or
|
||||||
|
`fore_shift` in the gun's own across/up/along frame, so a sideways nudge stays
|
||||||
|
sideways as the weapon pitches; the buttstock marker writes the shoulder pocket
|
||||||
|
for whichever pose is showing.
|
||||||
|
|
||||||
|
The anchors sit on the shoulder, and the arm chasing them moves the shoulder, so
|
||||||
|
a drag settles at 0.77x-1.13x of the mouse. Small enough to ignore — you stop
|
||||||
|
when it looks right — and measured, not assumed.
|
||||||
|
|
||||||
|
## Read before you touch anything
|
||||||
|
|
||||||
|
Load the reference that matches what you are doing. They are short and each one
|
||||||
|
is a list of things that cost a debugging cycle to learn.
|
||||||
|
|
||||||
|
- **`references/failure-modes.md`** — **read this first.** Seven characters
|
||||||
|
shipped "All checks passed" and four were visibly broken. What each failure
|
||||||
|
was, why the suite missed it, and the rule that generalises it to any model.
|
||||||
|
- **`references/separation.md`** — body vs. garments vs. hair: what must stay
|
||||||
|
separate, how cloth chains are detected and classed, why cloth is never
|
||||||
|
skinned to a leg, and the ZZZ-convention mapping.
|
||||||
|
- **`references/growing-cloth-bones.md`** — what to do when the source has no
|
||||||
|
skirt or hair bones: how the chains are fitted to the geometry and re-weighted.
|
||||||
|
- **`references/rigging.md`** — role resolution, hierarchy rebuild, cloth panel
|
||||||
|
subdivision, twist bones, joint helpers, the retarget maths.
|
||||||
|
- **`references/cloth-and-hair.md`** — the position-based spring solver, its
|
||||||
|
colliders, per-class tuning, collision hulls, LOD and cost.
|
||||||
|
- **`references/stylization.md`** — cel shading, outlines, the imported
|
||||||
|
line-work trap, eyes, materials.
|
||||||
|
- **`references/verification.md`** — every measuring tool, what each one
|
||||||
|
actually measures, and the pose-reading trap that invalidated all of them
|
||||||
|
once. **Read this before trusting any measurement.**
|
||||||
|
|
||||||
|
## Non-negotiables
|
||||||
|
|
||||||
|
1. **Never join meshes.** Per-part meshes are how body, cloth and hair stay
|
||||||
|
separable — for materials, for the outline pass, and for the cloth solver's
|
||||||
|
hull extraction.
|
||||||
|
2. **Never key cosmetic bones.** If a clip has tracks on skirt/hair bones, the
|
||||||
|
AnimationPlayer overwrites the solver every frame and the cloth goes rigid.
|
||||||
|
3. **Never skin cloth to a leg.** A vertex weighted 0.9 to a thigh cannot be
|
||||||
|
moved by its own cloth bone, so the solver loses the authority to push it out
|
||||||
|
of that thigh — and the leg still overtakes it. There was a
|
||||||
|
`bind_cloth_to_legs()`; it is deleted, and the note above its grave in
|
||||||
|
`retarget.py` says why.
|
||||||
|
4. **Never run `SkinLegRepair` on authored weights.** It snaps weights and
|
||||||
|
deletes triangles. It exists only to undo auto-rigging. It is gated on
|
||||||
|
`weights_authored`, which is MEASURED, not assumed.
|
||||||
|
5. **Measure from inside the modifier pass.** See `references/verification.md`.
|
||||||
|
6. **Never assume an axis.** Up, forward and scale are all measurable from the
|
||||||
|
skeleton. Assuming +Z is up scaled three characters 7x and left them on their
|
||||||
|
backs — and the height check passed on every one of them, because the number
|
||||||
|
being normalised always comes out right whether or not it was the right
|
||||||
|
number. See `references/failure-modes.md`.
|
||||||
|
7. **Never look a bone up by name.** `tools/rig_map.py` resolves roles and writes
|
||||||
|
them to the sidecar so nothing downstream has to guess. Any hardcoded spelling
|
||||||
|
— `_find_bone(["RightHand", ...])`, a `thigh`/`shin` substring test — is a rig
|
||||||
|
this project has not met yet. Four characters could not hold a gun because of
|
||||||
|
exactly one such lookup.
|
||||||
|
|
||||||
|
## After importing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . -s res://debug/spawn_smoke_test.gd # 29 checks
|
||||||
|
godot --headless --path . -s res://debug/surface_class_check.gd # every surface classified
|
||||||
|
godot --headless --path . -s res://debug/character_picker_check.gd # the escape-menu roster
|
||||||
|
godot --headless --path . -s res://debug/rig_anchor_check.gd # anchors move the weapon
|
||||||
|
godot --headless --path . -s res://debug/cloth_clip_check.gd # leg-through-cloth
|
||||||
|
godot --headless --path . -s res://debug/cloth_settle_check.gd # idle stability
|
||||||
|
godot --headless --path . -s res://debug/cloth_perf_check.gd # ms per character
|
||||||
|
godot --headless --path . -s res://debug/travel_dir_check.gd # legs face travel
|
||||||
|
```
|
||||||
|
|
||||||
|
And LOOK at it, which is where four of the last five real defects were found:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --path . -s res://debug/roster_capture.gd -- <dir> # every character, one shot each
|
||||||
|
godot --path . -s res://debug/ui_capture.gd -- <dir> # every menu screen
|
||||||
|
godot --path . res://debug/rig_lab.tscn -- shot <png> <skin>
|
||||||
|
```
|
||||||
|
|
||||||
|
`surface_class_check` fails on any surface that falls through to the heuristic
|
||||||
|
rather than resolving from the table. That is deliberate: a model whose names
|
||||||
|
stopped matching still RENDERS, because the fallback catches it — it just
|
||||||
|
quietly loses its per-class art direction, which is exactly the kind of
|
||||||
|
regression nothing else would report.
|
||||||
|
|
||||||
|
What "good" looks like on Taila, for calibration:
|
||||||
|
|
||||||
|
| Measure | Good | Bad |
|
||||||
|
|---|---|---|
|
||||||
|
| Idle skirt movement | < 0.1 deg/frame | 0.5+, or never decaying |
|
||||||
|
| Leg inside cloth, idle/walk | < 25 mm | 100 mm |
|
||||||
|
| Leg inside cloth, run/slide/dash | ~95 mm *(current, unsolved)* | — |
|
||||||
|
| Solver cost | ~2.6 ms/character | 10 ms |
|
||||||
|
| Stride vs. travel direction | < 10° (except a capped sidestep) | 90° |
|
||||||
|
| Bind-pose AABB | tall on the hips→head axis, others < 2.5 m | tallest axis is depth |
|
||||||
|
|
||||||
|
## Characters currently shipping
|
||||||
|
|
||||||
|
The six in `skins.json`, with what `surface_class_check` reports:
|
||||||
|
|
||||||
|
| Skin | Source | Cloth chains | Surfaces |
|
||||||
|
|---|---|---|---|
|
||||||
|
| taila | rigged, Sketchfab CC-BY | 35 | 18 — body 6, cloth 7, hair 1, linework 4 |
|
||||||
|
| kiyoko | VRoid, CC-BY | 20 | 13 — body 8, cloth 3, hair 2 |
|
||||||
|
| aria | VRoid, CC-BY | 15 | 15 — body 9, cloth 4, hair 2 |
|
||||||
|
| momo | VRoid, CC-BY | 9 | 5 — body 2, cloth 1, hair 2 |
|
||||||
|
| miku | unrigged source, auto-rigged | 0 | 4 — body 3, hair 1 *(one mesh, four surfaces)* |
|
||||||
|
| mannequin | Quaternius CC0, from the animation library | 0 | 2 — body 2 |
|
||||||
|
|
||||||
|
## Known-unsolved
|
||||||
|
|
||||||
|
- **Peak cloth clipping** in a run, slide and dash sits at ~95 mm of thigh
|
||||||
|
inside the skirt. Idle, walk and fall are clean. The solver sees the contact
|
||||||
|
and pushes on it every iteration; the remaining gap is a standing fight
|
||||||
|
between the collision and the garment's own shape constraints.
|
||||||
|
- **No foot IK.** Feet do not plant on ground height, so stairs and uneven
|
||||||
|
ground read as sliding.
|
||||||
|
- **No strafe or backpedal clips.** Direction is conveyed by yawing the hips
|
||||||
|
(`SkinnedPlayerModel._update_travel`), which is capped, so a pure sidestep
|
||||||
|
still runs its legs ~40° off the direction of travel.
|
||||||
|
- **momo's idle pose is wrong** — arms overhead and a pinched waist. Every
|
||||||
|
assertion passes on her: she loads, animates, classifies and mounts a weapon.
|
||||||
|
It shows up only in `roster_capture`.
|
||||||
|
|
||||||
|
**Diagnosed, not fixed.** Her `driven_bones` contains `Root_001` through
|
||||||
|
`Root_007` — and those are her HAIR roots. The Godot surface dump shows
|
||||||
|
`Hair_A` dominated by `Root_001_001:3203`, `Root_007:3203`, `Root_005:2642`.
|
||||||
|
So the animation is keying bones that physics is supposed to own, which is
|
||||||
|
non-negotiable #2 being violated by the role resolver rather than by a clip.
|
||||||
|
The corroboration is in the surface table: her hair surfaces report only 1.8%
|
||||||
|
and 9.4% chain share, because most of their vertices belong to `Root_00N`,
|
||||||
|
which is in no chain at all.
|
||||||
|
|
||||||
|
`Root_00N` matches no COSMETIC stem, so `is_cosmetic` does not catch it and
|
||||||
|
nothing keeps it out of the driven set. Fixing it by adding "root" to the
|
||||||
|
stems would be wrong — a rig whose actual root is called `Root` would lose its
|
||||||
|
hips. The fix is structural: a bone whose geometry is dominated by a mesh
|
||||||
|
classified `hair` is a hair bone, whatever it is called. The surface table
|
||||||
|
now makes that answerable at build time, which it was not when this rig was
|
||||||
|
imported. Not attempted here — it needs a Blender re-run and re-verification
|
||||||
|
of all six characters.
|
||||||
|
|
||||||
|
Her `head` role is also wrong (`Unused_Noname_010`, when a real `Head` bone
|
||||||
|
exists and is in her spine chain), and her spine chain runs two junk bones
|
||||||
|
PAST the head. Probably the same import; worth fixing in the same pass.
|
||||||
|
- **A stray `Icosphere` ships inside every skin GLB** — 42 vertices, no parent,
|
||||||
|
no vertex groups. It rides in from the animation library. Harmless, and now
|
||||||
|
skipped by construction rather than by name in `surface_map`, but the export
|
||||||
|
should not be producing it.
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Cloth and hair — `characters/spring_bones.gd`
|
||||||
|
|
||||||
|
A position-based (PBD) solver over the rig's own cosmetic bones, the same shape
|
||||||
|
as Magica Cloth 2's BoneCloth, which is the tool the Hoyoverse-style pipelines
|
||||||
|
are built around.
|
||||||
|
|
||||||
|
## Why cloth cannot be solved with weights
|
||||||
|
|
||||||
|
Weight a skirt to the thighs → trousers. Weight it to the hips → a rigid bell.
|
||||||
|
Neither is cloth. A skirt reads as cloth because it **lags** — it keeps going
|
||||||
|
when the hips stop, swings out through a turn, floats on the way up through a
|
||||||
|
jump. That is inertia, and inertia has to be integrated, not skinned.
|
||||||
|
|
||||||
|
## The solve
|
||||||
|
|
||||||
|
Installed as a `SkeletonModifier3D` **after** `ShooterPoseModifier`, so it reacts
|
||||||
|
to the final body pose (animation + lean/slide/aim layer).
|
||||||
|
|
||||||
|
1. **Every JOINT is a particle.** Bone `i` spans particle `i` to `i+1`, so a
|
||||||
|
bone's HEAD can move. This is the load-bearing choice: a contact with no
|
||||||
|
rotational leverage — a thigh against the top of a panel — is resolved by the
|
||||||
|
whole panel moving, which is what a real skirt does.
|
||||||
|
2. **Predict** with inertia, gravity and wind, in the chain ANCHOR's frame, so
|
||||||
|
travelling at a steady speed excites nothing.
|
||||||
|
3. **Relax everything together**, Gauss-Seidel: cross-panel links, then per
|
||||||
|
chain — bone length, bend limit, backstop, colliders.
|
||||||
|
4. **Convert to rotations once**, at the end, and feed back the poses the
|
||||||
|
skeleton ACTUALLY got.
|
||||||
|
|
||||||
|
Order matters: **links first, chains second**, so the last thing to touch any
|
||||||
|
particle is its collision. With the chains first, every iteration ended by
|
||||||
|
pulling neighbouring panels back toward their rest separation — straight into the
|
||||||
|
leg just cleared. Measured on a slide: 93 mm in, 95 mm out; with the links off
|
||||||
|
entirely the same frame solved to 27 mm.
|
||||||
|
|
||||||
|
Then a short tail of **collision-and-length-only** passes, because the bend limit
|
||||||
|
and the backstop are shape constraints and re-imposing them after each collision
|
||||||
|
makes the two argue rather than converge.
|
||||||
|
|
||||||
|
## What the previous version did wrong
|
||||||
|
|
||||||
|
One spring per bone plus FOUR repair passes behind it, each writing bone poses the
|
||||||
|
next read back and partly undid — and the last (an ancestor "lift") wrote poses
|
||||||
|
never fed back into the spring state, so every frame began pulling against a pose
|
||||||
|
the springs did not know about. That feedback was the skirt "glitching out".
|
||||||
|
Three bolt-on stages (a bodily chain push, an ancestor lift, a drape weight) all
|
||||||
|
existed because a rotation-only solver cannot clear a contact near the head it
|
||||||
|
rotates about.
|
||||||
|
|
||||||
|
## Per-class tuning (`TUNING`)
|
||||||
|
|
||||||
|
| | w | zeta | gravity | wind | stray | hinge | bend |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| hair | 15 | 0.45 | 1.6 | 0.30 | 0.45 | 26° | 52° |
|
||||||
|
| skirt | 12 | 0.48 | 2.0 | 0.18 | 1.25 | 78° | 55° |
|
||||||
|
| cloth | 13 | 0.55 | 1.9 | 0.18 | 1.10 | 62° | 52° |
|
||||||
|
|
||||||
|
- **`hinge` vs `bend`** are the same constraint meaning different things. Between
|
||||||
|
segments it is BENDING stiffness (how sharply cloth may crease) and belongs
|
||||||
|
tight. At the root it is a HINGE at the waistband — a knee coming up to hip
|
||||||
|
height puts a thigh horizontally through where the front panel hangs, and the
|
||||||
|
panel must ride onto it, which is most of a right angle. Magica's skirt guide
|
||||||
|
suggests 20° at the root, but that is for a chain whose first bone is a large
|
||||||
|
share of the skirt; Taila's first segment is 49 mm of a 288 mm panel, so 20°
|
||||||
|
there moves the panel below it by **17 mm**.
|
||||||
|
- **`stray`** is Magica's "Max Distance" — how far a particle may end from where
|
||||||
|
the animation alone would have put it. Tight on hair (it has nothing to get out
|
||||||
|
of the way of; this is what stops flailing), loose on cloth (a skirt has to lie
|
||||||
|
along a thigh that has gone horizontal in a slide).
|
||||||
|
- **`gravity` is small** because the authored rest pose already has the garment
|
||||||
|
hanging. A constant force offsets the resting particle by `g/w²`, so a large
|
||||||
|
value pulls the hem below where it was modelled — into the thigh it then has to
|
||||||
|
be pushed out of.
|
||||||
|
|
||||||
|
## Colliders
|
||||||
|
|
||||||
|
Five capsules, measured from the mesh by `retarget.py::_leg_colliders`:
|
||||||
|
|
||||||
|
- A **waist LID** (`lid: true`) across the pelvis. Magica's skirt guide is blunt
|
||||||
|
about this: one big sphere at the waist "acts as a lid that prevents particles
|
||||||
|
in the skirt from slipping into the body". Leg capsules alone only stop cloth
|
||||||
|
going through a thigh; nothing stops a panel swinging INWARD into the pelvis.
|
||||||
|
- **Tapered** thigh and shin capsules — separate head and tail radii. A limb is
|
||||||
|
not a cylinder: Taila's thigh is ~0.11 m at the hip and ~0.06 m above the knee.
|
||||||
|
Fitted as a least-squares line through ten bands, dropping the contaminated end
|
||||||
|
bands, with twist children folded in.
|
||||||
|
- `from: 0.10` — the capsule starts BELOW the hip joint. The top of a thigh is
|
||||||
|
hip, buried inside the body the skirt hangs from.
|
||||||
|
|
||||||
|
**Per-point rest clearance.** Each (bone, collider) point's radius is capped to
|
||||||
|
just inside where that point rests, so the authored rest pose is a valid state.
|
||||||
|
Without it, cloth hanging against a thigh is shoved out and pulled back every
|
||||||
|
frame forever. The cap is PER POINT, not per bone — scaling a whole bone by its
|
||||||
|
worst point switches collision off for every panel whose top hangs against the
|
||||||
|
thigh, which is all the ones that matter.
|
||||||
|
|
||||||
|
## Collision hulls come from the MESH
|
||||||
|
|
||||||
|
`SkinnedPlayerModel._cloth_hulls`, at load time: every vertex a cloth bone
|
||||||
|
dominates, binned into a ~20 mm grid, outermost cells kept, capped at 14 points.
|
||||||
|
|
||||||
|
The sidecar's ten farthest-point samples describe a panel's corners and hem and
|
||||||
|
leave its MIDDLE unsampled — exactly where a thigh comes through. The solver
|
||||||
|
reported every contact resolved while 158 vertices sat 95 mm inside a leg.
|
||||||
|
|
||||||
|
## There is no drape term
|
||||||
|
|
||||||
|
"Cloth takes a share of the leg's motion before the solver runs" is a real
|
||||||
|
technique (Hoyoverse rigs carry a partial constraint from the leg onto the upper
|
||||||
|
skirt bones). It was here to move panels the old rotation-only solver could not.
|
||||||
|
With it against without, over the movement sweep:
|
||||||
|
|
||||||
|
```
|
||||||
|
run 101 -> 92 mm fall 82 -> 49 mm dash 136 -> 95 mm
|
||||||
|
idle after a dash 103 -> 20 mm
|
||||||
|
```
|
||||||
|
|
||||||
|
Worse in every state but a walk, and 20× worse in stability (0.48 vs 0.05
|
||||||
|
deg/frame at a dead idle) because its target sat inside the leg the collision was
|
||||||
|
pushing out of. **If you reintroduce it, the target must be collision-free
|
||||||
|
first.** A naive "seat the reference on the limb" pass was tried and destabilised
|
||||||
|
the reference chain, because a parent's seat rotation cascades into every child.
|
||||||
|
|
||||||
|
## Cost and LOD
|
||||||
|
|
||||||
|
~2.6 ms per character per frame at full quality, three quarters of it collision.
|
||||||
|
It was 10.9 ms before the inner loop stopped rebuilding every capsule and
|
||||||
|
reallocating the hull array for every (bone, collider, pass).
|
||||||
|
|
||||||
|
`SpringBones.lod` 0–3 drops passes then collision;
|
||||||
|
`SkinnedPlayerModel._update_cloth_lod` picks it from camera distance
|
||||||
|
(6 / 14 / 28 m) four times a second.
|
||||||
|
|
||||||
|
If you add cloth bones, re-run `debug/cloth_perf_check.gd`. The cost is the
|
||||||
|
product of joints × colliders × hull points × passes and all four are easy to
|
||||||
|
raise by accident.
|
||||||
|
|
||||||
|
## Hair specifically
|
||||||
|
|
||||||
|
- Hair DOES collide now. It used to be excluded because a collision push happened
|
||||||
|
after the integrator and so was deaf to spring tuning — long back hair got
|
||||||
|
shoved out of a thigh and hauled back at stride frequency, which was the blur.
|
||||||
|
Inside the relaxation there is no such fight.
|
||||||
|
- Hair chains are NOT linked sideways; linking them stiffens them into rope.
|
||||||
|
- Hair sits silent at idle (0.005 deg/frame). If it does not, something is
|
||||||
|
driving its target — that was the drape, and it is the first thing to suspect.
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# How imports fail, and why the checks did not catch it
|
||||||
|
|
||||||
|
Seven characters shipped with "All checks passed" and four of them were visibly
|
||||||
|
broken in game — lying on their backs, seven times too big, facing backwards,
|
||||||
|
holding a gun that floated near their chest. Nothing in the verification suite
|
||||||
|
was wrong. It just never asked the questions that mattered.
|
||||||
|
|
||||||
|
**The generalised lesson, which is the whole of this page:**
|
||||||
|
|
||||||
|
> The suite verified that the character was *well-formed* — skeleton attached,
|
||||||
|
> weights authored, clips non-frozen, cloth unkeyed. It never verified that the
|
||||||
|
> character was *correct*: the right size, the right way up, the right way round,
|
||||||
|
> and reachable by the runtime. Structural validity and usable output are
|
||||||
|
> different properties, and a pipeline that only checks the first will ship the
|
||||||
|
> second broken every time a source deviates from the one it was written against.
|
||||||
|
|
||||||
|
Every check below is cheap. None of them existed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Up is not always +Z — measure it, never assume it
|
||||||
|
|
||||||
|
**Symptom:** the character is enormous and lying on their back.
|
||||||
|
**Hit:** aria, momo, hikari. **Confidence: certain** — measured, not inferred.
|
||||||
|
|
||||||
|
`flatten_and_scale()` sets the character's real-world size with
|
||||||
|
|
||||||
|
```python
|
||||||
|
height = hi.z - lo.z # Blender Z is up
|
||||||
|
s = target_height / height
|
||||||
|
```
|
||||||
|
|
||||||
|
which is right for a model that arrives Z-up in Blender, and catastrophic for one
|
||||||
|
that does not. If the character is actually lying along Blender's Y, `hi.z - lo.z`
|
||||||
|
measures their **thickness** — about 0.25 m — so `s = 1.75 / 0.25 ≈ 7`. The model
|
||||||
|
is scaled seven-fold *and* left on its back. One wrong assumption, both symptoms.
|
||||||
|
|
||||||
|
The bind-pose bounding boxes say it plainly. A correct character is tall on Y and
|
||||||
|
narrow on X and Z:
|
||||||
|
|
||||||
|
| | X | Y | Z | |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| taila | 1.24 | **1.75** | 0.69 | correct |
|
||||||
|
| kiyoko | 1.50 | **1.75** | 0.32 | correct |
|
||||||
|
| mannequin | 1.86 | **1.75** | 0.35 | correct |
|
||||||
|
| aria | 6.12 | 1.75 | **7.48** | tall axis is Z — lying down, ~7x too big |
|
||||||
|
| momo | 6.89 | 1.75 | **7.95** | same |
|
||||||
|
| hikari | 6.23 | 1.75 | **13.01** | same, and worse |
|
||||||
|
|
||||||
|
The 1.75 lands on Y for everyone because the exporter maps Blender Z to glTF Y.
|
||||||
|
That is exactly what makes the bug invisible: **the number you normalised always
|
||||||
|
comes out right, whether or not it was the right number.** A check on "is the
|
||||||
|
height 1.75" passes on all seven of these.
|
||||||
|
|
||||||
|
**Rule:** derive the up axis from the SKELETON, and rotate the model upright
|
||||||
|
before scaling anything. `flatten_and_scale()` now does this.
|
||||||
|
|
||||||
|
**Measure it from the FEET to the HIPS, not from the hips to the head.** The head
|
||||||
|
is not a reliable landmark: the spine walk ends on whatever the last non-cosmetic
|
||||||
|
bone in the chain is, and on a rig with a facial skeleton that can be a bone
|
||||||
|
sitting BELOW the hips. Momo's did, so the first version of this fix stood her
|
||||||
|
neatly on her head — correct size, correct proportions, upside down. Feet cannot
|
||||||
|
be mistaken; they are the bottom of a standing character on every rig, and
|
||||||
|
`foot.L/R` have resolved on every source met so far.
|
||||||
|
|
||||||
|
**And compare it to WORLD up, not to the model's own proportions.** The obvious
|
||||||
|
test — "is the spine the longest axis of the bounding box?" — catches nothing
|
||||||
|
here, because a model rotated as a whole is internally consistent: aria's spine
|
||||||
|
*is* her longest axis, she is just lying down. She passes that test comfortably.
|
||||||
|
The question is whether the character stands up in the world the game runs in,
|
||||||
|
which means asserting the spine runs along Blender +Z, full stop.
|
||||||
|
|
||||||
|
Two more numbers are worth asserting for free: the other two extents should be
|
||||||
|
under about 2.5 m, and the height itself should land in a human range. Those
|
||||||
|
three together are what separated the four good characters from the three broken
|
||||||
|
ones on the first run.
|
||||||
|
|
||||||
|
**Where this comes from:** all three casualties have bone names like `Hips`,
|
||||||
|
`Left leg`, `Upper Chest`, `Breast_L` — a VRM that someone imported into Blender,
|
||||||
|
renamed, and re-exported. Kiyoko kept raw VRoid `J_Bip_*` names and was fine. A
|
||||||
|
**Blender round-trip can bake an axis rotation into the export**, and that family
|
||||||
|
of files is common on Sketchfab. Treat "the bone names have been humanised" as a
|
||||||
|
signal to check the axes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The runtime must look bones up by ROLE, not by name
|
||||||
|
|
||||||
|
**Symptom:** the gun is not in the hands — it floats near the chest, and can
|
||||||
|
point backwards. **Hit:** aria, momo, kiyoko, hikari.
|
||||||
|
**Confidence: certain** — measured.
|
||||||
|
|
||||||
|
`SkinnedPlayerModel.set_weapon()` finds the hand with
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var hand_idx := _find_bone(["RightHand", "Hand_R", "hand.R"])
|
||||||
|
```
|
||||||
|
|
||||||
|
Three hardcoded spellings. Against the shipped roster:
|
||||||
|
|
||||||
|
| Skin | `hand.R` resolved in the sidecar | matched by `_find_bone` |
|
||||||
|
|---|---|---|
|
||||||
|
| taila, miku, mannequin | `DEF-hand.R` | yes |
|
||||||
|
| kiyoko | `J_Bip_R_Hand` | **no** |
|
||||||
|
| aria, momo, hikari | `Right wrist` | **no** |
|
||||||
|
|
||||||
|
When it misses, `set_weapon` falls back to parenting the weapon to the model root
|
||||||
|
at a fixed chest-height offset. The gun is then not attached to the character at
|
||||||
|
all; it hangs in space near the torso and inherits none of the arm's motion.
|
||||||
|
|
||||||
|
This is the same class of bug as `verify_character.py` looking for legs by the
|
||||||
|
substrings `thigh`/`shin`. **`tools/rig_map.py` exists precisely so that nothing
|
||||||
|
downstream has to guess a bone name, and the resolved roles are written to
|
||||||
|
`<model>.rig.json` for exactly this purpose — but only some consumers read them.**
|
||||||
|
|
||||||
|
**Rule:** every bone lookup anywhere in the runtime or the tools goes through the
|
||||||
|
sidecar roles, with a name heuristic only as a last-resort fallback. Grep for
|
||||||
|
`find_bone`, `findn(`, and any tuple of bone-name spellings; each one is a rig
|
||||||
|
this project has not met yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Facing is inferred and never verified
|
||||||
|
|
||||||
|
**Symptom:** the character runs backwards. **Hit:** kiyoko.
|
||||||
|
**Confidence: probable** — the mechanism is understood, the specific cause is not
|
||||||
|
yet isolated.
|
||||||
|
|
||||||
|
Two independent things decide which way a character ends up pointing:
|
||||||
|
`retarget.py::facing_correction()` computes a yaw to align the character's rest
|
||||||
|
pose with the library's, and `SkinnedPlayerModel.facing_flip` then applies a
|
||||||
|
blanket 180° because "glTF forward is +Z; players face -Z". If the source already
|
||||||
|
faces the other way, the two compose to a character running backwards — and
|
||||||
|
nothing anywhere measures the finished result.
|
||||||
|
|
||||||
|
**Rule:** facing is measurable from the skeleton — the toes are forward of the
|
||||||
|
ankles. `flatten_and_scale()` now snaps that to Blender -Y, the convention the
|
||||||
|
runtime flip is built around, so every character leaves the pipeline pointing the
|
||||||
|
same way whatever the source did. Kiyoko was 180 degrees off and is now correct.
|
||||||
|
|
||||||
|
Snap to the nearest QUARTER TURN, not to the measured angle: a rest pose with the
|
||||||
|
feet slightly splayed is not a character who is 7 degrees turned, and correcting
|
||||||
|
it as one puts a permanent yaw on the whole skeleton.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Generated cloth chains must be validated against the geometry they drive
|
||||||
|
|
||||||
|
**Symptom:** hair stretches wildly during animation.
|
||||||
|
**Hit:** miku. **Confidence: probable.**
|
||||||
|
|
||||||
|
`tools/cloth_bones.py` grows chains for a costume that has none, then **clears
|
||||||
|
each vertex's existing body weights** and re-assigns it to the fitted chain,
|
||||||
|
keeping the original only over the first 22%. That is correct when the polyline
|
||||||
|
actually follows the clump. When it does not — a large or forked island, a
|
||||||
|
mis-picked root end — vertices land on a bone travelling somewhere else entirely,
|
||||||
|
and linear-blend skinning turns that into stretching.
|
||||||
|
|
||||||
|
The tool reports how many chains it grew. It never checks whether they *work*.
|
||||||
|
|
||||||
|
**Rule:** after growing chains, verify per vertex that its assigned bone stays
|
||||||
|
near it — pose the chain a few degrees and assert the vertex moves with its bone
|
||||||
|
rather than away from it. And never destroy the original weights without a
|
||||||
|
fallback: a generated chain should blend against the body weight it replaced, so
|
||||||
|
a bad fit degrades to "stiff" rather than to "torn".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. A weapon has to be scaled to the arm that holds it
|
||||||
|
|
||||||
|
**Symptom:** hands flat and open, both fists bunched together at the grip, the
|
||||||
|
stock nowhere near the shoulder. **Hit:** every model.
|
||||||
|
**Confidence: certain** — measured and fixed.
|
||||||
|
|
||||||
|
Three separate causes, all of them "a constant where a measurement belonged".
|
||||||
|
|
||||||
|
**The gun was mounted with a constant rotation.** `set_weapon` used
|
||||||
|
`rotation_degrees = (0, 90, -90)`. A bone attachment is expressed in the BONE's
|
||||||
|
axes and no two rigs agree on those, so one constant mounts the weapon
|
||||||
|
differently on every character. It never needed to be right: the pose layer aims
|
||||||
|
the gun by rotating the WRIST until the weapon's forward lies on the aim line, so
|
||||||
|
handing it the IDENTITY means "forward is the hand bone's -Z" — true by
|
||||||
|
construction on any rig — and the wrist absorbs the roll.
|
||||||
|
|
||||||
|
**The gun was full size on a stylised character.** The set is modelled at
|
||||||
|
real-world scale (an M4 is 0.84 m butt to muzzle); these characters have 0.47 m
|
||||||
|
arms against an adult 0.52. That puts the handguard 0.66 m from the support
|
||||||
|
shoulder — 0.2 m beyond reach — so a loop slid the support hand back down the
|
||||||
|
weapon until it fitted. On Taila a support offset authored at 0.35 m collapsed to
|
||||||
|
**0.083 m**: two fists together at the grip, which reads as a two-handed pistol
|
||||||
|
grip, not a rifle.
|
||||||
|
|
||||||
|
The fix is not a fixed scale factor. The binding constraint is the SUPPORT arm:
|
||||||
|
its hand must reach `stock + fore` in front of the pocket, from a shoulder half a
|
||||||
|
shoulder-width off the weapon axis. Solve that triangle for the largest gun whose
|
||||||
|
handguard still lands inside the arm's reach. Taila and Kiyoko come out at
|
||||||
|
different scales from the same code, both with the support hand at its full
|
||||||
|
authored handguard distance and no sliding at all.
|
||||||
|
|
||||||
|
Also give the slide-back loop a FLOOR. A slightly straight support arm looks far
|
||||||
|
better than no handguard hold.
|
||||||
|
|
||||||
|
**Nothing posed the fingers.** Every hand was flat and open — the single loudest
|
||||||
|
tell that a character is not really holding anything. Fingers are now closed by
|
||||||
|
the pose layer, using an axis derived from each hand's OWN anatomy in the rest
|
||||||
|
pose, because no two rigs agree on finger bone orientation:
|
||||||
|
|
||||||
|
```
|
||||||
|
along wrist -> middle knuckle the length of the hand
|
||||||
|
palm middle knuckle -> thumb tip across it; the thumb OPPOSES the
|
||||||
|
fingers, so it is on the palm side by
|
||||||
|
construction — a fact about hands, not
|
||||||
|
a rig convention
|
||||||
|
curl along x palm turning about this swings the fingers
|
||||||
|
into the palm, not sideways
|
||||||
|
```
|
||||||
|
|
||||||
|
The trigger hand's index finger gets a much shallower curl than the rest — it
|
||||||
|
lies along the trigger. Curling it with the others is what makes a character look
|
||||||
|
like they are squeezing a bar of soap.
|
||||||
|
|
||||||
|
Finger bones now resolve by role too (`rig_map.DIGITS`), across all three naming
|
||||||
|
families met so far: Rigify `DEF-f_index.01.L`, VRoid `J_Bip_L_Index1`, and
|
||||||
|
Blender-export `IndexFinger1_L`. Segments are ordered by DEPTH BELOW THE HAND,
|
||||||
|
not by the number in the name — the numbering is not consistent between families,
|
||||||
|
but the hierarchy always runs knuckle to fingertip.
|
||||||
|
|
||||||
|
**The support hand came out upside down**, because its orientation was built as
|
||||||
|
a shortest arc plus a constant twist: align the hand's forearm line to the barrel
|
||||||
|
(`Quaternion(fa_rest_dir, aim_dir)`), then add 0.5 rad of roll. A shortest arc
|
||||||
|
says NOTHING about roll — it is the minimal rotation between two directions — so
|
||||||
|
the entire roll came from that constant, and a constant is right only for the rig
|
||||||
|
it was tuned on.
|
||||||
|
|
||||||
|
Orienting a hand onto something it grips is a FRAME-TO-FRAME problem, and framing
|
||||||
|
it that way leaves nothing free to guess:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl axis must lie along the object's axis, or the fingers close ACROSS the
|
||||||
|
handguard instead of around it
|
||||||
|
palm must face the object — up, for a hand supporting from underneath
|
||||||
|
along falls out of the other two (palm x curl)
|
||||||
|
```
|
||||||
|
|
||||||
|
Map the hand's rest anatomical frame onto that target and the roll is determined,
|
||||||
|
not chosen. Verified on both the Rigify-named mannequin and the VRoid-named
|
||||||
|
Kiyoko: fingers wrap the handguard from below, over the top.
|
||||||
|
|
||||||
|
**Rule:** anything expressed as a constant in a rig's local frame — a mount
|
||||||
|
rotation, a grip offset, a curl axis, a weapon size, a wrist twist — is a guess
|
||||||
|
about one skeleton. Derive it from the skeleton, or hand it to a solver that
|
||||||
|
already knows the answer. And when a rotation needs a specific ROLL, never build
|
||||||
|
it from a shortest arc: that operator has no opinion about roll, so whatever you
|
||||||
|
add afterwards is doing all the work.
|
||||||
|
|
||||||
|
## 6. Some sources are not salvageable, and the gate should say so
|
||||||
|
|
||||||
|
**Hit:** hikari. She failed every way at once — stretched and warped, tiny, far
|
||||||
|
away, gun backwards. Her rig has been through at least two toolchains: her
|
||||||
|
cosmetic bones are zero-length terminators, she carried a second armature with
|
||||||
|
its own clips, and her feet and her spine disagree about which way is up, so the
|
||||||
|
stand-up correction cannot resolve her either. She is now REJECTED by the gate
|
||||||
|
and removed from the roster rather than shipped broken.
|
||||||
|
|
||||||
|
**Rule:** a source that fails several unrelated checks is not a tuning problem,
|
||||||
|
it is a bad file. Spend the effort on finding a cleaner source, not on repairing
|
||||||
|
this one — and make sure the gate blocks it, because the failure mode before this
|
||||||
|
work was that everything passed and the breakage was only visible in game.
|
||||||
|
|
||||||
|
The vetting snippet in `separation.md` catches most of these before download:
|
||||||
|
several meshes, 50+ joints, cosmetic bones present. Add a look for duplicate
|
||||||
|
armatures and for bone chains whose bones are all at the same position.
|
||||||
|
|
||||||
|
## 7. Known-unsolved, and honestly so
|
||||||
|
|
||||||
|
Taila's legs still clip through the front of her skirt in a run, slide and dash
|
||||||
|
(~95 mm). See `cloth-and-hair.md`. The solver sees the contact and pushes on it
|
||||||
|
every iteration; what remains is a standing fight between the collision and the
|
||||||
|
garment's shape constraints, not a missing check.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The check that would have caught most of this
|
||||||
|
|
||||||
|
One pass over the finished GLB, before it is ever registered:
|
||||||
|
|
||||||
|
```
|
||||||
|
POSTURE tallest axis of the bind-pose AABB == the hips->head axis,
|
||||||
|
and the other two are under ~2.5 m (catches #1)
|
||||||
|
SCALE height within a few percent of --height (catches #1)
|
||||||
|
FACING toes forward of ankles, along the world forward the game
|
||||||
|
expects, AFTER facing_flip (catches #3)
|
||||||
|
REACHABLE every role the runtime looks up — hands, head, spine — resolves
|
||||||
|
through the sidecar and not by name (catches #2)
|
||||||
|
CLOTH every chain has measurable extent, and its vertices track it (#4)
|
||||||
|
```
|
||||||
|
|
||||||
|
None of these needs Blender or the engine; the bind-pose AABB and the inverse
|
||||||
|
bind matrices in the GLB are enough for the first four.
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Growing cloth bones on a model that has none
|
||||||
|
|
||||||
|
`tools/cloth_bones.py`, opt-in via `pipeline.py --grow-cloth`.
|
||||||
|
|
||||||
|
## When you need it
|
||||||
|
|
||||||
|
The spring solver simulates cloth **bones**. A garment with none is welded to
|
||||||
|
whatever body bone it was weighted to, and no runtime setting changes that. Every
|
||||||
|
auto-rigged model is in this state, and so is many a "rigged" download whose
|
||||||
|
skeleton is body-only.
|
||||||
|
|
||||||
|
Check before importing (no Blender needed — see `separation.md`): if the joint
|
||||||
|
list has nothing matching `hair|skirt|tail|ribbon`, the costume will not move.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Runs on the **rigged** model, before the retarget.
|
||||||
|
|
||||||
|
1. **Finds the geometry by material slot.** A slot called `hair` is hair. The
|
||||||
|
artist already answered the question, and on a joined mesh — which is what the
|
||||||
|
auto-rig leaves behind — the material slot is the only separation left.
|
||||||
|
2. **Splits it into clumps.**
|
||||||
|
- *Hair*: connected islands over the mesh's own edges. A strand is a connected
|
||||||
|
piece of surface; clustering by position would merge two ponytails passing
|
||||||
|
near each other and split one that bends.
|
||||||
|
- *Skirt*: radial wedges around the body's up axis. A skirt is ONE connected
|
||||||
|
surface, so islands would return the whole thing as a single piece — which
|
||||||
|
is the bell-shaped failure. Wedges are the ZZZ-convention panel grid.
|
||||||
|
3. **Fits a polyline down each clump** by binning vertices by distance from the
|
||||||
|
anchored end and taking each bin's centroid, so the chain follows the piece's
|
||||||
|
own curve. A straight root-to-tip line cuts the corner on a bent ponytail and
|
||||||
|
every vertex on the outside of that bend ends up on a bone travelling the
|
||||||
|
wrong way.
|
||||||
|
4. **Builds a bone chain along the polyline**, parented to the body bone that was
|
||||||
|
already holding that geometry.
|
||||||
|
5. **Re-weights** each vertex onto the two bones either side of where it projects,
|
||||||
|
blended by how far between them it lands — while keeping the ORIGINAL body
|
||||||
|
weight over the first `ROOT_BLEND` (22%) of the chain, so the scalp stays on
|
||||||
|
the skull and the waistband stays on the hips.
|
||||||
|
|
||||||
|
Then `retarget.py::describe_rig` finds the chains by name exactly as it would an
|
||||||
|
artist's, and writes tips, hulls and neighbours to the sidecar.
|
||||||
|
|
||||||
|
## Tuning
|
||||||
|
|
||||||
|
```
|
||||||
|
--hair-segments 3 bones per hair strand
|
||||||
|
--skirt-segments 4 bones per skirt panel
|
||||||
|
--skirt-panels 12 radial panels; more = opens around a leg more smoothly
|
||||||
|
--classes hair,skirt which material names to look for
|
||||||
|
```
|
||||||
|
|
||||||
|
`MIN_STRAND_LENGTH` (60 mm) and `MIN_STRAND_VERTS` (12) drop fringe and
|
||||||
|
ornaments. Simulating those costs the same as a ponytail and only ever produces
|
||||||
|
jitter around the face.
|
||||||
|
|
||||||
|
## Worked result: Miku
|
||||||
|
|
||||||
|
```
|
||||||
|
19 chains, 57 bones grown from one `hair` material slot
|
||||||
|
sidecar: 0 cloth chains -> 19
|
||||||
|
idle stability: 0.007-0.018 deg/frame (quiet)
|
||||||
|
mesh intact, no tearing (debug/character_look_capture.gd)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limits
|
||||||
|
|
||||||
|
- **It cannot find a garment that shares a material with the body.** Miku's skirt
|
||||||
|
is on her `body` slot, so she got hair chains and no skirt. Splitting by
|
||||||
|
geometry rather than by material would be the next step.
|
||||||
|
- **It does not set `weights_authored`.** An auto-rigged model still gets the
|
||||||
|
destructive load-time `SkinLegRepair`. That repair only touches cross-leg
|
||||||
|
vertices, so it leaves hair alone, but it is worth knowing.
|
||||||
|
- **Grown chains are a fallback, not a substitute for a rigged source.** They
|
||||||
|
follow the geometry, but an artist's chains carry intent — where a panel should
|
||||||
|
split, which strands move together — that no fit recovers.
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Rigging and retargeting
|
||||||
|
|
||||||
|
## Roles, not names
|
||||||
|
|
||||||
|
`tools/rig_map.py` resolves a skeleton to ROLES — `hips`, `spine[]`, `neck`,
|
||||||
|
`head`, and `limb[(role, side)]` for `thigh/shin/foot/toe/shoulder/upper_arm/
|
||||||
|
forearm/hand`. Matching is by whole tokens plus anatomy (chain length, position,
|
||||||
|
which bone is a child of which), so a Rigify `DEF-thigh.L`, a Mixamo
|
||||||
|
`mixamorig:LeftUpLeg` and a bespoke `Bip01_L_Thigh` all land on the same role.
|
||||||
|
|
||||||
|
This is what removed the need to destroy foreign skeletons. `roles.missing_core()`
|
||||||
|
is the gate: if the core roles cannot be found the pipeline stops rather than
|
||||||
|
guessing.
|
||||||
|
|
||||||
|
The resolved roles are written to `<model>.rig.json` and read at runtime by
|
||||||
|
`ShooterPoseModifier._resolve`, which aliases its library-flavoured names
|
||||||
|
(`DEF-hips`, `DEF-spine.001`…) onto whatever this rig calls them. Taila's hips
|
||||||
|
are `DEF-spine`, her head is `DEF-spine.006`, and she has **no bone with "neck"
|
||||||
|
in its name at all** — unresolved, every lean, aim pitch and slide head-lift
|
||||||
|
silently did nothing.
|
||||||
|
|
||||||
|
### Names lie. Anatomy does not.
|
||||||
|
|
||||||
|
Four sources that were not authored against the library's spelling each broke
|
||||||
|
role resolution in a different way. All four fixes are in; the lesson is that
|
||||||
|
**anything guessing anatomy from a name needs a structural fallback.**
|
||||||
|
|
||||||
|
- A bare `leg` is the SHIN on Mixamo (`LeftUpLeg` is the thigh) and the THIGH on
|
||||||
|
a rig whose shin is called `knee`. Same token, opposite bones, both common. So
|
||||||
|
`RigRoles` walks the leg upward from the foot and fills in whatever the names
|
||||||
|
could not, stepping over twist bones.
|
||||||
|
- Claims are granted **longest-stem first**. Taking the first role in
|
||||||
|
`LIMB_ORDER` that matched at all let shin's catch-all `"leg"` beat thigh's
|
||||||
|
exact `"upperleg"`, and the outcome depended on bone iteration order.
|
||||||
|
- Cosmetic and spring classes accept a **two-character positional suffix**:
|
||||||
|
`HairFL`, `HairFR`, `HairF_Top` tokenise to `hairfl` and matched nothing, so a
|
||||||
|
character imported with no hair chains at all. Two characters is short enough
|
||||||
|
that `forearm` and `earring` are still not swept in.
|
||||||
|
- VRoid spells legs `UpperLeg`/`LowerLeg`. Any CHECK that name-matches
|
||||||
|
`thigh`/`shin` will silently pass or silently fail on it — see
|
||||||
|
`verification.md`.
|
||||||
|
|
||||||
|
If a new source fails with `could not identify these bones`, dump the joint names
|
||||||
|
first (`separation.md` has a no-Blender snippet) and decide whether it is a
|
||||||
|
missing stem or a case only anatomy can settle.
|
||||||
|
|
||||||
|
## Rebuilding the hierarchy
|
||||||
|
|
||||||
|
A Rigify DEF-rig exports its chain roots parented straight to the armature root,
|
||||||
|
because Rigify drives them by constraint rather than by hierarchy. Left that way,
|
||||||
|
rotating the hips leaves the legs, skirt and hair floating in place.
|
||||||
|
|
||||||
|
`rebuild_hierarchy` re-attaches orphans: by anatomy where the role is known, and
|
||||||
|
by rest geometry (nearest plausible parent) otherwise. **Cloth may only attach to
|
||||||
|
the trunk.**
|
||||||
|
|
||||||
|
## Subdividing cloth panels
|
||||||
|
|
||||||
|
`subdivide_cloth_panels(arm, meshes, roles, segments=4)`.
|
||||||
|
|
||||||
|
A skirt panel that is a single bone from the waist is a rigid flap: it can only
|
||||||
|
rotate about its own head, and a contact near that head is unreachable at any
|
||||||
|
angle. Splitting each panel into a chain is what lets it bend, and it is why the
|
||||||
|
ZZZ-convention skirt is a grid rather than a fan.
|
||||||
|
|
||||||
|
On Taila this turns 21 panel bones into 21 chains of 4. The segment lengths come
|
||||||
|
out uneven (49/49/49/141 mm) because the last segment runs on to the hem.
|
||||||
|
|
||||||
|
Weights are redistributed along the panel as it is split, so the mesh follows the
|
||||||
|
new chain.
|
||||||
|
|
||||||
|
## Twist bones
|
||||||
|
|
||||||
|
A forearm or thigh twist bone takes half the roll of its parent so the skin does
|
||||||
|
not candy-wrap. They are detected (`is_segment_of`) and recorded in the sidecar's
|
||||||
|
`twist` list. They are also folded into the limb when measuring collider radii:
|
||||||
|
most of a thigh's surface belongs to `DEF-thigh.L.001`, and what is left
|
||||||
|
dominated by `DEF-thigh.L` is mostly hip flare, which fitted a 0.154 m radius —
|
||||||
|
a 30 cm thigh.
|
||||||
|
|
||||||
|
## Joint helpers
|
||||||
|
|
||||||
|
`SkinJointHelper.install` runs for EVERY model however it was rigged. Linear-blend
|
||||||
|
skinning collapses any joint by cos(angle/2) no matter how good the weights are;
|
||||||
|
measured at the knee, 0.77 without helpers against 0.99 with. They are updated
|
||||||
|
LAST, inside the modification pass, so each helper tracks whatever final rotation
|
||||||
|
its child bone ended up with.
|
||||||
|
|
||||||
|
## The retarget maths
|
||||||
|
|
||||||
|
Bake each clip as a **rest-relative delta**:
|
||||||
|
|
||||||
|
```
|
||||||
|
R_world = src_pose_rot * src_rest_rot⁻¹ what the clip does
|
||||||
|
tgt_rot = R_world * tgt_rest_rot done to THIS rig
|
||||||
|
```
|
||||||
|
|
||||||
|
Copying absolute world orientation instead — which is what a constraint bake does
|
||||||
|
— forces the library's bone ROLL onto a mesh bound with a different one, and
|
||||||
|
twists every limb by a constant offset.
|
||||||
|
|
||||||
|
Also handled: a facing correction (`facing_correction`) when the library and the
|
||||||
|
character face different ways, and a hips-height scale so a short character does
|
||||||
|
not float.
|
||||||
|
|
||||||
|
## Export flags that matter
|
||||||
|
|
||||||
|
```python
|
||||||
|
export_bake_animation=False,
|
||||||
|
export_optimize_animation_keep_anim_armature=False,
|
||||||
|
```
|
||||||
|
|
||||||
|
`keep_anim_armature` forces a track onto every bone whether or not the clip
|
||||||
|
touches it. Off, the skirt and hair export with **no tracks at all** and belong
|
||||||
|
entirely to the spring solver. This one flag is the animation/physics split.
|
||||||
|
|
||||||
|
## Height normalisation
|
||||||
|
|
||||||
|
`flatten_and_scale(arm, meshes, TARGET_HEIGHT)` — default 1.75 m. Applied before
|
||||||
|
the retarget so the library's stride matches the character's legs.
|
||||||
|
|
||||||
|
## When a model has no skeleton
|
||||||
|
|
||||||
|
`tools/autorig.py` will fit one, and the pipeline accepts the quality loss:
|
||||||
|
nearest-bone weights, cross-leg bleed, no cloth chains. `weights_authored` comes
|
||||||
|
out false, `SkinLegRepair` runs at load to snap the worst of it, and the
|
||||||
|
character will have no secondary motion. Prefer finding a rigged source.
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Body, garments, hair — what must stay separate
|
||||||
|
|
||||||
|
The single structural idea behind an anime-styled character rig, and the thing
|
||||||
|
every failure in this project traced back to.
|
||||||
|
|
||||||
|
## The convention this pipeline follows
|
||||||
|
|
||||||
|
Hoyoverse-class character rigs (Genshin, Star Rail, Zenless Zone Zero) are built
|
||||||
|
the same way, and the parts that matter are visible in any of their exported
|
||||||
|
assets and in the toolchains built around them (Magica Cloth 2, UnityChan
|
||||||
|
SpringBone, VRM's spring-bone spec — all of which exist because this shape is
|
||||||
|
the convention):
|
||||||
|
|
||||||
|
| Convention | What this repo does |
|
||||||
|
|---|---|
|
||||||
|
| Body, face, hair and each garment are SEPARATE meshes with separate materials | Never join meshes; 18 meshes on Taila are all kept |
|
||||||
|
| Skirts get a radial grid of bone chains — many panels, several segments each | 21 panels × 4 segments, subdivided at build time |
|
||||||
|
| Hair is chains of 2–4 bones from the scalp | Detected from the source rig; 14 chains on Taila |
|
||||||
|
| Cloth/hair bones carry NO animation keys; physics owns them | `export_optimize_animation_keep_anim_armature=False` |
|
||||||
|
| Physics colliders are a small set of capsules: thighs, shins, and a big one at the waist acting as a lid | 5 capsules, measured from the mesh (`_leg_colliders`) |
|
||||||
|
| Neighbouring skirt panels are linked sideways | 278 cross-panel distance links from shared vertices |
|
||||||
|
| Each surface is TAGGED with what it is, so shading can differ per class | `tools/surface_map.py` writes it; `SkinSurfaces` reads it |
|
||||||
|
| Cel shading with a ramp, plus a separate outline pass | `LevelMaterials.apply_toon_recursive` + `apply_character_look` |
|
||||||
|
|
||||||
|
Where we differ: their collider capsules and cloth parameters are hand-authored
|
||||||
|
per character by a technical artist. We MEASURE them from the model's own
|
||||||
|
geometry at build time, because there is no artist in this loop. That is the
|
||||||
|
whole reason `<model>.rig.json` exists.
|
||||||
|
|
||||||
|
Where there IS an artist in the loop, there is now somewhere to put the answer:
|
||||||
|
`debug/rig_lab.tscn` and the layered files behind it (see the SKILL). Measuring
|
||||||
|
is the default and hand-authoring is the override, rather than the other way
|
||||||
|
round.
|
||||||
|
|
||||||
|
## Separation is only half of it — the parts have to be NAMED
|
||||||
|
|
||||||
|
Keeping the meshes apart is structural. Knowing which is which is what lets
|
||||||
|
anything act on the difference, and until the surface table existed nothing did:
|
||||||
|
every surface of every character took one set of shading numbers, calibrated on
|
||||||
|
skin, because there was no way to ask whether a surface was hair.
|
||||||
|
|
||||||
|
The table lives in the sidecar as `surfaces`, keyed on the MATERIAL name — mesh
|
||||||
|
node names are `Object_7` through `Object_32` on every character in this game and
|
||||||
|
carry no meaning, while material names survive the glTF round trip intact and are
|
||||||
|
what the artist actually chose. It is decided three ways, in descending order of
|
||||||
|
how much it trusts them:
|
||||||
|
|
||||||
|
1. **the material name.** On VRoid exports it is formal —
|
||||||
|
`N00_000_00_Body_00_SKIN_Instance` carries its own class infix, and every
|
||||||
|
VRoid character here uses SKIN / FACE / EYE / HAIR / CLOTH.
|
||||||
|
2. **the weights.** Decisive when the name says nothing: a surface pulled by the
|
||||||
|
skirt chain is a skirt whatever it is called. The threshold is deliberately
|
||||||
|
low (5%), because VRoid welds the whole cap of the hair to the head bone and
|
||||||
|
springs only the strands — kiyoko's hair mesh is 85% head, and a majority rule
|
||||||
|
would call it skin.
|
||||||
|
3. **the material flags.** These catch line-work, which is the one class that is
|
||||||
|
not a surface of the character at all.
|
||||||
|
|
||||||
|
It is built from the same chains the spring solver uses, so the two can never
|
||||||
|
disagree about which bones are a skirt.
|
||||||
|
|
||||||
|
## Why the separation is load-bearing
|
||||||
|
|
||||||
|
**Materials.** The body wants skin shading, hair wants an anisotropic-ish ramp
|
||||||
|
and its own outline weight, cloth wants flat banding. One merged mesh gets one
|
||||||
|
treatment and everything reads as plastic.
|
||||||
|
|
||||||
|
**The cloth solver.** `SkinnedPlayerModel._cloth_hulls` extracts, per cloth bone,
|
||||||
|
the vertices that bone dominates — that is only meaningful while the garment is
|
||||||
|
its own mesh with its own weights. Merge the meshes and the solver has no way to
|
||||||
|
know which vertices are skirt.
|
||||||
|
|
||||||
|
**Weights.** A joined mesh rebound by nearest-bone weighting produced 2817
|
||||||
|
vertices pulled by BOTH legs on Taila (16% of the model, worst a dead 50/50).
|
||||||
|
Such a vertex sits between the legs and stays there while they separate,
|
||||||
|
stretching every triangle around it. That is the "squashing on jump" and the
|
||||||
|
"elongated boot".
|
||||||
|
|
||||||
|
## How cloth is detected and classed
|
||||||
|
|
||||||
|
`tools/rig_map.py::is_cosmetic` matches WHOLE TOKENS in a bone name against:
|
||||||
|
|
||||||
|
```
|
||||||
|
hair skirt cloth ribbon tail cape coat scarf sleeve breast bust
|
||||||
|
feather strap antenna wing (+ face/eye classes that must never swing)
|
||||||
|
```
|
||||||
|
|
||||||
|
Whole-token only — `shoulder` must not match `should`, and a bone called
|
||||||
|
`hair_root` is hair while `chairbone` is not.
|
||||||
|
|
||||||
|
`retarget.py::SPRING_CLASSES` is a NARROWER set: the classes that actually get
|
||||||
|
secondary motion. A face-shape or eye chain is cosmetic but must never swing.
|
||||||
|
|
||||||
|
Each chain lands in `<model>.rig.json` as:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "class": "skirt",
|
||||||
|
"root_parent": "DEF-spine.001",
|
||||||
|
"bones": ["DEF-skirt", "DEF-skirt.seg1", "DEF-skirt.seg2", "DEF-skirt.seg3"],
|
||||||
|
"tips": [[x,y,z], ...], // where each bone points, in its own space
|
||||||
|
"hulls": [[[x,y,z], ...], ...], // sample of the geometry it drives
|
||||||
|
"neighbours": [{"DEF-skirt.L": 10.7, ...}] // shared-vertex weight
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`tips` exists because **a glTF skeleton carries no bone tails at all**, and
|
||||||
|
Taila's skirt panel bones have no children either, so nothing in the skeleton
|
||||||
|
says which way a panel hangs. It is measured from the geometry the bone drives.
|
||||||
|
|
||||||
|
`neighbours` means SHARED VERTICES — the artist's own answer to which pieces of
|
||||||
|
cloth are sewn together. Adjacency by name or by rest distance would both be
|
||||||
|
guesses.
|
||||||
|
|
||||||
|
## The three rules that keep it intact
|
||||||
|
|
||||||
|
1. **Cloth may only ever parent to the trunk, never to a limb.**
|
||||||
|
`rebuild_hierarchy` enforces this. A skirt parented to a thigh becomes
|
||||||
|
trousers.
|
||||||
|
|
||||||
|
2. **Cloth is never SKINNED to a leg.** There was a `bind_cloth_to_legs()` that
|
||||||
|
gave cloth vertices near a thigh a share of that thigh, so the skirt would
|
||||||
|
ride the leg the way a real one does. It is deleted. A vertex weighted 0.9 to
|
||||||
|
a thigh cannot be moved by its own cloth bone, so the solver loses the
|
||||||
|
authority to push it out of that leg — and 0.9 of a rotation always lags the
|
||||||
|
surface doing 1.0 of it, so the leg overtakes it anyway. It also poisoned the
|
||||||
|
collider measurement: 2258 skirt vertices counted as thigh geometry and fitted
|
||||||
|
a 0.28 m thigh.
|
||||||
|
|
||||||
|
3. **Cloth bones carry no animation tracks.** If the exporter bakes rest-pose
|
||||||
|
tracks onto them (`keep_anim_armature`), the AnimationPlayer overwrites the
|
||||||
|
spring solver every frame.
|
||||||
|
|
||||||
|
## Worked example: why the two shipped characters differ so much
|
||||||
|
|
||||||
|
Both are in `assets/characters/skins/`. Compare their sidecars:
|
||||||
|
|
||||||
|
| | Taila | Miku |
|
||||||
|
|---|---|---|
|
||||||
|
| source had a skeleton | yes | **no — 5 meshes, 0 joints** |
|
||||||
|
| `weights_authored` | true | **false** |
|
||||||
|
| cloth chains | 35 (127 bones) | **0** |
|
||||||
|
| twist bones | 8 | **0** |
|
||||||
|
| meshes shipped | 18 | **1** |
|
||||||
|
|
||||||
|
Miku's source (`assets/characters/incoming/miku_test.glb`) is an unrigged mesh,
|
||||||
|
so she went through `autorig.py`: joined to one mesh, rebound by nearest-bone
|
||||||
|
weighting, no cloth chains. Her twin tails and skirt are dead geometry that
|
||||||
|
cannot move, and `SkinLegRepair` runs destructively on her every spawn.
|
||||||
|
|
||||||
|
Nothing downstream can recover this. **The single highest-leverage decision in
|
||||||
|
this whole pipeline is choosing a source model that already has a skeleton with
|
||||||
|
skirt and hair bones.** Everything else is recoverable; this is not.
|
||||||
|
|
||||||
|
A quick check on any candidate, without Blender:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, struct
|
||||||
|
with open(path,'rb') as f:
|
||||||
|
f.read(12); clen,_=struct.unpack('<II',f.read(8))
|
||||||
|
j=json.loads(f.read(clen))
|
||||||
|
nodes=[n.get('name','') for n in j['nodes']]
|
||||||
|
joints=[nodes[i] for s in j.get('skins',[]) for i in s['joints']]
|
||||||
|
print(len(j['meshes']), 'meshes', len(joints), 'joints')
|
||||||
|
print([n for n in joints if any(t in n.lower() for t in ('hair','skirt','tail','ribbon'))])
|
||||||
|
```
|
||||||
|
|
||||||
|
Several meshes, 50+ joints, and a non-empty cosmetic list means a good source.
|
||||||
|
|
||||||
|
## Checking a source model before importing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/verify_character.py <model.glb>
|
||||||
|
```
|
||||||
|
|
||||||
|
What you want to see: several meshes, bone names containing `skirt`/`hair`,
|
||||||
|
twist bones (`thigh.L.001`), and weights that are NOT all at 4 influences.
|
||||||
|
`weights_authored` in the sidecar is measured from exactly this and decides
|
||||||
|
whether the destructive load-time repair runs.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Stylization — the cel-shaded look
|
||||||
|
|
||||||
|
Two passes, applied at load in `SkinnedPlayerModel.load_model`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
LevelMaterials.apply_toon_recursive(scene) # world-wide toon shading
|
||||||
|
LevelMaterials.apply_character_look(scene) # character-only corrections
|
||||||
|
```
|
||||||
|
|
||||||
|
## The trap: imported models bring their own line-work
|
||||||
|
|
||||||
|
Anime models exported from MMD/VRoid/Blender toon setups very often ship the
|
||||||
|
outline **as geometry** — an inverted-hull shell of the mesh with a flat black,
|
||||||
|
UNTEXTURED material, plus separate flat cards for the eye whites, irises and the
|
||||||
|
pupil highlight. The mesh you import is not just the character; part of it is
|
||||||
|
already the drawing.
|
||||||
|
|
||||||
|
Toon-lighting that shell is what put a **white rim on every hair strand**. It is
|
||||||
|
an inverted hull whose normals face away from you; a lighting model that adds a
|
||||||
|
rim term lights it brightly exactly where it is supposed to read as ink.
|
||||||
|
|
||||||
|
`apply_character_look` therefore looks for the model's own line-work and handles
|
||||||
|
it flat and unshaded. "Untextured" alone is NOT the test — that made every
|
||||||
|
flat-coloured model render as a black silhouette, because Quaternius' mannequin
|
||||||
|
has two untextured materials (a yellow body, lilac joints) and both were hidden
|
||||||
|
as though they were an outline shell. The test asks three things instead: is it
|
||||||
|
named `eyes*`, is it drawn front-face-culled (the classic inverted-hull setup),
|
||||||
|
or is its albedo near-black. An ink shell is black; a flat-coloured character is
|
||||||
|
any colour at all.
|
||||||
|
|
||||||
|
That test now runs at BUILD time (`tools/surface_map.classify_linework`) and its
|
||||||
|
answer lives in the sidecar. `SkinSurfaces.guess()` is the same rule kept as the
|
||||||
|
runtime fallback, for a model with no surface table — and the cull-mode half of
|
||||||
|
it is re-run at runtime even when the table exists, because glTF has no way to
|
||||||
|
say "draw only the backfaces" and an inverted hull cannot survive the round trip
|
||||||
|
as a cull mode. Blender genuinely cannot see it; Godot can.
|
||||||
|
|
||||||
|
What it then does:
|
||||||
|
|
||||||
|
- **Outline hull** → made fully transparent rather than deleted. Deleting a
|
||||||
|
surface would renumber the rest and break the mesh's own skin bindings. The
|
||||||
|
game draws its own outline.
|
||||||
|
- **Eye cards** (`resource_name` starts with `eyes`) → flat ink, except anything
|
||||||
|
with `HL` in the name, which is the glint in the pupil and really is white.
|
||||||
|
|
||||||
|
If a newly imported character comes out with a white halo, a black silhouette,
|
||||||
|
or black eyes that should have irises, the line-work test and the name-matching
|
||||||
|
below it are where to look — now in `tools/surface_map.py`, mirrored by
|
||||||
|
`SkinSurfaces.guess()`. **Change both or neither**: a model with a surface table
|
||||||
|
would start rendering differently from one without.
|
||||||
|
|
||||||
|
## Per-class art direction
|
||||||
|
|
||||||
|
Because each surface says what it is, each class takes its own numbers
|
||||||
|
(`LevelMaterials.CHARACTER_LOOK`). `body` is deliberately identical to what every
|
||||||
|
surface used to get, so the calibration this was all built on does not move. The
|
||||||
|
others are departures, each for a reason:
|
||||||
|
|
||||||
|
| Class | Outline | Band | Why |
|
||||||
|
|---|---|---|---|
|
||||||
|
| body | 5.0 mm | 0.16 | unchanged — the baseline |
|
||||||
|
| cloth | 5.8 mm | 0.13 | a garment's silhouette is most of what separates a character from the background at range; folds need a defined terminator to read as fabric |
|
||||||
|
| hair | 3.4 mm | 0.20 | **the one that matters.** A hair mesh is dozens of near-parallel strands millimetres apart; at the body's 5 mm each strand's hull swallows its neighbour and the head reads as one solid dark cap |
|
||||||
|
| accessory | 6.8 mm | 0.10 | small, rigid, usually the most saturated thing on the character — meant to pop |
|
||||||
|
|
||||||
|
This required moving the outline from `material_overlay` on the INSTANCE to
|
||||||
|
`next_pass` on each surface's material. Miku's body, face and hair are three
|
||||||
|
surfaces of one mesh, so an instance-wide overlay can only ever give all three
|
||||||
|
the same weight.
|
||||||
|
|
||||||
|
Taila's eyes still render as black cards rather than amber irises. Her eye
|
||||||
|
surfaces are untextured, and the glTF import hands every untextured surface a
|
||||||
|
default near-white albedo, so colour cannot tell an iris card from a lash card
|
||||||
|
on her — the name is all there is, and `eyes*` currently means "ink". Unfixed.
|
||||||
|
|
||||||
|
## Materials on import: the unlit problem
|
||||||
|
|
||||||
|
Anime glTFs are very often exported "unlit": `KHR_materials_unlit`, a **black**
|
||||||
|
`baseColorFactor`, and the real texture wired to `emissiveTexture`. Renderers
|
||||||
|
honouring the unlit extension use base colour and ignore emission — so Blender
|
||||||
|
reads black, never references the images, and imports with `bpy.data.images`
|
||||||
|
**empty**. The character comes out a silhouette, and there is no node graph left
|
||||||
|
to patch afterwards.
|
||||||
|
|
||||||
|
`tools/gltf_fix.py` rewrites the container **before** import: emissive becomes
|
||||||
|
base colour, the unlit flag is dropped. It must run first — this is the first
|
||||||
|
thing `retarget.py::main` does, before `import_any`.
|
||||||
|
|
||||||
|
`fix_unlit_materials(meshes)` then repairs anything left inside Blender.
|
||||||
|
|
||||||
|
## What the toon pass does
|
||||||
|
|
||||||
|
`apply_toon_recursive` gives everything the game's banded ramp. `apply_character_look`
|
||||||
|
then softens the banding on characters, because re-banding an already-shaded
|
||||||
|
anime texture reads as gloss — the texture already contains its own shading and
|
||||||
|
the second pass fights it.
|
||||||
|
|
||||||
|
## Convention alignment
|
||||||
|
|
||||||
|
The Hoyoverse-class look is, broadly: a ramp texture indexed by NdotL for the
|
||||||
|
body, a separate ramp and often a dedicated shader for the face, an inverted-hull
|
||||||
|
outline whose width is vertex-colour-modulated, and specific handling for eyes
|
||||||
|
and hair highlights. This project does the simplified version — one banded ramp
|
||||||
|
plus a screen-space-ish ink treatment, and the model's own outline shell hidden
|
||||||
|
in favour of the game's. The face is NOT specially shaded here; if a character
|
||||||
|
comes out with harsh shadow shapes across the nose, that is the missing piece.
|
||||||
|
|
||||||
|
## Outline thickness
|
||||||
|
|
||||||
|
Lives with the toon material in `scenes/maps/level_materials.gd`
|
||||||
|
(`CHARACTER_INK` and the outline settings). This is the branch it was last
|
||||||
|
touched on — `feat/outline-thickness-and-tp-weapon-hold`.
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# Verification — and the trap that invalidated all of it
|
||||||
|
|
||||||
|
## READ THIS FIRST
|
||||||
|
|
||||||
|
**Godot restores every bone's local pose after the `SkeletonModifier3D` pass.**
|
||||||
|
|
||||||
|
So calling `force_update_all_bone_transforms()` and reading
|
||||||
|
`get_bone_global_pose()` from a `SceneTree` script, from `_process`, or anywhere
|
||||||
|
outside that pass recomputes the globals from the **animation alone**. The
|
||||||
|
shooter pose layer and the cloth solver are simply not in what you measure.
|
||||||
|
|
||||||
|
`debug/cloth_clip_check.gd` did exactly this. It reported the same ~95 mm of
|
||||||
|
leg-inside-skirt with collision fully enabled **and with the collision call
|
||||||
|
commented out**. Every number ever taken from that tool before 2026-07-26 is
|
||||||
|
void, and several rounds of "tuning did nothing" in the history were reading a
|
||||||
|
pose the solver never touched.
|
||||||
|
|
||||||
|
**To measure a pose layer, add your own `SkeletonModifier3D` as a child of the
|
||||||
|
`Skeleton3D` AFTER the one you care about, and snapshot inside its
|
||||||
|
`_process_modification()`.** The `PoseProbe` class in `cloth_clip_check.gd` and
|
||||||
|
`travel_dir_check.gd` is the pattern.
|
||||||
|
|
||||||
|
Two related traps:
|
||||||
|
|
||||||
|
- **Headless runs uncapped**, so the engine delta is sub-millisecond and anything
|
||||||
|
integrated barely moves. Set `SpringBones.fixed_delta = 1.0/60.0`.
|
||||||
|
- **A single frame of a locomotion clip measures the clip.** A run cycle twists
|
||||||
|
the torso against the hips by tens of degrees twice per stride, swamping
|
||||||
|
anything a pose layer does. Average over a stride.
|
||||||
|
|
||||||
|
## The tools
|
||||||
|
|
||||||
|
| Tool | Measures | Good |
|
||||||
|
|---|---|---|
|
||||||
|
| `spawn_smoke_test.gd` | spawn, skins, anim tree, camera, state cycling | 29 OK, 0 failures |
|
||||||
|
| `cloth_clip_check.gd` | leg-inside-cloth per movement state, per vertex | idle < 25 mm |
|
||||||
|
| `cloth_settle_check.gd` | deg/frame at a dead idle, contacts/frame | skirt < 0.1, hair < 0.01 |
|
||||||
|
| `cloth_perf_check.gd` | ms per character per frame | ~2.6 ms |
|
||||||
|
| `cloth_allow_check.gd` | how much of each limb the rest-clearance cap makes the solver blind to | 17–35 mm on Taila |
|
||||||
|
| `cloth_stretch_check.gd` | mesh tearing between panels | no 3× edges |
|
||||||
|
| `travel_dir_check.gd` | stride direction vs. travel direction | < 10° except a capped sidestep |
|
||||||
|
| `limb_deform_check.gd` | joint collapse | knee ~0.99 |
|
||||||
|
| `verify_character.py` | meshes, bones, weights of a SOURCE model | several meshes, cloth bones present |
|
||||||
|
| `surface_class_check.gd` | every surface resolves from the sidecar, not the fallback | 0 fallbacks on all six skins |
|
||||||
|
| `character_picker_check.gd` | the escape-menu roster: skeleton, clips, surfaces, and that the pose MOVES | 0 failures |
|
||||||
|
| `rig_anchor_check.gd` | a grip anchor physically moves the weapon, and clears | 0 failures |
|
||||||
|
| `anchor_shift_check.gd` | the hand anchors move in the GUN's frame, both poses | 0 failures |
|
||||||
|
| `anchor_drag_check.gd` | dragging a marker writes the knob the mouse asked for | 0 failures |
|
||||||
|
| `hold_pose_check.gd` | the lab shows only the selected pose's knobs; every wrist axis turns its hand | 0 failures |
|
||||||
|
| `wrist_gun_check.gd` | the wrist turns the hand and NOT the gun welded to it | hand ~28°, gun < 1° |
|
||||||
|
| `anim_capture.gd` / `orbit_capture.gd` | renders, for looking | — |
|
||||||
|
| `roster_capture.gd` | one photo of every character, from the picker | — |
|
||||||
|
| `ui_capture.gd` | one photo of every menu screen | — |
|
||||||
|
| `rest_pose_check.gd` | each rig's bind-pose limb directions vs. the library's | see below |
|
||||||
|
|
||||||
|
## What `rest_pose_check` actually established
|
||||||
|
|
||||||
|
It was written to test a suspicion — that the rest-relative retarget silently
|
||||||
|
assumes both rigs rest alike — and it disproved it. Miku's arms rest **41°** off
|
||||||
|
the animation library's and Taila's **32°**, and both animate correctly. The
|
||||||
|
delta retarget handles a rest-pose difference, which is what it is for. Do not
|
||||||
|
go looking there again.
|
||||||
|
|
||||||
|
It also demonstrates the measurement trap in miniature. Written as "the direction
|
||||||
|
from a bone to its FIRST CHILD", it reported kiyoko's and aria's legs 71° off —
|
||||||
|
because a thigh's first child is as likely to be a skirt bone as a shin, and it
|
||||||
|
was measuring the hang of a skirt panel. Pointing it at the next limb BY ROLE
|
||||||
|
dropped both to 1°. The same rule as everywhere else in this pipeline: resolve
|
||||||
|
roles, never take whatever the rig happens to hand you.
|
||||||
|
|
||||||
|
## Assert the consequence, not the plumbing
|
||||||
|
|
||||||
|
Three of these exist because the obvious check passes on a broken system.
|
||||||
|
|
||||||
|
- `character_picker_check` asserts the skeleton's pose CHANGES over a dozen
|
||||||
|
frames. Asking the model which clip it is playing does not work: that is a
|
||||||
|
variable the class sets on itself, and it reads `"Idle"` just as happily when
|
||||||
|
the animation tree is not ticking at all.
|
||||||
|
- `rig_anchor_check` asserts the weapon MOVES by the offset asked for. An anchor
|
||||||
|
system is easy to build so that the sliders move, the file saves and the JSON
|
||||||
|
round-trips while the gun does not budge — the value read into a variable
|
||||||
|
nobody consumed. It measures in the attachment's frame, not the world's:
|
||||||
|
the attachment tracks a bone on an animating skeleton, so a world-space delta
|
||||||
|
is mostly the idle animation.
|
||||||
|
- `anchor_shift_check` and `anchor_drag_check` both measure in the GUN's frame
|
||||||
|
rather than the world's, and have to. The hold BREATHES — a
|
||||||
|
`sin(_time * 2.2) * 0.012` on the muzzle pitch — so no anchor is ever at the
|
||||||
|
same world position twice, and comparing absolute positions reported a 3.5 mm
|
||||||
|
error that was the character inhaling. Taking each anchor relative to the one
|
||||||
|
it hangs off and rotating into the current gun basis cancels the breathing,
|
||||||
|
the ADS blend and the recoil kick exactly, because all three move the basis
|
||||||
|
and the anchor together.
|
||||||
|
- `hold_pose_check` measures the wrists through a `PoseProbe`, and had to learn
|
||||||
|
it the same way everything else did: reading `get_bone_pose_rotation` from the
|
||||||
|
SceneTree reported every wrist axis as turning the hand by **0.0 degrees** —
|
||||||
|
the identical answer it would give if the wrists had never been implemented.
|
||||||
|
See READ THIS FIRST. That trap is still the most expensive one in this repo.
|
||||||
|
- `surface_class_check` FAILS on a surface that falls through to the heuristic
|
||||||
|
instead of resolving from the table. A model whose names stopped matching still
|
||||||
|
renders — the fallback catches it — and quietly loses its per-class art
|
||||||
|
direction. Nothing else would report that.
|
||||||
|
|
||||||
|
And four of the last five real defects came from LOOKING, not from asserting:
|
||||||
|
a preview showing the back of the character's head, a turntable that carried on
|
||||||
|
from the previous character, an unstyled list, and momo's idle pose. Every one
|
||||||
|
passed every assertion. Run `roster_capture` and `ui_capture` and open the PNGs.
|
||||||
|
|
||||||
|
Run them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . -s res://debug/<tool>.gd
|
||||||
|
godot --headless --path . -s res://debug/<tool>.gd -- res://assets/characters/skins/<name>.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
Scripts run with `-s` MUST extend `SceneTree`. A `Node` script never quits and
|
||||||
|
hangs forever.
|
||||||
|
|
||||||
|
## Measure the right quantity
|
||||||
|
|
||||||
|
`cloth_clip_check.gd` used to report "how much CLOSER the leg got than the artist
|
||||||
|
modelled it". A hem 200 mm clear of a shin legitimately comes 180 mm closer when
|
||||||
|
the leg kicks out in a slide, and counting that as a failure buried the real
|
||||||
|
clipping under motion the character is supposed to have. It now reports how far
|
||||||
|
INSIDE a capsule a cloth vertex is, over and above however far inside it was
|
||||||
|
modelled — only cloth actually within the capsule can be showing a leg through.
|
||||||
|
|
||||||
|
It also applies the collider's `from` offset, so it tests the same band of thigh
|
||||||
|
the solver is defending. Measuring the full bone tests the hip cap the solver
|
||||||
|
deliberately excludes and reports it as clipping no tuning can fix.
|
||||||
|
|
||||||
|
## What the suite still does not check
|
||||||
|
|
||||||
|
It verifies that a character is WELL-FORMED, not that it is CORRECT. Those are
|
||||||
|
different properties, and only the first was ever asserted — which is how four
|
||||||
|
characters shipped "All checks passed" while lying on their backs, seven times
|
||||||
|
too large, facing backwards, or unable to hold a gun. See `failure-modes.md`.
|
||||||
|
|
||||||
|
`posture` and `bone roles reachable at runtime` are now hard checks. Still
|
||||||
|
missing, and worth adding when a source next exposes them: facing measured on the
|
||||||
|
OUTPUT, and per-vertex validation that a generated cloth chain actually tracks
|
||||||
|
the geometry it was given.
|
||||||
|
|
||||||
|
## Diagnosing "the solver isn't working"
|
||||||
|
|
||||||
|
In order:
|
||||||
|
|
||||||
|
1. **Is the measurement inside the modifier pass?** (Above. Do this first.)
|
||||||
|
2. **Does the solver SEE the contact?** `debug_hit_report()` — bone → deepest
|
||||||
|
overlap it found. If ~0 while the mesh is deep inside a leg, the collision
|
||||||
|
hull does not cover the geometry that is clipping.
|
||||||
|
3. **Does it CONVERGE?** `debug_residual_report()` — overlap left after the
|
||||||
|
relaxation. Seen 93 mm, left 95 mm is a standing fight, not slow convergence;
|
||||||
|
quadrupling the iterations will buy nothing. Find what is pulling back.
|
||||||
|
4. **Only then, tune.**
|
||||||
|
|
||||||
|
That order was learned the hard way: the drape, the bend limits, the backstop,
|
||||||
|
the iteration count and the hull sampling were each suspected and tested, and
|
||||||
|
the answer was in step 1.
|
||||||
|
|
||||||
|
## Also run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . -s res://movement/tests/run_fsm_tests.gd # 11 tests
|
||||||
|
godot --headless --path . --check-only --script res://<file>.gd # syntax
|
||||||
|
```
|
||||||
|
|
||||||
|
Autoload identifiers report false "not found" errors under `--check-only` —
|
||||||
|
ignore those.
|
||||||
@@ -57,6 +57,11 @@ __pycache__/
|
|||||||
# Asset pipeline staging (raw downloads, not game-ready)
|
# Asset pipeline staging (raw downloads, not game-ready)
|
||||||
assets/characters/incoming/
|
assets/characters/incoming/
|
||||||
|
|
||||||
|
# Copyrighted visual references (local-only; never imported or exported)
|
||||||
|
/.reference_assets/
|
||||||
|
/assets/characters/local_reference/
|
||||||
|
/assets/characters/skins/skins.local.json
|
||||||
|
|
||||||
# Misc
|
# Misc
|
||||||
*LF*
|
*LF*
|
||||||
tags
|
tags
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# CMU backward locomotion source
|
||||||
|
|
||||||
|
`_cmu_locomotion.glb` contains one animation-only clip:
|
||||||
|
|
||||||
|
- `RunBackward` — CMU Graphics Lab Motion Capture Database, subject 76,
|
||||||
|
trial 11: “quick large steps backwards.”
|
||||||
|
|
||||||
|
Source BVH:
|
||||||
|
https://github.com/una-dinosauria/cmu-mocap/blob/master/data/076/76_11.bvh
|
||||||
|
|
||||||
|
Original motion database and trial description:
|
||||||
|
https://mocap.cs.cmu.edu/
|
||||||
|
|
||||||
|
The BVH is Bruce Hahne’s conversion of the original CMU motion-capture data.
|
||||||
|
CMU’s FAQ permits copying, modifying, and redistributing the data (including
|
||||||
|
commercial use), requests acknowledgment, and prohibits selling the database
|
||||||
|
itself. This project uses a cropped, in-place gait cycle as part of the game,
|
||||||
|
not as a standalone resale of the database.
|
||||||
|
|
||||||
|
Build command:
|
||||||
|
|
||||||
|
```text
|
||||||
|
blender --background --python tools/build_cmu_locomotion_library.py -- 76_11.bvh assets/characters/animations/_cmu_locomotion.glb
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Godot TPS Demo directional animations
|
||||||
|
|
||||||
|
Source: https://github.com/godotengine/tps-demo
|
||||||
|
|
||||||
|
The animation-only `_directional.glb` contains the `strafe_front-cycle`,
|
||||||
|
`strafe_back-cycle`, `strafe_left-cycle`, and `strafe_right-cycle` actions from
|
||||||
|
the Godot Third Person Shooter Demo player model. Meshes, textures, and
|
||||||
|
unrelated animations were removed. The game retargets these authored cycles to
|
||||||
|
its Quaternius-compatible character skeleton.
|
||||||
|
|
||||||
|
Original assets Copyright (c) 2018 Juan Linietsky and Fernando Miguel Calabró.
|
||||||
|
|
||||||
|
Distributed under the Creative Commons Attribution 3.0 license:
|
||||||
|
https://creativecommons.org/licenses/by/3.0/
|
||||||
|
|
||||||
|
Changes: animation-only extraction, retargeting, in-place root motion, and
|
||||||
|
runtime phase-synchronized blending with the project's authored walk/run clips.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
Universal Animation Library 2 [Standard]
|
||||||
|
Author: Quaternius
|
||||||
|
Source: https://quaternius.itch.io/universal-animation-library-2
|
||||||
|
|
||||||
|
License:
|
||||||
|
CC0 1.0 Universal (CC0 1.0)
|
||||||
|
Public Domain Dedication
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
The committed _library_v2.glb is the non-root-motion Unreal/Godot export from
|
||||||
|
the free Standard package. The game drives world movement; authored vertical
|
||||||
|
body motion remains in the clips.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
Mesh2Motion Human Animation Library
|
||||||
|
|
||||||
|
Source:
|
||||||
|
https://github.com/Mesh2Motion/mesh2motion-app
|
||||||
|
https://github.com/Mesh2Motion/mesh2motion-assets
|
||||||
|
|
||||||
|
License:
|
||||||
|
CC0 1.0 Universal (CC0 1.0)
|
||||||
|
Public Domain Dedication
|
||||||
|
https://creativecommons.org/publicdomain/zero/1.0/
|
||||||
|
|
||||||
|
The committed _mesh2motion.glb is an animation-only subset of
|
||||||
|
static/animations/human-addon-animations.glb. It retains these authored clips:
|
||||||
|
|
||||||
|
- Run_Anime
|
||||||
|
- Strafe_left
|
||||||
|
- Strafe_right
|
||||||
|
- Flying Forward Super
|
||||||
|
|
||||||
|
Meshes, textures, and unrelated actions were removed. Run_Anime is retargeted
|
||||||
|
offline into the four cardinal high-speed travel headings and the wall-run
|
||||||
|
loop; the animator's keyframed limb motion is preserved. Flying Forward Super
|
||||||
|
provides the grapple flight pose.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Authored wall-run animation source
|
||||||
|
|
||||||
|
`_wallrun.glb` contains the animation-only UE4 Mannequin clips
|
||||||
|
`WallRunLeft` and `WallRunRight`.
|
||||||
|
|
||||||
|
- Author: Uisco
|
||||||
|
- Source: https://uisco.itch.io/wall-running-animations
|
||||||
|
- Original files: `wall_run_left.FBX`, `wall_run_right.FBX`
|
||||||
|
- Published: 2021-03-24
|
||||||
|
- Listing: free / name-your-own-price game asset
|
||||||
|
|
||||||
|
The source listing does not state a named open-content license. Keep this
|
||||||
|
provenance file with the embedded game asset, credit Uisco, and do not resell
|
||||||
|
or redistribute the clips as a standalone animation pack.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Anime Maid Character",
|
||||||
|
"author": "413 Games",
|
||||||
|
"author_url": "https://413games.itch.io/",
|
||||||
|
"license": "CC0 1.0 Universal / Public Domain",
|
||||||
|
"license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||||
|
"source_url": "https://413games.itch.io/maid-character",
|
||||||
|
"changes": "Original rig and materials retained, retargeted to the Papaya-Shooter animation library; game-native mecha equipment added at runtime."
|
||||||
|
}
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
{
|
||||||
|
"roles": {
|
||||||
|
"hips": "mixamorig:Hips",
|
||||||
|
"head": "mixamorig:Head",
|
||||||
|
"neck": "mixamorig:Neck",
|
||||||
|
"spine": [
|
||||||
|
"mixamorig:Spine",
|
||||||
|
"mixamorig:Spine1",
|
||||||
|
"mixamorig:Spine2",
|
||||||
|
"mixamorig:Neck",
|
||||||
|
"mixamorig:Head"
|
||||||
|
],
|
||||||
|
"shoulder.L": "mixamorig:LeftShoulder",
|
||||||
|
"shoulder.R": "mixamorig:RightShoulder",
|
||||||
|
"toe.R": "mixamorig:RightToeBase",
|
||||||
|
"toe.L": "mixamorig:LeftToeBase",
|
||||||
|
"forearm.L": "mixamorig:LeftForeArm",
|
||||||
|
"forearm.R": "mixamorig:RightForeArm",
|
||||||
|
"thigh.R": "mixamorig:RightUpLeg",
|
||||||
|
"thigh.L": "mixamorig:LeftUpLeg",
|
||||||
|
"foot.R": "mixamorig:RightFoot",
|
||||||
|
"foot.L": "mixamorig:LeftFoot",
|
||||||
|
"hand.R": "mixamorig:RightHand",
|
||||||
|
"hand.L": "mixamorig:LeftHand",
|
||||||
|
"upper_arm.L": "mixamorig:LeftArm",
|
||||||
|
"upper_arm.R": "mixamorig:RightArm",
|
||||||
|
"shin.L": "mixamorig:LeftLeg",
|
||||||
|
"shin.R": "mixamorig:RightLeg"
|
||||||
|
},
|
||||||
|
"fingers": {
|
||||||
|
"thumb.L": [
|
||||||
|
"mixamorig:LeftHandThumb1",
|
||||||
|
"mixamorig:LeftHandThumb2",
|
||||||
|
"mixamorig:LeftHandThumb3",
|
||||||
|
"mixamorig:LeftHandThumb4",
|
||||||
|
"mixamorig:LeftHandThumb4_end"
|
||||||
|
],
|
||||||
|
"index.L": [
|
||||||
|
"mixamorig:LeftHandIndex1",
|
||||||
|
"mixamorig:LeftHandIndex2",
|
||||||
|
"mixamorig:LeftHandIndex3",
|
||||||
|
"mixamorig:LeftHandIndex4",
|
||||||
|
"mixamorig:LeftHandIndex4_end"
|
||||||
|
],
|
||||||
|
"middle.L": [
|
||||||
|
"mixamorig:LeftHandMiddle1",
|
||||||
|
"mixamorig:LeftHandMiddle2",
|
||||||
|
"mixamorig:LeftHandMiddle3",
|
||||||
|
"mixamorig:LeftHandMiddle4",
|
||||||
|
"mixamorig:LeftHandMiddle4_end"
|
||||||
|
],
|
||||||
|
"ring.L": [
|
||||||
|
"mixamorig:LeftHandRing1",
|
||||||
|
"mixamorig:LeftHandRing2",
|
||||||
|
"mixamorig:LeftHandRing3",
|
||||||
|
"mixamorig:LeftHandRing4",
|
||||||
|
"mixamorig:LeftHandRing4_end"
|
||||||
|
],
|
||||||
|
"pinky.L": [
|
||||||
|
"mixamorig:LeftHandPinky1",
|
||||||
|
"mixamorig:LeftHandPinky2",
|
||||||
|
"mixamorig:LeftHandPinky3",
|
||||||
|
"mixamorig:LeftHandPinky4",
|
||||||
|
"mixamorig:LeftHandPinky4_end"
|
||||||
|
],
|
||||||
|
"thumb.R": [
|
||||||
|
"mixamorig:RightHandThumb1",
|
||||||
|
"mixamorig:RightHandThumb2",
|
||||||
|
"mixamorig:RightHandThumb3",
|
||||||
|
"mixamorig:RightHandThumb4",
|
||||||
|
"mixamorig:RightHandThumb4_end"
|
||||||
|
],
|
||||||
|
"index.R": [
|
||||||
|
"mixamorig:RightHandIndex1",
|
||||||
|
"mixamorig:RightHandIndex2",
|
||||||
|
"mixamorig:RightHandIndex3",
|
||||||
|
"mixamorig:RightHandIndex4",
|
||||||
|
"mixamorig:RightHandIndex4_end"
|
||||||
|
],
|
||||||
|
"middle.R": [
|
||||||
|
"mixamorig:RightHandMiddle1",
|
||||||
|
"mixamorig:RightHandMiddle2",
|
||||||
|
"mixamorig:RightHandMiddle3",
|
||||||
|
"mixamorig:RightHandMiddle4",
|
||||||
|
"mixamorig:RightHandMiddle4_end"
|
||||||
|
],
|
||||||
|
"ring.R": [
|
||||||
|
"mixamorig:RightHandRing1",
|
||||||
|
"mixamorig:RightHandRing2",
|
||||||
|
"mixamorig:RightHandRing3",
|
||||||
|
"mixamorig:RightHandRing4",
|
||||||
|
"mixamorig:RightHandRing4_end"
|
||||||
|
],
|
||||||
|
"pinky.R": [
|
||||||
|
"mixamorig:RightHandPinky1",
|
||||||
|
"mixamorig:RightHandPinky2",
|
||||||
|
"mixamorig:RightHandPinky3",
|
||||||
|
"mixamorig:RightHandPinky4",
|
||||||
|
"mixamorig:RightHandPinky4_end"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"chains": [],
|
||||||
|
"twist": [],
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 0,
|
||||||
|
"material": "N00_000_00_Body_00_SKIN (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "skin",
|
||||||
|
"why": "material name says 'skin'",
|
||||||
|
"verts": 5452,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 1,
|
||||||
|
"material": "N00_000_00_HairBack_00_HAIR (Instance)",
|
||||||
|
"class": "hair",
|
||||||
|
"detail": "hair",
|
||||||
|
"why": "material name says 'hair'",
|
||||||
|
"verts": 760,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 2,
|
||||||
|
"material": "N00_002_03_Tops_01_CLOTH (Instance)",
|
||||||
|
"class": "cloth",
|
||||||
|
"detail": "garment",
|
||||||
|
"why": "material name says 'garment'",
|
||||||
|
"verts": 908,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 3,
|
||||||
|
"material": "N00_001_01_Shoes_01_CLOTH (Instance)",
|
||||||
|
"class": "cloth",
|
||||||
|
"detail": "garment",
|
||||||
|
"why": "material name says 'garment'",
|
||||||
|
"verts": 390,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 4,
|
||||||
|
"material": "N00_000_00_FaceMouth_00_FACE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "face",
|
||||||
|
"why": "material name says 'face'",
|
||||||
|
"verts": 1053,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 5,
|
||||||
|
"material": "N00_000_00_EyeIris_00_EYE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "eyes",
|
||||||
|
"why": "material name says 'eyes'",
|
||||||
|
"verts": 214,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 6,
|
||||||
|
"material": "N00_000_00_EyeHighlight_00_EYE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "eyes",
|
||||||
|
"why": "material name says 'eyes'",
|
||||||
|
"verts": 38,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 7,
|
||||||
|
"material": "N00_000_00_Face_00_SKIN (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "face",
|
||||||
|
"why": "material name says 'face'",
|
||||||
|
"verts": 2266,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 8,
|
||||||
|
"material": "N00_000_00_EyeWhite_00_EYE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "eyes",
|
||||||
|
"why": "material name says 'eyes'",
|
||||||
|
"verts": 290,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 9,
|
||||||
|
"material": "N00_000_00_FaceBrow_00_FACE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "face",
|
||||||
|
"why": "material name says 'face'",
|
||||||
|
"verts": 52,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 10,
|
||||||
|
"material": "N00_000_00_FaceEyeline_00_FACE (Instance)",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "face",
|
||||||
|
"why": "material name says 'face'",
|
||||||
|
"verts": 126,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Body",
|
||||||
|
"surface": 11,
|
||||||
|
"material": "N00_000_Hair_00_HAIR (Instance)",
|
||||||
|
"class": "hair",
|
||||||
|
"detail": "hair",
|
||||||
|
"why": "material name says 'hair'",
|
||||||
|
"verts": 7355,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"colliders": [
|
||||||
|
{
|
||||||
|
"bone": "mixamorig:Hips",
|
||||||
|
"child": "mixamorig:Spine",
|
||||||
|
"from": 0.0,
|
||||||
|
"radius_head": 0.1341,
|
||||||
|
"radius_tail": 0.1341,
|
||||||
|
"radius": 0.1341,
|
||||||
|
"lid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "mixamorig:LeftUpLeg",
|
||||||
|
"child": "mixamorig:LeftLeg",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0985,
|
||||||
|
"radius_tail": 0.0605,
|
||||||
|
"radius": 0.0605
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "mixamorig:RightUpLeg",
|
||||||
|
"child": "mixamorig:RightLeg",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0985,
|
||||||
|
"radius_tail": 0.0605,
|
||||||
|
"radius": 0.0605
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "mixamorig:LeftLeg",
|
||||||
|
"child": "mixamorig:LeftFoot",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1172,
|
||||||
|
"radius_tail": 0.0507,
|
||||||
|
"radius": 0.0507
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "mixamorig:RightLeg",
|
||||||
|
"child": "mixamorig:RightFoot",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1172,
|
||||||
|
"radius_tail": 0.0507,
|
||||||
|
"radius": 0.0507
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weights_authored": true,
|
||||||
|
"driven_bones": [
|
||||||
|
"mixamorig:Head",
|
||||||
|
"mixamorig:Hips",
|
||||||
|
"mixamorig:LeftArm",
|
||||||
|
"mixamorig:LeftFoot",
|
||||||
|
"mixamorig:LeftForeArm",
|
||||||
|
"mixamorig:LeftHand",
|
||||||
|
"mixamorig:LeftLeg",
|
||||||
|
"mixamorig:LeftShoulder",
|
||||||
|
"mixamorig:LeftToeBase",
|
||||||
|
"mixamorig:LeftUpLeg",
|
||||||
|
"mixamorig:Neck",
|
||||||
|
"mixamorig:RightArm",
|
||||||
|
"mixamorig:RightFoot",
|
||||||
|
"mixamorig:RightForeArm",
|
||||||
|
"mixamorig:RightHand",
|
||||||
|
"mixamorig:RightLeg",
|
||||||
|
"mixamorig:RightShoulder",
|
||||||
|
"mixamorig:RightToeBase",
|
||||||
|
"mixamorig:RightUpLeg",
|
||||||
|
"mixamorig:Spine",
|
||||||
|
"mixamorig:Spine1",
|
||||||
|
"mixamorig:Spine2"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Anime Girl Rigged Anime model",
|
||||||
|
"uid": "fbccf5c5a7b244e7ab04fa44da19c621",
|
||||||
|
"author": "dequeijospizza",
|
||||||
|
"author_url": "https://sketchfab.com/dequeijospizza",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/anime-girl-rigged-anime-model-fbccf5c5a7b244e7ab04fa44da19c621"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 259 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "Kiyoko School Girl",
|
||||||
|
"uid": "072667d7b2b3468e9baff483b27c3a09",
|
||||||
|
"author": "Kasujin",
|
||||||
|
"author_url": "https://sketchfab.com/Kasujin",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/kiyoko-school-girl-072667d7b2b3468e9baff483b27c3a09"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 896 B |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 256 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Universal Animation Library \u2014 Mannequin",
|
||||||
|
"author": "Quaternius",
|
||||||
|
"license": "CC0 1.0 Universal (Public Domain Dedication)",
|
||||||
|
"url": "https://quaternius.com/",
|
||||||
|
"source": "assets/characters/animations/_library.glb",
|
||||||
|
"note": "The reference mannequin shipped inside the animation library this project already uses. No attribution required under CC0; recorded anyway, and because the library's own LICENSE asks that Quaternius be credited."
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
{
|
||||||
|
"roles": {
|
||||||
|
"hips": "DEF-hips",
|
||||||
|
"head": "DEF-head",
|
||||||
|
"neck": "DEF-neck",
|
||||||
|
"spine": [
|
||||||
|
"DEF-spine.001",
|
||||||
|
"DEF-spine.002",
|
||||||
|
"DEF-spine.003",
|
||||||
|
"DEF-neck",
|
||||||
|
"DEF-head"
|
||||||
|
],
|
||||||
|
"shoulder.L": "DEF-shoulder.L",
|
||||||
|
"shoulder.R": "DEF-shoulder.R",
|
||||||
|
"upper_arm.L": "DEF-upper_arm.L",
|
||||||
|
"upper_arm.R": "DEF-upper_arm.R",
|
||||||
|
"forearm.R": "DEF-forearm.R",
|
||||||
|
"forearm.L": "DEF-forearm.L",
|
||||||
|
"thigh.L": "DEF-thigh.L",
|
||||||
|
"thigh.R": "DEF-thigh.R",
|
||||||
|
"foot.R": "DEF-foot.R",
|
||||||
|
"foot.L": "DEF-foot.L",
|
||||||
|
"shin.L": "DEF-shin.L",
|
||||||
|
"shin.R": "DEF-shin.R",
|
||||||
|
"hand.R": "DEF-hand.R",
|
||||||
|
"hand.L": "DEF-hand.L",
|
||||||
|
"toe.L": "DEF-toe.L",
|
||||||
|
"toe.R": "DEF-toe.R"
|
||||||
|
},
|
||||||
|
"fingers": {
|
||||||
|
"thumb.L": [
|
||||||
|
"DEF-thumb.01.L",
|
||||||
|
"DEF-thumb.02.L",
|
||||||
|
"DEF-thumb.03.L"
|
||||||
|
],
|
||||||
|
"index.L": [
|
||||||
|
"DEF-f_index.01.L",
|
||||||
|
"DEF-f_index.02.L",
|
||||||
|
"DEF-f_index.03.L"
|
||||||
|
],
|
||||||
|
"middle.L": [
|
||||||
|
"DEF-f_middle.01.L",
|
||||||
|
"DEF-f_middle.02.L",
|
||||||
|
"DEF-f_middle.03.L"
|
||||||
|
],
|
||||||
|
"ring.L": [
|
||||||
|
"DEF-f_ring.01.L",
|
||||||
|
"DEF-f_ring.02.L",
|
||||||
|
"DEF-f_ring.03.L"
|
||||||
|
],
|
||||||
|
"pinky.L": [
|
||||||
|
"DEF-f_pinky.01.L",
|
||||||
|
"DEF-f_pinky.02.L",
|
||||||
|
"DEF-f_pinky.03.L"
|
||||||
|
],
|
||||||
|
"thumb.R": [
|
||||||
|
"DEF-thumb.01.R",
|
||||||
|
"DEF-thumb.02.R",
|
||||||
|
"DEF-thumb.03.R"
|
||||||
|
],
|
||||||
|
"index.R": [
|
||||||
|
"DEF-f_index.01.R",
|
||||||
|
"DEF-f_index.02.R",
|
||||||
|
"DEF-f_index.03.R"
|
||||||
|
],
|
||||||
|
"middle.R": [
|
||||||
|
"DEF-f_middle.01.R",
|
||||||
|
"DEF-f_middle.02.R",
|
||||||
|
"DEF-f_middle.03.R"
|
||||||
|
],
|
||||||
|
"ring.R": [
|
||||||
|
"DEF-f_ring.01.R",
|
||||||
|
"DEF-f_ring.02.R",
|
||||||
|
"DEF-f_ring.03.R"
|
||||||
|
],
|
||||||
|
"pinky.R": [
|
||||||
|
"DEF-f_pinky.01.R",
|
||||||
|
"DEF-f_pinky.02.R",
|
||||||
|
"DEF-f_pinky.03.R"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"chains": [],
|
||||||
|
"twist": [],
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"mesh": "Mannequin",
|
||||||
|
"surface": 0,
|
||||||
|
"material": "M_Main",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "skin",
|
||||||
|
"why": "no name or weight evidence \u2014 treated as body",
|
||||||
|
"verts": 3391,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Mannequin",
|
||||||
|
"surface": 1,
|
||||||
|
"material": "M_Joints",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "skin",
|
||||||
|
"why": "no name or weight evidence \u2014 treated as body",
|
||||||
|
"verts": 5157,
|
||||||
|
"textured": false,
|
||||||
|
"chain_share": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"colliders": [
|
||||||
|
{
|
||||||
|
"bone": "DEF-hips",
|
||||||
|
"child": "DEF-spine.001",
|
||||||
|
"from": 0.0,
|
||||||
|
"radius_head": 0.1381,
|
||||||
|
"radius_tail": 0.1381,
|
||||||
|
"radius": 0.1381,
|
||||||
|
"lid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-thigh.L",
|
||||||
|
"child": "DEF-shin.L",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1037,
|
||||||
|
"radius_tail": 0.0733,
|
||||||
|
"radius": 0.0733
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-thigh.R",
|
||||||
|
"child": "DEF-shin.R",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1037,
|
||||||
|
"radius_tail": 0.0733,
|
||||||
|
"radius": 0.0733
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-shin.L",
|
||||||
|
"child": "DEF-foot.L",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0927,
|
||||||
|
"radius_tail": 0.0516,
|
||||||
|
"radius": 0.0516
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-shin.R",
|
||||||
|
"child": "DEF-foot.R",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0927,
|
||||||
|
"radius_tail": 0.0516,
|
||||||
|
"radius": 0.0516
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weights_authored": true,
|
||||||
|
"driven_bones": [
|
||||||
|
"DEF-f_index.01.L",
|
||||||
|
"DEF-f_index.01.R",
|
||||||
|
"DEF-f_index.02.L",
|
||||||
|
"DEF-f_index.02.R",
|
||||||
|
"DEF-f_index.03.L",
|
||||||
|
"DEF-f_index.03.R",
|
||||||
|
"DEF-f_middle.01.L",
|
||||||
|
"DEF-f_middle.01.R",
|
||||||
|
"DEF-f_middle.02.L",
|
||||||
|
"DEF-f_middle.02.R",
|
||||||
|
"DEF-f_middle.03.L",
|
||||||
|
"DEF-f_middle.03.R",
|
||||||
|
"DEF-f_pinky.01.L",
|
||||||
|
"DEF-f_pinky.01.R",
|
||||||
|
"DEF-f_pinky.02.L",
|
||||||
|
"DEF-f_pinky.02.R",
|
||||||
|
"DEF-f_pinky.03.L",
|
||||||
|
"DEF-f_pinky.03.R",
|
||||||
|
"DEF-f_ring.01.L",
|
||||||
|
"DEF-f_ring.01.R",
|
||||||
|
"DEF-f_ring.02.L",
|
||||||
|
"DEF-f_ring.02.R",
|
||||||
|
"DEF-f_ring.03.L",
|
||||||
|
"DEF-f_ring.03.R",
|
||||||
|
"DEF-foot.L",
|
||||||
|
"DEF-foot.R",
|
||||||
|
"DEF-forearm.L",
|
||||||
|
"DEF-forearm.R",
|
||||||
|
"DEF-hand.L",
|
||||||
|
"DEF-hand.R",
|
||||||
|
"DEF-head",
|
||||||
|
"DEF-hips",
|
||||||
|
"DEF-neck",
|
||||||
|
"DEF-shin.L",
|
||||||
|
"DEF-shin.R",
|
||||||
|
"DEF-shoulder.L",
|
||||||
|
"DEF-shoulder.R",
|
||||||
|
"DEF-spine.001",
|
||||||
|
"DEF-spine.002",
|
||||||
|
"DEF-spine.003",
|
||||||
|
"DEF-thigh.L",
|
||||||
|
"DEF-thigh.R",
|
||||||
|
"DEF-thumb.01.L",
|
||||||
|
"DEF-thumb.01.R",
|
||||||
|
"DEF-thumb.02.L",
|
||||||
|
"DEF-thumb.02.R",
|
||||||
|
"DEF-thumb.03.L",
|
||||||
|
"DEF-thumb.03.R",
|
||||||
|
"DEF-toe.L",
|
||||||
|
"DEF-toe.R",
|
||||||
|
"DEF-upper_arm.L",
|
||||||
|
"DEF-upper_arm.R",
|
||||||
|
"root"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
{
|
||||||
|
"roles": {
|
||||||
|
"hips": "DEF-hips",
|
||||||
|
"head": "DEF-head",
|
||||||
|
"neck": "DEF-neck",
|
||||||
|
"spine": [
|
||||||
|
"DEF-spine.001",
|
||||||
|
"DEF-spine.002",
|
||||||
|
"DEF-spine.003",
|
||||||
|
"DEF-neck",
|
||||||
|
"DEF-head"
|
||||||
|
],
|
||||||
|
"shoulder.L": "DEF-shoulder.L",
|
||||||
|
"shoulder.R": "DEF-shoulder.R",
|
||||||
|
"upper_arm.L": "DEF-upper_arm.L",
|
||||||
|
"upper_arm.R": "DEF-upper_arm.R",
|
||||||
|
"forearm.L": "DEF-forearm.L",
|
||||||
|
"forearm.R": "DEF-forearm.R",
|
||||||
|
"thigh.L": "DEF-thigh.L",
|
||||||
|
"thigh.R": "DEF-thigh.R",
|
||||||
|
"foot.R": "DEF-foot.R",
|
||||||
|
"foot.L": "DEF-foot.L",
|
||||||
|
"shin.R": "DEF-shin.R",
|
||||||
|
"shin.L": "DEF-shin.L",
|
||||||
|
"hand.L": "DEF-hand.L",
|
||||||
|
"hand.R": "DEF-hand.R",
|
||||||
|
"toe.R": "DEF-toe.R",
|
||||||
|
"toe.L": "DEF-toe.L"
|
||||||
|
},
|
||||||
|
"fingers": {
|
||||||
|
"thumb.L": [
|
||||||
|
"DEF-thumb.01.L",
|
||||||
|
"DEF-thumb.02.L",
|
||||||
|
"DEF-thumb.03.L"
|
||||||
|
],
|
||||||
|
"index.L": [
|
||||||
|
"DEF-f_index.01.L",
|
||||||
|
"DEF-f_index.02.L",
|
||||||
|
"DEF-f_index.03.L"
|
||||||
|
],
|
||||||
|
"middle.L": [
|
||||||
|
"DEF-f_middle.01.L",
|
||||||
|
"DEF-f_middle.02.L",
|
||||||
|
"DEF-f_middle.03.L"
|
||||||
|
],
|
||||||
|
"ring.L": [
|
||||||
|
"DEF-f_ring.01.L",
|
||||||
|
"DEF-f_ring.02.L",
|
||||||
|
"DEF-f_ring.03.L"
|
||||||
|
],
|
||||||
|
"pinky.L": [
|
||||||
|
"DEF-f_pinky.01.L",
|
||||||
|
"DEF-f_pinky.02.L",
|
||||||
|
"DEF-f_pinky.03.L"
|
||||||
|
],
|
||||||
|
"thumb.R": [
|
||||||
|
"DEF-thumb.01.R",
|
||||||
|
"DEF-thumb.02.R",
|
||||||
|
"DEF-thumb.03.R"
|
||||||
|
],
|
||||||
|
"index.R": [
|
||||||
|
"DEF-f_index.01.R",
|
||||||
|
"DEF-f_index.02.R",
|
||||||
|
"DEF-f_index.03.R"
|
||||||
|
],
|
||||||
|
"middle.R": [
|
||||||
|
"DEF-f_middle.01.R",
|
||||||
|
"DEF-f_middle.02.R",
|
||||||
|
"DEF-f_middle.03.R"
|
||||||
|
],
|
||||||
|
"ring.R": [
|
||||||
|
"DEF-f_ring.01.R",
|
||||||
|
"DEF-f_ring.02.R",
|
||||||
|
"DEF-f_ring.03.R"
|
||||||
|
],
|
||||||
|
"pinky.R": [
|
||||||
|
"DEF-f_pinky.01.R",
|
||||||
|
"DEF-f_pinky.02.R",
|
||||||
|
"DEF-f_pinky.03.R"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"chains": [],
|
||||||
|
"twist": [],
|
||||||
|
"surfaces": [
|
||||||
|
{
|
||||||
|
"mesh": "Object_2",
|
||||||
|
"surface": 0,
|
||||||
|
"material": "body",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "skin",
|
||||||
|
"why": "material name says 'skin'",
|
||||||
|
"verts": 1500,
|
||||||
|
"textured": true,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Object_2",
|
||||||
|
"surface": 1,
|
||||||
|
"material": "body_parts",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "skin",
|
||||||
|
"why": "material name says 'skin'",
|
||||||
|
"verts": 330,
|
||||||
|
"textured": true,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Object_2",
|
||||||
|
"surface": 2,
|
||||||
|
"material": "hair",
|
||||||
|
"class": "hair",
|
||||||
|
"detail": "hair",
|
||||||
|
"why": "material name says 'hair'",
|
||||||
|
"verts": 500,
|
||||||
|
"textured": true,
|
||||||
|
"chain_share": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mesh": "Object_2",
|
||||||
|
"surface": 3,
|
||||||
|
"material": "face",
|
||||||
|
"class": "body",
|
||||||
|
"detail": "face",
|
||||||
|
"why": "material name says 'face'",
|
||||||
|
"verts": 461,
|
||||||
|
"textured": true,
|
||||||
|
"chain_share": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"colliders": [
|
||||||
|
{
|
||||||
|
"bone": "DEF-hips",
|
||||||
|
"child": "DEF-spine.001",
|
||||||
|
"from": 0.0,
|
||||||
|
"radius_head": 0.1915,
|
||||||
|
"radius_tail": 0.1915,
|
||||||
|
"radius": 0.1915,
|
||||||
|
"lid": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-thigh.L",
|
||||||
|
"child": "DEF-shin.L",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1435,
|
||||||
|
"radius_tail": 0.1304,
|
||||||
|
"radius": 0.1304
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-thigh.R",
|
||||||
|
"child": "DEF-shin.R",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.1084,
|
||||||
|
"radius_tail": 0.1084,
|
||||||
|
"radius": 0.1084
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-shin.L",
|
||||||
|
"child": "DEF-foot.L",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0862,
|
||||||
|
"radius_tail": 0.0728,
|
||||||
|
"radius": 0.0728
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bone": "DEF-shin.R",
|
||||||
|
"child": "DEF-foot.R",
|
||||||
|
"from": 0.1,
|
||||||
|
"radius_head": 0.0862,
|
||||||
|
"radius_tail": 0.0728,
|
||||||
|
"radius": 0.0728
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"weights_authored": false,
|
||||||
|
"driven_bones": [
|
||||||
|
"DEF-f_index.01.L",
|
||||||
|
"DEF-f_index.01.R",
|
||||||
|
"DEF-f_index.02.L",
|
||||||
|
"DEF-f_index.02.R",
|
||||||
|
"DEF-f_index.03.L",
|
||||||
|
"DEF-f_index.03.R",
|
||||||
|
"DEF-f_middle.01.L",
|
||||||
|
"DEF-f_middle.01.R",
|
||||||
|
"DEF-f_middle.02.L",
|
||||||
|
"DEF-f_middle.02.R",
|
||||||
|
"DEF-f_middle.03.L",
|
||||||
|
"DEF-f_middle.03.R",
|
||||||
|
"DEF-f_pinky.01.L",
|
||||||
|
"DEF-f_pinky.01.R",
|
||||||
|
"DEF-f_pinky.02.L",
|
||||||
|
"DEF-f_pinky.02.R",
|
||||||
|
"DEF-f_pinky.03.L",
|
||||||
|
"DEF-f_pinky.03.R",
|
||||||
|
"DEF-f_ring.01.L",
|
||||||
|
"DEF-f_ring.01.R",
|
||||||
|
"DEF-f_ring.02.L",
|
||||||
|
"DEF-f_ring.02.R",
|
||||||
|
"DEF-f_ring.03.L",
|
||||||
|
"DEF-f_ring.03.R",
|
||||||
|
"DEF-foot.L",
|
||||||
|
"DEF-foot.R",
|
||||||
|
"DEF-forearm.L",
|
||||||
|
"DEF-forearm.R",
|
||||||
|
"DEF-hand.L",
|
||||||
|
"DEF-hand.R",
|
||||||
|
"DEF-head",
|
||||||
|
"DEF-hips",
|
||||||
|
"DEF-neck",
|
||||||
|
"DEF-shin.L",
|
||||||
|
"DEF-shin.R",
|
||||||
|
"DEF-shoulder.L",
|
||||||
|
"DEF-shoulder.R",
|
||||||
|
"DEF-spine.001",
|
||||||
|
"DEF-spine.002",
|
||||||
|
"DEF-spine.003",
|
||||||
|
"DEF-thigh.L",
|
||||||
|
"DEF-thigh.R",
|
||||||
|
"DEF-thumb.01.L",
|
||||||
|
"DEF-thumb.01.R",
|
||||||
|
"DEF-thumb.02.L",
|
||||||
|
"DEF-thumb.02.R",
|
||||||
|
"DEF-thumb.03.L",
|
||||||
|
"DEF-thumb.03.R",
|
||||||
|
"DEF-toe.L",
|
||||||
|
"DEF-toe.R",
|
||||||
|
"DEF-upper_arm.L",
|
||||||
|
"DEF-upper_arm.R",
|
||||||
|
"root"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "DANDADAN - Momo Ayase (3D Model) + DL",
|
||||||
|
"uid": "6ac6c3476a1f4b8da1c7de7e98a7c83c",
|
||||||
|
"author": "HiGuys920",
|
||||||
|
"author_url": "https://sketchfab.com/higuys920",
|
||||||
|
"license": "CC Attribution",
|
||||||
|
"license_slug": "by",
|
||||||
|
"source_url": "https://sketchfab.com/3d-models/dandadan-momo-ayase-3d-model-dl-6ac6c3476a1f4b8da1c7de7e98a7c83c"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "3D Anime Female Adventurer Character Model",
|
||||||
|
"version": "1.2",
|
||||||
|
"author": "Dawn to Dusk Games",
|
||||||
|
"author_url": "https://dawn-to-dusk-games.itch.io/",
|
||||||
|
"license": "CC0 1.0 Universal",
|
||||||
|
"license_url": "https://creativecommons.org/publicdomain/zero/1.0/",
|
||||||
|
"source_url": "https://dawn-to-dusk-games.itch.io/3d-anime-female-adventurer",
|
||||||
|
"changes": "Outfit 01 retargeted to the Papaya-Shooter animation library; game-native mecha equipment added at runtime."
|
||||||
|
}
|
||||||
@@ -5,14 +5,96 @@
|
|||||||
"name": "Miku",
|
"name": "Miku",
|
||||||
"description": "Hatsune Miku \u2014 Virtual Idol",
|
"description": "Hatsune Miku \u2014 Virtual Idol",
|
||||||
"model": "res://assets/characters/skins/miku.glb",
|
"model": "res://assets/characters/skins/miku.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#168F98",
|
||||||
|
"accent": "#252632",
|
||||||
|
"skin": "#F5CDBB",
|
||||||
|
"glove": "#20212C",
|
||||||
|
"nail": "#20A9B1",
|
||||||
|
"hand_style": "fingerless",
|
||||||
|
"sleeve_ratio": 0.78
|
||||||
|
},
|
||||||
"unlocked": true
|
"unlocked": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "taila",
|
"id": "taila",
|
||||||
"name": "Taila",
|
"name": "Taila",
|
||||||
"description": "Original anime character by Partaevil (CC-BY)",
|
"description": "",
|
||||||
"model": "res://assets/characters/skins/taila.glb",
|
"model": "res://assets/characters/skins/taila.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#171D32",
|
||||||
|
"accent": "#E8772D",
|
||||||
|
"skin": "#EFC6B7",
|
||||||
|
"glove": "#151826",
|
||||||
|
"nail": "#E8772D",
|
||||||
|
"hand_style": "glove",
|
||||||
|
"sleeve_ratio": 0.78
|
||||||
|
},
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mannequin",
|
||||||
|
"name": "Mannequin",
|
||||||
|
"description": "Quaternius reference mannequin (CC0)",
|
||||||
|
"model": "res://assets/characters/skins/mannequin.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#D9692D",
|
||||||
|
"accent": "#6D4C9B",
|
||||||
|
"skin": "#D9692D",
|
||||||
|
"glove": "#6D4C9B",
|
||||||
|
"nail": "#F0A14B",
|
||||||
|
"hand_style": "android",
|
||||||
|
"sleeve_ratio": 0.84
|
||||||
|
},
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kiyoko",
|
||||||
|
"name": "Kiyoko",
|
||||||
|
"description": "Kiyoko School Girl \u2014 VRoid (CC-BY, Kasujin)",
|
||||||
|
"model": "res://assets/characters/skins/kiyoko.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#ECE7E3",
|
||||||
|
"accent": "#253151",
|
||||||
|
"skin": "#F0C6B2",
|
||||||
|
"glove": "#253151",
|
||||||
|
"nail": "#E9A9B4",
|
||||||
|
"hand_style": "bare",
|
||||||
|
"sleeve_ratio": 0.32
|
||||||
|
},
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "aria",
|
||||||
|
"name": "Aria",
|
||||||
|
"description": "Anime Girl Rigged \u2014 VRoid (CC-BY, dequeijospizza)",
|
||||||
|
"model": "res://assets/characters/skins/aria.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#E8E8E5",
|
||||||
|
"accent": "#27252D",
|
||||||
|
"skin": "#EFC7B7",
|
||||||
|
"glove": "#27252D",
|
||||||
|
"nail": "#DFA0AE",
|
||||||
|
"hand_style": "bare",
|
||||||
|
"sleeve_ratio": 0.30
|
||||||
|
},
|
||||||
|
"unlocked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "momo",
|
||||||
|
"name": "Momo",
|
||||||
|
"description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)",
|
||||||
|
"model": "res://assets/characters/skins/momo.glb",
|
||||||
|
"viewmodel": {
|
||||||
|
"sleeve": "#D66F98",
|
||||||
|
"accent": "#34303A",
|
||||||
|
"skin": "#F0C5B2",
|
||||||
|
"glove": "#34303A",
|
||||||
|
"nail": "#D66F98",
|
||||||
|
"hand_style": "fingerless",
|
||||||
|
"sleeve_ratio": 0.80
|
||||||
|
},
|
||||||
"unlocked": true
|
"unlocked": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"skins": {
|
||||||
|
"aria": {
|
||||||
|
"ak47": {
|
||||||
|
"curl_thumb": 0.0,
|
||||||
|
"curl_trigger": 2.0,
|
||||||
|
"curl_wrap": 0.655,
|
||||||
|
"fore_shift": [
|
||||||
|
-0.103000000119209,
|
||||||
|
0.034000001847744,
|
||||||
|
0.017000000923872
|
||||||
|
],
|
||||||
|
"grip_shift": [
|
||||||
|
0.0199999995529652,
|
||||||
|
-0.145999997854233,
|
||||||
|
-0.00899999961256981
|
||||||
|
],
|
||||||
|
"gun_fore": 0.157,
|
||||||
|
"gun_stock": 0.06,
|
||||||
|
"pitch_hip": 0.0610000000000001,
|
||||||
|
"pocket_hip": [
|
||||||
|
0.00700000021606684,
|
||||||
|
-0.146999999880791,
|
||||||
|
0.0329999998211861
|
||||||
|
],
|
||||||
|
"pole_l_hip": [
|
||||||
|
0.493999987840652,
|
||||||
|
-0.433999985456467,
|
||||||
|
-0.18299999833107
|
||||||
|
],
|
||||||
|
"pole_r_hip": [
|
||||||
|
-0.114000000059605,
|
||||||
|
-0.25900000333786,
|
||||||
|
-0.526000022888184
|
||||||
|
],
|
||||||
|
"weapon_scale": 0.53,
|
||||||
|
"wrist_l_hip": [
|
||||||
|
-1.307000041008,
|
||||||
|
-0.0480000004172325,
|
||||||
|
0.0410000011324883
|
||||||
|
],
|
||||||
|
"wrist_r_hip": [
|
||||||
|
-0.421999990940094,
|
||||||
|
1.60000002384186,
|
||||||
|
1.28299999237061
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,96 @@
|
|||||||
|
#ifndef ANIME_CLOUDS_INCLUDED
|
||||||
|
#define ANIME_CLOUDS_INCLUDED
|
||||||
|
|
||||||
|
// Organic 2D cloud field shared by the actual Sky shader and the fullscreen
|
||||||
|
// ink composite. Broad domain-warped masses define the cloud, a second rotated
|
||||||
|
// octave breaks up its contour into billows, and a quiet high-frequency layer
|
||||||
|
// erodes the edge. Unlike a grid of radial primitives, no stage exposes a
|
||||||
|
// circle or a square as the final silhouette.
|
||||||
|
|
||||||
|
float cloud_hash(vec2 p) {
|
||||||
|
// Sine-free hash: substantially cheaper when the field is sampled several
|
||||||
|
// times to estimate the direction the sun meets the cloud.
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float cloud_noise(vec2 p) {
|
||||||
|
vec2 i = floor(p);
|
||||||
|
vec2 f = fract(p);
|
||||||
|
// Quintic interpolation keeps both the value and its slope continuous at
|
||||||
|
// cell boundaries, so a density threshold cannot reveal the noise grid.
|
||||||
|
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
|
||||||
|
float a = cloud_hash(i);
|
||||||
|
float b = cloud_hash(i + vec2(1.0, 0.0));
|
||||||
|
float c = cloud_hash(i + vec2(0.0, 1.0));
|
||||||
|
float d = cloud_hash(i + vec2(1.0, 1.0));
|
||||||
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float cloud_fbm(vec2 p) {
|
||||||
|
mat2 turn = mat2(vec2(0.80, 0.60), vec2(-0.60, 0.80));
|
||||||
|
float value = cloud_noise(p) * 0.52;
|
||||||
|
p = turn * p * 2.03 + vec2(9.17, 4.31);
|
||||||
|
value += cloud_noise(p) * 0.27;
|
||||||
|
p = turn * p * 2.07 + vec2(3.73, 12.61);
|
||||||
|
value += cloud_noise(p) * 0.14;
|
||||||
|
p = turn * p * 2.01 + vec2(15.19, 7.07);
|
||||||
|
value += cloud_noise(p) * 0.07;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float cloud_detail(vec2 p) {
|
||||||
|
mat2 turn = mat2(vec2(0.86, 0.51), vec2(-0.51, 0.86));
|
||||||
|
return cloud_noise(turn * p * 2.75 + vec2(23.7, 6.1)) * 0.68
|
||||||
|
+ cloud_noise(p * 5.20 + vec2(2.9, 31.4)) * 0.32;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float cloud_field(vec2 p) {
|
||||||
|
// Low-frequency vector warp makes the mass curl and fork without making
|
||||||
|
// the outline busy. The two components use unrelated offsets so this does
|
||||||
|
// not merely slide the source noise along its own contour.
|
||||||
|
vec2 domain = p * 0.34;
|
||||||
|
vec2 warp = vec2(
|
||||||
|
cloud_noise(domain + vec2(17.2, 4.8)),
|
||||||
|
cloud_noise(domain + vec2(3.1, 29.6))) - 0.5;
|
||||||
|
vec2 q = p + warp * 1.35;
|
||||||
|
|
||||||
|
float mass = cloud_fbm(q * 0.52);
|
||||||
|
float billows = cloud_fbm(
|
||||||
|
mat2(vec2(0.74, 0.67), vec2(-0.67, 0.74)) * q * 1.18
|
||||||
|
+ vec2(11.3, 8.7));
|
||||||
|
float erosion = cloud_detail(q);
|
||||||
|
float density = mass * 0.70 + billows * 0.24 + erosion * 0.06;
|
||||||
|
|
||||||
|
// A third, much broader field separates the weather into distinct banks.
|
||||||
|
// Without this macro mask a perfectly organic contour can still connect
|
||||||
|
// across half the dome and read as one enormous sheet instead of clouds.
|
||||||
|
float islands = cloud_fbm(q * 0.24 + vec2(41.7, 18.3));
|
||||||
|
return density - (1.0 - islands) * 0.16;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float cloud_sun_response(vec2 p, vec3 sun_direction) {
|
||||||
|
// Treat density as a painted height field. Sampling a short distance toward
|
||||||
|
// and away from the sun tells us which edge faces it. This gives the cloud
|
||||||
|
// a bright leading rim and a colored lee side, and naturally reverses when
|
||||||
|
// the DirectionalLight turns.
|
||||||
|
vec2 sun_xz = sun_direction.xz;
|
||||||
|
float horizontal = length(sun_xz);
|
||||||
|
vec2 axis = horizontal > 0.0001
|
||||||
|
? sun_xz / horizontal : vec2(0.7071, 0.7071);
|
||||||
|
float toward = cloud_field(p + axis * 0.075);
|
||||||
|
float away = cloud_field(p - axis * 0.075);
|
||||||
|
float directional_slope = (away - toward) / 0.15;
|
||||||
|
// A high sun lights more of the cloud face; a low sun makes the directional
|
||||||
|
// division stronger and leaves a broader colored underside.
|
||||||
|
float overhead = clamp(sun_direction.y, 0.0, 1.0);
|
||||||
|
return smoothstep(-0.22, 0.22, directional_slope + overhead * 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbh0unyul1h7d
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
shader_type sky;
|
||||||
|
|
||||||
|
// Cel sky: a painted gradient with rounded, flat-toned anime clouds.
|
||||||
|
//
|
||||||
|
// ProceduralSkyMaterial gives a smooth two-colour ramp, and a smooth ramp over
|
||||||
|
// 40% of the frame is exactly what tools/levels.py was reporting as dead%:
|
||||||
|
// somewhere between a quarter and a third of every shot was a single flat
|
||||||
|
// shade with nothing in it. Sky is the cheapest real estate in the picture and
|
||||||
|
// it was empty.
|
||||||
|
//
|
||||||
|
// The clouds are drawn the way an anime background artist draws them — a
|
||||||
|
// silhouette filled with two or three flat tones and a crisp edge, NOT a soft
|
||||||
|
// volumetric puff. That is both the correct style and the cheap option.
|
||||||
|
|
||||||
|
uniform vec3 top_color : source_color = vec3(0.18, 0.40, 0.85);
|
||||||
|
uniform vec3 horizon_color : source_color = vec3(0.72, 0.88, 0.98);
|
||||||
|
uniform vec3 ground_color : source_color = vec3(0.24, 0.22, 0.30);
|
||||||
|
uniform vec3 cloud_color : source_color = vec3(1.0, 0.99, 0.97);
|
||||||
|
uniform vec3 cloud_shadow : source_color = vec3(0.72, 0.75, 0.88);
|
||||||
|
// Raise to thin the clouds out. This is a threshold on the noise, so it is the
|
||||||
|
// knob for "how much sky is covered", not a density.
|
||||||
|
uniform float cloud_cover : hint_range(0.0, 1.0) = 0.52;
|
||||||
|
uniform float cloud_scale : hint_range(0.2, 8.0) = 1.6;
|
||||||
|
uniform float cloud_softness : hint_range(0.001, 0.2) = 0.035;
|
||||||
|
// Slow. This is a competitive shooter and anything the eye can track in the
|
||||||
|
// sky is a distraction; it only needs to move enough that the sky is not a
|
||||||
|
// photograph.
|
||||||
|
uniform float drift : hint_range(0.0, 0.02) = 0.0022;
|
||||||
|
uniform float horizon_falloff : hint_range(0.1, 2.0) = 0.45;
|
||||||
|
uniform float sun_halo : hint_range(0.0, 2.0) = 0.35;
|
||||||
|
|
||||||
|
|
||||||
|
#include "res://assets/shaders/anime_clouds.gdshaderinc"
|
||||||
|
|
||||||
|
|
||||||
|
void sky() {
|
||||||
|
float up = clamp(EYEDIR.y, 0.0, 1.0);
|
||||||
|
float down = clamp(-EYEDIR.y, 0.0, 1.0);
|
||||||
|
vec3 col = EYEDIR.y >= 0.0
|
||||||
|
? mix(horizon_color, top_color, pow(up, horizon_falloff))
|
||||||
|
: mix(horizon_color, ground_color, pow(down, 0.62));
|
||||||
|
|
||||||
|
// Soft halo around the sun. The procedural sky drew a disk here; a broad
|
||||||
|
// glow reads better under the filmic shoulder and does not clip.
|
||||||
|
if (LIGHT0_ENABLED) {
|
||||||
|
float d = clamp(dot(EYEDIR, LIGHT0_DIRECTION), 0.0, 1.0);
|
||||||
|
col += LIGHT0_COLOR * pow(d, 24.0) * sun_halo;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EYEDIR.y > 0.005) {
|
||||||
|
// Project the dome onto a plane overhead, so clouds compress toward the
|
||||||
|
// horizon the way real ones do instead of smearing across the zenith.
|
||||||
|
vec2 uv = EYEDIR.xz / (EYEDIR.y + 0.18) * cloud_scale + vec2(TIME * drift, 0.0);
|
||||||
|
float n = cloud_field(uv);
|
||||||
|
|
||||||
|
// The edge remains clean enough for the anime style, but it follows a
|
||||||
|
// layered organic field instead of revealing primitive circles.
|
||||||
|
float edge = max(cloud_softness, fwidth(n) * 1.35);
|
||||||
|
float shape = smoothstep(cloud_cover - edge, cloud_cover + edge, n);
|
||||||
|
vec3 sun_direction = LIGHT0_ENABLED
|
||||||
|
? normalize(LIGHT0_DIRECTION)
|
||||||
|
: normalize(vec3(0.45, 0.55, -0.70));
|
||||||
|
vec3 sun_color = LIGHT0_ENABLED
|
||||||
|
? LIGHT0_COLOR : vec3(1.0, 0.91, 0.72);
|
||||||
|
float facing = cloud_sun_response(uv, sun_direction);
|
||||||
|
float core = smoothstep(
|
||||||
|
cloud_cover + 0.045 - edge,
|
||||||
|
cloud_cover + 0.155 + edge, n);
|
||||||
|
float carving = smoothstep(
|
||||||
|
0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7)));
|
||||||
|
// Three painted tones respond to the actual DirectionalLight: a colored
|
||||||
|
// lee side, a broad lit face, and a narrow sunward silver lining.
|
||||||
|
float lit_face = clamp(
|
||||||
|
0.12 + core * 0.25 + facing * 0.58 + carving * 0.05,
|
||||||
|
0.0, 1.0);
|
||||||
|
vec3 lit_color = mix(cloud_color, sun_color, 0.18);
|
||||||
|
vec3 cloud = mix(cloud_shadow, lit_color, lit_face);
|
||||||
|
float boundary = 1.0 - smoothstep(
|
||||||
|
cloud_cover + edge, cloud_cover + 0.105 + edge, n);
|
||||||
|
float silver_lining = boundary * facing * facing
|
||||||
|
* mix(0.10, 0.24, clamp(sun_direction.y, 0.0, 1.0));
|
||||||
|
cloud += sun_color * silver_lining;
|
||||||
|
|
||||||
|
// Fade out near the horizon, where the projection stretches a cloud
|
||||||
|
// into an infinitely long streak.
|
||||||
|
col = mix(col, cloud, shape * smoothstep(0.0, 0.22, EYEDIR.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
COLOR = col;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cx2rt7638txa1
|
||||||
@@ -1,57 +1,314 @@
|
|||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
render_mode unshaded, fog_disabled, depth_draw_never, depth_test_disabled, cull_disabled;
|
render_mode unshaded, fog_disabled, depth_draw_never, depth_test_disabled, cull_disabled;
|
||||||
|
|
||||||
// Screen-space ink outline: detects depth and normal discontinuities and
|
// ── The 3D-to-2D pass ────────────────────────────────────────────────────────
|
||||||
// draws thin dark edges over the scene — the drawn-line pass that unifies
|
//
|
||||||
// every model (kit, hero, character) into the cel look. Attach to a
|
// One fullscreen quad doing three jobs, in this order:
|
||||||
// fullscreen quad (POSITION override) so it runs for whatever camera renders.
|
//
|
||||||
|
// sky repaint the background, because this pass owns those pixels
|
||||||
|
// ink screen-space line work from the depth buffer
|
||||||
|
// grade the anime split-tone
|
||||||
|
//
|
||||||
|
// ## Lines come from a SECOND DIFFERENCE of linearised depth
|
||||||
|
//
|
||||||
|
// This is the change that matters, and it is worth spelling out because the
|
||||||
|
// previous version of this file did the usual thing and it was wrong:
|
||||||
|
//
|
||||||
|
// float ddiff = abs(dl - dr) + abs(du - dd); // <- first difference
|
||||||
|
//
|
||||||
|
// A first difference measures the SLOPE of the depth buffer, and slope is large
|
||||||
|
// wherever a surface is oblique to the camera — which the ground always is. So
|
||||||
|
// the road, the pavement, every roof and every long wall seen at an angle all
|
||||||
|
// carried ink, and the threshold had to be raised until it smeared rather than
|
||||||
|
// drew. Raising it then lost the real silhouettes at distance, which is why
|
||||||
|
// `depth_threshold` had to be scaled by `dc` to stop far geometry dissolving
|
||||||
|
// into noise: the whole formula was fighting itself.
|
||||||
|
//
|
||||||
|
// A second difference measures CURVATURE, and curvature is exactly zero across
|
||||||
|
// any planar surface no matter how oblique it is to the camera. So it fires on
|
||||||
|
// real silhouettes and real creases and on nothing else — a flat road grazing
|
||||||
|
// the camera produces literally no signal, and the threshold can therefore be
|
||||||
|
// set low enough to catch a distant railing.
|
||||||
|
//
|
||||||
|
// The two signs are kept apart and drawn differently, which is also from the
|
||||||
|
// reference: positive curvature (the near side of a silhouette, a convex ridge)
|
||||||
|
// inks strongly, negative curvature (an inside corner, where two surfaces meet
|
||||||
|
// away from the viewer) inks faintly. That is what an animator does — the
|
||||||
|
// outside line of a shape is heavy, the contact line inside it is light.
|
||||||
|
|
||||||
uniform sampler2D screen_tex : hint_screen_texture, filter_linear;
|
uniform sampler2D screen_tex : hint_screen_texture, filter_linear, repeat_disable;
|
||||||
uniform sampler2D depth_tex : hint_depth_texture, filter_nearest;
|
uniform sampler2D depth_tex : hint_depth_texture, filter_nearest, repeat_disable;
|
||||||
uniform sampler2D normal_tex : hint_normal_roughness_texture, filter_nearest;
|
uniform sampler2D normal_roughness_tex : hint_normal_roughness_texture,
|
||||||
|
filter_nearest, repeat_disable;
|
||||||
|
|
||||||
uniform vec4 ink_color : source_color = vec4(0.07, 0.06, 0.09, 1.0);
|
group_uniforms ink;
|
||||||
uniform float depth_threshold : hint_range(0.0, 4.0) = 1.1;
|
uniform vec4 ink_color : source_color = vec4(0.224, 0.196, 0.310, 1.0);
|
||||||
uniform float normal_threshold : hint_range(0.0, 1.0) = 0.55;
|
// How far apart the taps sit, in pixels. This IS the line weight. The reference
|
||||||
uniform float edge_strength : hint_range(0.0, 1.0) = 0.8;
|
// runs 1.35 at a 1.5-2x internal resolution; this project renders at 1x, and the
|
||||||
uniform float max_distance : hint_range(10.0, 500.0) = 220.0;
|
// brief asks for a slightly harder and thicker line than the reference, so it
|
||||||
|
// sits at 2.0 — about a two-pixel line at 1080p.
|
||||||
|
uniform float ink_thickness : hint_range(0.5, 6.0) = 2.3;
|
||||||
|
// Curvature at which a convex edge reaches full ink. Small, because the second
|
||||||
|
// difference is already normalised by distance and produces almost nothing on
|
||||||
|
// flat ground — this can be sensitive in a way the old first-difference test
|
||||||
|
// could never afford to be.
|
||||||
|
uniform float ink_sensitivity : hint_range(0.0005, 0.05) = 0.0034;
|
||||||
|
uniform float ink_concave : hint_range(0.002, 0.2) = 0.024;
|
||||||
|
uniform float ink_concave_amount : hint_range(0.0, 1.0) = 0.45;
|
||||||
|
// Hardness: where the ramp to full ink STARTS, as a fraction of the threshold.
|
||||||
|
// The reference effectively runs 0.32 — ink begins to appear at a third of the
|
||||||
|
// threshold curvature, which is a soft, tapering line. Pushing it toward 1.0
|
||||||
|
// gives a harder, more decisive one, which is what the brief asks for.
|
||||||
|
//
|
||||||
|
// It cannot BE 1.0: that is a step(), and a step on a diagonal edge is a
|
||||||
|
// staircase that shimmers the moment anything moves.
|
||||||
|
//
|
||||||
|
// It also sets the NOISE FLOOR, and that is not a side effect to ignore. A
|
||||||
|
// grazing ground plane quantises in the depth buffer into a staircase, and a
|
||||||
|
// staircase has a large second difference at every step — at 0.32 those steps
|
||||||
|
// ink faintly and the road comes out finely hatched, which is precisely what a
|
||||||
|
// first render at a wider tap spacing showed.
|
||||||
|
uniform float ink_hardness : hint_range(0.1, 0.95) = 0.62;
|
||||||
|
// How much of the surface's own colour survives inside the line. Low but not
|
||||||
|
// zero: pure flat ink over every edge reads as pasted on, a whisper of the
|
||||||
|
// underlying hue reads as drawn.
|
||||||
|
uniform float ink_hue_bleed : hint_range(0.0, 0.6) = 0.14;
|
||||||
|
uniform float ink_strength : hint_range(0.0, 1.0) = 1.0;
|
||||||
|
// Let the far background dissolve into haze instead of turning into a mess of
|
||||||
|
// busy line work.
|
||||||
|
uniform float ink_fade_start : hint_range(5.0, 400.0) = 85.0;
|
||||||
|
uniform float ink_fade_end : hint_range(10.0, 900.0) = 220.0;
|
||||||
|
// Depth taps land on the sky. Clamping rather than special-casing them keeps
|
||||||
|
// the silhouette signal huge but bounded, so an object against the sky inks at
|
||||||
|
// full weight on BOTH sides of its edge — which is where the reference's early
|
||||||
|
// return leaves the line only half as thick as it should be.
|
||||||
|
uniform float ink_sky_depth : hint_range(50.0, 4000.0) = 900.0;
|
||||||
|
|
||||||
float linear_depth(vec2 uv, mat4 inv_proj) {
|
// ── The grade ────────────────────────────────────────────────────────────────
|
||||||
float d = texture(depth_tex, uv).r;
|
//
|
||||||
|
// A split-tone: cool violet into the darks, warm paper-white into the lights,
|
||||||
|
// plus a lift that keeps shadow off the floor. Godot tonemaps and converts to
|
||||||
|
// sRGB after this pass, so unlike the reference this stays in linear and does
|
||||||
|
// NOT do its own sRGB conversion — doing both would gamma the frame twice.
|
||||||
|
group_uniforms grade;
|
||||||
|
uniform bool grade_enabled = true;
|
||||||
|
uniform vec3 grade_shadow_tint : source_color = vec3(0.678, 0.659, 0.816);
|
||||||
|
uniform vec3 grade_light_tint : source_color = vec3(1.0, 0.969, 0.910);
|
||||||
|
uniform float grade_saturation : hint_range(0.0, 2.0) = 1.12;
|
||||||
|
uniform float grade_lift : hint_range(0.0, 0.2) = 0.028;
|
||||||
|
uniform float grade_warmth : hint_range(0.0, 0.3) = 0.05;
|
||||||
|
uniform float grade_vignette : hint_range(0.0, 0.6) = 0.15;
|
||||||
|
|
||||||
|
// The fullscreen pass owns the background pixels as well as the geometry ones.
|
||||||
|
// Godot's screen copy holds only the viewport clear colour at far depth, so
|
||||||
|
// sampling screen_tex there silently erases the real Environment sky — these
|
||||||
|
// repaint it with the same palette and law as anime_sky.gdshader, and
|
||||||
|
// LevelEnvironment keeps the two materials in sync.
|
||||||
|
group_uniforms sky;
|
||||||
|
uniform vec3 sky_top_color : source_color = vec3(0.18, 0.40, 0.85);
|
||||||
|
uniform vec3 sky_horizon_color : source_color = vec3(0.72, 0.88, 0.98);
|
||||||
|
uniform vec3 sky_ground_color : source_color = vec3(0.24, 0.22, 0.30);
|
||||||
|
uniform vec3 sky_cloud_color : source_color = vec3(1.0, 0.99, 0.97);
|
||||||
|
uniform vec3 sky_cloud_shadow : source_color = vec3(0.72, 0.75, 0.88);
|
||||||
|
uniform float sky_cloud_cover : hint_range(0.0, 1.0) = 0.46;
|
||||||
|
uniform float sky_cloud_scale : hint_range(0.2, 8.0) = 1.6;
|
||||||
|
uniform float sky_cloud_softness : hint_range(0.001, 0.2) = 0.035;
|
||||||
|
uniform float sky_drift : hint_range(0.0, 0.02) = 0.0022;
|
||||||
|
uniform float sky_horizon_falloff : hint_range(0.1, 2.0) = 0.45;
|
||||||
|
uniform vec3 sky_sun_direction = vec3(0.45, 0.34, -0.82);
|
||||||
|
uniform vec3 sky_sun_color : source_color = vec3(1.0, 0.91, 0.72);
|
||||||
|
uniform float sky_sun_halo : hint_range(0.0, 2.0) = 0.35;
|
||||||
|
|
||||||
|
|
||||||
|
#include "res://assets/shaders/anime_clouds.gdshaderinc"
|
||||||
|
|
||||||
|
|
||||||
|
vec3 painted_sky(vec3 dir) {
|
||||||
|
float up = clamp(dir.y, 0.0, 1.0);
|
||||||
|
float down = clamp(-dir.y, 0.0, 1.0);
|
||||||
|
vec3 col = dir.y >= 0.0
|
||||||
|
? mix(sky_horizon_color, sky_top_color, pow(up, sky_horizon_falloff))
|
||||||
|
: mix(sky_horizon_color, sky_ground_color, pow(down, 0.62));
|
||||||
|
float sun = pow(max(dot(dir, normalize(sky_sun_direction)), 0.0), 28.0);
|
||||||
|
col += sky_sun_color * sun * sky_sun_halo;
|
||||||
|
|
||||||
|
if (dir.y > 0.005) {
|
||||||
|
vec2 uv = dir.xz / (dir.y + 0.18) * sky_cloud_scale
|
||||||
|
+ vec2(TIME * sky_drift, 0.0);
|
||||||
|
float n = cloud_field(uv);
|
||||||
|
float edge = max(sky_cloud_softness, fwidth(n) * 1.35);
|
||||||
|
float shape = smoothstep(
|
||||||
|
sky_cloud_cover - edge, sky_cloud_cover + edge, n);
|
||||||
|
vec3 sun_direction = normalize(sky_sun_direction);
|
||||||
|
float facing = cloud_sun_response(uv, sun_direction);
|
||||||
|
float core = smoothstep(
|
||||||
|
sky_cloud_cover + 0.045 - edge,
|
||||||
|
sky_cloud_cover + 0.155 + edge, n);
|
||||||
|
float carving = smoothstep(
|
||||||
|
0.38, 0.64, cloud_detail(uv + vec2(2.4, 9.7)));
|
||||||
|
float lit_face = clamp(
|
||||||
|
0.12 + core * 0.25 + facing * 0.58 + carving * 0.05,
|
||||||
|
0.0, 1.0);
|
||||||
|
vec3 lit_color = mix(sky_cloud_color, sky_sun_color, 0.18);
|
||||||
|
vec3 cloud = mix(sky_cloud_shadow, lit_color, lit_face);
|
||||||
|
float boundary = 1.0 - smoothstep(
|
||||||
|
sky_cloud_cover + edge,
|
||||||
|
sky_cloud_cover + 0.105 + edge, n);
|
||||||
|
float silver_lining = boundary * facing * facing
|
||||||
|
* mix(0.10, 0.24, clamp(sun_direction.y, 0.0, 1.0));
|
||||||
|
cloud += sky_sun_color * silver_lining;
|
||||||
|
col = mix(col, cloud, shape * smoothstep(0.0, 0.22, dir.y));
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Linear view depth, with the sky pinned to a finite value so the second
|
||||||
|
// difference across a silhouette is large but never infinite.
|
||||||
|
float linear_depth(vec2 uv, float d, mat4 inv_proj) {
|
||||||
|
// Godot 4.3+ is reverse-Z: the far plane and the sky read exactly 0.
|
||||||
|
if (d <= 0.000001) {
|
||||||
|
return ink_sky_depth;
|
||||||
|
}
|
||||||
vec4 ndc = vec4(uv * 2.0 - 1.0, d, 1.0);
|
vec4 ndc = vec4(uv * 2.0 - 1.0, d, 1.0);
|
||||||
vec4 view = inv_proj * ndc;
|
vec4 view = inv_proj * ndc;
|
||||||
return -view.z / view.w;
|
return min(-view.z / view.w, ink_sky_depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vec3 view_position(vec2 uv, float raw_depth, mat4 inv_proj) {
|
||||||
|
vec4 ndc = vec4(uv * 2.0 - 1.0, raw_depth, 1.0);
|
||||||
|
vec4 view = inv_proj * ndc;
|
||||||
|
return view.xyz / view.w;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vec3 screen_normal(vec2 uv) {
|
||||||
|
return normalize(texture(normal_roughness_tex, uv).xyz * 2.0 - 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vec3 apply_grade(vec3 c, vec2 uv) {
|
||||||
|
if (!grade_enabled) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
float l = dot(c, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
|
||||||
|
// Split-tone: cool violet in the darks, warm paper white in the lights.
|
||||||
|
float k = smoothstep(0.02, 0.55, l);
|
||||||
|
c *= mix(grade_shadow_tint, grade_light_tint, k);
|
||||||
|
|
||||||
|
// A gentle overall warmth, like late afternoon light through blossom.
|
||||||
|
c += vec3(grade_warmth, grade_warmth * 0.45, 0.0) * l * 0.35;
|
||||||
|
|
||||||
|
// Keep the shadows readable — never crushed to black.
|
||||||
|
c += grade_lift * (1.0 - k);
|
||||||
|
|
||||||
|
c = mix(vec3(l), c, grade_saturation);
|
||||||
|
|
||||||
|
float r = length(uv - 0.5) * 1.42;
|
||||||
|
c *= 1.0 - grade_vignette * pow(clamp(r, 0.0, 1.0), 2.6);
|
||||||
|
|
||||||
|
return max(c, vec3(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
|
POSITION = vec4(VERTEX.xy, 1.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec2 px = 1.0 / VIEWPORT_SIZE;
|
vec2 px = 1.0 / VIEWPORT_SIZE;
|
||||||
vec2 uv = SCREEN_UV;
|
vec2 uv = SCREEN_UV;
|
||||||
|
|
||||||
float dc = linear_depth(uv, INV_PROJECTION_MATRIX);
|
// Background first: where the depth buffer has no geometry the screen copy
|
||||||
float dl = linear_depth(uv - vec2(px.x, 0.0), INV_PROJECTION_MATRIX);
|
// holds nothing but the clear colour, so those pixels are painted rather
|
||||||
float dr = linear_depth(uv + vec2(px.x, 0.0), INV_PROJECTION_MATRIX);
|
// than sampled. They still go through the ink test below — an object read
|
||||||
float du = linear_depth(uv - vec2(0.0, px.y), INV_PROJECTION_MATRIX);
|
// against the sky should be inked on the sky side of its edge too, which is
|
||||||
float dd = linear_depth(uv + vec2(0.0, px.y), INV_PROJECTION_MATRIX);
|
// what makes the silhouette the heaviest line in the frame.
|
||||||
|
float raw_depth = texture(depth_tex, uv).r;
|
||||||
|
vec3 col;
|
||||||
|
if (raw_depth <= 0.000001) {
|
||||||
|
vec4 far_view = INV_PROJECTION_MATRIX * vec4(uv * 2.0 - 1.0, 0.0, 1.0);
|
||||||
|
vec3 view_dir = normalize(far_view.xyz / far_view.w);
|
||||||
|
vec3 world_dir = normalize((INV_VIEW_MATRIX * vec4(view_dir, 0.0)).xyz);
|
||||||
|
col = painted_sky(world_dir);
|
||||||
|
} else {
|
||||||
|
col = texture(screen_tex, uv).rgb;
|
||||||
|
}
|
||||||
|
|
||||||
// Depth edge: neighbor difference scaled by distance so far geometry
|
vec2 t = px * ink_thickness;
|
||||||
// doesn't dissolve into noise.
|
float raw_l = texture(depth_tex, uv - vec2(t.x, 0.0)).r;
|
||||||
float ddiff = abs(dl - dr) + abs(du - dd);
|
float raw_r = texture(depth_tex, uv + vec2(t.x, 0.0)).r;
|
||||||
float depth_edge = step(depth_threshold * (0.5 + dc * 0.08), ddiff);
|
float raw_u = texture(depth_tex, uv + vec2(0.0, t.y)).r;
|
||||||
|
float raw_d = texture(depth_tex, uv - vec2(0.0, t.y)).r;
|
||||||
|
float dc = linear_depth(uv, raw_depth, INV_PROJECTION_MATRIX);
|
||||||
|
float dl = linear_depth(uv - vec2(t.x, 0.0), raw_l, INV_PROJECTION_MATRIX);
|
||||||
|
float dr = linear_depth(uv + vec2(t.x, 0.0), raw_r, INV_PROJECTION_MATRIX);
|
||||||
|
float du = linear_depth(uv + vec2(0.0, t.y), raw_u, INV_PROJECTION_MATRIX);
|
||||||
|
float dd = linear_depth(uv - vec2(0.0, t.y), raw_d, INV_PROJECTION_MATRIX);
|
||||||
|
|
||||||
// Normal edge: crease lines on same-depth corners.
|
// Second difference of linear depth, normalised by distance so that a given
|
||||||
vec3 nc = texture(normal_tex, uv).xyz * 2.0 - 1.0;
|
// real-world crease inks with the same weight near and far.
|
||||||
vec3 nl = texture(normal_tex, uv - vec2(px.x, 0.0)).xyz * 2.0 - 1.0;
|
float sx = (dl + dr - 2.0 * dc) / dc;
|
||||||
vec3 nu = texture(normal_tex, uv - vec2(0.0, px.y)).xyz * 2.0 - 1.0;
|
float sy = (du + dd - 2.0 * dc) / dc;
|
||||||
float ndiff = length(nc - nl) + length(nc - nu);
|
|
||||||
float normal_edge = step(normal_threshold, ndiff) * step(ddiff, depth_threshold);
|
|
||||||
|
|
||||||
float fade = 1.0 - smoothstep(max_distance * 0.6, max_distance, dc);
|
float convex = max(0.0, sx) + max(0.0, sy);
|
||||||
float edge = max(depth_edge, normal_edge * 0.6) * edge_strength * fade;
|
float concave = max(0.0, -sx) + max(0.0, -sy);
|
||||||
|
|
||||||
vec3 scene = texture(screen_tex, uv).rgb;
|
float edge = smoothstep(
|
||||||
ALBEDO = mix(scene, ink_color.rgb, edge);
|
ink_sensitivity * ink_hardness, ink_sensitivity, convex);
|
||||||
|
edge = max(edge, smoothstep(
|
||||||
|
ink_concave, ink_concave * 3.4, concave) * ink_concave_amount);
|
||||||
|
|
||||||
|
// Depth precision forms a staircase on a flat floor viewed almost edge-on.
|
||||||
|
// Its second difference looks like curvature even though every tapped point
|
||||||
|
// belongs to the same plane; beyond a camera-dependent distance the old test
|
||||||
|
// therefore inked the entire road as one dark band. Confirm that curvature
|
||||||
|
// with the normal/depth prepass before drawing it. A true edge either changes
|
||||||
|
// normal, leaves the centre tangent plane, or crosses from geometry to sky.
|
||||||
|
if (edge > 0.0001) {
|
||||||
|
float sky_edge = step(raw_depth, 0.000001)
|
||||||
|
!= step(raw_l, 0.000001) || step(raw_depth, 0.000001)
|
||||||
|
!= step(raw_r, 0.000001) || step(raw_depth, 0.000001)
|
||||||
|
!= step(raw_u, 0.000001) || step(raw_depth, 0.000001)
|
||||||
|
!= step(raw_d, 0.000001) ? 1.0 : 0.0;
|
||||||
|
float geometry_evidence = sky_edge;
|
||||||
|
if (raw_depth > 0.000001 && sky_edge < 0.5) {
|
||||||
|
vec3 nc = screen_normal(uv);
|
||||||
|
vec3 nl = screen_normal(uv - vec2(t.x, 0.0));
|
||||||
|
vec3 nr = screen_normal(uv + vec2(t.x, 0.0));
|
||||||
|
vec3 nu = screen_normal(uv + vec2(0.0, t.y));
|
||||||
|
vec3 nd = screen_normal(uv - vec2(0.0, t.y));
|
||||||
|
float normal_turn = max(max(1.0 - dot(nc, nl), 1.0 - dot(nc, nr)),
|
||||||
|
max(1.0 - dot(nc, nu), 1.0 - dot(nc, nd)));
|
||||||
|
|
||||||
|
vec3 pc = view_position(uv, raw_depth, INV_PROJECTION_MATRIX);
|
||||||
|
vec3 pl = view_position(uv - vec2(t.x, 0.0), raw_l, INV_PROJECTION_MATRIX);
|
||||||
|
vec3 pr = view_position(uv + vec2(t.x, 0.0), raw_r, INV_PROJECTION_MATRIX);
|
||||||
|
vec3 pu = view_position(uv + vec2(0.0, t.y), raw_u, INV_PROJECTION_MATRIX);
|
||||||
|
vec3 pd = view_position(uv - vec2(0.0, t.y), raw_d, INV_PROJECTION_MATRIX);
|
||||||
|
float off_plane = max(max(abs(dot(nc, normalize(pl - pc))),
|
||||||
|
abs(dot(nc, normalize(pr - pc)))),
|
||||||
|
max(abs(dot(nc, normalize(pu - pc))),
|
||||||
|
abs(dot(nc, normalize(pd - pc)))));
|
||||||
|
float normal_evidence = smoothstep(0.006, 0.045, normal_turn);
|
||||||
|
float plane_evidence = smoothstep(0.012, 0.080, off_plane);
|
||||||
|
geometry_evidence = max(normal_evidence, plane_evidence);
|
||||||
|
}
|
||||||
|
edge *= geometry_evidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fade on the nearest real surface involved in the edge, not on dc: a sky
|
||||||
|
// pixel beside a nearby roof would otherwise be measured at 900 m and its
|
||||||
|
// half of the silhouette would fade out while the roof's half did not.
|
||||||
|
float near_d = min(dc, min(min(dl, dr), min(du, dd)));
|
||||||
|
edge *= 1.0 - smoothstep(ink_fade_start, ink_fade_end, near_d);
|
||||||
|
edge *= ink_strength;
|
||||||
|
|
||||||
|
vec3 line = mix(ink_color.rgb, col * 0.42, ink_hue_bleed);
|
||||||
|
col = mix(col, line, clamp(edge, 0.0, 1.0));
|
||||||
|
|
||||||
|
ALBEDO = apply_grade(col, uv);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
shader_type spatial;
|
||||||
|
|
||||||
|
// ── The cel law ──────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Ported from the Sakura Crossing renderer (three.js MeshToonMaterial + a
|
||||||
|
// patched toon BRDF). Two ideas, and the second one is the one that matters:
|
||||||
|
//
|
||||||
|
// 1. Direct light is QUANTISED against a hand-authored ramp — a tiny
|
||||||
|
// nearest-filtered texture sampled at `dot(N,L) * 0.5 + 0.5`. So a surface
|
||||||
|
// is never lit by a falloff, only ever by one of 2-5 flat values.
|
||||||
|
//
|
||||||
|
// 2. The darker bands are HUE-SHIFTED toward a cool violet rather than being
|
||||||
|
// a darker version of the base colour:
|
||||||
|
//
|
||||||
|
// irradiance = band * mix(shadow_tint, vec3(1.0), band)
|
||||||
|
//
|
||||||
|
// The band drives its own tint, so full light is untinted, the mid band is
|
||||||
|
// slightly cooled and the darkest band is almost entirely shadow-hue. That
|
||||||
|
// hue shift in shadow is most of what separates "anime cel" from
|
||||||
|
// "low-poly 3D" — a wall whose shade is just 40% of its lit colour reads
|
||||||
|
// as an unlit polygon; a wall whose shade swings violet reads as painted.
|
||||||
|
//
|
||||||
|
// Godot's light() is called per light and adds into DIFFUSE_LIGHT, and its
|
||||||
|
// Lambert term already carries the 1/PI that three.js puts in BRDF_Lambert, so
|
||||||
|
// the two pipelines agree once the ramp value is substituted for `dot(N,L)`.
|
||||||
|
//
|
||||||
|
// This is a SEPARATE shader from toon.gdshader on purpose. That one carries the
|
||||||
|
// weapon-viewmodel colour shaping and the character band calibration, both
|
||||||
|
// tuned against imported textures that are already painted with cel shading.
|
||||||
|
// This one is for FLAT-COLOURED WORLD GEOMETRY, which is what the reference is
|
||||||
|
// built out of end to end, and it would be wrong to make either compromise for
|
||||||
|
// the other.
|
||||||
|
|
||||||
|
// ── Ramps ────────────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// The reference's stop tables, kept as raw 0-255 values so they can be checked
|
||||||
|
// against the source line by line. These are LINEAR values, not sRGB: three.js
|
||||||
|
// gives a DataTexture no colour space, so the stops are used as authored, and
|
||||||
|
// the same has to be true here or every band lands too bright.
|
||||||
|
//
|
||||||
|
// 2/3/4/5 the standard ramps. 3 is the default and does most of the world.
|
||||||
|
// 6 "soft" — high key, 2 bands, for pale masses (blossom, cloud,
|
||||||
|
// 7 "soft3" — high key, 3 bands plaster) that must stay light even
|
||||||
|
// on the shadow side. Without these a
|
||||||
|
// cherry canopy goes grey the moment
|
||||||
|
// it turns away from the sun, which
|
||||||
|
// is the one thing a blossom tree may
|
||||||
|
// never do.
|
||||||
|
const float RAMP_2[5] = float[](96.0, 255.0, 255.0, 255.0, 255.0);
|
||||||
|
const float RAMP_3[5] = float[](92.0, 178.0, 255.0, 255.0, 255.0);
|
||||||
|
const float RAMP_4[5] = float[](80.0, 142.0, 202.0, 255.0, 255.0);
|
||||||
|
const float RAMP_5[5] = float[](74.0, 124.0, 172.0, 214.0, 255.0);
|
||||||
|
const float RAMP_SOFT[5] = float[](180.0, 255.0, 255.0, 255.0, 255.0);
|
||||||
|
const float RAMP_SOFT3[5] = float[](172.0, 214.0, 255.0, 255.0, 255.0);
|
||||||
|
|
||||||
|
// Which ramp, and how many of its stops are live.
|
||||||
|
// 0 = 2 band 1 = 3 band (default) 2 = 4 band 3 = 5 band
|
||||||
|
// 4 = soft 2 band (high key) 5 = soft 3 band (high key)
|
||||||
|
uniform int ramp_id : hint_range(0, 5) = 1;
|
||||||
|
|
||||||
|
uniform vec4 albedo_color : source_color = vec4(1.0);
|
||||||
|
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
|
uniform bool has_texture = false;
|
||||||
|
uniform bool use_vertex_color = false;
|
||||||
|
|
||||||
|
// The cool violet the shadow bands swing toward. 0x6c5f8c is the reference's
|
||||||
|
// default; individual materials shift it (warmer for timber and blossom, bluer
|
||||||
|
// for metal and water) the same way the reference's `cel({tint})` does.
|
||||||
|
uniform vec4 shadow_tint : source_color = vec4(0.424, 0.373, 0.549, 1.0);
|
||||||
|
|
||||||
|
// Faceted shading. The reference sets `flatShading: true` on essentially
|
||||||
|
// everything, and it is not a shortcut — a quantised ramp over SMOOTH normals
|
||||||
|
// puts a curved band boundary across a curved surface, which reads as an
|
||||||
|
// airbrushed gradient no matter how few steps it has. Faceting forces every
|
||||||
|
// band edge onto a polygon edge, which is where a cel painter would put it.
|
||||||
|
uniform bool flat_shading = true;
|
||||||
|
|
||||||
|
// ── Cast shadow ──────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// The reference folds shadow attenuation into the light colour, so an occluded
|
||||||
|
// pixel there goes to zero direct light and is carried entirely by ambient.
|
||||||
|
// That works when the whole world is one authored scene; in a shooter, where
|
||||||
|
// players have to be read against the geometry they are standing on, a cast
|
||||||
|
// shadow that reaches black swallows them.
|
||||||
|
//
|
||||||
|
// So occlusion drives the surface to the ramp's DARKEST STOP — fully tinted,
|
||||||
|
// scaled by cast_shadow_depth — rather than to nothing. The result is still a
|
||||||
|
// hard, dark, clearly separate shape (which is what an animator inks), but it
|
||||||
|
// keeps hue, so a character standing in the shade of a shopfront stays legible.
|
||||||
|
uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.72;
|
||||||
|
uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.30;
|
||||||
|
|
||||||
|
// Band-edge anti-aliasing floor, in NdotL. Nearest-filtered quantisation is
|
||||||
|
// a hard step, and a hard step on a slowly-curving surface crawls and
|
||||||
|
// stair-steps as the camera moves — this is shading aliasing, so MSAA cannot
|
||||||
|
// touch it. See light() for how the screen-space term works.
|
||||||
|
uniform float band_softness : hint_range(0.0, 0.5) = 0.012;
|
||||||
|
|
||||||
|
// Rim and specular are OFF by default and should mostly stay that way: the
|
||||||
|
// reference has neither, and on large level surfaces a toon rim reads as a
|
||||||
|
// giant soft blob smeared across a wall.
|
||||||
|
uniform float rim_strength : hint_range(0.0, 2.0) = 0.0;
|
||||||
|
uniform float rim_width : hint_range(0.0, 1.0) = 0.28;
|
||||||
|
|
||||||
|
// ── Surface law ──────────────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Carried over from toon.gdshader, because the problem it solves is real here
|
||||||
|
// too: a flat cel colour is the style, but a twelve-metre wall holding exactly
|
||||||
|
// one value is not stylised, it is empty. Both default to off — the reference
|
||||||
|
// answers this with geometry (every fascia, sill and downpipe is modelled), and
|
||||||
|
// where this map does the same it does not need them.
|
||||||
|
group_uniforms detail;
|
||||||
|
uniform float seam_scale = 0.0; // metres per panel; 0 = off
|
||||||
|
uniform float seam_strength : hint_range(0.0, 1.0) = 0.24;
|
||||||
|
uniform float seam_width : hint_range(0.001, 0.2) = 0.010;
|
||||||
|
uniform float grade_height = 0.0; // metres to fade over; 0 = off
|
||||||
|
uniform float grade_strength : hint_range(0.0, 1.0) = 0.18;
|
||||||
|
uniform float grade_floor = 0.0; // world Y the grade starts from
|
||||||
|
|
||||||
|
varying vec3 world_pos;
|
||||||
|
varying vec3 world_normal;
|
||||||
|
varying float normal_slope;
|
||||||
|
|
||||||
|
|
||||||
|
float ramp_stop(int i) {
|
||||||
|
// GLSL ES 3.0 will not index a const array with a non-constant expression on
|
||||||
|
// every driver, so the ramp is selected by branch and the stop by a small
|
||||||
|
// unrolled pick. Both indices are uniform-or-loop-constant, so this costs
|
||||||
|
// nothing measurable and works everywhere.
|
||||||
|
float s[5] = RAMP_3;
|
||||||
|
if (ramp_id == 0) { s = RAMP_2; }
|
||||||
|
else if (ramp_id == 2) { s = RAMP_4; }
|
||||||
|
else if (ramp_id == 3) { s = RAMP_5; }
|
||||||
|
else if (ramp_id == 4) { s = RAMP_SOFT; }
|
||||||
|
else if (ramp_id == 5) { s = RAMP_SOFT3; }
|
||||||
|
float v = s[0];
|
||||||
|
if (i == 1) { v = s[1]; }
|
||||||
|
else if (i == 2) { v = s[2]; }
|
||||||
|
else if (i == 3) { v = s[3]; }
|
||||||
|
else if (i >= 4) { v = s[4]; }
|
||||||
|
return v / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int ramp_bands() {
|
||||||
|
if (ramp_id == 0 || ramp_id == 4) { return 2; }
|
||||||
|
if (ramp_id == 2) { return 4; }
|
||||||
|
if (ramp_id == 3) { return 5; }
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float seam_lines(vec2 uv) {
|
||||||
|
vec2 g = abs(fract(uv) - 0.5);
|
||||||
|
vec2 fw = fwidth(uv) + 0.0001;
|
||||||
|
vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g);
|
||||||
|
return max(line.x, line.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float panel_seam(vec3 p, vec3 n) {
|
||||||
|
vec3 w = pow(abs(n), vec3(4.0));
|
||||||
|
w /= (w.x + w.y + w.z);
|
||||||
|
float s = seam_lines(p.zy / seam_scale) * w.x
|
||||||
|
+ seam_lines(p.xz / seam_scale) * w.y
|
||||||
|
+ seam_lines(p.xy / seam_scale) * w.z;
|
||||||
|
float cell_px = length(fwidth(p)) / seam_scale;
|
||||||
|
return s * (1.0 - smoothstep(0.25, 0.5, cell_px));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec3 base = albedo_color.rgb;
|
||||||
|
if (has_texture) {
|
||||||
|
base *= texture(albedo_texture, UV).rgb;
|
||||||
|
}
|
||||||
|
if (use_vertex_color) {
|
||||||
|
base *= COLOR.rgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seam_scale > 0.0) {
|
||||||
|
base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength;
|
||||||
|
}
|
||||||
|
if (grade_height > 0.0) {
|
||||||
|
float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0);
|
||||||
|
base *= mix(1.0 - grade_strength, 1.0, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flat_shading) {
|
||||||
|
// VERTEX is view-space position here, so its screen derivatives span the
|
||||||
|
// triangle's own plane and their cross product is the true face normal.
|
||||||
|
// Forced to face the viewer, because the winding of that cross product
|
||||||
|
// flips with the handedness of the projection and a back-to-front normal
|
||||||
|
// lights the facet from behind.
|
||||||
|
vec3 fn = normalize(cross(dFdx(VERTEX), dFdy(VERTEX)));
|
||||||
|
NORMAL = fn * sign(dot(fn, VIEW));
|
||||||
|
}
|
||||||
|
|
||||||
|
ALBEDO = base;
|
||||||
|
ROUGHNESS = 1.0;
|
||||||
|
SPECULAR = 0.0;
|
||||||
|
|
||||||
|
// Screen-space rate of turn of the normal, measured here because
|
||||||
|
// derivatives are a fragment-stage operation — taking fwidth() inside
|
||||||
|
// light(), which runs once per light, is undefined on some drivers.
|
||||||
|
// Faceted geometry has a slope of zero across a facet, so this only ever
|
||||||
|
// widens the band edge where the surface is genuinely curving.
|
||||||
|
normal_slope = length(fwidth(NORMAL));
|
||||||
|
|
||||||
|
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
|
||||||
|
rim = smoothstep(1.0 - rim_width, 1.0, rim);
|
||||||
|
EMISSION = base * rim * rim_strength;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void light() {
|
||||||
|
float ndotl = dot(NORMAL, LIGHT);
|
||||||
|
|
||||||
|
// Quantise. `t` is the reference's ramp lookup coordinate exactly — the
|
||||||
|
// texture is sampled at dot(N,L) * 0.5 + 0.5 with NearestFilter, so band
|
||||||
|
// boundaries fall at evenly spaced values of that coordinate and nowhere
|
||||||
|
// else. With the 3-stop ramp that puts them at dot(N,L) = ±1/3.
|
||||||
|
int bands = ramp_bands();
|
||||||
|
float fb = clamp(ndotl * 0.5 + 0.5, 0.0, 0.999999) * float(bands);
|
||||||
|
int idx = int(floor(fb));
|
||||||
|
float frac_b = fb - float(idx);
|
||||||
|
|
||||||
|
float hi = ramp_stop(idx);
|
||||||
|
float lo = ramp_stop(max(idx - 1, 0));
|
||||||
|
|
||||||
|
// Anti-aliasing budget. band_softness is in NdotL, which says nothing about
|
||||||
|
// how many PIXELS a transition covers; normal_slope does. Widening the step
|
||||||
|
// to at least the on-screen rate of turn keeps every band edge about a pixel
|
||||||
|
// wide, and leaves edges that are already wider exactly as authored. The
|
||||||
|
// d(fb)/d(ndotl) factor of bands*0.5 converts the NdotL width into the
|
||||||
|
// band-index units `frac_b` is measured in.
|
||||||
|
float w = max(band_softness, normal_slope * 0.9) * float(bands) * 0.5;
|
||||||
|
float band = mix(lo, hi, smoothstep(0.0, max(w * 2.0, 0.0001), frac_b));
|
||||||
|
|
||||||
|
// The hue shift. Band drives its own tint: full light is untinted, the
|
||||||
|
// darkest band is very nearly pure shadow hue.
|
||||||
|
vec3 lit = band * mix(shadow_tint.rgb, vec3(1.0), band);
|
||||||
|
|
||||||
|
// Cast shadow: down to the darkest stop, fully tinted, never to black.
|
||||||
|
float floor_band = ramp_stop(0);
|
||||||
|
vec3 occluded = floor_band * mix(shadow_tint.rgb, vec3(1.0), floor_band)
|
||||||
|
* cast_shadow_depth;
|
||||||
|
float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION);
|
||||||
|
vec3 shade = mix(occluded, lit, occ);
|
||||||
|
|
||||||
|
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dk5nb354iw044
|
||||||
@@ -9,20 +9,97 @@ shader_type spatial;
|
|||||||
uniform vec4 albedo_color : source_color = vec4(1.0);
|
uniform vec4 albedo_color : source_color = vec4(1.0);
|
||||||
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
|
uniform sampler2D albedo_texture : source_color, filter_linear_mipmap, repeat_enable;
|
||||||
uniform bool has_texture = true;
|
uniform bool has_texture = true;
|
||||||
|
uniform bool use_vertex_color = false;
|
||||||
uniform bool use_triplanar = false;
|
uniform bool use_triplanar = false;
|
||||||
uniform float triplanar_tile = 2.0; // world units per texture tile
|
uniform float triplanar_tile = 2.0; // world units per texture tile
|
||||||
|
|
||||||
uniform float band_edge : hint_range(-1.0, 1.0) = 0.05; // NdotL where light band starts
|
uniform float band_edge : hint_range(-1.0, 1.0) = 0.05; // NdotL where light band starts
|
||||||
uniform float band_softness : hint_range(0.001, 0.5) = 0.04;
|
uniform float band_softness : hint_range(0.001, 0.5) = 0.04;
|
||||||
uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter band
|
uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter band
|
||||||
|
// How dark the mid band sits below full light. Near 1.0 the second step almost
|
||||||
|
// disappears, which is what textures that already carry painted cel shading
|
||||||
|
// (imported anime characters) need — a hard step on top of painted shading
|
||||||
|
// reads as a glossy stripe sweeping across the hair.
|
||||||
|
uniform float mid_tone : hint_range(0.0, 1.0) = 0.82;
|
||||||
uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint
|
uniform vec4 shadow_color : source_color = vec4(0.62, 0.65, 0.78, 1.0); // cool shadow tint
|
||||||
uniform float rim_strength : hint_range(0.0, 2.0) = 0.35;
|
// How far a CAST shadow drops below the form-shadow tint. Cel art draws the
|
||||||
uniform float rim_width : hint_range(0.0, 1.0) = 0.65;
|
// two separately: the terminator (a surface turning away from the sun) is a
|
||||||
uniform float specular_strength : hint_range(0.0, 1.0) = 0.25;
|
// tone step, a cast shadow (something standing in the way) is a darker, harder
|
||||||
uniform float specular_shininess : hint_range(1.0, 128.0) = 24.0;
|
// shape. See light() for why collapsing them made benches float.
|
||||||
|
uniform float cast_shadow_depth : hint_range(0.0, 1.0) = 0.62;
|
||||||
|
// Width of the cast-shadow edge, in ATTENUATION. Small keeps it crisp like
|
||||||
|
// inked artwork; not zero, or the shadow map's own stair-steps show.
|
||||||
|
uniform float cast_shadow_softness : hint_range(0.01, 1.0) = 0.35;
|
||||||
|
// Matte-anime defaults: zero specular (any stepped glint reads as shine
|
||||||
|
// sweeping across hair/cloth when the camera moves), whisper of rim.
|
||||||
|
uniform float rim_strength : hint_range(0.0, 2.0) = 0.05;
|
||||||
|
uniform float rim_width : hint_range(0.0, 1.0) = 0.28;
|
||||||
|
uniform float specular_strength : hint_range(0.0, 1.0) = 0.0;
|
||||||
|
uniform float specular_shininess : hint_range(1.0, 128.0) = 64.0;
|
||||||
|
// Viewmodel/readability controls. Both default to zero so world materials are
|
||||||
|
// byte-for-byte unchanged. The first keeps near-black authored metal from
|
||||||
|
// collapsing below display range; the second is a controlled painted fill,
|
||||||
|
// useful in a transparent viewmodel viewport that has no environment ambient.
|
||||||
|
uniform float albedo_floor : hint_range(0.0, 0.5) = 0.0;
|
||||||
|
uniform float ambient_fill : hint_range(0.0, 1.0) = 0.0;
|
||||||
|
// Viewmodel color shaping. Defaults preserve every world/character material;
|
||||||
|
// weapon profiles opt in so dark receiver metal, pale trim and painted parts
|
||||||
|
// do not all converge on the same middle grey under a uniform fill.
|
||||||
|
uniform float albedo_contrast : hint_range(0.5, 2.0) = 1.0;
|
||||||
|
uniform float albedo_saturation : hint_range(0.0, 2.0) = 1.0;
|
||||||
|
uniform float albedo_gain : hint_range(0.25, 1.5) = 1.0;
|
||||||
|
uniform float viewmodel_light_strength : hint_range(0.0, 2.0) = 0.0;
|
||||||
|
uniform vec3 viewmodel_key_direction = vec3(-0.42, 0.58, 0.70);
|
||||||
|
uniform vec3 viewmodel_key_color : source_color = vec3(1.0, 0.91, 0.80);
|
||||||
|
uniform vec3 viewmodel_fill_color : source_color = vec3(0.72, 0.80, 1.0);
|
||||||
|
// Viewmodel ink controls. The old deterministic light was an additive
|
||||||
|
// key/fill wash: it made every normal visible, but also lifted receiver
|
||||||
|
// recesses and polymer undersides toward the same middle grey. These controls
|
||||||
|
// make that painted light a four-tone cel ramp and add a thin curvature ink
|
||||||
|
// pass. They default off/neutral so world and character materials are
|
||||||
|
// unchanged.
|
||||||
|
uniform float viewmodel_shadow_tone : hint_range(0.0, 1.0) = 0.18;
|
||||||
|
uniform float viewmodel_mid_tone : hint_range(0.0, 1.0) = 0.52;
|
||||||
|
uniform float viewmodel_crease_strength : hint_range(0.0, 1.0) = 0.0;
|
||||||
|
uniform float viewmodel_crease_threshold : hint_range(0.001, 1.0) = 0.12;
|
||||||
|
|
||||||
|
// ── Surface law ──────────────────────────────────────────────────────────────
|
||||||
|
// Flat cel colour is the style, but a wall that is ONE value from end to end is
|
||||||
|
// not stylised, it is empty — tools/levels.py measured 0.28 mean adjacent-pixel
|
||||||
|
// difference across the probe, i.e. essentially nothing but the silhouettes.
|
||||||
|
// These give a surface its own quiet structure without touching the flat-shaded
|
||||||
|
// look: everything here is authored into ALBEDO, so the cel bands still land on
|
||||||
|
// top of it exactly as before.
|
||||||
|
//
|
||||||
|
// All default to OFF (scale 0). A material opts in through LevelMaterials'
|
||||||
|
// surface laws, so props, vehicles and foliage keep plain flat colour and only
|
||||||
|
// architecture and ground get the treatment.
|
||||||
|
group_uniforms detail;
|
||||||
|
// Panel seams: a world-space grid of thin darker lines, triplanar so it needs
|
||||||
|
// no UVs and stays the same physical size on every surface it crosses. This is
|
||||||
|
// the single biggest change to how "built" a surface reads.
|
||||||
|
uniform float seam_scale = 0.0; // metres per panel; 0 = off
|
||||||
|
uniform float seam_strength : hint_range(0.0, 1.0) = 0.30;
|
||||||
|
uniform float seam_width : hint_range(0.001, 0.2) = 0.012; // fraction of a panel
|
||||||
|
// Vertical grade: surfaces darken toward the ground. A painter would put this
|
||||||
|
// in by hand — it is ambient occlusion as artwork rather than as a screen
|
||||||
|
// effect, it costs nothing, and it works at any distance, which SSAO does not.
|
||||||
|
uniform float grade_height = 0.0; // metres to fade over; 0 = off
|
||||||
|
uniform float grade_strength : hint_range(0.0, 1.0) = 0.16;
|
||||||
|
uniform float grade_floor = 0.0; // world Y the grade starts from
|
||||||
|
// Halftone: screen-tone dots inside the shadow band. The signature manga move
|
||||||
|
// and ZZZ does use it, but sparingly — off unless a material asks.
|
||||||
|
uniform float halftone_scale = 0.0; // screen px per dot; 0 = off
|
||||||
|
uniform float halftone_strength : hint_range(0.0, 1.0) = 0.35;
|
||||||
|
|
||||||
varying vec3 world_pos;
|
varying vec3 world_pos;
|
||||||
varying vec3 world_normal;
|
varying vec3 world_normal;
|
||||||
|
// How fast the normal is turning across one pixel, measured in fragment() and
|
||||||
|
// read in light(). This is the band-edge anti-aliasing budget — see light().
|
||||||
|
varying float normal_slope;
|
||||||
|
// Screen-tone dot mask, measured in fragment() where FRAGCOORD lives and spent
|
||||||
|
// in light() where the shadow band is known.
|
||||||
|
varying float halftone_dot;
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
@@ -42,6 +119,33 @@ vec3 sample_triplanar(vec3 p, vec3 n) {
|
|||||||
return cx * w.x + cy * w.y + cz * w.z;
|
return cx * w.x + cy * w.y + cz * w.z;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One axis-plane's worth of panel grid. Returns 1 on a seam line, 0 on the
|
||||||
|
// panel face, anti-aliased by the on-screen size of a cell so the lines neither
|
||||||
|
// shimmer at distance nor turn into fat bars up close — a seam drawn with
|
||||||
|
// step() looks fine in a screenshot and crawls badly the moment anything moves.
|
||||||
|
float seam_lines(vec2 uv) {
|
||||||
|
vec2 g = abs(fract(uv) - 0.5);
|
||||||
|
vec2 fw = fwidth(uv) + 0.0001;
|
||||||
|
vec2 line = smoothstep(0.5 - seam_width - fw, 0.5 - seam_width + fw, g);
|
||||||
|
return max(line.x, line.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Triplanar panel seams, weighted the same way the albedo triplanar is so the
|
||||||
|
// lines agree with the texture across a corner instead of crossfading against
|
||||||
|
// it. Far enough away that a cell is sub-pixel the whole thing fades out,
|
||||||
|
// otherwise the grid turns into moiré.
|
||||||
|
float panel_seam(vec3 p, vec3 n) {
|
||||||
|
vec3 w = pow(abs(n), vec3(4.0));
|
||||||
|
w /= (w.x + w.y + w.z);
|
||||||
|
float s = seam_lines(p.zy / seam_scale) * w.x
|
||||||
|
+ seam_lines(p.xz / seam_scale) * w.y
|
||||||
|
+ seam_lines(p.xy / seam_scale) * w.z;
|
||||||
|
float cell_px = length(fwidth(p)) / seam_scale;
|
||||||
|
return s * (1.0 - smoothstep(0.25, 0.5, cell_px));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec3 base = albedo_color.rgb;
|
vec3 base = albedo_color.rgb;
|
||||||
if (has_texture) {
|
if (has_texture) {
|
||||||
@@ -51,30 +155,137 @@ void fragment() {
|
|||||||
base *= texture(albedo_texture, UV).rgb;
|
base *= texture(albedo_texture, UV).rgb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (use_vertex_color) {
|
||||||
|
base *= COLOR.rgb;
|
||||||
|
}
|
||||||
|
base *= albedo_gain;
|
||||||
|
float source_luma = dot(base, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
base = mix(vec3(source_luma), base, albedo_saturation);
|
||||||
|
// Non-clipping power curve. Imported gun greys arrive as very small LINEAR
|
||||||
|
// values (an sRGB 0.17 receiver is roughly 0.025 here); a conventional 18%
|
||||||
|
// linear contrast pivot clips all of those finishes to exactly zero. The
|
||||||
|
// reciprocal curve expands their separation without moving black or white.
|
||||||
|
base = pow(max(base, vec3(0.0)),
|
||||||
|
vec3(1.0 / max(albedo_contrast, 0.001)));
|
||||||
|
if (albedo_floor > 0.0) {
|
||||||
|
float luma = dot(base, vec3(0.2126, 0.7152, 0.0722));
|
||||||
|
base = min(base * max(1.0, albedo_floor / max(luma, 0.001)), vec3(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surface law, into the albedo so the cel bands still light it normally.
|
||||||
|
if (seam_scale > 0.0) {
|
||||||
|
base *= 1.0 - panel_seam(world_pos, world_normal) * seam_strength;
|
||||||
|
}
|
||||||
|
if (grade_height > 0.0) {
|
||||||
|
float h = clamp((world_pos.y - grade_floor) / grade_height, 0.0, 1.0);
|
||||||
|
base *= mix(1.0 - grade_strength, 1.0, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A one-pixel technical-ink accent where the surface normal changes
|
||||||
|
// quickly. Unlike an inverted hull this follows receiver cut-lines and
|
||||||
|
// bevels without exploding on the weapon FBXs' split hard normals.
|
||||||
|
float local_normal_slope = length(fwidth(normalize(NORMAL)));
|
||||||
|
float vm_crease = smoothstep(
|
||||||
|
viewmodel_crease_threshold,
|
||||||
|
viewmodel_crease_threshold * 2.6,
|
||||||
|
local_normal_slope);
|
||||||
|
base *= 1.0 - vm_crease * viewmodel_crease_strength;
|
||||||
|
|
||||||
|
// Screen-tone dots, rotated 45° the way a real screentone sheet is. Spent
|
||||||
|
// in light(), which is where we know whether this pixel is in shadow.
|
||||||
|
halftone_dot = 0.0;
|
||||||
|
if (halftone_scale > 0.0) {
|
||||||
|
vec2 hp = FRAGCOORD.xy / halftone_scale;
|
||||||
|
vec2 r = vec2(hp.x - hp.y, hp.x + hp.y) * 0.70710678;
|
||||||
|
halftone_dot = smoothstep(0.36, 0.30, length(fract(r) - 0.5));
|
||||||
|
}
|
||||||
|
|
||||||
ALBEDO = base;
|
ALBEDO = base;
|
||||||
ROUGHNESS = 1.0;
|
ROUGHNESS = 1.0;
|
||||||
SPECULAR = 0.0;
|
SPECULAR = 0.0;
|
||||||
|
|
||||||
|
// Screen-space rate of change of the normal, for the band-edge width in
|
||||||
|
// light(). It has to be measured here: derivatives are a fragment-stage
|
||||||
|
// operation, and light() runs once per light — taking fwidth() inside that
|
||||||
|
// loop is undefined behaviour on some drivers.
|
||||||
|
normal_slope = length(fwidth(NORMAL));
|
||||||
|
|
||||||
// Rim: brighten grazing angles for that inked-silhouette pop.
|
// Rim: brighten grazing angles for that inked-silhouette pop.
|
||||||
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
|
float rim = 1.0 - clamp(dot(normalize(VIEW), NORMAL), 0.0, 1.0);
|
||||||
rim = smoothstep(1.0 - rim_width, 1.0, rim);
|
rim = smoothstep(1.0 - rim_width, 1.0, rim);
|
||||||
EMISSION = base * rim * rim_strength;
|
// Deterministic view-space key/fill for the separate transparent viewmodel
|
||||||
|
// viewport. Directional lights parented to its camera are renderer- and
|
||||||
|
// world-sharing-sensitive; this painted light map follows the gun normals
|
||||||
|
// directly and therefore survives every level environment.
|
||||||
|
float vm_ndotl = dot(normalize(NORMAL), normalize(viewmodel_key_direction));
|
||||||
|
float vm_leave_shadow = smoothstep(-0.24, -0.04, vm_ndotl);
|
||||||
|
float vm_enter_light = smoothstep(0.18, 0.34, vm_ndotl);
|
||||||
|
float vm_highlight = smoothstep(0.64, 0.78, vm_ndotl);
|
||||||
|
float vm_tone = mix(viewmodel_shadow_tone, viewmodel_mid_tone,
|
||||||
|
vm_leave_shadow);
|
||||||
|
vm_tone = mix(vm_tone, 0.82, vm_enter_light);
|
||||||
|
vm_tone = mix(vm_tone, 1.0, vm_highlight);
|
||||||
|
vec3 vm_light = mix(viewmodel_fill_color, viewmodel_key_color,
|
||||||
|
vm_enter_light);
|
||||||
|
EMISSION = base * (
|
||||||
|
vec3(rim * rim_strength + ambient_fill)
|
||||||
|
+ vm_light * vm_tone * viewmodel_light_strength);
|
||||||
}
|
}
|
||||||
|
|
||||||
void light() {
|
void light() {
|
||||||
float ndotl = dot(NORMAL, LIGHT);
|
float ndotl = dot(NORMAL, LIGHT);
|
||||||
// Shadowing folds into the band test so shadow edges band too.
|
|
||||||
float lit = ndotl * ATTENUATION;
|
// Band-edge width, with a screen-space floor.
|
||||||
float band = smoothstep(band_edge - band_softness, band_edge + band_softness, lit);
|
//
|
||||||
float mid = smoothstep(mid_band_edge - band_softness, mid_band_edge + band_softness, lit);
|
// band_softness is measured in NdotL, which says nothing about how many
|
||||||
// 3 tones: shadow tint -> base band (0.82) -> full light.
|
// PIXELS the transition actually covers. On a slowly-curving surface it can
|
||||||
float tone = mix(0.82, 1.0, mid);
|
// span a third of a wall; on a tight curve, or simply at distance, it
|
||||||
|
// collapses below one pixel and the terminator crawls and stair-steps as
|
||||||
|
// the camera moves. MSAA cannot touch this — it is shading aliasing, not
|
||||||
|
// geometry. normal_slope is how much the normal turns across one pixel, so
|
||||||
|
// widening the step to at least that much keeps every band edge about a
|
||||||
|
// pixel wide while leaving edges that are already wider exactly as authored.
|
||||||
|
// Flat surfaces have a slope of zero and so keep their crisp break.
|
||||||
|
float w = max(band_softness, normal_slope * 0.9);
|
||||||
|
|
||||||
|
// FORM shadow — the surface turning away from the light. This is the cel
|
||||||
|
// terminator, and its numbers are what the whole look was calibrated on, so
|
||||||
|
// it still lands exactly where it did.
|
||||||
|
float band = smoothstep(band_edge - w, band_edge + w, ndotl);
|
||||||
|
float mid = smoothstep(mid_band_edge - w, mid_band_edge + w, ndotl);
|
||||||
|
// 3 tones: shadow tint -> base band (mid_tone) -> full light.
|
||||||
|
float tone = mix(mid_tone, 1.0, mid);
|
||||||
vec3 shade = mix(shadow_color.rgb, vec3(tone), band);
|
vec3 shade = mix(shadow_color.rgb, vec3(tone), band);
|
||||||
|
|
||||||
|
// CAST shadow — something is standing between this pixel and the light.
|
||||||
|
//
|
||||||
|
// These used to be one thing: NdotL was multiplied by ATTENUATION and the
|
||||||
|
// PRODUCT was banded, so a cast shadow could never be darker than the
|
||||||
|
// terminator's own tint, about 0.65 of full light. Under the old ambient
|
||||||
|
// flood (Neon Alley used to run 1.35) that came out as a dip of roughly a
|
||||||
|
// seventh — which is why a bench standing in direct sun threw nothing onto
|
||||||
|
// the pavement, and why tools/levels.py measured a 1st percentile of 119 on
|
||||||
|
// a daylit frame: nothing in it was dark, so nothing rested on anything.
|
||||||
|
//
|
||||||
|
// Cel art draws the two separately, and so does this now: the terminator
|
||||||
|
// keeps its calibrated tone step, and occlusion drops below it to
|
||||||
|
// cast_shadow_depth as a harder, deeper shape.
|
||||||
|
float occ = smoothstep(0.0, cast_shadow_softness, ATTENUATION);
|
||||||
|
shade = mix(shadow_color.rgb * cast_shadow_depth, shade, occ);
|
||||||
|
|
||||||
|
// Screen tone, in the shadow only — that is where a manga artist puts it,
|
||||||
|
// and confining it there means it reads as shading rather than as a filter
|
||||||
|
// laid over the picture. Scaled by how deep in shadow the pixel is, so the
|
||||||
|
// dots fade in across the terminator instead of switching on at it.
|
||||||
|
float in_shadow = (1.0 - band) * (1.0 - occ * 0.4);
|
||||||
|
shade *= 1.0 - halftone_dot * in_shadow * halftone_strength;
|
||||||
|
|
||||||
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
|
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR / PI * shade;
|
||||||
|
|
||||||
// Stepped specular dot for glossy toon highlights.
|
// Stepped specular dot for glossy toon highlights. Gated by occ so a
|
||||||
|
// highlight cannot sit inside a shadow.
|
||||||
vec3 h = normalize(VIEW + LIGHT);
|
vec3 h = normalize(VIEW + LIGHT);
|
||||||
float spec = pow(clamp(dot(NORMAL, h), 0.0, 1.0), specular_shininess);
|
float spec = pow(clamp(dot(NORMAL, h), 0.0, 1.0), specular_shininess);
|
||||||
spec = smoothstep(0.5 - band_softness, 0.5 + band_softness, spec);
|
spec = smoothstep(0.5 - w, 0.5 + w, spec);
|
||||||
SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band;
|
SPECULAR_LIGHT += LIGHT_COLOR * spec * specular_strength * band * occ;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ render_mode cull_front, unshaded;
|
|||||||
// culled, leaving a colored shell visible only at the silhouette.
|
// culled, leaving a colored shell visible only at the silhouette.
|
||||||
|
|
||||||
uniform vec4 outline_color : source_color = vec4(0.06, 0.05, 0.09, 1.0);
|
uniform vec4 outline_color : source_color = vec4(0.06, 0.05, 0.09, 1.0);
|
||||||
uniform float outline_width : hint_range(0.0, 0.1) = 0.02;
|
uniform float outline_width : hint_range(0.0, 0.1) = 0.006;
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
VERTEX += NORMAL * outline_width;
|
VERTEX += NORMAL * outline_width;
|
||||||
|
|||||||
@@ -13,9 +13,35 @@ from these CC0 packs, no attribution required:
|
|||||||
- Kenney Interface Sounds — https://kenney.nl/assets/interface-sounds (CC0)
|
- Kenney Interface Sounds — https://kenney.nl/assets/interface-sounds (CC0)
|
||||||
- Kenney Music Jingles — https://kenney.nl/assets/music-jingles (CC0)
|
- Kenney Music Jingles — https://kenney.nl/assets/music-jingles (CC0)
|
||||||
|
|
||||||
Exceptions (still procedural from `generate_sounds.py`): `wind.wav`,
|
Runtime masters are built by `tools/build_audio_assets.py`. It retains the
|
||||||
`wallrun.wav`, `slide.wav` — seamless noise loops that the Kenney one-shots
|
recorded transient, layers a filtered body and short reflection tail, controls
|
||||||
can't replace.
|
peaks, and constructs seamless movement beds from pinned source recordings.
|
||||||
|
|
||||||
Menu music: `music_menu.wav` — "Menu Music" by mrpoly, CC0,
|
The industrial movement/UI pass uses:
|
||||||
|
|
||||||
|
- "Wind Loop" — field recording by Jonathan Shaw (InspectorJ), loop edit by
|
||||||
|
AntumDeluge, CC-BY 3.0,
|
||||||
|
https://opengameart.org/content/wind-loop
|
||||||
|
- "Scrapes" by AntumDeluge — recorded cinder-block friction, CC0,
|
||||||
|
https://opengameart.org/content/scrapes
|
||||||
|
- "Mechanical Sounds" by Brian MacIntosh — clunks, ratchets and impacts, CC0,
|
||||||
|
https://opengameart.org/content/mechanical-sounds
|
||||||
|
- "Steam boiler sound loop" by bart — metallic generator source, CC0,
|
||||||
|
https://opengameart.org/content/steam-boiler-sound-loop
|
||||||
|
- "Mech Stomp / Step Sound" by hc/qubodup — low mechanical body, CC0,
|
||||||
|
https://opengameart.org/content/mech-stomp-step-sound
|
||||||
|
- "Mechanical Explosion" by Spring Spring — industrial debris layer, CC0,
|
||||||
|
https://opengameart.org/content/mechanical-explosion
|
||||||
|
- "Air whoosh" by pyranostudios — movement/thrust air layer, CC0,
|
||||||
|
https://opengameart.org/content/air-whoosh
|
||||||
|
- "Rocket Engine" by theMinesAreShakin — low jump/dash thruster body, CC0,
|
||||||
|
https://opengameart.org/content/rocket-engine
|
||||||
|
|
||||||
|
Every remote input is SHA-256 pinned in the builder. No continuous runtime cue
|
||||||
|
is made from generated random noise.
|
||||||
|
|
||||||
|
Current menu music: "PYNCHON" by James Gargette/cinameng, CC0,
|
||||||
|
https://opengameart.org/content/pynchon
|
||||||
|
|
||||||
|
Legacy menu music retained as `music_menu.ogg`: "Menu Music" by mrpoly, CC0,
|
||||||
https://opengameart.org/content/menu-music
|
https://opengameart.org/content/menu-music
|
||||||
|
|||||||