Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MOCHI shader — architecture notes

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.

Why a custom light() function

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.

Data flow

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.

Why the four Style dials aren't simple multipliers

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.

Why material identity survives stylization

Nothing in light() flattens METALLIC/ROUGHNESS to a fixed look:

  • ROUGHNESS widens the specular smoothstep band and lowers its effective power → rougher materials get a bigger, softer highlight blob.
  • METALLIC boosts highlight intensity (metal_boost) → metal reads punchier than plastic/stone at the same light angle.
  • SPECULAR = 0 in fragment() only disables the engine's environment reflection contribution (which tends to look wet/plastic); it has no effect on the highlight light() 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.

Outline is a separate shader on purpose

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 modes

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.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages