Topic 10 · Rendering & VFX
VFX
General VFX rules for any Unity project: layered effects, particles, shaders, warm-up, cost and accessibility.
Vendor-neutral guidance for building effects in any Unity project, in URP or HDRP, with Shader Graph, the built-in Particle System or VFX Graph. Use it when you add the first effect to a project and again before you ship. Rules that were learned on a specific page stay there with their measurements and provenance; this page gives the short version and links to the full rule.
Build an effect: the layered recipe
- EFFECT-IS-A-STACK-OF-CHEAP-LAYERS — build each effect from a few cheap layers, not one rich one.
- Rule: nested soft additive glows, a shaped body (a flipbook, or a scrolled and eroded texture), accents (sparks, rings, spikes) and, only where needed, a little alpha-blended smoke. A bolt can be three nested stretched additive quads; a fire can be two glows, a flame and sparks.
- Why: a few cheap layers read richer than one expensive one, and each can be tuned or cut on its own.
- Check: every layer has a job you can name, and removing any layer visibly loses something.
- BLOOM-BRIGHTNESS-IS-STACKED-ADDITIVE — reach the bloom threshold by stacking LDR additive layers, not one HDR colour.
- Rule: several One/One additive layers that sum past the bloom threshold give more control and cost less than one quad with an HDR multiplier. Use the default render queues; additive layers need no sorting, so the blend mode does the layering. Keep alpha blending for smoke and soft rings.
- Check: the effect reads the same with bloom on and off, apart from the glow.
- MOTION-SELLS-THE-LOOK — timing and texture motion matter more than texture detail.
- Rule: hard
smoothstepalpha edges; a two- or three-tone colour ramp over the life of the effect; short flash pops staggered in small beats; near-instant scale-in; shape and erosion textures scrolling at different speeds; mesh layers (cones, rings, planes) with offset pivots for whooshes that move outward. - Check: play the effect at quarter speed; each beat is visible and the layers do not move in lockstep.
- EXPLOSIONS-POP-THEN-ERODE — never a linear scale-up.
- Rule: a violent front-loaded pop (an ease-out curve), then the layers erode away through a noise dissolve with a glowing burn edge; fade alpha only in the last beat.
- Why: one mesh with a linear scale or fade reads as dated, whatever the texture quality.
- Check: the silhouette changes over the burst instead of only growing.
- SOLID-FORM-PLUS-ENERGY — a physical core is one mesh wrapped in additive shells.
- Rule: when an effect must read as an object (a crystal bolt, a rune disc), use one low-poly mesh as the core and wrap it with one to three additive energy layers. A bare lit mesh at gameplay distance reads as a prop, not magic. Pivot at the flight centre.
- Check: at the gameplay camera the effect reads as energy, not as a dropped prop.
- PICK-THE-PATH-BY-WHAT-COST-SCALES-WITH — layered meshes for many actors, pooled particles for events.
- Rule: effects that fly with many actors (projectiles, trails, auras) are mesh layers drawn instanced. Particle systems and VFX Graph are for event-rate effects (impacts, casts, heals) from a capped pool. Never put a particle system on every unit of a high-count actor.
- Check: doubling the unit count does not double the number of live particle systems.
Particle systems
- ONE-SHOTS-FIRE-ON-AN-EDGE — bursts fire once, on a state change, never from a per-frame condition.
- Rule: emit from the transition (a value crossing a threshold, a flag going true, a counter incrementing) with
ParticleSystem.Emit(count)or a Burst entry, and use Sub Emitters for secondary sparks instead of spawning new systems from code. A per-frame follower only moves the system; it never callsClearandPlayevery frame. - Why: a burst fired from an
Updatecondition restarts every frame and turns into a fountain, or pins particles at age 0 where size-over-lifetime curves make them invisible. - Check: holding the trigger value constant produces no new bursts. Full rules: ONE-SHOTS-ARE-EDGE-TRIGGERS and PARTICLE-SHOW-IS-EDGE-TRIGGERED on the Animation (Mecanim) page.
- PARTICLE-SYSTEMS-ARE-POOLED — attached effects come from a pool and return to it.
- Rule: pre-create effect prefabs, attach on use, and on release call
Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear)(or let the effect finish) before returning it. If the pool scales the root, every descendant system usesscalingMode = Hierarchy, and scale is reset on return. - Check: using and releasing an effect 100 times creates no new GameObjects after warm-up. Full rule: POOLED-PARTICLE-SCALE-INCLUDES-DESCENDANTS on the Animation page.
- SUB-EMITTERS-AND-MODULES-HAVE-TRAPS — author sub-emitters and curves so they cannot misfire.
- Rule: sub-emitters have Play On Awake off; enabled velocity-over-lifetime axes share one curve mode; a test that calls
Simulateresumes playback before it judgesIsAlive. - Check: Play mode shows no velocity-mode errors and no sub-emitter firing on its own. Full rules: SUB-EMITTER-PLAYS-ON-ITS-OWN, PARTICLE-VELOCITY-AXES-SHARE-CURVE-MODE and PARTICLE-SIMULATE-PAUSES-THE-CLOCK on the Animation page.
Shaders and materials
- SHADER-GRAPH-TARGETS-MATCH-THE-PIPELINE — a graph without your pipeline's target draws nothing, silently.
- Rule: every effect graph lists the target of the pipeline you ship (URP or HDRP); converted vendor graphs are checked too.
- Check: open the graph's Graph Settings and confirm the target. Full rule: SHADERGRAPH-TARGET-MUST-MATCH-THE-PIPELINE on the Rendering & URP page.
- ADDITIVE-MEANS-THE-BLEND-MODE — check the blend state, not the material name.
- Rule: a glow material named "Additive" can still be alpha-blended and draw black squares; read its blend mode. A layered hit core (flare, glow, ring, sparks) uses additive materials, because stacked alpha-blended HDR white saturates to an opaque disc.
- Check: no glow quad shows a dark or opaque square in Play. Full rules: ADDITIVE-NAME-IS-NOT-ADDITIVE-BLEND and LAYERED-HIT-CORE-IS-ADDITIVE on the Rendering page.
- SOFT-PARTICLES-NEED-DEPTH — soft particles render blank without the camera depth texture.
- Rule: either enable the depth texture on every renderer that draws them, or strip the soft-particle keyword on tiers that turn depth off. Depth and scene-colour features (soft particles, depth glow, screen distortion) also cost mobile bandwidth, so keep them off high-density effects.
- Check: test on the mobile renderer asset, not only in the Scene view. Full rule: SOFT-PARTICLES-NEED-THE-DEPTH-TEXTURE on the Rendering page.
- DECALS-PROJECT-INTO-THE-SURFACE — impact decals point into the hit and are parented to what they mark.
- Rule: point the projector's +Z into the surface; parent the mark to the object it sits on.
- Check: live shots from several angles show no floating or missing marks. Full rule: DECAL-FACES-INTO-THE-HIT on the Rendering page.
- ONE-PARAMETER-DRIVES-THE-EFFECT — a material-driven effect is one 0..1 float, and gameplay writes only that.
- Rule: derive mask thresholds, emission, pulse speed and dissolve edge from one float (for example
_Amount). For steps the player must notice, shape the float with a curve or thresholds instead of a slow fade. - Why: one value is easy to animate, save, replicate, reset and test; separate flags drift out of sync and leave half-applied looks.
- Check: scrub the value from 0 to 1; every stage appears in order, and 0 is exactly the clean look.
- PER-INSTANCE-VALUES-NEVER-NEW-MATERIALS (HARD) — set per-renderer values without creating materials at runtime.
- Rule: cache
Shader.PropertyToIDonce. Use one reusedMaterialPropertyBlock, a GPU-instanced property, Entities Graphics per-instance overrides ([MaterialProperty]), or a material instance created once at spawn and destroyed with the object. Never touchrenderer.materialin a per-frame path: each first access clones the material. - Why: clones leak and break batching. In URP and HDRP a property block also drops the renderer out of the SRP Batcher, so for crowds prefer instanced properties or a few shared materials.
- Check: the Memory Profiler shows the same material count before and after a long fight.
- VALUES-NOT-KEYWORDS — stages and intensities change floats, not shader keywords.
- Rule: use a keyword only for a genuinely different feature, and prefer
shader_feature_local. A keyword enabled at runtime that no build material uses has no variant in the player. Remember the player strips anything it cannot prove is used. - Check: the build's variant report does not grow when you add a stage. Full rule: SHADER-VARIANTS-STRIP-IN-THE-PLAYER on the Rendering page.
Warm-up and first play
- VFX-GRAPH-WARMS-BY-BEING-PLAYED (HARD) — play each graph once during loading.
- Rule: the first enable of a VFX Graph compiles its shaders and builds its buffers on that frame; no preload list covers it. Play it once, off-screen, while loading.
- Check: the first in-game play has no hitch. Full rule with measurements on the Rendering page; for other shaders see GPU-WARMUP-NEEDS-A-REAL-DRAW on the Performance & memory page.
- VFX-GRAPH-ONLY-WHERE-COMPUTE-IS-SAFE — choose VFX Graph or the Particle System by platform.
- Rule: VFX Graph needs compute shaders; use it for dense effects on PC and console, and the built-in Particle System on low-end mobile and WebGL. Particle shaders with procedural instancing can skip their draws on some Vulkan devices.
- Check: the effect draws on the lowest target device. See PROCEDURAL-PARTICLE-INSTANCING-SKIPS-ON-VULKAN on the Rendering page.
- VENDOR-DURATION-IS-A-LIFETIME — read a vendor graph's exposed curves before driving it from code.
- Rule: to hold a one-shot graph, re-play it before it ends instead of raising its duration.
- Check: the effect clears on release. Full rule: VENDOR-VFX-DURATION-IS-A-LIFETIME on the Rendering page.
Readability: hit cores, flashes and streaks
- JUDGE-EFFECTS-AT-GAMEPLAY-DISTANCE — accept an effect from the game camera, not zoomed in.
- Rule: glows narrower than the core, no full impact prefab replayed at every hop of a chained streak or beam, and a final look check at the real camera distance.
- Check: a capture from the gameplay camera reads as the effect, not as one white blob. Full rule on the Rendering page.
- FLASHES-ARE-CAPPED-TOGETHER — tint plus emission, and flash plus light, are budgeted as a sum.
- Rule: cap a hit flash's tint and emission together so the unit never turns into a white silhouette under bloom; size a flash particle from the range of the light it sits on.
- Check: struck units stay readable with bloom on. Full rules: HIT-FLASH-CAPS-TINT-PLUS-EMISSION and FLASH-AND-LIGHT-ADD-UP on the Rendering page.
- READS-WITHOUT-HUE — every effect state is readable with colour removed.
- Rule: pair each hue with a non-colour cue: shape, pattern or motion. Give each effect type one hue family and reuse it in the world, the particles and the UI icon.
- Check: view effects through a greyscale or colour-blindness filter; each stays distinct.
Lifecycle and networking
- ONE-OWNER-FOR-APPLY-TICK-CLEAR (HARD) — one component owns an effect from start to clear.
- Rule: the owner sets the start value, attaches pooled effects, writes the shader parameter only when it changes, and on clear sets the value to 0 and returns every effect it turned on.
- Check: ending the effect removes every visual in the same frame.
- CLEANUP-ON-EVERY-EXIT-PATH (HARD) — death, despawn, scene unload and pool return all run the clear.
- Rule: call the same clear from every exit path (
OnDisableis a safe catch-all) and reset the parameter on the property block or material. - Why: pooled objects keep their property block values, so a fresh spawn can walk out of the pool mid-effect.
- Check: apply an effect, kill the object, and spawn until the pool reuses it; it is clean.
- REPLICATE-THE-VALUE-NOT-THE-VISUALS — the network carries the parameter; clients derive the effect.
- Rule: replicate only the authoritative value, quantized, and let each client set its own shader, particles and audio. Detect start and end as state transitions on each client.
- Check: a late joiner shows the same state without a replay of onset effects.
Cost and accessibility
- ZERO-ALLOCATION-BY-DEFAULT applies to effects too.
- Rule: cached property IDs, a reused
MaterialPropertyBlock, no LINQ or closures in effect ticks, pooled systems (see ZERO-ALLOCATION-BY-DEFAULT on the C# style page). - Check: Profiler GC Alloc is 0 B on frames with many effects running.
- OVERDRAW-IS-THE-REAL-COST — stacked transparent layers cost fill rate.
- Rule: keep alpha smoke few and large, keep particle sizes in check near the camera, cap simultaneous puffs, and use alpha clipping where you can.
- Check: Scene view Overdraw mode or the Rendering Debugger with the camera pushed into the densest effect.
- PARTICLE-CAPS-ARE-MEASURED-ON-DEVICE — caps come from a real draw on the lowest target device.
- Rule:
Max Particlesis a ceiling and CPU render time is not GPU cost; set caps from GPU frame time with the effect's pixels on screen. Mobile defaults first, with a raised ceiling for PC from the same pools. - Check: the device capture is recorded next to the cap. Full rule: PARTICLE-COST-NEEDS-A-REAL-DRAW on the Performance & memory page.
- NO-RAPID-FULL-SCREEN-FLASHES (HARD) — no large-area flash faster than three per second.
- Rule: keep full-screen or large-area flashes below three per second (WCAG 2.3.1), avoid saturated red flashes, prefer a slow edge vignette, and offer a screen-effects intensity setting. Never put critical information only in a screen effect.
- Check: record the worst case and count flashes per second.
- EFFECT-IS-PROVEN-BY-ITS-OUTPUT — a settings readback is not proof an effect renders.
- Check: look at the pixels or the pass counters. Full rule on the Rendering page.
Related rules elsewhere
Two locked, field-tested rules live on general pages and apply to every effect:
- ONE-SHOTS-ARE-EDGE-TRIGGERS on the Animation (Mecanim) page: impact bursts, per-tick puffs and pickup flashes fire once on a rising edge, with the baseline primed so late joiners and pooled objects never replay them.
- EMISSIVE-MAPS-NEED-AN-EMISSIVE-COLOR on the Rendering & URP page: glowing or pulsing materials need a non-black emissive colour, emission enabled and the
_EMISSIONvariant in the build.