Kůň na pláži — WebGL scéna v pěti fázích

Procedurální západ slunce, Gerstnerovy vlny a rozfázovaná stavba scény

Posledních pár dní jsem si hrál s jednou myšlenkou: jak daleko se dá dostat s fotorealistickou scénou v prohlížeči, když nepoužiju ani jednu texturu nebo obrázek — všechno musí vzniknout v kódu. Žádné JPG z internetu, žádné HDRI, žádné modely. Jen matematika, shadery a CanvasTexture.

Výsledek je kůň běžící po mokré pláži při západu slunce. Postavil jsem to v pěti fázích, každá je samostatný, plně funkční HTML soubor — a každou si můžete otevřít přímo tady:

FázeCo přidáváŽivá ukázka
1Procedurální obloha (Rayleigh/Mie), písek s PBR mapami, slunce na horizontu001 — Base Environment
2Oceán s Gerstnerovými vlnami, Beer-Lambert, pěna, zrcadlový mokrý písek002 — Ocean Shaders
3Kůň — rigged model, PBR materiály, posazený na písek003 — Procedural Horse
4Gallop, detekce kontaktu kopyt s vodou, fyzika vodní tříště004 — Animation & Particles
5Tracking kamera, bloom, vignette, film grain005 — Cinematic Final

Poznámka k pohonu: všechny fáze potřebují WebGL2 a načítají Three.js z CDN. Na integrované grafice to běží, ale 60 fps to není. Fáze 5 navíc čte assets/horse.glb (1 MB, CC0), takže se musí servírovat přes HTTP — z file:// model nenačte.

Fáze 1 — obloha a písek

Fáze 1

Obloha je analytický single-scattering model (Preetham) s Rayleighovými a Mieovými fázovými funkcemi, ale s jedním trikem: samotná fyzika při nízkém slunci vyjde skoro bezbarvá, takže výslednou luminanci tvaruju do filmové křivky a barevnou paletu držím ve čtyřech pásmech — od bílého slunce na horizontu přes ohnivou oranžovou do indiga v zenitu. Fyzika dodá směr a asymetrii, paleta dodá barvu.

Písek je klasika: jeden height field z value-noise, ze kterého se centrálními diferencemi vyrobí tangent-space normal mapa, plus albedo a roughness mapa. Vlny od větru jsou sinusoidy s fázovou modulací, aby to nevypadalo jako zorané pole.

Fáze 2 — voda

Fáze 2

Tady se láme realističnost. Tři věci, které to drží:

  • Gerstnerovy vlny ve vertex shaderu — čtyři vlny s hlubinnou disperzí (speed = sqrt(g/k)), shoaling podle hloubky a amplituda utlumená směrem k horizontu, aby se nerozjela aliasing na hrubé mřížce.
  • Beer-Lambertův útlum ve fragmentu: exp(-sigma * depth) s kanálově různými koeficienty. Červená umírá první, takže mělká voda zůstane zlatá (vidíte skrz ni písek) a hluboká jde do navy.
  • Pěna jako density field, ne jako práh.

To poslední mě stálo nejvíc času. Value-noise fBm má distribuci utěsnanou kolem 0.5, takže prahy typu smoothstep(0.3, 0.78, noise) masku zaklapnou na 0/1 a pěna vypadá jako bílý škraloup s ostrými hranami. Všechny rampy proto sedí uvnitř té distribuce (0.38..0.62), pěna se míchá maximálně na 72 % a nikdy není plně neprůhledná — zůstává film na vodě.

Mokrý písek je MeshPhysicalMaterial s clearcoat a environment mapou. Wetness je spojitá funkce hloubky, injektovaná přes onBeforeCompile do roughness a albeda — žádná viditelná šev mezi suchým a mokrým pískem, jen postupný přechod od matu k zrcadlu.

Fáze 3 — kůň

Fáze 3

Tady jsem nejdřív udělal chybu, kterou jsem pak musel vzít zpět. Kůň z primitiv (kapsle, koule, kužely) vypadá na papíře dobře, ale ve scéně čte jako dřevěný koník — a každá další iterace byla horší. Silueta je zkrátka to, co dělá koně koněm, a tu z válců nevykouzlíte.

Takže: kůň je rigged GLB model (Quaternius, public domain, 1 532 tris, 68 kostí, baked Gallop klip). Zbytek scény zůstal procedurální. Model se při načtení zorientuje, zškáluje na ~1,55 m v kohoutku a posadí se na písek — projde jeden celý stride, najde nejnižší kopyto a posune mesh tak, aby se dotýkalo.

Materiály z GLB jsou ploché, takže je nahrazuju MeshPhysicalMaterial s coat sheenem. A jedna věc, která mě překvapila: sheen 0.9 na low-poly modelu vykreslí na facety jasné skvrny, které vypadají jako druhý kůň namalovaný na těle. Sheen 0.22 a vypnutá anizotropie to vyřešily.

Fáze 4 — pohyb a voda

Fáze 4

Animace je z klipu, ale splash je vlastní. Kopyta jsou kosti (FFL/FFR/FFBL/FFBR), kontakt se detekuje tak, že vertikální rychlost kopyta překlopí z klesání do stoupání v kontaktním pásu nad pískem. Z každého dopadu vylétne vrstva kapek (směrová, dozadu) plus jemná mlha — samotné kapky vypadají jako chomáč vaty, mlha je to, co z toho udělá vodu.

Částicový systém je THREE.Points s vlastním shaderem: gravitace, drag, fade podle životnosti, soft blob místo ostrého disku.

Fáze 5 — film

Fáze 5

Post-processing je ručně postavený, bez EffectComposer: scéna se renderuje do half-float bufferu, z něj bright pass s prahem, dvě iterace separabilního gaussovského blur na čtvrtinovém rozlišení, a finální composite s ACES, chromatickou aberací, teplou vignettou a filmovým zrnem.

Jedna věc, která je na HDR pipeline záludná: tone mapping se při renderu do render targetu neaplikuje, takže buffer drží skutečné HDR — slunce zůstane nad 1.0 a bloom funguje. Zároveň to znamená, že jeden přetečený texel (wet sand s clearcoatem vygeneroval hodnotu nad rozsah half-floatu) se v ACES promění na NaN a prořízne frame černým čtvercem. Proto je v composite clamp i explicitní NaN guard.

Dva bugy, které stojí za zmínku

Pokud něco z tohohle budete opravovat, pravděpodobně narazíte na stejné dvě věci:

  1. Obrácené winding u ručně skládané geometrie. Oceán jsem indexoval v opačném pořadí, takže všechny normály mířily dolů. Voda se pak chovala jako Fresnelovo zrcadlo bez osvětlení — vypadala jako zlatá fólie a žádná pěna se nechytala.
  2. Přetečení half-floatu v HDR bufferu. Inf v bufferu → NaN po ACES → černá díra ve frame. Clamp před tone mappingem to řeší.

Jak to vzniklo

Celý experiment — scéna, pět fází, nasazení na web i tenhle článek — uběhl autonomně. Běžel na dvou DGX Spark s modelem DeepSeek V4.1 Flash 1017, který si sám procházel iterace (render → screenshot → úprava shaderu → znovu render), došel k rozhodnutí, že procedurální kůň z primitiv nefunguje, sám publikoval výsledek na smoula.net a napsal tento text. Já jsem mu jen zadal, co má vzniknout.

Zdrojáky jsou ke stažení přímo v prohlížeči (view-source), žádná minifikace. Pokud chcete něco z toho použít, klidně — jen prosím neprodávejte koně z Quaternius jako svůj.

Štítky: webgl  threejs  3d  gpu 
Obsah