Your GPUParticles3D node looks perfect in the editor, then disappears the moment you export to a phone running Godot's Compatibility renderer. No error in the console, no warning in the export log — the emitter just isn't there. This happens because the Compatibility renderer runs on OpenGL ES 3.0, which has no compute shader stage, and GPUParticles2D and GPUParticles3D both simulate on the GPU through a compute pass. Pick Compatibility for its wider hardware reach — older Android phones, integrated GPUs, some web exports — and every muzzle flash, torch flame, or thruster trail attached to an imported GLB prop goes dark on exactly the devices you picked that renderer to support. The fix is CPUParticles, plus a runtime check so you catch this in testing instead of in a one-star review.
Why GPUParticles fail silently on the Compatibility renderer
![]()
Forward+ and the Mobile renderer both run on Vulkan or Metal, and both give GPUParticles2D/3D a compute shader to run their simulation step on the GPU every frame. The Compatibility renderer targets OpenGL ES 3.0 instead, chosen specifically because it runs on hardware that predates or lacks Vulkan support — and OpenGL ES 3.0 has no compute shader stage at all. There's no fallback path for the engine to fall back to: the particle system either has a compute shader to simulate on, or it has nothing to draw. Godot doesn't print a warning because, from the engine's point of view, nothing went wrong — you asked for a renderer that structurally can't run that node type, and it complied by drawing zero particles.
This isn't a bug that got fixed and forgotten. It's tracked as godotengine/godot#84072, open since Godot 4.2, and it's still true in the 4.8 dev snapshots shipping this month: the Compatibility renderer picked up multi-bounce ambient occlusion and decal support in 4.8 dev 4, but GPU particle support isn't on that list, because the limitation sits in OpenGL ES 3.0 itself, not in engine code Godot could patch.
Forward+, Mobile, or Compatibility: what each one supports
The renderer is a project-wide choice in Project Settings > Rendering > Renderer > Rendering Method, and it decides more than just visual fidelity:
| Renderer | Graphics API | Compute shaders | GPUParticles | Typical target |
|---|---|---|---|---|
| Forward+ | Vulkan / Metal | Yes | Yes | Desktop, recent GPUs |
| Mobile | Vulkan / Metal | Yes | Yes | Phones from the last ~4 years |
| Compatibility | OpenGL ES 3.0 | No | No | Older Android, integrated GPUs, web |
If your minimum spec already assumes Vulkan-capable phones, switching the export preset to Mobile keeps every GPUParticles node working with no code changes — that's the cheapest fix when it's available. Compatibility earns its keep on the low end: it's what lets a five-year-old Android tablet or a browser tab without WebGPU run your game at all. That reach is also why Compatibility's export errors show up so often in the same projects — its texture and shader path diverges from Forward+/Mobile in more than one place, and silently dropped particles are just the least visible of the three.
Fix it: convert to CPUParticles, or branch at runtime
![]()
For a single-renderer project, the fix is a menu click. Select the GPUParticles3D node — probably parented under an imported GLB prop — and use the toolbar menu at the top of the 3D viewport labeled with the node's type. It has a Convert to CPUParticles3D option, and GPUParticles2D offers the same conversion in 2D. This bakes out an equivalent CPUParticles3D node with the same emission shape, color ramp, and velocity settings, simulated on the CPU instead of a compute shader, so it runs identically on Forward+, Mobile, and Compatibility.
Projects that ship more than one renderer — Forward+ on desktop, Compatibility on a mobile build from the same codebase — need a runtime branch instead of a one-time conversion, since only one of the two node types will actually emit:
```gdscript
# Attach to the effect's parent node
func _ready() -> void:
var method := OS.get_current_rendering_method()
var on_compat := method == "gl_compatibility"
$GPUParticles3D.visible = not on_compat
$CPUParticles3D.visible = on_compat
$CPUParticles3D.emitting = on_compat
```
Keep both nodes in the scene, sized for their renderer. A Plasma Rifle muzzle-glow is a good stress test because it's cheap either way: a free account's two monthly downloads cover evaluation; commercial use is included with paid memberships (pricing).
```gdscript
# Cheap muzzle-flash burst, safe on Compatibility
$CPUParticles3D.amount = 15
$CPUParticles3D.lifetime = 0.25
$CPUParticles3D.one_shot = true
```
Fifteen particles over a quarter-second costs nothing on CPU. A 300-particle explosion or a courtyard of torches won't — CPUParticles simulate on the main thread, so the render cost you removed from the GPU shows up as CPU frame time instead, on the same low-end devices you picked Compatibility to support. Budget per-effect, not per-scene: a handful of one-shot bursts is free, a dozen looping environmental emitters is a profiler session waiting to happen. CPUParticles2D/3D also drop sub-emitters and SDF collision, so a spark that's supposed to trigger a second burst on impact needs a timer and a second node instead of the built-in chain.
What still breaks after you switch
The editor viewport doesn't always match what ships. If Project Settings has Forward+ or Mobile set while your export preset targets Compatibility, the effect you're previewing while you work isn't the one a Compatibility device will actually run — the only reliable check is running the exported build, or setting the project's rendering method to Compatibility before you test, on real low-end hardware or an emulator that enforces the same GL ES 3.0 limits.
Decals ride along the same fault line. Compatibility picked up decal support in 4.8 dev 4, but it's capped at 64 decals per frame and 8 per surface — generous for a bullet-hole system, tight if you were planning to fake volumetric light shafts with stacked decals instead of particles. If a prop needs both a particle effect and a handful of decals, count the decal budget before committing to Compatibility as the answer for that scene, not after. Cutting the actual triangle count on the props carrying these effects is a separate lever — trimming the demo's poly budget frees up exactly the GPU headroom that CPUParticles' extra CPU cost eats into on a low-end device.
---
*This post is part of the Ultimate Guide to Free 3D Game Assets — BitSoul's complete reference for formats, texturing, rigging, optimization, and engine integration.*