Skip to content

t1k:cocos:playable:particle-ui

FieldValue
Moduleplayable
Version0.15.0
Effortmedium
Tools

Keywords: 2d, 3d to ui, ads, canvas, cocos, invisible, node scale, particle, particlesystem, playable, render particle in ui, scale particle, tiny, too small, ui, uimeshrenderer, uitransform, vfx

/t1k:cocos:playable:particle-ui

Make a 3D cc.ParticleSystem prefab render correctly (and at a usable size) inside a Canvas / UI tree. Two independent steps, both applied by scripts/particle-3d-to-ui.cjs (pure file-based, no editor/MCP needed):

  1. UI components — a 3D particle will NOT draw under a Canvas until its node has a cc.UIMeshRenderer. Add cc.UITransform + cc.UIMeshRenderer to every particle node; add cc.UITransform to the root node.
  2. Scale by data, never by node.scale — UI/Canvas space is pixel-scaled (huge vs. 3D metres), so a ported effect looks microscopic. Multiply the spatial params by a factor. Do NOT set node.setScale() / _lscale — Cocos scales the simulation oddly and the particles smear/clip (this is the whole reason the skill exists).
Terminal window
# add UI comps AND scale x60 in one pass (recommended first run)
node scripts/particle-3d-to-ui.cjs --ui --scale 60 assets/.../Hit_VFX.prefab assets/.../Death_VFX.prefab
# tune only (already UI-ready): multiply another x3, or shrink /3 with --scale 0.3333
node scripts/particle-3d-to-ui.cjs --scale 3 assets/.../Hit_VFX.prefab

--ui is idempotent (never double-adds components). --scale compounds on each run.

A 3D effect authored in world-units (Unity/Cocos sizes ~0.1–8) must become tens of pixels in a 1080×1920 UI. Empirically:

Design resolutionGood starting factorNotes
1080×1920 (portrait playable)×50–60×60 was the sweet spot in production (CarBattle VFX).
720×1280×30–40scale roughly with design width

Tuning method: run --ui --scale 20 first, view in-editor, then step by ×3 / ÷3 (--scale 3, --scale 0.3333) until the effect fills the intended UI area. 3–4 steps converges. Judge by eye against the UI, not the 3D scene.

Scaled ×N (length / velocity / acceleration): startSize(+X/Y/Z), startSpeed, gravityModifier, shape radius/length/position, velocity / force / limit-velocity modules, child-node local positions. rateOverDistance is divided by N (particles-per-distance → keep trail density).

Left unchanged (time / angle / color / count): lifetime, rotation, start color + alpha, burst counts, rateOverTime, the size-over-life curve (it is a normalized 0–1 multiplier — magnitude already lives in startSize), texture-sheet, renderer lengthScale/velocityScale.

Terminal window
node -e 'const p=JSON.parse(require("fs").readFileSync(process.argv[1]));
const psN=new Set(p.filter(o=>o.__type__=="cc.ParticleSystem").map(o=>o.node.__id__));
for(const n of psN){const c=(p[n]._components||[]).map(x=>p[x.__id__].__type__);
if(c.filter(t=>t=="cc.UITransform").length!=1||c.filter(t=>t=="cc.UIMeshRenderer").length!=1)throw"bad "+n;}
const ids=p.filter(o=>o.__type__=="cc.CompPrefabInfo").map(o=>o.fileId);
if(new Set(ids).size!=ids.length)throw"dup fileId"; console.log("OK");' your.prefab

Confirm: each PS node has exactly one cc.UITransform + one cc.UIMeshRenderer; root has cc.UITransform; every cc.CompPrefabInfo.fileId is unique; JSON still parses. Then open the prefab under a Canvas in the editor to reimport.

  • cc.UIMeshRenderer only works under a Canvas. The particle node must be in the UI/2D tree; dropped in a 3D scene node it does nothing.
  • Shared-CurveRange double-scale trap. startSize and startSizeX usually reference the SAME CurveRange object ({"__id__":6}). Scaling by field name hits it twice → ×N². The script dedups by __id__; any hand-rolled scaler MUST too (symptom: a ×20 request producing ×400 sizes).
  • Never use node.scale to resize a Cocos particle — the emitter shape and stretch billboards distort; always scale the params. This is the skill’s core lesson.
  • --scale compounds. Two --scale 3 runs = ×9. To undo an overshoot, run the inverse (--scale 0.3333), don’t git-revert-then-redo unless you want the exact prior state.
  • Component insertion is append-only. New cc.UITransform / cc.UIMeshRenderer / cc.CompPrefabInfo objects are pushed to the array tail and their {__id__} back-refs registered in node._components — never renumber existing __id__s (breaks every reference in the prefab).
  • emitFrom / mesh render modes are untouched — this skill is about UI-readiness + size, not fidelity. For 3D-mesh particles or blend/texture fidelity see t1k:cocos:playable:unity-particle.