[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-94543":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":12,"openIssues":12,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":13,"stars7d":13,"stars30d":14,"stars90d":13,"forks30d":13,"starsTrendScore":13,"compositeScore":15,"rankGlobal":8,"rankLanguage":8,"license":16,"archived":17,"fork":17,"defaultBranch":18,"hasWiki":17,"hasPages":17,"topics":19,"createdAt":8,"pushedAt":8,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":13,"starSnapshotCount":13,"syncStatus":12,"lastSyncTime":23,"discoverSource":24},94543,"LinearAbiltyCastingThreeJS","achrefelouafi\u002FLinearAbiltyCastingThreeJS","achrefelouafi",null,"JavaScript",364,66,2,0,94,51.88,"MIT License",false,"main",[],"2026-08-24 04:01:22","# Elemental Sandbox\n\nA skillshot VFX sandbox built with **Three.js**, **Vite** and hand-written **GLSL**.\n\nFive abilities and two ways to aim them. Four are **line casts**: press the key to arm, a\nLeague-of-Legends style arrow appears on the ground and swings with the mouse, click to fire. The\nfifth is a **far cast**: the arrow is replaced by a circle with a deliberately thick boundary that\nfollows the cursor and answers the only question a ground-targeted AoE has to answer before you\ncommit — how much space is this going to take.\n\n**Q — Frost Lance.** A fracture front races out along the line while a field of ice crystals\ntears up out of the floor behind it — small and dense at your feet, opening into a wall of blades\nat the far end, with a cluster thrown up around the impact point.\n\n**E — Storm Lance.** A bolt leaves the caster's hand and a bundle of lightning filaments is drawn\nout behind the strike front, holds while it gutters and re-strikes, then blows out. Sparks come\noff it the whole way, the floor underneath takes a branching electric burn and a dark scorch, and\nthe far end gets a shell of ionised air.\n\n**R — Cinder Fall.** A burning rock is lobbed downrange on an arc, trailing a raymarched wake of\nburning gas and heating up the whole way: the lava seams splitting its surface prise wider and\nbrighter as it comes in. It detonates on arrival, throws its own shattered chunks across the floor, and tears the\nground open into a network of molten cracks that keep glowing while the crater burns out.\n\n**F — Nova Beam.** The caster winds a ball of light up in both hands, pulling motes in out of the\nair, then lets a column of it out along the line — white-hot core, cyan sheath, gold ribbons\nspiralling around it and shock discs racing down it. It *holds* there, burning into the floor and\nthrowing spray back up the beam, before collapsing to a thread and blinking out. The only cast in\nthe sandbox that is still happening a second after it landed.\n\n**V — Voltaic Snare.** The far cast. A leash of current is whipped out across the floor, and where\nit lands the ring snaps open past its own radius and pulls back onto it: a violet column tears up\nout of the middle, tendrils crawl outward to the boundary, arcs run around the rim and the whole\ndisc burns. It holds there re-striking and hauling the air up into the pillar, then collapses to a\nthread. The circle you measured out before the click is exactly the circle you get.\n\nEverything you can see is generated. There are no textures, no sprite sheets and no meshes on\ndisk except the character: the crystals are procedural geometry, the bolt is a strip of ribbon\nplaced entirely by a vertex shader, the meteor is an icosphere cratered and sliced by fracture\nplanes on the CPU, the beam is a parametric tube drawn three times at three radii, the snare's\nwhole cage is that same ribbon strip threaded along four different parametric paths, the arrow, the\ntargeting circle, the rime, the burns and the molten cracks are signed-distance and noise shaders,\nand the mist, sparks, chips and glitter are GPU particles.\n\n**Every parameter is a live slider** — 938 of them — and they stay live while the simulation is\npaused. That is the point of the project: freeze a frame mid-eruption, mid-strike or mid-burn with\n**P**, then reshape the silhouette, the palette and the timing against a still image.\n\nReferences for the look: `icecast.jpg`, `thundercast.jpg`, `superbeam.jpg` and\n`electricalboost.jpg`.\n\n---\n\n## Quick start\n\n```bash\nnpm install\n```\n\n```bash\nnpm run dev\n```\n\nThen open the URL Vite prints (default \u003Chttp:\u002F\u002F127.0.0.1:5173>).\n\n```bash\nnpm run build\n```\n\n```bash\nnpm run preview\n```\n\n### Assets\n\nSix binary assets are served from `public\u002F` and loaded automatically at boot:\n\n| File | Purpose |\n| --- | --- |\n| `public\u002Fmodels\u002FIdle.fbx` | Rigged character **and** its idle animation clip |\n| `public\u002Fmodels\u002Fdiffuse.png` | The character's colour map |\n| `public\u002Fmodels\u002Fcast1.fbx` | Cast animation |\n| `public\u002Fmodels\u002Fcast2.fbx` | Cast animation |\n| `public\u002Fmodels\u002Fcast3.fbx` | Cast animation — the default for Frost Lance, Root Snare and Glacier Crown |\n| `public\u002Fhdri\u002Fspruit_sunrise.hdr` | HDR probe used for image-based lighting and crystal reflections |\n\nAll four FBX files are Mixamo exports of the same rig, each carrying a skinned mesh plus one\nanimation stack. The character comes from the idle file; the cast files are loaded for their clip\nalone, and the duplicate rig that arrives with each one is released the moment its `AnimationClip`\nhas been taken. Clips bind to the skeleton by bone name, which is the whole reason an animation\nauthored in another file plays here without retargeting.\n\nThe rig ships no material, so `diffuse.png` is loaded beside it and assigned as the colour map when\nthe imported materials are converted to PBR — an FBX that *does* carry an embedded texture keeps its\nown, since that map is authored against its own UVs.\n\nEvery ability picks the clip it throws — `castAnim` in its settings block, a dropdown under **The\ncast** in its editor folder. Out of the box slots 1, 5 and 6 — Frost Lance, Root Snare and Glacier\nCrown — throw `cast3`, and the other three throw `cast1`. The clip is a one-shot laid over\nthe looping idle, with `character.castBlendIn` \u002F `castBlendOut` as the two edges of that overlap.\n\nThe HDR is loaded as image-based lighting and as the reflection source for the ice — it is never\nshown as a visible sky. The stage keeps its flat dark backdrop.\n\n---\n\n## Controls\n\n| Input | Action |\n| --- | --- |\n| **Q** (or **1**) | Arm Frost Lance — press again to put it away |\n| **E** (or **2**) | Arm Storm Lance — press again to put it away |\n| **R** (or **3**) | Arm Cinder Fall — press again to put it away |\n| **F** (or **4**) | Arm Nova Beam — press again to put it away |\n| **V** (or **5**) | Arm Voltaic Snare — the far cast, aimed with a circle |\n| **Move the mouse** | Swing the aim arrow, or move the far-cast circle |\n| **Left click** | Cast along the arrow, or drop the circle where it is |\n| **Esc** \u002F **right click** | Cancel an armed cast |\n| **Right mouse + drag** | Orbit the camera |\n| **Scroll** | Zoom |\n| **G** | Show\u002Fhide the VFX editor |\n| **P** | Pause \u002F resume — *the editor keeps applying* |\n| **C** | Clear all active effects |\n| **H** | Hide the controls panel |\n\n`range` and `minRange` are per ability, so the indicator's reach changes with the slot you have\nselected. Aiming closer than the selected ability's `minRange` tints it red and refuses the cast;\nset `minRange` to 0 if you would rather cast at your own feet, which is what the Snare ships with —\na trap you cannot drop on yourself is missing half its uses. Cooldowns are per ability too, so\nspending one slot never locks the other out.\n\n---\n\n## Project layout\n\n```\nsrc\u002F\n  abilities\u002F      Ability base class (the travelling front), IceAbility, ThunderAbility,\n                  MeteorAbility, BeamAbility, SnareAbility, pooling manager\n  animation\u002F      FBX character loading, AnimationMixer, the per-ability cast clips,\n                  the procedural cast lunge\n  assets\u002F         Procedural crystal and asteroid geometry, the bolt ribbon strip,\n                  the beam tube and its shock discs\n  config\u002F         settings.js — the single source of truth for every parameter\n  core\u002F           App, Renderer, CameraRig, Time, Layers, shared frame uniforms\n  effects\u002F        Aim arrow, far-cast circle, ground decals, fissures, bursts,\n                  light pool, shake, flash\n  input\u002F          InputManager (events) and AimController (both targeting shapes)\n  loaders\u002F        AssetLoader with a shared LoadingManager\n  materials\u002F      IceMaterial, LightningMaterial, MeteorMaterial,\n                  VolumetricFireMaterial, BeamMaterial, SnareMaterial\n  particles\u002F      GPU particle system + engine and rate emitters\n  postprocessing\u002F Composer pipeline, grade shader, distortion shader\n  shaders\u002Flib\u002F    Shared GLSL: noise library, common helpers\n  ui\u002F             HUD, lil-gui editor, preset manager, styles\n  utils\u002F          Maths, colour cache, pooling, disposal, shader patching\n  world\u002F          Environment (stage lighting), floor, dust, contact shadows\n  archive\u002F        The retired four-element sandbox — see archive\u002FREADME.md\n```\n\n---\n\n## How it fits together\n\n### Settings are the API\n\n`src\u002Fconfig\u002Fsettings.js` holds every tweakable value. Nothing else owns that state: shaders,\nparticle systems, lights and post passes *read* those objects every frame. That is what makes the\neditor work with no rebuild — moving a slider changes the ice field that is already standing, the\nnext cast, the environment and the post stack at once. Preset loading deep-merges *into* the same\nobjects so every live binding stays valid.\n\n```js\nimport { settings } from '.\u002Fconfig\u002Fsettings.js';\nsettings.ice.height = 7;          \u002F\u002F visible on the next frame, even mid-cast\nsettings.thunder.jitter = 1.2;    \u002F\u002F re-kinks a bolt that is already in the air\nsettings.global.timeScale = 0.1;  \u002F\u002F slow the whole cast to a crawl\n```\n\nAbility blocks are keyed by their id in `ELEMENTS`, and the shared systems that need to know\n\"which ability is the player holding\" — the aim controller, the cooldowns, the HUD — look it up as\n`settings[element]`. The four fields they rely on being present are `range`, `minRange`, `speed`\nand `cooldown`; a far cast adds a fifth, `zoneRadius`. Everything else in a block is that ability's\nown business.\n\n### The rule that makes \"edit while paused\" work\n\nA spike record in `IceAbility` stores **only what the dice decided**: a position *fraction* along\nthe line, a signed lateral *fraction*, and a handful of unitless jitters. Not one metre, radian or\nsecond is captured when the cast starts. Every dimension is resolved against `settings.ice` inside\nthe update loop, which runs on a zero-length frame too.\n\nSo dragging `height` re-grows a field that is already standing; dragging `lean` re-tilts it;\ndragging `clumping` re-packs it toward the centre line. The only values a record *does* capture\nare timestamps — the moment its own eruption was triggered. Those are events, not dimensions.\n\nThe four *shape* controls (`facets`, `taper`, `roughness`, `bend`) cannot be expressed as a\nper-instance transform, so they are baked into the geometry instead — and a six-sided crystal is\njust 60 triangles, cheap enough to regenerate outright rather than approximate in a vertex shader.\n`IceAbility#_syncGeometry` hashes those four values and rebuilds the three crystal meshes when the\nhash changes, which is what keeps them live sliders rather than restart-required constants.\n\n### Aiming\n\n`AimController` raycasts the pointer onto the ground plane **every frame**, not only on mouse\nmove, so orbiting the camera with a cast armed swings the indicator under a stationary cursor. It\nclamps the distance into `[minRange, range]`, tracks a 0..1 reveal envelope, and emits a single\n`cast` event carrying an origin, a unit direction and a distance — which is exactly the signature\n`Ability#spawn` takes. It decides nothing about what the cast does.\n\nIt runs on **real** time rather than the scaled simulation delta, so the indicator keeps animating\nwhile the sandbox is paused.\n\nThere are two indicators and one controller. Which one is drawn comes from\n`ELEMENT_META[element].cast` — `CastShape.LINE` or `CastShape.ZONE` — and that is the *only* thing\nthe two shapes disagree about. Arming, clamping, validating, revealing and firing are shared, and\nboth end in the same three-argument `cast` event, because from the targeting side a far cast is a\nline cast you only care about the far end of. That is why zone targeting needed no change in\n`Ability`, `AbilityManager` or `App`: `SnareAbility` reads its centre as `pointAt(1)` and works\noutward from there.\n\n### The far-cast circle\n\n`ZoneIndicator` is the arrow's opposite number, and it is built out of the same two ideas: metres,\nand no textures.\n\nThe **footprint** is one quad whose fragment shader remaps UV into metres from the target, so the\nboundary stays 0.34 m thick whether the circle is 2 m or 8 m across. The band is deliberately the\nheaviest mark on screen — it is the whole message — and it is split about the nominal radius by\n`boundaryBias` rather than centred on it, so its *outer* lip stays honest about where the effect\nends. Inside there is a rim-weighted wash, contour rings travelling outward, warped filaments and a\nreticle whose downrange arm is longer, because the quad carries the caster's yaw and that arm is\ntherefore the heading.\n\nThe **reach ring** at the caster is the bolt's ribbon strip bent into a circle: `(t, side)` in,\nworld position out. A quad big enough to hold a 20 m range would be 40 m across and shade a\nscreenful of discarded fragments for one thin line.\n\nThe circle **snaps out past its radius and settles back** when the cast is armed, and the trap does\nthe same thing when it lands. A circle that grows linearly reads as a UI element; one that\novershoots reads as something the caster did.\n\n### The arrow is one SDF\n\n`AimIndicator` is a single ground quad. Its fragment shader remaps UV into **metres measured from\nthe caster**, so every control in `settings.aim` is a real measurement — the shaft stays 0.42 m\nwide whether the cast is 3 m or 15 m long.\n\nThe silhouette is a rounded union of a box (the shaft) and iq's exact triangle SDF (the head);\nthe cheap half-plane intersection leaves visible corner artefacts on a wedge this shallow. From\nthat one distance field the shader derives the outline, the rim-weighted interior wash, the\nchevrons (a phase skewed by `|x|`, which turns flat bands into arrowheads pointing the way the\ncast does), the frost noise and voronoi plates, the ring at the caster's feet, the range cap arc,\na six-fold frost rosette pinned to the impact point, and the sweep-out when the ability is armed.\n\n### The ice\n\n`materials\u002FIceMaterial.js` patches a `MeshStandardMaterial` rather than replacing it, so the\ncrystals cast and receive the stage's real shadows and pick up the HDR probe. The stylisation is\ninjected on top:\n\n- **Thickness tint** — a facet seen head-on has the longest path through the crystal, so it\n  darkens toward `colorDeep`; grazing edges stay pale. This is the term that makes the field read\n  as a solid you can see *into* rather than as blue plastic.\n- **Internal fracture** — ridged noise sampled in **world** space, so the crack planes stay a fixed\n  physical size whether a spike is ankle-high or three metres tall, and neighbouring crystals look\n  quarried from the same block.\n- **Feather frost and rime** — fbm sampled in **local** space (0..1 up the crystal), so the milky\n  veining and the frost creeping up from the base follow each spike's own axis however it is\n  scaled or leaned.\n- **Glint** — a hard-thresholded high-frequency field scrolling in world space, biased toward\n  grazing angles, which is where real ice catches.\n- **Birth flash** — a per-instance attribute the ability drives from 1 to 0 over `birthFade`, so a\n  crystal is lit from within for the moment it erupts.\n\nThree `InstancedMesh`es share one material. Three rather than one because the *facets* differ, not\njust the proportions — per-instance scaling alone cannot buy that silhouette variety, and three\ndraw calls is a cheap price.\n\n### The lightning\n\n`ThunderAbility` takes the \"no dimensions on the CPU\" rule further than the ice does: there is no\npath object at all. The bolt is one `InstancedBufferGeometry` — a flat ladder of quads in\n*parameter* space, where each vertex carries only `(t, side)`: how far along the bolt it is, and\nwhich edge of the ribbon it is on. One instance is one filament. `materials\u002FLightningMaterial.js`\nturns that pair into a world position every frame, so a single strip serves a bolt of any length,\nany shape and any width.\n\nThree things stack to make the shape:\n\n- **the axis** — a straight line from the hand to the impact point, bowed by `sag`. The only part\n  that knows where the cast is pointing.\n- **the fan** — a constant per-filament offset in the plane perpendicular to the axis, opening\n  from `spreadNear` at the hand to `spread` at the target and rolling around the axis with\n  `twist`. This is what separates one filament from the next.\n- **the kinks** — octaves of *linearly* interpolated value noise. Linear on purpose: smoothstep\n  would round the corners off, and the corners are the entire reason it reads as lightning rather\n  than as a wobbly tube.\n\nThe ribbon is turned to face the camera by crossing the local tangent with the view vector, which\nis why the bolt keeps its apparent thickness from any angle without ever being a screen-space\nline. It is drawn twice — a wide soft halo underneath and the hot core on top — because drawing\nthe glow as real ribbon rather than leaving it to bloom is what keeps it *attached* to every kink.\n\nTwo clocks run the flicker. `restrike` snaps every filament onto a new shape N times a second,\nand `crawl` slides the kinks continuously in between; together they stop a held bolt from looking\nlike a static ribbon. A cast captures exactly one number — a seed, so two casts do not draw the\nidentical bolt — and resolves every metre, radian and second against `settings.thunder` each\nframe. That is why dragging `jitter` re-kinks a bolt that is already in the air.\n\nThe ground burns are worth a note as a thing *not* to do. The first version sampled the filament\nfield on `atan(y, x)`, which hands every radius along a given bearing the same value and draws\ndead-straight spokes out of the centre — a firework, not a burn. Sampling the same noise in the\nplane and warping the lookup is what lets the filaments meander and fork.\n\n### The beam\n\nThe Nova Beam shares the bolt's rule — no dimensions on the CPU — and reaches the opposite look\nwith it. Where the bolt's whole charm is that its noise is *piecewise-linear* and keeps its\ncorners, every noise term in the beam is smooth, stretched hard along the flow and crawling\ndownrange. A beam that kinks is a bolt.\n\nIt is a real tube rather than a camera-facing ribbon, because a column this thick has to *have* a\ncross-section: the silhouette must bow correctly when you orbit it, the far wall must add through\nthe near one, and the shock discs have to hug it. `createBeamTubeGeometry` is the ribbon strip one\ndimension richer — every vertex carries `(t, a)`, how far along the barrel it is and how far around\n— and `materials\u002FBeamMaterial.js` turns that pair into a world position each frame.\n\nThat one tube is drawn three times, and the trick is in how the three are weighted:\n\n- **halo** — widest, nothing but a rim term. The atmosphere the beam is shoving out of the way.\n- **sheath** — rim-weighted, so it reads as *hollow* and its silhouette edges are its brightest part.\n- **core** — narrow, and weighted the **opposite** way: brightest where the view ray runs down the\n  barrel and its path through the tube is longest.\n\nRim-weighted outside, axis-weighted inside, both faces adding: that is a volume integral, cheaply,\nand the inversion is the entire reason the middle reads as a solid rod of light instead of as a lit\npipe. Widen `coreWidth` or push `coreFill` up and the three layers collapse into one white tube —\nthe cyan sheath and the gold coils are only legible because the core leaves them room.\n\nTwo more instanced passes put structure on it. The **coils** are the bolt's ribbon strip bent into a\nhelix, camera-facing and warm on purpose — the colour split is what stops them dissolving into the\nsheath. The **shock discs** are an instanced annulus whose phase is `fract(index \u002F count + time ×\nspeed)`, so the train is a pure function of the clock and there is no queue on the CPU. Both place\nthemselves against the same `beamRadius()` the tube uses, which is why all five stay welded together\nwhen the profile is dragged.\n\nThe beam is also the one ability with a **fourth beat**. The other three run travel → impact →\nfade; this one puts a wind-up in front of that, and it needed nothing from the base class:\n`advance()` simply refuses to let the front leave the hand until the orb is up to power, so `IMPACT`\nbecomes the burn and the phase machine is untouched. The far end therefore has an impact that keeps\nhappening — spray thrown back up the line, pressure shells shed off the burning point, dust and\nshockwave rings pushed across the floor, all rate-throttled through the same fractional-rate emitter\nthe particles use so every rate is a live slider.\n\n### The snare\n\nThe Voltaic Snare is the first ability built around a *point* instead of a line, and the thing that\nholds it together is that `zoneRadius` is read in exactly one place per consumer and nowhere is it\ncopied: the indicator measures it out, the tendrils end on it, the rim arcs run along it, the field\nburns it and the column's throat and flare are fractions of it. Drag it and all five move together,\nmid-cast, with the clock stopped.\n\nThe whole cage — the whip that plants it, the pillar, the tendrils and the rim arcs — is **one\ninstanced ribbon strip**, the same one the bolt and the beam's coils are drawn on. A filament's\n*role* is decided in the vertex shader by testing its instance index against four live counts, and\nthe role picks which parametric path it is threaded along:\n\n- **leash** — a sagging line from the hand to the travelling tip, dropped onto the floor.\n- **column** — a twisting climb whose radius opens from `throat` to `columnSpread`.\n- **tendril** — a meander running outward, its veer a per-filament constant rather than noise, so\n  it curves the way a discharge that has committed to a direction does.\n- **rim** — an arc travelling around the boundary, hopping over it at mid-span.\n\nEvery offset then lives in a frame taken by finite difference off that path, which is what lets one\nkink function serve a vertical pillar and a filament crawling flat across the floor. The two\nground-hugging roles damp the vertical component of that offset and clamp above the floor — a kink\nwith a free `y` buries half of every tendril and the effect reads as a broken dotted line. Setting\na count to zero retires the role outright, which is how the leash disappears on the frame the ring\ntakes over. Two draw calls cover all four roles, however many filaments are in the air.\n\nThe **field** is a quad rather than a pooled decal for one reason: a decal captures its radius when\nit spawns, and this circle has to re-scale under `zoneRadius` while it is standing. Its veins are\nsampled in the plane and domain warped — the same lesson the bolt's ground burns taught, and for\nthe same reason.\n\nThe one thing worth stealing for the next far cast is the **snap**: the ring opens on\n`Easing.outCubic` multiplied by a bump that peaks late and dies at exactly 1, so it overshoots its\nradius and pulls back onto it, and the pillar climbs on the same clock 1.7× slower. The ground goes\nfirst, then the air breaks down over it.\n\n### Adding another ability\n\n1. Add a settings block in `config\u002Fsettings.js` and an entry in `ELEMENTS` \u002F `ELEMENT_META`.\n2. Subclass `Ability` and implement `createShaders`, `createParticles`, `onTravel`, `onImpact`,\n   `onFade`.\n3. Register the class in `abilities\u002FAbilityManager.js`.\n4. Add an editor folder in `ui\u002FEditor.js`, and a sigil in `ui\u002Fglyphs.js`.\n5. Bind a key in `input\u002FInputManager.js` — it emits `ability` with the 0-based slot index, which\n   `App` maps through `ELEMENTS`.\n\nTo make it a **far cast** instead of a line cast, add two things and nothing else: `cast:\nCastShape.ZONE` in its `ELEMENT_META` entry, and a `zoneRadius` in its settings block. The circle\nindicator, the reach ring, the snap-out and the whole targeting loop come for free, and the ability\nreads its centre as `pointAt(1)`.\n\nEverything else — pooling, the travelling front, the local frame, lights, phases, per-ability\ncooldowns, the aim reach and camera framing — is inherited or driven off `ELEMENTS`. The HUD\nbuilds its slots from that array, so a new ability appears in the bar on its own.\n\n### Particles\n\n`particles\u002FParticleSystem.js` is a GPU-simulated, instanced-quad system. Motion (velocity, gravity,\nanalytic drag, curl turbulence, vortex swirl), size-over-lifetime, the colour gradient and alpha\nfade are all evaluated in the shader from per-instance attributes; the CPU only ever writes spawn\ndata, and only the slots that changed are uploaded. Particles live in a ring buffer, so spamming\nthe ability recycles slots instead of allocating. Silhouettes (soft, smoke, streak, leaf, chip,\nring) are procedural — there are no sprite textures anywhere in the project.\n\nFrost Lance uses three systems: **mist** (non-additive, so the fog genuinely occludes and gives the\nfield depth), **shards** (lit chips under gravity) and **glitter** (additive, negative gravity — the\nrising plume that is the signature of the reference frame).\n\nStorm Lance uses four: **sparks** (velocity-stretched streaks under gravity), **motes** (the slow\nionised drift around the bolt), **smoke** (non-additive haze off the scorched floor) and **debris**\n(lit chips). Its sparks are emitted from several points along the bolt each frame rather than one:\na beam sheds along its whole length, and a single origin makes every batch read as a starburst.\n\nNova Beam uses four as well, and works one of them twice: its **motes** are the intake spiralling\n*into* the orb while it charges and the drift shed off the column once it is firing — the same glow,\nthrown the other way. Its **sparks** are thrown radially off the barrel and then dragged downrange\nby `sparkForward`, which is the read that says \"pressure\"; the bolt's fall instead, and that one\ndifference does a lot of the work of keeping the two abilities apart.\n\n### Render pipeline\n\nPer frame:\n\n1. **Depth prepass** — the opaque world into a half-res packed-depth buffer. Every VFX shader\n   samples it for soft intersections, so nothing cuts a hard line into the ground. The crystals sit\n   on `LAYER.WORLD`, so mist and glitter fade softly against them.\n2. **Distortion pass** — meshes on the distortion layer write screen-space UV offsets into a second\n   half-res buffer. Nothing writes to it in the current build; the pass is kept because it is the\n   hook a refraction effect would use.\n3. **Composer** — scene → refraction warp → bloom → tone map (ACES) → grade.\n\nThe grade pass folds chromatic aberration, lift\u002Fgain\u002Fcontrast\u002Fsaturation\u002Ftemperature, vignette,\nfilm grain and the impact flash into one resample.\n\nShadows come from a single directional light whose orthographic shadow camera is re-centred on the\ncharacter each frame and fitted to a 52 m box at 4096² (~1.3 cm\u002Ftexel). The `three\u002Faddons` CSM\nmodule was tried first and removed: it replaces three's `lights_fragment_begin` chunk *globally*,\nso any material not explicitly registered with it silently loses all directional lighting.\n\nContact shadows are a real render: the character's depth is captured from below into a 256²\ntarget, blurred twice and projected onto the ground.\n\n---\n\n## Editor and presets\n\nPress **G** for the panel. Folders: Presets, Global, Aim indicator, Far-cast circle, Frost Lance,\nStorm Lance, Cinder Fall, Nova Beam, Voltaic Snare, Environment, Post processing, Camera,\nCharacter. Every folder starts collapsed — there are enough controls here that one open section\npushes the rest off the screen.\n\n- **Global** multipliers scale everything at once (speed, glow, noise, particles, lights, impact\n  intensity, camera shake, time scale…).\n- **Aim indicator** — the arrow's silhouette in metres, its outline and fill, the chevrons and\n  frost, and the rings and rosette.\n- **Far-cast circle** (40 controls) — the boundary band, the interior, the ticks, sweep and\n  reticle, the reach ring, and the snap-out. Shared by every far cast, so it is filed with the\n  targeting rather than with any one ability.\n- **Frost Lance** (113 controls, 25 of them colours) — the cast, the footprint, the silhouette,\n  the crystal itself, the eruption timing, the ice material, the frost on the ground,\n  mist\u002Fchips\u002Fglitter, the impact and the dynamic light.\n- **Storm Lance** (123 controls, 34 of them colours) — the cast, where the bolt leaves the hand,\n  the bundle, one filament, the ribbon, flicker and restrike, the bolt's colour, the burns on the\n  ground, sparks\u002Fmotes\u002Fsmoke\u002Fdebris, the muzzle and impact, and the dynamic light.\n- **Nova Beam** (176 controls) — the cast, where it leaves the hands, the column, the core\u002Fsheath\u002F\n  halo stack, the surface and its flow, the beam's colour, the coils, the shock discs, the charge\n  and its intake, what the floor does, sparks\u002Fmotes\u002Fsteam\u002Fdebris, release\u002Fimpact\u002Fburn, and the two\n  dynamic lights.\n- **Voltaic Snare** (174 controls, 33 of them colours) — the cast and its footprint, the leash, the\n  column, the tendrils, the rim arcs, the shared filament shape and flicker, the ribbon and its\n  colour, the field on the floor, the burns, sparks\u002Fupdraft\u002Fsmoke\u002Fdebris, throw\u002Fsnap\u002Fhold, and the\n  dynamic light.\n- **Presets** save to `localStorage`, and can be duplicated, deleted, exported to JSON, imported\n  from JSON, or reset to the shipped defaults.\n\nEvery ability exposes **every** colour it draws with, and none is derived from another: the crystal\npalette, the bolt palette, the beam's four layers and its coils and discs, the ground marks, the\nimpact shells, the shockwave rings, the screen flashes, and a four-stop lifetime gradient\n(`birth → early → late → death`) for each particle system. Tinting the fog without touching the ice,\nor cooling the sparks to orange while the filaments stay blue, is a picker away.\n\nPresets are plain snapshots of the settings tree, so an exported file is readable and editable by\nhand.\n\nKnobs worth knowing about, because they reshape their ability the most:\n\n- `ice.heightCurve` — how late the ramp climbs; raise it and the field stays low until it explodes\n  at the target. `ice.frontBias` below 1 crowds the crystals toward the impact point.\n- `thunder.jitter` and `thunder.jitterScale` — how violently the bolt kinks, and how often.\n  `thunder.strands` and `thunder.spread` set how wide the bundle reads, and `thunder.restrike`\n  how hard it strobes. Those five carry the character of the effect.\n- `beam.radius` and `beam.flare` — how heavy the column reads and how hard it opens out where it\n  lands. `beam.charge` and `beam.lifetime` are the wind-up and the hold, which are what make this\n  ability feel unlike the other three, and `beam.coreWidth` \u002F `beam.coreFill` decide whether the\n  layers stay separable or blow out to white.\n- `snare.zoneRadius` — the one number the whole far cast is built on. It resizes the targeting\n  circle, the tendrils, the rim arcs, the burnt field and the pillar's throat together, live.\n  After that, `snare.snapTime` and `snare.height` carry the moment it opens, and `snare.tendrils` \u002F\n  `snare.rimArcs` \u002F `snare.strands` decide how much of that footprint is actually lit.\n- `zone.boundary` and `zone.snap` — how thick the far-cast circle's edge reads, and how hard it\n  overshoots on the way out. Between them they decide whether the indicator feels like a UI overlay\n  or like something the caster is doing.\n\n---\n\n## Performance notes\n\n- Abilities, decals, bursts and particles are pooled, per type. Twelve casts in a row build at most\n  **four** instances of an ability and then stop allocating.\n- The whole crystal field is three draw calls regardless of crystal count; the cap is 288.\n- A whole bolt is **two** draw calls regardless of filament count; the cap is 24 filaments at 72\n  samples each. Nothing about the path touches the CPU, so `strands` is nearly free.\n- A whole snare — leash, pillar, tendrils and rim arcs — is **two** draw calls plus one for the\n  field, regardless of how many filaments are in it; the cap is 56 across the four roles. As with\n  the bolt, none of the shape touches the CPU, so raising `tendrils` or `rimArcs` is nearly free.\n  Its targeting circle is two more: one quad and one ring strip.\n- A whole beam is **six** draw calls regardless of how many coils and discs are on it — three tube\n  passes over one shared geometry, plus one instanced draw each for the coils, the discs and the\n  charge orb. As with the bolt, none of the shape touches the CPU, so `coils` and `rings` are\n  nearly free. It takes two of the six dynamic lights (the column and the caster's hands), so four\n  concurrent beams would exhaust the pool; `LightPool.acquire()` returns null and every use of the\n  handle is guarded.\n- The six dynamic point lights are created at boot and parked at zero intensity rather than added\n  and removed — changing the light count forces three to recompile every material.\n- Shadow maps update exactly once per frame even though the scene is rendered several times.\n- `renderer.compileAsync()` runs during boot so the first cast never stutters on shader compile.\n- Pixel ratio is capped at 1.75; the depth and distortion buffers are half resolution.\n\nMeasured on a default cast: 32 draw calls idle, ~69 with a full ice field standing and ~49 with a\nbolt in the air, ~1150 live particles. A snare standing with its cage, field and rim burns is ~45\ndraw calls and ~480 live particles, and arming its circle costs two. Four concurrent casts —\nthe pool's ceiling, whichever slots they came from — peaks at ~186 draw calls and five of the six\ndynamic lights.\n\nLive counters (FPS, live particles, instances, draw calls) are in the top-right of the HUD.\n\n---\n\n## The archive\n\n`src\u002Farchive\u002F` holds the previous incarnation of this project: a four-element bending sandbox\n(fire, water, earth, air) cast along a freehand-drawn spline, plus a walk mode that let the avatar\nride the same stroke. None of it is imported by the live app, so Vite never bundles it.\n\nIt was retired because this build replaced path drawing with a linear skillshot, which removed the\ninput every one of those systems was built on. The raymarched flame and water surfaces in\nparticular are worth mining. See `src\u002Farchive\u002FREADME.md` for what is in there and how to restore a\npiece of it.\n\n---\n\n## Known rough edges\n\n- Crystals are drawn with `transparent: true` and `depthWrite: true`. That is the right trade for\n  near-opaque ice and it keeps the field from sorting through itself, but at low `ice.opacity` the\n  sorting artefacts between overlapping spikes become visible.\n- The eruption front is a straight line on a flat floor. Both assumptions are baked in — the ground\n  is a single plane at y = 0, and the aim raycast targets that plane.\n- The distortion pass runs with nothing writing to it. It costs a half-res clear per frame.\n- The impact cluster is placed radially around the end point, so at very short cast distances it\n  can overlap the band behind it more than it should.\n- The far cast inherits the flat-floor assumption twice over: the circle is drawn on a single quad\n  at `y = 0`, and the snare's tendrils and rim arcs are placed against that same plane. Neither\n  would drape over a step.\n- Both the targeting circle and the snare's field are additive, so the footprint brightens the\n  floor rather than shading it. On a pale floor the boundary would need a non-additive pass under\n  it to stay readable.\n\n---\n\n## Licence\n\nCode is provided as-is for the purposes of this project. The bundled HDR probe and the character\nFBX retain their original licences.\n","这是一个基于Three.js构建的技能特效沙盒项目，专注于实时渲染游戏中的直线型与范围型技能命中效果。核心功能包括五种高度定制化的技能VFX（如冰霜长矛、闪电长矛、陨石坠落等），全部采用手写GLSL着色器实现，无外部纹理或预烘焙模型，所有几何体（晶体、光束、裂痕、电弧等）均为程序化生成。技术特点涵盖顶点着色器驱动的动态几何、射线投射瞄准、参数化路径建模及实时物理反馈式动画。适用于游戏开发中技能特效原型设计、VFX程序员学习着色器编程与实时渲染管线优化，以及独立开发者快速验证技能表现逻辑。","2026-08-12 02:30:03","CREATED_QUERY"]