Compare commits

..
Author SHA1 Message Date
Nicholas Butzke bb34764d59 leg jump swap ani fix 2026-08-10 12:57:41 -04:00
Nicholas Butzke 0ce626ad42 jump ani fixing and transition animations 2026-08-10 12:50:00 -04:00
Nicholas Butzke 833e936bcd ani fixed 2026-08-10 12:17:59 -04:00
Nicholas Butzke 9358746582 ani 2026-08-09 01:31:44 -04:00
Nicholas Butzke 922983429e big 2026-08-02 02:20:02 -04:00
Nicholas ButzkeandClaude Opus 5 61669627db chore(debug): track the UID Godot generated for menu_capture.gd
Godot writes a .uid sidecar for every script and this one landed after the
commit that added debug/menu_capture.gd. All 140 of the others are in version
control; an untracked one drifts to a different UID on the next machine that
imports the project.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 13:09:15 -04:00
Nicholas ButzkeandClaude Opus 5 2efc21b18d feat(menu): PLAY is one press, and the level cards are photographs
Reaching a game was Singleplayer → pick a level. Reaching a multiplayer game
was Multiplayer → Host Game → wait → pick a level → wait, and the gamemode
dropdown had exactly one entry which set a string nothing read.

PLAY now starts the last map and mode played, and says which — "Akiba Crossing
• Deathmatch" under the button, so pressing it is a promise rather than a leap.
Everything else on the home screen is a detour from that, which is the right
shape for a menu: the common case is a button, not a path. Hosting is one press
and lands in the lobby already hosting; the lobby puts map, mode, players and
start on ONE screen; Enter connects, so typing an address does not then require
reaching for the mouse.

This is the lesson HoYoverse published about Zenless Zone Zero's first months
more loudly than anything else they have written. Their postmortem on the TV
mode names three complaints — it took too long, it sat between the player and
the combat, and there was too much of it early — and 1.2 removed it from the
story entirely rather than shortening it. Time spent BEFORE the thing the
player came for is not neutral, it is a cost.

The cards were a two-stop gradient generated from two colours in a meta file,
whose hover state was `use_hdr = true` — not a visible change on any of them.
They are photographs now, shot by debug/map_preview_capture.gd, which took
three attempts to get right and each attempt is a comment in the file:

  - framing each map from OUTSIDE by merging every VisualInstance3D's AABB
    produced five tiny dioramas floating on a table, and one solid black
    rectangle. That is a minimap, and a minimap is not a photograph.
  - standing at a spawn point fixed three maps and left two black: fps_blockout
    is genuinely dark and most of its spawns face an unlit wall, and
    procedural_arena builds its geometry at runtime so no fixed offset is
    reliably inside it.
  - so the tool now RENDERS several vantages and scores each result — mean
    luminance times its standard deviation, because brightness alone picks the
    empty sky and variance alone picks a high-contrast corner of a dark room.
    Their product picks a photograph. A black rectangle passes any check that
    only asks whether the camera ended up somewhere sensible.

Also fixed, and caught by looking at the screenshot: the selected mode chip was
unreadable. Its glyph is ink on volt, and the theme gave every button a 5 px INK
outline — an ink glyph inside an ink outline is not outlined, it is five pixels
fatter, and on a small chip that is a solid blob. Button labels now carry no
outline at all, which is the same reasoning debug/ui_contrast_check.gd already
encodes: an outline separates a glyph from a backdrop it cannot beat alone, and
a label on a solid chip does not have that problem. The chips keep their heavy
ink border, so nothing about the drawn look changes.

`current_gamemode` is normalised on the way in, because it used to hold a
display string and six callers still pass one. An unrecognised id compares
unequal to GameMode.GUN_GAME and would silently disable that mode's weapon
issuing — half a mode is worse than none.

The menu's background character is the player's OWN skin holding their weapon,
rather than the box-and-capsule mannequin that stood there before.

spawn smoke 0, game modes 30/30, movement 11/11, contrast 108/108, HUD layout
PASS, weapon holds 0, dances 0.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 12:40:50 -04:00
Nicholas ButzkeandClaude Opus 5 986179854d feat(modes): matches now end, and something is at stake when they do
There was a string — `current_gamemode`, always "Deathmatch" — an OptionButton
with one entry in it, and a five-minute timer that counted to zero, set
`match_active = false`, and did nothing else. No winner was declared, no summary
appeared, the clock froze at 00:00, and players carried on shooting each other
in a match that had stopped counting. Nothing was tracked beyond a running kill
count. A match did not end so much as stop mattering, and everything a player
does in the last minute only matters if there is a last minute.

globals/game_mode.gd answers the three questions a mode has to answer — how you
score, when it ends, who won — for three modes:

  Deathmatch        25 frags or 10 minutes.
  Team Deathmatch   two squads to 50, teams balanced by COUNT on join (a 4v4
                    that loses three from one side must refill the short side;
                    round-robin on join order leaves it 4v1 forever), friendly
                    fire off and enforced on the server before damage is even
                    broadcast — a mode where the damage lands but the kill does
                    not count is worse than either.
  Gun Game          every kill promotes you a rung and swaps your weapon. The
                    score IS the rung and the limit IS the ladder's length, so
                    finishing the ladder and reaching the score limit are the
                    same event and only one win condition exists.

`check_win` is a pure function of the stats and the clock, deliberately, because
a win condition that can only be exercised by playing a whole match is one
nobody tests — and the previous one never was. debug/game_mode_check.gd runs 30
cases over it: a tie is a DRAW rather than a win for whoever came first out of
the dictionary; a team match is decided on the TEAM's total, which a per-player
check never reaches; every ladder rung names a weapon that exists, or that rung
softlocks the mode; and promoting past the top clamps, because the winning kill
promotes before the match-end RPC lands.

Tracking now covers score, team, current streak, best streak and ladder rung.
Score and kills are separate numbers because in Gun Game they coincide and in
anything with an objective they would not. The scoreboard ranks by score, shows
the mode's own noun for it, and puts the limit on the top line — a win condition
players cannot see is one they cannot play toward.

ui/match_summary.gd gives the ending somewhere to happen: who won, WHY (time and
frag limit are different stories about the same scoreline), team totals, full
standings, and a way out that is not alt-F4. Play Again is host-only on a server.

Three bugs found on the way, all of which only became bugs once matches could
actually end: loading a level reset the clock but not the scores, so the second
match on a server would have ended on its first kill; `.rpc()` on an offline
peer does not call locally, so singleplayer had no killfeed and no stat sync at
all; and a rocket already in the air at the whistle could change the result
after the summary was on screen.

spawn smoke 0, game modes 30/30, movement 11/11.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 12:28:32 -04:00
Nicholas ButzkeandClaude Opus 5 f1a4f7df52 feat(emotes): five dances, built like animation, behind a radial dial
The shared clip library ships exactly one `Dance_Loop`, and five copies of one
clip is not five dances. What the runtime does have is a procedural pose layer
over a real skeleton with spring-driven hair and cloth, which is enough — if
the motion is constructed the way an animator would construct it rather than
the way a programmer reaches for first.

Wiring sine waves to bones is that first reach, and everyone can tell. A raw
sine moves fastest through the middle and slowest at the ends by the same
amount on every channel, all in phase, forever. It floats. It has no weight, no
accent, and no sense that one part of the body is driving and the rest is
following. Four principles fix it, and all four are cheap:

  OVERLAP        the body is a chain. Hips lead, spine follows a beat later,
                 head last. One subtraction — `beat - lag * i` — and the spring
                 solver then carries it out through the hair and skirt for free,
                 because the dance layer runs before it.
  ACCENT         a dance HITS poses. `shape` bends the wave so it hangs at the
                 extremes and snaps between them, which is what a key-and-
                 breakdown pass produces by hand.
  WEIGHT         the HIPS translate, not just rotate. A body that never leaves
                 its own axis reads as a puppet on a stick.
  CONTRAST       Robot deliberately breaks all of the above — zero lag,
                 quantised motion — and reads as mechanical precisely because
                 the other four do not.

Spin spots its head: it holds a heading against the turn and whips round to
catch up, which is what a real dancer does to keep from getting dizzy and the
most recognisable thing about a turn.

The dial is a radial menu because every option is then the SAME DISTANCE from
where the pointer starts — the choice is a direction, and a direction becomes
muscle memory in a way "the fourth row down" does not. Selection is by ANGLE
alone, so a flick and a careful nudge do the same thing. HOLD to open, release
to commit; a tap too short to have aimed replays the last emote, which is what
the button did before, so the old habit still works. Pressing while already
dancing just stops — having to aim at something in order to STOP would be the
most annoying possible way to build this.

debug/dance_check.gd asserts the overlap, and getting it to measure that took
four wrong measurements, each of which is now a comment where it was made:

  - correlating the hips' TRANSLATION against the head's position relative to
    them compared two different quantities at different periods; it ranked the
    Robot, whose lag is zero by construction, as the most overlapped routine.
  - a signed scalar `angle * sign of the axis's largest component` is
    DISCONTINUOUS — as a rocking bone passes back through rest the axis flips —
    so smooth Two-Step measured a full-range jump per frame, which is exactly
    what quantised motion looks like.
  - a bone's GLOBAL rotation carries every ancestor's, so the head correlates
    with the hips at lag zero however delayed the head itself is.
  - and the hips and head are driven by different channels anyway.

Measuring two links of the SAME chain, as local rotation vectors, agrees with
the authored lag: Spin measures 9 frames against 8.4 authored, Two-Step 7
against 6.6, Robot 0. The Robot is checked on the property it actually has —
its jump per frame is 0.41 of its range against 0.03-0.06 for the others.

RigRoles is pulled out of ShooterPoseModifier so the dance layer resolves bones
the same way rather than carrying a second copy. Two copies is how a rig ends up
animating correctly under one modifier and not the other.

spawn smoke 0 failures, 11/11 movement, 21/21 weapon-hold pairs, contrast 108/108.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 12:20:33 -04:00
Nicholas ButzkeandClaude Opus 5 a13ae50f95 feat(rig): every weapon gets its own hold, so the silhouette names the gun
`_apply_rifle_hold` did exactly what its name said, to everything. A knife, an
AWP and a rocket launcher were all solved as a rifle — stock in the shoulder
pocket, support hand out along the barrel, muzzle on the aim line — so in third
person every character stood the same way whatever they carried, and the only
thing distinguishing a sniper from a shotgun was the ~30 cm of gun mesh in
their hands. At the distance an enemy is usually seen that is nothing.

It costs more than looking wrong. A character's pose is the fastest available
answer to "what is about to happen to me": a tube on a shoulder means take
cover, a blade held low means they have to close, a rifle at low ready means
they have not seen you. One hold throws all of that away. It is also the thing
HoYoverse's team say they chase in Zenless Zone Zero — characters read by
silhouette first, and they refuse to settle on one construction method because
one method limits how distinguishable the results can be.

weapons/weapon_hold_profiles.gd gives each of the twelve weapons a style, and a
style is not a bundle of slider values. Three of its differences cannot be
expressed on the rifle solve at all, and those are the ones that carry:

  support   WHERE the off hand goes and HOW IT IS TURNED there — wrapped round
            a handguard, cupped against the firing fist, hooked under a tube
            palm-up, or released entirely. Sending a hand somewhere new without
            re-orienting it gives a hand teleported to the new spot still shaped
            for the old one.
  mount     whether the weapon's rear sits IN the shoulder pocket, ON TOP of the
            shoulder, or nowhere near it.
  head      whether the head comes down to the stock, or leans away to clear a
            tube. The cheek weld is the sniper silhouette, and its inverse is
            what says a launcher is resting on that shoulder.

A blade RELEASES the off arm back to the animation, so it swings with the run
cycle instead of gripping a handguard that is not there — most of what makes a
one-handed weapon read as one-handed — and closes a full fist, because an index
left straight along a knife handle reads as a mistake, not as discipline.

Layered strictly UNDER the existing tuning, so aria's hand-tuned AK-47 hold is
byte-for-byte what it was. WeaponHoldTuning.default_for is weapon-aware now,
which it had to be: the rig lab SAVES every knob it shows, so without it,
opening the lab on the knife and pressing save would silently overwrite the
blade profile with the rifle spec and put the character back to holding a knife
like an AK with nothing to indicate it had happened.

debug/weapon_hold_check.gd asserts the consequence, not the plumbing — storing
an enum and reading it back proves nothing. It measures where the hands and head
ACTUALLY end up, from inside the modifier pass (outside it, Godot restores the
local poses and every weapon reports an identical rifle) and in the shoulder's
own frame, because the hold breathes and two samples of the SAME weapon
otherwise differ by more than two different weapons do. 21 weapon pairs, all
distinguishable; the knife's off-hand weight measured at 0.01.

Two findings only measuring produced. Pushing `gun_fore` further out for the
sniper does NOTHING — the reach solver slides the support hand back down the
handguard until the arm can get there, so it landed at 0.388 m against the
rifle's 0.387. Raising the whole weapon is what makes a scoped rifle read.

And one only LOOKING produced, via debug/hold_capture.gd: the shotgun's barrel
passed through the character's chest. Every assertion passed — the hands were
exactly where they had been asked to go — but +x is toward the centreline, so
dropping the pocket and pushing it across at once swings the muzzle into the
torso.

rig_anchor_check still reports aria: her hand-tuned wrist rotates the anchor
offset. Pre-existing, and this halves it — it was 2 failures on main, now 1.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 12:05:43 -04:00
Nicholas ButzkeandClaude Opus 5 c4bcbc7fd1 feat(ui): the first-person HUD is one thing, in the game's own hand
The reticle was five white ColorRects, pasted byte-identically into three
level runtime scripts. The vitals were two stock ProgressBars with a flat
colour override, inlined 1200 lines into the movement controller. The ammo
count — the number a shooter's player looks at most — was drawn by the LEVEL,
in a black rounded panel that shared nothing with the menus, and it had a
special case in it (`elif active_weapon is DoubleBarrelShotgun`) because that
weapon never declared a name or a capacity.

Everything that describes A PLAYER now belongs to ui/player_hud.gd, and a
level owns the level. The immediate symptom that fixed: the screen had two
ammo panels on it at once, in two different styles, overlapping in the corner.

What each piece now says, rather than merely shows:

  ui/crosshair.gd    one drawn reticle instead of five rectangles, so it can
                     BLOOM — open with speed, airtime and each shot, snap shut
                     on ADS. That is the accuracy readout of the whole game and
                     five ColorRects could not express it. Every stroke is
                     drawn twice, ink underneath, because a 2 px white line
                     disappears over pale concrete exactly when aim matters.
                     The hit confirmation is the same cross at 45 degrees, so
                     it lands where the eye already is.
  ui/vital_bar.gd    segmented, so remaining health can be COUNTED rather than
                     estimated, with a drain ghost that holds the old value for
                     a beat — the gap between fill and ghost is the size of the
                     hit, which a bar that merely gets shorter never tells you.
  ui/ability_chip.gd dash and grapple as a wipe across a chip rather than a
                     tinted JPEG with a 12 px number under it. A shape changing
                     size is readable in peripheral vision; 12 px type is not.
  chain meter        promoted out of the debug panel. Movement is this game's
                     first stated pillar and chaining is its skill expression,
                     so the count is a score, not a diagnostic.

The numerals moved OFF the bars and beside them. Text centred on a two-tone
bar cannot be given a colour that beats both the fill and the trough — that is
the 2.4:1 debug/ui_contrast_check.gd measured on the old HUD — so this fixes it
at the source rather than leaning on an outline to rescue it.

debug/hud_layout_check.gd measures where every element actually lands, which is
how three real bugs were found rather than squinted at: `set_anchors_preset`
moves the anchors and LEAVES THE OFFSETS, so the reticle spanned the viewport
with a size of exactly (0,0) and drew itself in the top-left corner; a
PRESET_CENTER applied after the ring's own `_ready` undid its centring; and a
BOX CONTAINER's own `alignment` is what pushes content to an edge, not a
SHRINK_END flag on the box, whose minimum width depends on children that may be
hidden. The ammo card was hanging off the right edge of the screen because of
the last one.

DoubleBarrelShotgun now declares `weapon_name` and `max_shells` like every
other weapon, and the four inline `2`s are gone.

spawn smoke 0 failures, 11/11 movement tests, contrast 108/108, layout PASS.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 11:51:22 -04:00
Nicholas ButzkeandClaude Opus 5 414026f001 feat(ui): every control state is checked for readability, not trusted
The palette has two light accents and one very dark one, and a control
changes its FILL on hover and press. Paper text that reads at 18:1 on the
resting near-black chip inverts to paper-on-yellow the moment the pointer
arrives, which is 1.1:1 — invisible. That is the standard way a stylised UI
becomes unreadable, and it was live on the OptionButton dropdown that every
settings row uses: PopupMenu draws its papaya hover fill but keeps
`font_color` unless `font_hover_color` is set, and it was not set.

So the label now follows the fill. `ink_for(fill)` picks the legible glyph
colour by contrast ratio, and every state's text, icon and outline is derived
from its own fill through it — including the states nobody remembers exist:
`hover_pressed` on a toggle (a CheckButton read as OFF while you touched it),
icon colours on a CheckBox that is all icon, and the list hover that used to
be the same papaya as list SELECTION, so the row you pointed at looked like
the row you had chosen.

debug/ui_contrast_check.gd interrogates the BUILT theme rather than the
palette — a table compared against itself agrees by construction and catches
nothing — and fails below the WCAG floor. It found the PopupMenu gap, a Tree
hover asking for a `font_hovered_color` Godot does not have, and the health
bar's readout at 2.4:1 over its own fill.

That last one is not fixable as a colour pair: a centred readout straddles a
hot papaya fill and a near-black trough, and no single colour beats both. What
carries it is the heavy ink outline this theme puts on every glyph, which is
its first stated rule and the same mechanism that keeps menu text legible
straight over the 3D scene. The check models that as a fallback route —
outline vs backdrop 3:1, glyph vs outline 4.5:1, at least 4 px — granted only
where the backdrop genuinely varies, and substituting two ratios for one
rather than waiving the requirement.

Disabled text moves from 3.2:1 to 5.2:1 on the way past. A greyed-out
"Start Match" is information; an illegible smudge is not.

Also styled, because the theme had simply never mentioned them and Godot's
defaults are grey-on-grey: scrollbars, Tree, SpinBox, ProgressBar, tooltips,
LineEdit read-only and selected text.

108 pairs checked, all passing.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-07-28 02:39:25 -04:00
Dotts f0b0d19847 Merge pull request 'Feat/outline thickness and tp weapon hold' (#22) from feat/outline-thickness-and-tp-weapon-hold into main
Reviewed-on: #22
2026-07-27 23:22:53 -07:00
296 changed files with 44390 additions and 19794 deletions
+5
View File
@@ -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
Binary file not shown.
@@ -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 Hahnes conversion of the original CMU motion-capture data.
CMUs 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
```
Binary file not shown.
@@ -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.
Binary file not shown.
@@ -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.
Binary file not shown.
@@ -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.
Binary file not shown.
@@ -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.
Binary file not shown.
@@ -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"
]
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+202 -202
View File
@@ -1,207 +1,207 @@
{ {
"roles": { "roles": {
"hips": "DEF-hips", "hips": "DEF-hips",
"head": "DEF-head", "head": "DEF-head",
"neck": "DEF-neck", "neck": "DEF-neck",
"spine": [ "spine": [
"DEF-spine.001", "DEF-spine.001",
"DEF-spine.002", "DEF-spine.002",
"DEF-spine.003", "DEF-spine.003",
"DEF-neck", "DEF-neck",
"DEF-head" "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": {}
}
], ],
"shoulder.L": "DEF-shoulder.L", "colliders": [
"shoulder.R": "DEF-shoulder.R", {
"upper_arm.R": "DEF-upper_arm.R", "bone": "DEF-hips",
"upper_arm.L": "DEF-upper_arm.L", "child": "DEF-spine.001",
"forearm.L": "DEF-forearm.L", "from": 0.0,
"forearm.R": "DEF-forearm.R", "radius_head": 0.1381,
"thigh.L": "DEF-thigh.L", "radius_tail": 0.1381,
"thigh.R": "DEF-thigh.R", "radius": 0.1381,
"foot.R": "DEF-foot.R", "lid": true
"foot.L": "DEF-foot.L", },
"shin.R": "DEF-shin.R", {
"shin.L": "DEF-shin.L", "bone": "DEF-thigh.L",
"hand.R": "DEF-hand.R", "child": "DEF-shin.L",
"hand.L": "DEF-hand.L", "from": 0.1,
"toe.R": "DEF-toe.R", "radius_head": 0.1037,
"toe.L": "DEF-toe.L" "radius_tail": 0.0733,
}, "radius": 0.0733
"fingers": { },
"thumb.L": [ {
"DEF-thumb.01.L", "bone": "DEF-thigh.R",
"DEF-thumb.02.L", "child": "DEF-shin.R",
"DEF-thumb.03.L" "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
}
], ],
"index.L": [ "weights_authored": true,
"DEF-f_index.01.L", "driven_bones": [
"DEF-f_index.02.L", "DEF-f_index.01.L",
"DEF-f_index.03.L" "DEF-f_index.01.R",
], "DEF-f_index.02.L",
"middle.L": [ "DEF-f_index.02.R",
"DEF-f_middle.01.L", "DEF-f_index.03.L",
"DEF-f_middle.02.L", "DEF-f_index.03.R",
"DEF-f_middle.03.L" "DEF-f_middle.01.L",
], "DEF-f_middle.01.R",
"ring.L": [ "DEF-f_middle.02.L",
"DEF-f_ring.01.L", "DEF-f_middle.02.R",
"DEF-f_ring.02.L", "DEF-f_middle.03.L",
"DEF-f_ring.03.L" "DEF-f_middle.03.R",
], "DEF-f_pinky.01.L",
"pinky.L": [ "DEF-f_pinky.01.R",
"DEF-f_pinky.01.L", "DEF-f_pinky.02.L",
"DEF-f_pinky.02.L", "DEF-f_pinky.02.R",
"DEF-f_pinky.03.L" "DEF-f_pinky.03.L",
], "DEF-f_pinky.03.R",
"thumb.R": [ "DEF-f_ring.01.L",
"DEF-thumb.01.R", "DEF-f_ring.01.R",
"DEF-thumb.02.R", "DEF-f_ring.02.L",
"DEF-thumb.03.R" "DEF-f_ring.02.R",
], "DEF-f_ring.03.L",
"index.R": [ "DEF-f_ring.03.R",
"DEF-f_index.01.R", "DEF-foot.L",
"DEF-f_index.02.R", "DEF-foot.R",
"DEF-f_index.03.R" "DEF-forearm.L",
], "DEF-forearm.R",
"middle.R": [ "DEF-hand.L",
"DEF-f_middle.01.R", "DEF-hand.R",
"DEF-f_middle.02.R", "DEF-head",
"DEF-f_middle.03.R" "DEF-hips",
], "DEF-neck",
"ring.R": [ "DEF-shin.L",
"DEF-f_ring.01.R", "DEF-shin.R",
"DEF-f_ring.02.R", "DEF-shoulder.L",
"DEF-f_ring.03.R" "DEF-shoulder.R",
], "DEF-spine.001",
"pinky.R": [ "DEF-spine.002",
"DEF-f_pinky.01.R", "DEF-spine.003",
"DEF-f_pinky.02.R", "DEF-thigh.L",
"DEF-f_pinky.03.R" "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"
] ]
},
"chains": [],
"twist": [],
"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"
],
"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": {}
}
]
} }
Binary file not shown.
+224 -224
View File
@@ -1,229 +1,229 @@
{ {
"roles": { "roles": {
"hips": "DEF-hips", "hips": "DEF-hips",
"head": "DEF-head", "head": "DEF-head",
"neck": "DEF-neck", "neck": "DEF-neck",
"spine": [ "spine": [
"DEF-spine.001", "DEF-spine.001",
"DEF-spine.002", "DEF-spine.002",
"DEF-spine.003", "DEF-spine.003",
"DEF-neck", "DEF-neck",
"DEF-head" "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": {}
}
], ],
"shoulder.L": "DEF-shoulder.L", "colliders": [
"shoulder.R": "DEF-shoulder.R", {
"upper_arm.L": "DEF-upper_arm.L", "bone": "DEF-hips",
"upper_arm.R": "DEF-upper_arm.R", "child": "DEF-spine.001",
"forearm.L": "DEF-forearm.L", "from": 0.0,
"forearm.R": "DEF-forearm.R", "radius_head": 0.1915,
"thigh.L": "DEF-thigh.L", "radius_tail": 0.1915,
"thigh.R": "DEF-thigh.R", "radius": 0.1915,
"foot.R": "DEF-foot.R", "lid": true
"foot.L": "DEF-foot.L", },
"shin.R": "DEF-shin.R", {
"shin.L": "DEF-shin.L", "bone": "DEF-thigh.L",
"hand.R": "DEF-hand.R", "child": "DEF-shin.L",
"hand.L": "DEF-hand.L", "from": 0.1,
"toe.R": "DEF-toe.R", "radius_head": 0.1435,
"toe.L": "DEF-toe.L" "radius_tail": 0.1304,
}, "radius": 0.1304
"fingers": { },
"thumb.L": [ {
"DEF-thumb.01.L", "bone": "DEF-thigh.R",
"DEF-thumb.02.L", "child": "DEF-shin.R",
"DEF-thumb.03.L" "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
}
], ],
"index.L": [ "weights_authored": false,
"DEF-f_index.01.L", "driven_bones": [
"DEF-f_index.02.L", "DEF-f_index.01.L",
"DEF-f_index.03.L" "DEF-f_index.01.R",
], "DEF-f_index.02.L",
"middle.L": [ "DEF-f_index.02.R",
"DEF-f_middle.01.L", "DEF-f_index.03.L",
"DEF-f_middle.02.L", "DEF-f_index.03.R",
"DEF-f_middle.03.L" "DEF-f_middle.01.L",
], "DEF-f_middle.01.R",
"ring.L": [ "DEF-f_middle.02.L",
"DEF-f_ring.01.L", "DEF-f_middle.02.R",
"DEF-f_ring.02.L", "DEF-f_middle.03.L",
"DEF-f_ring.03.L" "DEF-f_middle.03.R",
], "DEF-f_pinky.01.L",
"pinky.L": [ "DEF-f_pinky.01.R",
"DEF-f_pinky.01.L", "DEF-f_pinky.02.L",
"DEF-f_pinky.02.L", "DEF-f_pinky.02.R",
"DEF-f_pinky.03.L" "DEF-f_pinky.03.L",
], "DEF-f_pinky.03.R",
"thumb.R": [ "DEF-f_ring.01.L",
"DEF-thumb.01.R", "DEF-f_ring.01.R",
"DEF-thumb.02.R", "DEF-f_ring.02.L",
"DEF-thumb.03.R" "DEF-f_ring.02.R",
], "DEF-f_ring.03.L",
"index.R": [ "DEF-f_ring.03.R",
"DEF-f_index.01.R", "DEF-foot.L",
"DEF-f_index.02.R", "DEF-foot.R",
"DEF-f_index.03.R" "DEF-forearm.L",
], "DEF-forearm.R",
"middle.R": [ "DEF-hand.L",
"DEF-f_middle.01.R", "DEF-hand.R",
"DEF-f_middle.02.R", "DEF-head",
"DEF-f_middle.03.R" "DEF-hips",
], "DEF-neck",
"ring.R": [ "DEF-shin.L",
"DEF-f_ring.01.R", "DEF-shin.R",
"DEF-f_ring.02.R", "DEF-shoulder.L",
"DEF-f_ring.03.R" "DEF-shoulder.R",
], "DEF-spine.001",
"pinky.R": [ "DEF-spine.002",
"DEF-f_pinky.01.R", "DEF-spine.003",
"DEF-f_pinky.02.R", "DEF-thigh.L",
"DEF-f_pinky.03.R" "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"
] ]
},
"chains": [],
"twist": [],
"colliders": [
{
"bone": "DEF-hips",
"child": "DEF-spine.001",
"from": 0.0,
"radius_head": 0.1913,
"radius_tail": 0.1913,
"radius": 0.1913,
"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"
],
"surfaces": [
{
"mesh": "Object_2",
"surface": 0,
"material": "body",
"class": "body",
"detail": "skin",
"why": "material name says 'skin'",
"verts": 1496,
"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": {}
}
]
} }
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -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."
}
File diff suppressed because it is too large Load Diff
+54
View File
@@ -5,6 +5,15 @@
"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
}, },
{ {
@@ -12,6 +21,15 @@
"name": "Taila", "name": "Taila",
"description": "", "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 "unlocked": true
}, },
{ {
@@ -19,6 +37,15 @@
"name": "Mannequin", "name": "Mannequin",
"description": "Quaternius reference mannequin (CC0)", "description": "Quaternius reference mannequin (CC0)",
"model": "res://assets/characters/skins/mannequin.glb", "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 "unlocked": true
}, },
{ {
@@ -26,6 +53,15 @@
"name": "Kiyoko", "name": "Kiyoko",
"description": "Kiyoko School Girl \u2014 VRoid (CC-BY, Kasujin)", "description": "Kiyoko School Girl \u2014 VRoid (CC-BY, Kasujin)",
"model": "res://assets/characters/skins/kiyoko.glb", "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 "unlocked": true
}, },
{ {
@@ -33,6 +69,15 @@
"name": "Aria", "name": "Aria",
"description": "Anime Girl Rigged \u2014 VRoid (CC-BY, dequeijospizza)", "description": "Anime Girl Rigged \u2014 VRoid (CC-BY, dequeijospizza)",
"model": "res://assets/characters/skins/aria.glb", "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 "unlocked": true
}, },
{ {
@@ -40,6 +85,15 @@
"name": "Momo", "name": "Momo",
"description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)", "description": "Momo Ayase, DANDADAN (CC-BY, HiGuys920)",
"model": "res://assets/characters/skins/momo.glb", "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
} }
] ]
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+96
View File
@@ -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
+90
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
uid://cx2rt7638txa1
+291 -34
View File
@@ -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);
} }
+258
View File
@@ -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;
}
+1
View File
@@ -0,0 +1 @@
uid://dk5nb354iw044
+212 -8
View File
@@ -9,6 +9,7 @@ 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
@@ -21,15 +22,84 @@ uniform float mid_band_edge : hint_range(-1.0, 1.0) = 0.55; // second, brighter
// reads as a glossy stripe sweeping across the hair. // reads as a glossy stripe sweeping across the hair.
uniform float mid_tone : hint_range(0.0, 1.0) = 0.82; 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
// How far a CAST shadow drops below the form-shadow tint. Cel art draws the
// two separately: the terminator (a surface turning away from the sun) is a
// tone step, a cast shadow (something standing in the way) is a darker, harder
// 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 // Matte-anime defaults: zero specular (any stepped glint reads as shine
// sweeping across hair/cloth when the camera moves), whisper of rim. // 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_strength : hint_range(0.0, 2.0) = 0.05;
uniform float rim_width : hint_range(0.0, 1.0) = 0.28; 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_strength : hint_range(0.0, 1.0) = 0.0;
uniform float specular_shininess : hint_range(1.0, 128.0) = 64.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;
@@ -49,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) {
@@ -58,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
// PIXELS the transition actually covers. On a slowly-curving surface it can
// 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. // 3 tones: shadow tint -> base band (mid_tone) -> full light.
float tone = mix(mid_tone, 1.0, mid); 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;
} }
+30 -4
View File
@@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More