This file documents mochi.gdshader and mochi_outline.gdshader themselves.
It ships inside addons/mochi/ — this whole folder is the portable plugin;
copy it as-is into any other project's addons/ directory and it works the
same way there. For the project as a whole (installing the addon, presets,
the demo scene, the preset script, known limitations, validation results),
see the README.md at the root of this repository/host project.
Godot's default spatial material pipeline (Burley diffuse + Schlick-GGX specular) is physically based. Remapping it into something cozy/stylized by fighting its curves from the outside is possible but awkward, and the built-in ambient/IBL pipeline pulls the result back toward "physically plausible" regardless of style intent.
MOCHI instead defines its own light() function and takes over the entire
direct-lighting response — diffuse, shadow banding, specular, and rim — and
disables the engine's built-in ambient (render_mode ambient_light_disabled)
in favor of a small hand-written hemisphere ambient term computed in
fragment(). The practical effect: MOCHI looks the same regardless of a
scene's WorldEnvironment / GI setup, because it doesn't read from either.
fragment()
├─ sample surface inputs (albedo/normal/metallic/roughness/emission)
├─ color harmonization (variation → saturation → contrast → brightness → tint)
├─ atmosphere (optional distance desaturation/tint)
├─ write ALBEDO / METALLIC / ROUGHNESS, SPECULAR = 0
└─ write EMISSION = user emission + manual hemisphere ambient
light() (called once per affecting light, per pixel)
├─ resolve the 4 macro Style dials into concrete per-feature values
├─ wrapped diffuse: NdotL → wrap → smoothstep band → shadow_strength floor
├─ shadow tinting: mix(ALBEDO, ALBEDO*shadow_tint, ...) by the above
├─ DIFFUSE_LIGHT += tinted albedo * light color * ATTENUATION * lit
├─ stylized specular: half-vector → smoothstep band, shaped by
│ ROUGHNESS/METALLIC from the material
├─ SPECULAR_LIGHT += highlight * light color * ATTENUATION * SPECULAR_AMOUNT
└─ rim: fresnel term gated to the shadow side, added into DIFFUSE_LIGHT
Every step above uses a smoothstep/mix-based soft transition rather than
a hard NdotL > threshold cut, per the brief's core visual philosophy — the
one exception a project may choose to add is shadow_threshold, which only
shifts where the existing soft band sits, it does not harden it.
cozy_amount, stylization, softness, and goofy_amount each call into a
small mochi_resolve_*() helper (one per affected feature) that nudges that
feature by a different weighted amount. For example shadow_softness is
touched by both cozy_amount (widened because harsh shadows read as cold)
and softness (the same knob, used directly) — with different weights — so
the two dials stay perceptually distinct instead of scaling one number.
See the STYLE RESOLUTION section of mochi.gdshader for the exact weights;
they're deliberately simple linear nudges with a comment on each one's
rationale, not a black box.
Nothing in light() flattens METALLIC/ROUGHNESS to a fixed look:
ROUGHNESSwidens the specular smoothstep band and lowers its effective power → rougher materials get a bigger, softer highlight blob.METALLICboosts highlight intensity (metal_boost) → metal reads punchier than plastic/stone at the same light angle.SPECULAR = 0infragment()only disables the engine's environment reflection contribution (which tends to look wet/plastic); it has no effect on the highlightlight()computes itself.
So the same MOCHI material, with only metallic/roughness changed (either
as flat values or via the optional textures), reads differently on wood,
stone, and metal without any per-material shader branching.
mochi_outline.gdshader is a small, independent unshaded shader meant to
be assigned as a material's next_pass, using the classic
expand-along-normal-then-cull-front-faces technique. It does not touch
mochi.gdshader at all. This keeps the main shader's light() focused on
one job, keeps outline entirely optional and zero-cost when unused (a null
next_pass costs nothing), and matches how most production stylized shaders
in Godot actually structure outlines. See the top-level README's "Known
limitations" section for what its outline_softness control does and does
not do.
debug_mode short-circuits light() to output a single isolated quantity
into DIFFUSE_LIGHT (grayscale, so it's readable regardless of material
color): the raw geometric wrap/soft-light term, the raw shadow-map
ATTENUATION, the specular term alone, or the rim term alone. "Ambient"
mode returns immediately from every light and lets fragment()'s manual
ambient term (written to EMISSION) be the only visible contribution. All
debug branches are gated on a single uniform int, so they cost nothing
when debug_mode == 0 — GPUs don't pay a divergence penalty for a branch
that's identical across every pixel in the draw call.