t1k:cocos:playable:unity-particle
| Field | Value |
|---|---|
| Module | playable |
| Version | 2.14.4 |
| Effort | high |
| Tools | — |
Keywords: billboard, bubble, convert, curverange, emitter, gradientrange, mat, particle, particlesystem, ripped, shapemodule, sizeovertime, vfx
How to invoke
Section titled “How to invoke”/t1k:cocos:playable:unity-particleUnity ParticleSystem → Cocos 3.8.7 ParticleSystem
Section titled “Unity ParticleSystem → Cocos 3.8.7 ParticleSystem”Port a Unity ParticleSystem prefab (e.g. from a ripped/exported project) into a Cocos Creator 3.8.7 cc.ParticleSystem prefab. Cocos has no editor importer for Unity particles, so the prefab is hand-authored as JSON by cloning the serialization shape of a known-good Cocos particle prefab and rewriting the module values from the Unity source.
Inputs — what to provide when you invoke this skill
Section titled “Inputs — what to provide when you invoke this skill”Provide at least the absolute path to the Unity ParticleSystem prefab. Everything else (its material .mat + texture) is resolved from that file.
| Input | Required | Default | Example |
|---|---|---|---|
Unity .prefab absolute path | ✅ yes | — | C:\Users\me\Downloads\Rip\ExportedProject\Assets\GameObject\BubbleInPipe.prefab |
Cocos output folder (where to write the new .prefab) | optional | assets/game-assets/vfx/Prefabs/3D/ | assets/game-assets/vfx/Prefabs/3D/ |
| Output prefab name | optional | same as the Unity prefab | BubbleInPipe |
| Blend override | optional | infer from Unity .mat (transparent → alpha-blend, else additive) | alpha-blend | additive |
| Import the source texture? | optional | yes | yes | reuse <existing texture uuid> |
Copy-paste invocation:
Convert the Unity VFX at
C:\…\BubbleInPipe.prefabinto a Cocos particle prefab underassets/game-assets/vfx/Prefabs/3D/, nameBubbleInPipe, alpha-blend.
⛔ If the absolute .prefab path is missing, STOP and ask for it before doing anything else — never guess a path. If the output folder is omitted, use the default above and say so.
When to use
Section titled “When to use”- A Unity
.prefabwhose GameObject has aParticleSystem(!u!198) +ParticleSystemRenderer(!u!199). - You need it as a Cocos
.prefabreferencing a Cocoscc.Material+cc.Texture2D.
Not for: Unity VFX Graph / Shuriken sub-emitter trees with mesh particles (only the common billboard case is covered here — flag the gaps).
Workflow
Section titled “Workflow”- Read the Unity source — the
.prefab(ParticleSystem+ParticleSystemRendererblocks) and the material it references (m_Materials[0]guid →.mat) to get the texture guid and blend (_Blend/_SrcBlend/_DstBlend/ render queue). - Find a Cocos skeleton — locate an existing project particle prefab with a single
cc.ParticleSystemto clone the exact__id__graph (grep'"__type__": "cc.ParticleSystem"'and pick the file with one match). Never author the id-graph from memory — clone it. - Map the values — use the tables below to translate every module. Keep the skeleton’s
__id__layout identical; only change scalar values, curve keys, enum ints, and material/texture uuids. - Author
.prefab+.meta— write the JSON, generate a fresh v4 uuid for the.meta(importer: "prefab",syncNodeName). - Import texture + material — copy the Unity texture PNG into the project, hand-write its image
.meta(clone a sibling PNG.meta, new uuid, same6c48atexture sub-id), and make a builtin-particle material (see Material section). - Validate —
node -e "JSON.parse(fs.readFileSync(...))"on every file; grep the new uuids project-wide to confirm zero collisions; confirm the prefab no longer references the skeleton’s original material/texture uuids.
Cannot visually verify without the editor — say so. The editor reimports the new assets on focus; the prefab renders only after import.
Parameter mapping (Unity → Cocos)
Section titled “Parameter mapping (Unity → Cocos)”Full field-by-field lookup tables (emitter/main module, simulation-space & scale-space enums,
minMaxState → CurveRange.mode, shape-type enum, size/color-over-life, renderer mode): see
references/parameter-mapping.md. The operative safety warnings from those tables are
condensed below in ## Gotchas — read that first, since several enum pairs share the same
int range with different meanings and a literal copy corrupts the port silently.
Material & texture import (critical)
Section titled “Material & texture import (critical)”The particle material must use the builtin-particle effect — reuse the effect uuid an existing project particle material already references (it is a builtin, not a local .effect file). The blend is chosen by _techIdx:
_techIdx | Blend | Use for |
|---|---|---|
| 0 | Additive | glows, sparks, light orbs, neon |
| 1 | Alpha-blend | translucent/soft particles — smoke, glass bubbles, dust |
Match the Unity material: transparent render queue + alpha _Blend → techIdx 1; additive → techIdx 0. Clone a sibling .mtl of the right techIdx, swap _props[0].mainTexture to the imported texture’s @6c48a uuid, set tintColor, and write a .meta (importer: "material", new uuid).
The builtin-particle tinted fragment shader multiplies 2.0 * vertexColor * tintColor * texture. Therefore the neutral/no-op material tint is 0.5 grey: { r: 128, g: 128, b: 128, a: 128 } in 0–255 color values (equivalently 0.5, 0.5, 0.5, 0.5), not white. A white {255,255,255,255} tint makes the 2.0 factor double both RGB and alpha, which can turn soft/translucent particles bright, opaque, and hard-edged.
Texture import: copy the PNG, clone a sibling image .meta, give it a fresh top-level uuid and the same 6c48a texture sub-id (<uuid>@6c48a is what the renderer references), keep redirect → <uuid>@6c48a.
Cocos cc.ParticleSystem id-graph (skeleton)
Section titled “Cocos cc.ParticleSystem id-graph (skeleton)”A single-PS prefab is a flat array of objects referenced by __id__. Canonical layout (clone, don’t memorize):
0 cc.Prefab 1 cc.Node 2 cc.ParticleSystem 3 cc.CompPrefabInfo4 startColor GradientRange 5..7 startSize{,Y,Z} CurveRange8 startSpeed 9..11 startRotation{X,Y,Z} 12 startDelay 13 startLifetime14 gravityModifier 15 rateOverTime 16 rateOverDistance 17 cc.Burst 18 burst count CurveRange19 ColorOvertimeModule 20 GradientRange 21 cc.Gradient 22..25 cc.AlphaKey26 ShapeModule 27 arcSpeed 28 SizeOvertimeModule 29 size CurveRange 30 cc.RealCurve 31..33 x/y/z34 VelocityOvertimeModule 35..38 · 39 ForceOvertimeModule 40..42 · 43 LimitVelocityOvertimeModule 44..4748 RotationOvertimeModule 49..51 · 52 TextureAnimationModule 53 CurveRange 54 RealCurve 55 CurveRange56 NoiseModule · 57 TrailModule 58..61 · 62 cc.ParticleSystemRenderer · 63 cc.PrefabInfoEach tunable lives in its own cc.CurveRange object (mode 0 constant / mode 3 min-max / mode 1 spline). Disabled modules keep _enable: false with zeroed CurveRanges — leave them intact from the skeleton.
See references/conversion-checklist.md for the step-by-step file-by-file checklist.
Gotchas
Section titled “Gotchas”- Clone the id-graph; never hand-number it. A wrong
__id__reference silently corrupts the prefab. - Unity and Cocos order the simulation-space enum oppositely (Unity Local 0 / World 1; Cocos World 0 / Local 1). Never pass
moveWithTransformthrough — invert it. A wrong value is silent: the prefab loads fine, but a world-space trail becomes emitter-locked and a moving emitter (rocket, projectile) drags its smoke along instead of leaving it behind.scalingModeis a third enum again — translate it separately. - Shape ints are not interchangeable either. Unity
ShapeModule.typeand CocosshapeTypeoverlap on 0–4 with different meanings — a copied Unity Cone (4) becomes a Cocos Hemisphere (4) and loses its direction. Always go through the shape table. - Renderer mode IS a straight pass-through, unlike simulation-space and shape.
m_RenderMode0–4 (Billboard/Stretch/HorizontalBillboard/VerticalBillboard/Mesh) map identity to_renderMode— copy the int directly. The one exception is Unity5(None, a disabled renderer): it has no Cocos render-mode value, so map it to disabling the renderer component, never to4(Mesh) with a null mesh. randomDirectionAmountis inert in Cocos 3.8.7 — declared on the shape module, never read byemit(). Any Unity direction randomisation is dropped; the ported spread looks tighter than the source. Don’t hunt cone angle or start-speed for it.- Box
emitFromMUST be3(Volume), never0(Base). Base is auto-corrected to Volume only in the editorshapeTypesetter, not on prefab load — a hand-authoredemitFrom: 0on a Box leaves emit positions uninitialized → particles scatter outside the box (the “spawns everywhere / infinite” bug). Same trap: Sphere/Hemisphere need3(Volume), not0/1. _mainTextureneeds the@6c48asub-uuid, not the bare image uuid.- techIdx is the #1 fidelity bug: additive vs alpha-blend completely changes the look; a glass bubble on additive becomes a glowing ring.
- Unity particle sorting is a hierarchy conversion step, not a field mapping. Make competing emitters siblings and order them back-to-front; move a highest-sorted parent emitter onto its own last child when necessary.
- uuid uniqueness: grep every generated uuid across
assets/before finishing. - Tiny Unity sizes (0.1–0.15) stay faithful but may look invisible at the project’s scene scale — keep the authored values, don’t silently inflate. Make it visible by scaling in data, not on the transform: run
t1k:cocos:playable:particle-ui’sscripts/particle-3d-to-ui.cjs --scale N, which multiplies the spatial params. Under a Canvas,node.setScale()is the wrong tool outright — that skill exists because node-scaling smears stretched billboards and distorts the emitter shape. Even in a pure 3D scene it only earns its keep on a single-emitter prefab, and it multiplies particle size, not burst extent (references/parameter-mapping.md§ “Node scale scales SIZE, not travel”). scaleSpace: 1(Local) ignores the parent chain — it resolvesnode.getScale()on the emitter’s own node, so scaling a scaled root never reaches child emitters and they render sub-pixel with no error. UsescaleSpace: 0(World) on every child, or scale each child node. Looks fine on single-emitter prefabs, which is why it survives review.scaleSpace(SIZE) and_simulationSpace(POSITION) resolve scale through different paths, and a compensating-scale child falls into a variant of the trap above. Particle SIZE never touchescc_matWorld— it comes straight from the scale uniform (scale.xyz * a_texCoord1, i.e.scale_uniform * startSize), where CocosParticleSpaceisWorld = 0, Local = 1(the inverse of the intuitive Unity-first reading). Emit POSITION on a Local-space node, by contrast, does pass through the fullcc_matWorld. So a child node holding a deliberate counter/compensating scale (to keep some other sibling visually stable under a scaled parent) still emits from the geometrically-correct position underscaleSpace: 1— but its particles render at that node’s own small compensating scale, i.e. silently shrunk, because SIZE bypassed the matrix that would have folded the compensation back out. SetscaleSpace: 0(World) on that child so SIZE readsgetWorldScale()instead, agreeing with where POSITION already lands. Seereferences/parameter-mapping.md§ “WhatscaleSpaceactually reads” for the full engine-call table.- The serialized key is
_enable, notenable— and an absent key means the class DEFAULT, not “off”. Cocos strips properties that equal their default when serializing (serialization/deserialize.ts: “Instances of the same class may have different Masks due to different default properties removed”), so a module’s flag is simply missing from the JSON whenever it holds the default. For every over-lifetime module and the shape module that default isfalse(animator/*-overtime.ts,emitter/shape-module.ts), so absent does mean off for those — but a grep for"enable"finds nothing even on a module explicitly turned on ("_enable": true), which reads identically to disabled. Never conclude module state from a key search; check_enableexplicitly and know the class default. - The
.prefabasset is not authoritative for a scene instance. A node placed in a scene is acc.PrefabInstancewhosepropertyOverridesare applied at expand time (scene-graph/prefab/utils.ts→applyPropertyOverrides), so the live component can differ from every value in the asset file. When the answer matters, read it back at runtime (Cocos MCPmanage_component get_all) instead of trusting the serialized file. startSpeed 0means particles don’t move (grow/fade in place) — preserve it; don’t “helpfully” add velocity unless asked.- Dispatch every
MinMaxCurvefield onminMaxState— Unity serializes all four slots (scalar,minScalar,maxCurve,minCurve) whatever the mode, so the slots the mode does not name are stale leftovers from earlier edits, not hidden “real” values. Take only the one the state names:0constant →scalar,1curve →maxCurve,2two curves →minCurve/maxCurve,3random between two constants →minScalar..scalar. UnderminMaxState: 0thescalaris authoritative even when it is0— a non-zerominScalarbeside it does not override it. Holds forcountCurve,rateOverTime,startSpeed,startSize,startLifetimealike (references/parameter-mapping.md§ “UnityminMaxState→ CocosCurveRange.mode”). cc.RealInterpolationModeisLINEAR = 0,CONSTANT = 1,CUBIC = 2(cc.d.ts,export enum RealInterpolationMode). Hand-authoring acc.RealCurvekeyframe for a size/color-over-lifetime module and writinginterpolationMode: 1intending “linear” instead selects CONSTANT, which holds each keyframe’s value until the next key. A 0→1 ramp then evaluates to 0 for the entire 0..1 range, reaching 1 only att = 1, past the particle’s life — sostartSize * curveis 0 every frame and the emitter renders nothing. There is no error, no warning, and no missing-reference marker; the symptom is simply an invisible sub-emitter while its siblings render fine, which reads as a material or texture problem and sends you debugging the wrong thing. Author linear ramps asinterpolationMode: 0. Notefx_dongnap-style prefabs commonly cloned as a serialization skeleton useinterpolationMode: 2(CUBIC) with explicit tangents, so copying the skeleton and editing only thevaluefields is safe — the trap is hand-writing a fresh keyframe.- When deriving a UI scale factor by measuring an existing converted prefab against its Unity source, multiply in the reference prefab’s node
_lscalechain — not just its serialized spatial values. Measured case:ItemSparkleYellow.prefabserialized values suggested a ×60 factor (Unity size 0.7 → 42, radius 0.66 → 39.6, speed 0.1–0.3 → 6–18, gravity −0.05 → −3), but its root node also carries_lscale: 7, so the real end-to-end factor is ~420. Using 60 produced particles 12–24 px wide on a 1080-wide canvas (under 2% of screen width) for 0.6 s — effectively invisible, and with no error anywhere. Effective factor = serialized-value ratio × node-scale chain of the reference. - A looping emitter is the wrong control for verifying a burst-only one-shot effect.
ItemSparkleYellowisloop: true, rateOverTime: 25/4, bursts: []so it renders whenever you look at it; aloop: false+ single-burst prefab plays once on node creation and is over in well under a second, and moving the node does not retrigger it. Pick a non-looping burst prefab as the structural control (e.g.game-assets/vfx/Prefabs/3D/ExplodeBlue.prefab), and document the replay procedure for the user: the ParticleSystem component’s Play/Stop buttons in the Inspector, or toggling the node’sactivecheckbox. - Prefer multiplying spatial params in data over setting node
_lscalewhen any emitter uses_renderMode: 1(stretched billboard) — node scale smears and distorts stretched particles. This is whyparticle-3d-to-ui.cjs --scale Nscales data rather than the transform. - A Unity emitter with
rateOverTime: 0,m_BurstCount: 0,m_Bursts: [],looping: 1, and a non-zerorateOverDistanceundermoveWithTransform: 1(World) is a deliberate distance-driven trail emitter — not a lossy or truncated export. It is correctly silent while the node is static and emits only as it travels; do not “repair” it by restoring bursts from the upstream original. Measured case: one session read exactly this signature as a damaged regeneration, imported burst counts from the Epic Toon FX original, setloop: false, and switched simulation space to Local — three changes that each independently destroyed the trail. rateOverDistancescales INVERSELY with the spatial scale factor — the one spatial parameter that does. It is particles per world unit of distance, so the Cocos value isunityRate / N(N = px per Unity unit), notunityRate × Nlike size/speed/etc. Measured case: N=420, so Unity rates 2 and 10 became0.004761905and0.023809524— one particle every 210 px and every 42 px of travel. Porting the raw 2 and 10 as particles-per-pixel would request 2,160 and 10,800 particles over a 1080 px drag — instantly past_capacity(1000) and choking the frame. Sanity-check every distance rate: compute the particle count over a full-screen drag and reject the conversion if it is absurd in either direction.loop: trueis mandatory for any distance-driven emitter. Incocos/particle/particle-system.ts_emit()(3.8.7, lines ≈1446–1471), when_time > duration + startDelayandloopis false,_isEmittingis set false and the function returns before therateOverDistanceblock is reached. A non-looping distance emitter therefore dies afterdurationseconds even with every rate converted correctly.- The simulation-space enum is INVERTED between the engines, and for a trail the off-by-one is load-bearing, not cosmetic. Cocos
SpaceisWorld = 0, Local = 1, Custom = 2; UnitymoveWithTransformis0 = Local, 1 = World— so UnitymoveWithTransform: 1maps to Cocos_simulationSpace: 0. In Local space every particle rides along with the emitter: nothing stays behind and you get a clump stuck to the node instead of a streak. Off-by-one here is silent, unlike shape enums which visibly break. - Run from the parent project root, not inside a submodule.