DEV Community

Richard Fu
Richard Fu

Posted on • Originally published at richardfu.net on

Unity WebGL Particles Invisible? Causes and How to Fix It

I spent the better part of a day chasing a celebration firework that played perfectly in the Unity editor and rendered absolutely nothing in the WebGL build. No errors. The particles spawned, played, sat right in front of the camera — and stayed invisible.

I diagnosed it confidently four times. The first three were wrong. The only thing that ever found truth was a dumb A/B test in the actual build — not in the editor, not by reading shaders, not by theorizing. That turned out to be the real lesson, so I’ll tell it honestly — wrong turns and all — and then map out what WebGL genuinely can and can’t do with particles.

The setup

A Unity 6 / URP mobile-style game shipped to WebGL. The win screen opens a treasure chest with a firework burst and a soft neon glow over the whole scene. Two things broke only in the WebGL build:

  1. The bloom/glow was missing.
  2. The firework was completely invisible — despite the particle system clearly running.

They turned out to be unrelated, which is part of why this took so long.

Wrong turn #1 — right, but only for the glow: shader variant stripping

URP strips “unused” shader variants from builds to save size, and the editor never strips. So editor-works / build-doesn’t should make you suspect stripping first.

The missing bloom was exactly that — URP’s Bloom post-processing variant was being stripped on WebGL (Strip Unused Post Processing Variants). Disabling it brought the glow back (I later moved the glow to MK Glow , a renderer feature — see the quality-tier trap below).

Good fix — for the glow. Then I made the classic mistake: I assumed the firework had the same villain.

Wrong turn #2 — “the Shader Graph must render black on GLES3”

The firework was from a popular Shader-Graph-based pack (Cartoon FX Remaster). I instrumented the celebration code:


firework diag @t=2.5s: totalParticles=164 anyPlaying=True anyVisible=True
shaders=[Cartoon FX/Remaster/Particle Procedural Glow, Particle Ubershader]

Enter fullscreen mode Exit fullscreen mode

Particles exist , system playing , renderer on-screen , real shaders (not the magenta error shader). Yet no visible color — even when I cranked a glow pass to blow out the whole screen, the firework area simply never appeared.

My conclusion sounded airtight: Unity WebGL = WebGL 2.0 = OpenGL ES 3.0 , and Unity cross-compiles its HLSL down to GLSL ES 3.00. For a big particle “ubershader,” I figured the cross-compiler was emitting GLSL that compiles but outputs black — a translation bug that bites only on the GLES3 path, never in the editor (Metal/D3D).

Plausible. Confident. Wrong — as I’d find out two diagnoses later.

The fix that worked — for a different reason than I thought

I swapped the Shader-Graph firework for an Epic Toon FX firework — CPU ParticleSystem (Shuriken) + URP/Particles/Unlit. Plain, old, boring tech. It rendered in the editor immediately.

…and was invisible in the WebGL build. Again.

Wrong turn #3 → the first confirmed truth: Soft Particles need a depth texture

Same signature — plays in editor, audio + feedback fire (so it’s emitting), nothing on screen. But this time the cause was real and provable. I inspected the live materials:


shader: Universal Render Pipeline/Particles/Unlit
keywords: [..._SOFTPARTICLES_ON...] _SoftParticlesEnabled = 1
URP 'High' (WebGL tier): supportsCameraDepthTexture = False

Enter fullscreen mode Exit fullscreen mode

Soft Particles — the fade where a particle meets opaque geometry — needs the camera depth texture , and the WebGL tier wasn’t generating one. With no depth bound, the fade reads an unbound texture: desktop returns “far” (visible); WebGL GLES3 returns 0 → alpha ≈ 0 → fully transparent. Same material, opposite result per platform. No shader error — it compiles fine and fades itself to nothing.

Confirmed via the WebGL console (Playwright): zero shader errors — a runtime fade, not a compile failure. The fix was one toggle: turn Soft Particles off (an open-air firework never intersects geometry, so they did nothing anyway). The firework finally rendered on WebGL. 🎆 It’s also why the coins were fine the whole time: opaque, no soft particles, no depth dependency.

So I’d “solved” it — and, generalizing, told myself the original CFXR firework must have been soft particles too. Tidy unified theory. Also wrong.

The reckoning: a 30-second A/B test demolishes my Shader-Graph theory

The chests also carried a CFXR glow halo (CFXR3 LightGlow A (Loop)). If “CFXR renders black on WebGL” were true, those would be invisible too. So I tested it the only way that matters — in the build : two chests at the start line, one with the CFXR glow, one without.

The one with the CFXR glow glowed. On WebGL. The pack I’d declared “renders black” was rendering just fine.

So I pulled the shader keywords of a CFXR material that works (the glow) and one that fails (the firework):


WORKS "cfxr stretch ray blur add" → _CFXR_ADDITIVE, _FADING_ON
FAILS "cfxr stretch trait blur add" → _CFXR_ADDITIVE, _FADING_ON ← identical

Enter fullscreen mode Exit fullscreen mode

Identical shader, identical keywords, identical soft-particle settings — one renders, one doesn’t. That kills every shader-level theory at once: not the Shader Graph, not the variant, not soft particles, not HDR. The same compiled shader draws in the loop-glow and draws nothing in the firework burst.

The real differentiator is at the ParticleSystem / effect level — the failing effects are complex one-shot bursts (sub-emitters, velocity, lifetime curves, the pack’s lifecycle script); the working one is a simple loop glow. Something in the burst machinery doesn’t survive the WebGL runtime — particles spawn and the renderer reports on-screen, but they contribute no visible pixels. I never pinned the exact module, and didn’t need to: the game ships the working glow + the fixed CPU firework.

The honest scoreboard:

  • The glow was shader-variant stripping. ✅ correct
  • The CPU firework was Soft Particles + no depth. ✅ confirmed
  • “CFXR renders black on GLES3”? ❌ wrong. CFXR renders on WebGL; some of its burst effects don’t, for a non-shader reason I never proved.

Three confident diagnoses, one survivor. The thing that exposed each wrong one was the build , not the editor.

Which Unity WebGL particles actually render (a GPU-feature wall)

The useful map. WebGL 2.0 / GLES3 supports modern vertex/fragment shaders (GLSL ES 3.00) but has no compute, geometry, or tessellation shaders.

Tech Runs on WebGL2? Why
Built-in / URP standard shaders ✅ always simple frag/vert, cross-compiles cleanly
CPU ParticleSystem (Shuriken) CPU sim + standard shaders
Shader Graph ✅ usually generates a normal shader; mostly fine on GLES3
VFX Graph ❌ never GPU-simulated via compute shaders — absent on WebGL2
Geometry / tessellation ❌ never absent on WebGL2

I deliberately softened the Shader Graph row from my first draft — I’d written “can render black,” but my own A/B test later showed the Shader Graph renders fine and the failures were elsewhere. Shader Graph is an authoring tool that emits an ordinary shader: treat it as “usually fine, verify in build,” not “cursed.”

VFX Graph is the categorical wall: it simulates particles on the GPU via compute shaders, and WebGL2 has no compute, so it can’t run at all. That’s why “Realistic Fireworks — VFX Graph, supports URP!” asset-store packs are a non-starter for WebGL.

“Supports URP” describes the render pipeline, not the platform. Your constraint is WebGL, not URP.

A bonus trap: the editor lied via the quality tier

When I added MK Glow it rendered in the WebGL build but did nothing in the editor — sliders had no effect. I burned time on render-graph theories before introspecting the live editor:


QualityLevel 2 (PC) → PC_Renderer (no MK Glow)
QualityLevel 3 (High)→ Default_Forward_Renderer (has MK Glow) ← WebGL uses this

Enter fullscreen mode Exit fullscreen mode

The editor was previewing on the PC tier — a different renderer than WebGL’s High tier — so my feature wasn’t even in the active pipeline. Setting the editor to High gave instant preview. Bonus gotchas from the same hole: the Scene view doesn’t render custom renderer-feature passes (only the Game view does), and Volume edits made during Play mode revert on Stop.

Unity ↔ three.js (because the compute wall is universal)

The same wall shows up in three.js — with a different escape hatch.

Concept Unity three.js
Shader authoring abstraction Shader Graph TSL (Three.js Shading Language)
Raw shader code HLSL / ShaderLab GLSL / WGSL
GPU compute Compute Shaders (HLSL) WebGPU compute / TSL compute fns
GPU particle sim (compute) VFX Graph GPU-compute particles (TSL + WebGPU)
CPU particle sim Shuriken ParticleSystem CPU-updated Points/sprites

Two things people (me included) get backwards:

  • TSL ≈ Shader Graph, not VFX Graph. It’s an authoring layer — you write shading logic in a JS node API and TSL compiles it to WGSL (WebGPU) or GLSL (WebGL2 fallback). It can also author compute, but that’s one capability, not its identity.
  • The equivalent of VFX Graph is GPU-compute particles built with TSL compute on the WebGPU renderer — the technique, not the language.

The strategic difference today: three.js is well ahead on WebGPU (its WebGPURenderer + TSL are production-usable, and WebGPU browser support is real and growing), so you can ship GPU-compute particles there if you target WebGPU. Unity’s web export is still WebGL2/GLES3 (its WebGPU backend is experimental), so for a shipping Unity web game you assume no compute → no VFX Graph , and CPU particles + standard shaders are the lane. The “realistic GPU fireworks” I wanted would be on the table in three.js-on-WebGPU; on Unity-on-WebGL2, the boring CPU ParticleSystem was the right — and only — answer.

Takeaways

  1. Test in the actual build — every time. The editor renders through a different backend (Metal/D3D), often a different quality tier, and never strips shaders. Every wrong diagnosis came from trusting the editor or my own reasoning; every correction came from the build.
  2. Invisible particles have several unrelated causes — shader-variant stripping, Soft Particles (no depth texture), and particle-system quirks all produce the same “spawns, on-screen, invisible.” Don’t pattern-match the second case to the first.
  3. Soft Particles need the depth texture the WebGL tier doesn’t provide → silent fade to invisible. Turn them off, or enable Depth Texture (at the cost of a pass).
  4. VFX Graph never ships to WebGL (needs compute). Shader Graph usually does — verify in build; don’t assume it’s “black on GLES3.”
  5. “Supports URP” ≠ “supports WebGL.” Platform, not pipeline, is the constraint.
  6. Know when to stop. I never fully pinned why the CFXR firework burst was invisible here (CFXR otherwise renders fine on WebGL — the chest glow proves it). Once a reliable alternative worked, the forensics weren’t worth it.
  7. Stay humble about confident diagnoses. I had three airtight ones. A 30-second in-build A/B test beat all of them.

The firework glows now — a CPU particle system from decade-old tech, lit by a renderer-feature glow, rendering happily in a browser. Sometimes the boring tool is the right tool. And the fastest debugger is often two objects side by side in the actual build.

The post Unity WebGL Particles Invisible? Causes and How to Fix It appeared first on Richard Fu.

Top comments (0)