本文へ移動
cccskills
無料GitHub で公開日本語紹介

melonjs-particles-and-trails

melonJSで爆発や煙などのパーティクル、動きに沿う軌跡を作る方法を扱い、放出設定や2D・3DのTrailの使い分けも確認できます。

原文原文の説明を見る

Use this skill for particle effects and motion trails in melonJS — ParticleEmitter configuration, burst versus stream emission, the referenceSpace frame of reference introduced in 20.2, 3D particle motion through elevation and minSpread/maxSpread in 20.8, and the Trail renderable. Covers the emitter that silently does nothing until you call an emit method. Triggers on: Trail3d, ribbon, exhaust trail, speed streak, ski track, tyre mark, ribbonNormal, ParticleEmitter, particles, burstParticles, streamParticles, referenceSpace, ParticleEmitterSettings, elevation, minSpread, maxSpread, spawn area, Trail, explosion, smoke, sparks, debris, ring, exhaust, emitter, tintOverLife, colour over life, color over life, flame, fire.

melonjs/melonJSスター 6,429(GitHub 6,429 + cccskills 0)

実行スコア 未検証

インストール方法を見る

こんなときに便利

  • 爆発や煙の演出を作りたいとき
  • 排気や移動跡を描きたいとき
  • パーティクルの色や広がりを調整したいとき
  • 表示されないエフェクトの原因を探したいとき

日本語での紹介

できること

melonJSのParticleEmitterとTrailを使い、爆発、煙、火花、炎、排気や移動跡などの演出を作る方法を扱います。パーティクルはエミッターを追加するだけでは表示されず、burstParticles()かstreamParticles()を呼んで放出を始めます。画像シートのフレーム選択、寿命に応じた色の変化、発生範囲、ブレンド方法も説明します。20.2以降のreferenceSpaceで移動物体に追従する煙と空間に残る軌跡を使い分け、20.8以降の高度や広がりで3Dの放出方向を調整できます。連続した帯状の軌跡には2DのTrail、3DシーンではTrail3dを選びます。

こんなときに便利

ゲームの演出を加えたいときや、パーティクルが出ない、動く対象に雲全体がついてくる、3Dカメラで軌跡が平らに見えるといった問題を調べるときに役立ちます。多数のエフェクトを扱う際の設定や描画負荷の確認にも使えます。

使い方の例

  • 「一度だけ飛び散る爆発パーティクルを設定して」
  • 「移動する船の後ろに残る排気の軌跡を作りたい」
  • 「3Dシーンで地面に沿うタイヤ跡を描く方法を教えて」

注意点

機能によって必要なmelonJSのバージョンが異なります。Trail3dは20.9.0以降で、Camera3dが必要です。

この紹介文は、公開されている SKILL.md をもとに AI(OpenAI Codex)が作成しました。正確な仕様は下の原文を確認してください。

含まれるファイル(1)

  • SKILL.md17.1 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Particles and trails

An emitter does nothing until you tell it to emit

Adding a ParticleEmitter to the world is not enough. Nothing appears, and nothing warns you.

const emitter = new ParticleEmitter(x, y, {
    image: loader.getImage("spark"),
    totalParticles: 60,
    speed: 4,
    minLife: 200, maxLife: 800,
});

world.addChild(emitter);
emitter.streamParticles();     // continuous — or:
emitter.burstParticles();      // one shot

streamParticles(ms) takes an optional run time; without one it uses the duration setting, which defaults to Infinity — a stream runs until stopStream(). burstParticles(n) launches n, or totalParticles.

Sprite-sheet particles

Use framewidth and frameheight (the same names as on Sprite) to divide image into a regular grid. Each omitted dimension defaults to that of the image. Frames are numbered from zero left-to-right, then top-to-bottom; only whole cells count, so partial cells along the right/bottom edges are ignored. Margins and spacing are not supported.

const explosion = new ParticleEmitter(x, y, {
    image: loader.getImage("explosion-sheet"),
    framewidth: 32, frameheight: 32,
    frames: [0, 1, 2, 3],
    minLife: 400, maxLife: 800,
});
world.addChild(explosion);
explosion.burstParticles();

Omit frames to use every cell. Each particle traverses the sequence once over its own lifetime, with equal time for each entry; repeated indices hold a frame longer. Existing scale, opacity and rotation still apply. Bounds and the rotation pivot use one frame's dimensions, rather than the whole sheet.

Set randomFrame: true for debris: a particle chooses one entry at birth and keeps it until it dies. Different frames share the same image and GPU batch. Without sheet settings, particles still draw the whole image as before, and that includes randomFrame on its own: it needs a grid to choose from, so pair it with framewidth/frameheight or it does nothing.

Note the variety is in SHAPE, not colour. tint belongs to the emitter, so every particle from one emitter is tinted alike however many frames it picks between. For a colour that changes over each particle's life, use tintOverLife (below) rather than drawing one frame per colour.

Frame dimensions must be positive integers that fit inside the image, and an explicit frames sequence must be nonempty and contain valid integer indices. Invalid configurations throw RangeError before changing an emitter during reset(). The frame sequence is copied on construction/reset; resetting changes only future particles, so ones already alive finish with their original image, frame size and sequence.

Colour over life: tintOverLife

tintOverLife is the list of tints a particle takes over its life: the first at birth, the last at the end, evenly spaced and blended in between. Give as many CSS colours as you like (repeat one to hold it longer); it replaces tint.

// a flame: white hot, then orange, then deep red as it rises and dies
const flame = new ParticleEmitter(x, y, {
    image: softDot,                     // a white texture takes the tints as they are
    tintOverLife: ["#fff3b0", "#ffc455", "#ff8a26", "#e45414", "#a0280c"],
    textureAdditive: true,
});
  • Colour only. Opacity is still the built-in fade over the particle's life; the alpha of a colour in the list is ignored. On an additive emitter, a darker colour is a dimmer particle, so fold a "fainter" step into the colour itself.
  • 32 steps. However many colours, the blend is taken in 32 steps, the same on every renderer: a particle changes colour at most 32 times. That matters on Canvas, which keeps a tinted copy of the texture for every colour it is asked to draw: 32 at most per texture.
  • Author the texture white. tint multiplies, so a coloured texture muddies every step.
  • It works alongside sprite-sheet frames (one picks the image, the other the colour), and a list that is empty, not an array, or holds a non-string throws a TypeError from the constructor or reset(), which then leaves the emitter as it was. The list is copied: change it through reset().

Where particles are born

width and height are the spawn AREA, and both default to 1 — so by default every particle launches from the same pixel. That is fine for a fountain and wrong for an explosion: additive sprites piled on one point clip all three channels there however well they are tinted, so a burst reads as one bright flash rather than as debris.

Give it the size of whatever broke. The area is measured from the emitter's own position, NOT centred on it, so offset by half or the whole burst sits down and to the right of its source:

const area = radius * 2;
const emitter = new ParticleEmitter(x - area / 2, y - area / 2, {
    width: area, height: area,
});

Particles in 3D

By default a particle keeps the depth it was born at for its whole life: it moves across the emitter's plane and never toward or away from the camera. Under a Camera3d that reads as a flat sticker anywhere but dead ahead.

elevation lifts the launch out of that plane and elevationVariation spreads it. angle is the azimuth within the plane, as it always was:

const emitter = new ParticleEmitter(x, y, {
    angle: 0, angleVariation: Math.PI * 2,
    elevation: 0, elevationVariation: Math.PI / 2,   // a sphere of debris
    speed: 9,
});
world.addChild(emitter, z);     // the depth slice the burst belongs to
emitter.burstParticles();

speed is the length of the whole 3D vector, so an "all directions" burst covers the sphere evenly instead of bunching toward the poles.

Both default to 0, and that is a gate rather than the 3D path with zeros in it: an emitter that asks for no elevation runs the same two trig calls it always did, writes no depth and starts no sort.

The launch is in WORLD axes. vel.x and vel.y add to the particle's pos.x/pos.y and velZ adds to its depth, so angle and elevation describe a direction in the world, not on the screen. Building them from a camera basis works only while the camera's axes ARE the world axes, which stops being true the moment you pose a Camera3d. For a direction d already in world terms the pair is angle: Math.atan2(-d.y, d.x), elevation: Math.asin(d.z).

Cones, rings and spheres: aiming around an axis

angleVariation and elevationVariation are sampled INDEPENDENTLY, spreading particles over a rectangle of azimuth by elevation. That cannot describe a shape defined against a direction. A ring of debris leaving a point on a sphere is the set of directions at ninety degrees to that point's normal, and on a rectangle the elevation that satisfies it is a function of the azimuth, so no pair of variations gets there.

Set maxSpread and angle/elevation become an AXIS instead. Each particle leaves at a polar angle between minSpread and maxSpread off that axis, around a uniformly random azimuth:

minSpread … maxSpreadshape
0 … 0.5a tight cone along the axis
0 … Math.PIthe whole sphere
either value at Math.PI / 2a flat disc perpendicular to the axis

An explosion on a planet's surface is a ring tangent to it: set the axis to the surface normal and use minSpread: Math.PI / 2 - 0.3, maxSpread: Math.PI / 2. Keeping the band at or inside ninety degrees is what stops debris tilting back into the body it came off. angleVariation and elevationVariation are unused while maxSpread is set.

Sorting, and why it is keyed off the blend mode

A 3D burst only needs depth sorting when its blend is order dependent, and the emitter decides that for you. Additive blending is commutative, so an additive burst looks the same however it is ordered and pays nothing; "normal" is not, so a 3D burst drawn with it is sorted back to front.

The two differ in more than cost. Additive sprites overlapping near the centre of a burst sum to white however they are tinted, so debris that has to keep its colour wants textureAdditive: false and accepts the sort. A common shape is two emitters: a solid, sorted body of debris with a sparser additive spray of sparks over it.

The default particle sprite is a filled SQUARE, which additive blending hides and normal blending does not. Pass your own soft image for non-additive debris.

For one-shot effects, autoDestroyOnComplete: true removes the emitter once its particles have finished. The onComplete callback fires at that same moment either way.

For repeated effects (muzzle flashes, impacts) keep one long-lived emitter and re-aim it per shot, rather than allocating an emitter per event.

referenceSpace — which frame particles live in

Added in 20.2. This decides what a particle's position is measured from, and it is the difference between an effect that trails behind a moving object and one that drags along with it.

valueparticles are measured fromuse for
"local" (default)the emitterflames, auras, anything welded to the object
"world"the container the emitter sits intrails, smoke, exhaust, dust
a Containerthat containera moving frame — snow inside a moving carriage
// exhaust that stays where it was emitted, so the ship flies away from it
const exhaust = new ParticleEmitter(x, y, { referenceSpace: "world", /* … */ });

With the default "local", a moving emitter carries its whole cloud along — which is right for a torch flame and wrong for a smoke trail. Changing it at runtime re-bases live particles, so nothing jumps.

Particle tuning changed in 20.2

The 20.2 particle transform fix corrected a drift where drawn positions ran past simulated ones. Particles now travel roughly half as far over their life as they did before.

Consequence: speed values tuned on 19.x or earlier look wrong on 20.2+. If you are copying emitter settings from older code or older tutorials, expect to raise speed or maxLife.

Trails

Trail is a ribbon that follows a target — use it rather than hand-rolling a position history. Its constructor takes one argument, the options object — no x, y pair like every other renderable:

const trail = new Trail({
    target: sprite,             // a renderable or a plain Vector2d
    length: 24,                 // max points kept (default 20)
    width: 20,                  // max ribbon width in px (default 10)
    lifetime: 500,              // per-point lifetime in ms (default 500)
    widthCurve: [1, 0],         // 0 = head, 1 = tail
    gradient: ["#fff", "#f80", "#f000"],
});
trail.blendMode = "additive";
world.addChild(trail, 5);

Omit target and feed it yourself with trail.addPoint(x, y) — the mode for a sword slash, where the ribbon follows a weapon tip rather than an object. Points closer together than minDistance (default 4 px) are dropped.

Trail is 2D. Use Trail3d in a 3D scene (20.9.0 and later). Trail stores points without a z and draws connected screen-space quads, which carry one depth for the whole renderable, so under a Camera3d the ribbon stays flat and does not narrow along its own length.

const exhaust = new Trail3d(0, 0, {
    target: ship,                 // samples pos.x / pos.y / pos.z
    width: 6,                     // world units, and free to change per frame
    lifetime: 260,
    gradient: ["#eaffff", "#2ea8ff", "#2ea8ff00"],
});
exhaust.blendMode = "additive";
app.world.addChild(exhaust);      // no z: it derives its own from its points

Trail3d is a Mesh, which is most of the difference:

  • billboard is spelled and valued exactly as on Sprite3d — "spherical" (the default here) faces the camera on all axes, true / "cylindrical" faces it but stays upright, false uses the ribbonNormal you give it. billboard: false, ribbonNormal: [0, -1, 0] lays the ribbon in the ground plane for a tyre mark or a wake.

  • tint works, and is the per-frame colour lever. On Trail it does nothing at all, because the primitive batcher reads the draw colour and never the tint. setColor() / setGradient() rebuild the gradient for a change of state; they allocate, so they are not per-frame.

    If the colour is going to change, author the gradient WHITE. tint multiplies, so tinting a cyan ribbon red gives muddy brown rather than red. A neutral gradient hands the hue entirely to the tint, which is what lets one lever take a trail from white through to red as something overheats.

  • A post effect reaches it properly, where Trail has to force a screen capture.

  • It is cheaper: one indexed draw call, against one filled path per segment.

  • length is fixed at construction, because the geometry is sized once. Assigning maxPoints throws rather than silently truncating the ribbon, and a length that is not a finite number throws at construction.

  • It needs a Camera3d. Under a 2D camera it draws nothing and warns once.

A world-space ParticleEmitter (referenceSpace: "world") is still the right answer when you want discrete puffs rather than a continuous ribbon.

Performance

  • Particles come from a shared pool, so a steady-state emitter allocates nothing. Each emitter is still a Container that updates and draws every frame, so one busy emitter beats many idle ones.
  • totalParticles (default 50) is a cap on how many are alive at once in stream mode; burstParticles() launches that many in one go, or the count you pass.
  • framesToSkip (default 0) skips n updates between simulation steps — the cheapest lever on an emitter with many particles.
  • Blending is per particle, set through the blendMode setting or by assigning emitter.blendMode (which reaches the particles already alive as well as later ones). Additive is cheap everywhere: fixed-function on both GPU backends, native lighter on Canvas. The expensive modes are the advanced CSS ones (overlay, soft-light, color-dodge, darken, …), which on WebGL 2 and WebGPU cost a destination capture plus a shader composite per blended draw.

Symptom → cause

symptomcause
trail looks flat under a Camera3dTrail is 2D; use Trail3d
particles clutter a minimap or a second cameraevery camera draws them: set the emitter's visibilityLayer to a layer that camera's cullMask leaves out (see melonjs-camera-and-drawing)
Trail3d draws nothing, with a console warningit needs a Camera3d; the app's cameraClass is still the 2D default
trail.tint does nothingTrail cannot be tinted at all; Trail3d can
assigning maxPoints throwsTrail3d sizes its geometry once; pass length to the constructor
Trail3d: \length` must be a finite number`a computed length came out NaN; it is refused rather than silently rendering nothing
emitter added but nothing appearsstreamParticles() / burstParticles() never called
the cloud follows a moving emitterdefault referenceSpace: "local" — use "world" for trails
particles do not travel far enoughsettings tuned pre-20.2, when drawn travel was doubled
one-shot emitters accumulatemissing autoDestroyOnComplete: true
every particle of an emitter is the same colour all its lifetint is one value for the emitter: use tintOverLife for a colour that changes as it ages
a tintOverLife colour's transparency has no effectthe list sets the colour only; opacity is the particle's fade
tintOverLife colours look muddythe texture is coloured; author it white so the tints come through as given
frame rate drops with many effectsan emitter per event instead of one re-aimed emitter
a 3D burst is invisible under a Camera3dbefore 20.8 a particle was culled at twice its depth; update
the whole burst launches from one pixelwidth/height are the spawn area and default to 1
the burst sits down-right of its sourcethe spawn area starts at the emitter's position; offset by half
the middle of a burst goes whiteadditive sprites overlapping — textureAdditive: false, or spread them
debris chunks look like squaresthe default sprite is a filled square; pass your own image
a ring tilts with the camera instead of staying on a surfacethe launch axis is in WORLD axes, not the camera basis

Related skills

  • melonjs-renderables — draw order and blend modes
  • melonjs-effects-and-shaders — blend mode cost on each backend

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

コメント

ログインなしで書き込めます。使ってみた感想、質問、動かなかった環境などを気軽にどうぞ。

まだコメントはありません。最初のコメントを書いてみませんか。

送信するとすぐに公開されます。メールアドレスや電話番号などの個人情報は書かないでください。URL は 2 つまでです。

0 / 1000 文字

このフォームは reCAPTCHA で保護されており、Google のプライバシーポリシーと利用規約が適用されます。

同じリポジトリのスキル

melonjs

無料概要あり

melonJSを使ったゲーム制作で、目的に合う専門ガイドを選ぶための入口です。初期化から描画、入力、物理演算、音声、2D・3D機能まで案内します。

  • melonJSで作る内容に合うガイドを探したいとき
  • 新しいゲームの初期化手順を知りたいとき
  • 3Dやモデル読み込みの参照先を探したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-20-migration

無料概要あり

melonJS 19.x以前の知識を20.x向けに更新し、初期化や描画効果、廃止・非推奨APIの変更点を確認して移行時の不具合を調べられます。

  • 19.x以前のゲームを20.xへ移行したいとき
  • 古いAPIが見つからない原因を調べたいとき
  • 非推奨の描画効果を置き換えたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-3d

無料概要あり

melonJSの3D・2.5Dゲーム向けに、カメラ、メッシュ、ライト、奥行き順、透明表現やフォグの設定と座標系の注意点を確認できます。

  • 3Dカメラをステージに設定したいとき
  • メッシュやビルボードを配置したいとき
  • 奥行き順や透明表現を調整したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-3d-assets

無料概要あり

melonJSでglTF・GLBやOBJ・MTLの3Dモデルを読み込み、マテリアルやライト、アニメーション、影、インスタンスの扱い方を確認できます。

  • glTF・GLBのシーンを読み込みたいとき
  • OBJモデルとMTL素材を使いたいとき
  • モデルの向きや大きさを調整したいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-audio

無料概要あり

melonJSで効果音やBGMを読み込み、再生・停止・音量調整するほか、位置に応じた音や音声合成、ミックス全体へのエフェクト設定を扱うスキルです。

  • 効果音とBGMを読み込みたいとき
  • ゲーム内の位置に応じて音を鳴らしたいとき
  • 音声ファイルなしで短い音を作りたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs-camera-and-drawing

無料概要あり

melonJSのカメラ追従や画面効果、座標変換、図形描画を実装する際の要点を確認できます。描画のクリッピングやCanvasへの焼き込み、ピクセルの読み出しも扱います。

  • プレイヤーを追うカメラの範囲を設定したいとき
  • ワールド座標と画面座標を変換したいとき
  • 独自描画に図形やグラデーションを使いたいとき
melonjs/melonJS6,4292026年10月29日 更新

melonjs のスキルをすべて見る

このスキルの問題を報告する