粒子效果使用指南
直接创建粒子效果
通过App中的addParticle增加粒子效果
示例
const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
app.addParticle({
system: {
// 粒子发射周期 发射5秒后进入下一个粒子发射周期。
duration: 5,
// 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
looping: true,
// 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
prewarm: false,
//instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
// 粒子从发生到消失的时间长短
startLife: new vjmap3d.IntervalValue(4, 5),
// 粒子初始发生时候的速度
startSpeed: new vjmap3d.ConstantValue(1),
// 粒子初始大小
startSize: new vjmap3d.IntervalValue(1, 2),
//startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
// startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
// 粒子初始颜色
startColor: new vjmap3d.ColorRange(
new THREE.Vector4(0, 1, 1, 1),
new THREE.Vector4(1, 0, 0, 1)
),
// 世界空间坐标系
worldSpace: false,
//maxParticle: 100,
// 发射频率按时间 1秒发射的数量。
emissionOverTime: new vjmap3d.ConstantValue(0),
// 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
emissionBursts: [
{
// 粒子爆发的时间,不能超过粒子系统的Duration
time: 0,
// 发射多少个
count: new vjmap3d.ConstantValue(10),
// 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
cycle: 1,
// 两次循环的间隔时间(⚠️ 同上,未实现)
interval: 0.01,
// 每次爆发的可能性
probability: 1
}
],
// 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
// CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
shape: new vjmap3d.PointEmitter(),
material: new THREE.MeshBasicMaterial({
blending: THREE.NormalBlending,
transparent: true,
map: texture
//side: DoubleSide,
}),
// 纹理总的图像数里面的位置索引
startTileIndex: new vjmap3d.ConstantValue(0),
// 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
uTileCount: 1,
// 纹理垂直方向有几张图片
vTileCount: 1,
renderOrder: 2,
//
// BillBoard = 0, 粒子始终面向摄像机。
// StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
// Mesh = 2,
// Trail = 3,
// HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
// VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
renderMode: vjmap3d.RenderMode.VerticalBillBoard,
rendererEmitterSettings: {
// ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
// 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
speedFactor: 0,
// 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
lengthFactor: 2
}
},
simulations: new vjmap3d.SpeedOverLife(
new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
),
position: [-5, 0, 0]
});
通过App中的removeParticle删除粒子效果
通过particle.toJSON()导出粒子场景
⚠️ 直接对发射器调用
particle.toJSON()时,粒子系统的材质/纹理写进了内部临时 meta 后被丢弃,导出的 JSON 不含材质,无法再用loadParticle/loadParticleFromJson加载。应包一层 Group 再序列化(直接替换children,不要用add()——add会把发射器从场景中摘下导致粒子停止渲染):const wrap = new THREE.Group(); wrap.children = [particle]; const json = wrap.toJSON(); // materials/textures/images 都在 json 里
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
let particle = app.addParticle({
system: {
// 粒子发射周期 发射5秒后进入下一个粒子发射周期。
duration: 5,
// 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
looping: true,
// 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
prewarm: false,
//instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
// 粒子从发生到消失的时间长短
startLife: new vjmap3d.IntervalValue(4, 5),
// 粒子初始发生时候的速度
startSpeed: new vjmap3d.ConstantValue(1),
// 粒子初始大小
startSize: new vjmap3d.IntervalValue(1, 2),
//startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
// startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
// 粒子初始颜色
startColor: new vjmap3d.ColorRange(
new THREE.Vector4(0, 1, 1, 1),
new THREE.Vector4(1, 0, 0, 1)
),
// 世界空间坐标系
worldSpace: false,
//maxParticle: 100,
// 发射频率按时间 1秒发射的数量。
emissionOverTime: new vjmap3d.ConstantValue(0),
// 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
emissionBursts: [
{
// 粒子爆发的时间,不能超过粒子系统的Duration
time: 0,
// 发射多少个
count: new vjmap3d.ConstantValue(10),
// 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
cycle: 1,
// 两次循环的间隔时间(⚠️ 同上,未实现)
interval: 0.01,
// 每次爆发的可能性
probability: 1
}
],
// 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
// CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
shape: new vjmap3d.PointEmitter(),
material: new THREE.MeshBasicMaterial({
blending: THREE.NormalBlending,
transparent: true,
map: texture
//side: DoubleSide,
}),
// 纹理总的图像数里面的位置索引
startTileIndex: new vjmap3d.ConstantValue(0),
// 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
uTileCount: 1,
// 纹理垂直方向有几张图片
vTileCount: 1,
renderOrder: 2,
//
// BillBoard = 0, 粒子始终面向摄像机。
// StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
// Mesh = 2,
// Trail = 3,
// HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
// VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
renderMode: vjmap3d.RenderMode.VerticalBillBoard,
rendererEmitterSettings: {
// ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
// 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
speedFactor: 0,
// 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
lengthFactor: 2
}
},
simulations: new vjmap3d.SpeedOverLife(
new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
),
position: [-5, 0, 0]
});
const ui = await app.getConfigPane({ title: "粒子", style: { width: "250px"}});
ui.appendChild({
type: "button",
label: '导出JSON',
value: async ()=>{
// 包一层 Group 序列化,否则导出的 JSON 不含材质(见上文说明)
const wrap = new THREE.Group();
wrap.children = [particle];
vjmap3d.downloadBlob(JSON.stringify(wrap.toJSON(),0,4), "particle.json")
}
})
ui.appendChild({
type: "button",
label: '删除粒子',
value: async ()=>{
app.removeParticle(particle);
}
})
从文件中加载粒子效果
通过App中的loadParticle创建粒子效果
let particle = await app.loadParticle(env.assetsPath + "json/magic_zone.json");
如果需要对加载的数据进行修改,可通过属性进行设置
let particle2 = await app.loadParticle(env.assetsPath + "json/magic_zone.json", {
// 可对加载的数据进行修改
onData: data => {
data.object.children[0].children[0].ps.startColor.color = {r: 0,g: 1,b: 1,a: 1};
data.object.children[1].children[0].ps.startColor.color = { r: 0,g: 1,b: 1,a: 0.1764706};
data.object.children[2].ps.startColor.color = { r: 0, g: 1, b: 1, a: 1 };
},
position: [-5, 0, 3],
scale: [1.5, 1.5, 1.5]
});
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
let particle = await app.loadParticle(env.assetsPath + "json/magic_zone.json");
let particle2 = await app.loadParticle(env.assetsPath + "json/magic_zone.json", {
// 可对加载的数据进行修改
onData: data => {
data.object.children[0].children[0].ps.startColor.color = {r: 0,g: 1,b: 1,a: 1};
data.object.children[1].children[0].ps.startColor.color = { r: 0,g: 1,b: 1,a: 0.1764706};
data.object.children[2].ps.startColor.color = { r: 0, g: 1, b: 1, a: 1 };
},
position: [-5, 0, 3],
scale: [1.5, 1.5, 1.5]
});
从导出的JSON字符串中加载粒子
通过App中的loadParticleFromJson加载JSON字符串加载粒子效果
批量创建粒子效果
通过App中的loadParticleFromRes加载粒子效果资源,再克隆批量创建
let particleRes = await vjmap3d.ResManager.loadRes(
env.assetsPath + "json/magic_zone.json",
true,
"particle"
);
for(let i = 0; i < 20; i++) {
for(let j = 0; j < 20; j++) {
let color = vjmap3d.randColor();
app.loadParticleFromRes(particleRes.clone(), {
onAddSystem: (obj, system) => {
// 增加前对每一个粒子效果修改属性
system.startColor.color.x = color.r;
system.startColor.color.y = color.g;
system.startColor.color.z = color.b;
},
position: [i * 5 - 50, 0, j * 5- 50],
scale: Math.random() * 2
});
}
}
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
let particleRes = await vjmap3d.ResManager.loadRes(
env.assetsPath + "json/magic_zone.json",
true,
"particle"
);
for(let i = 0; i < 20; i++) {
for(let j = 0; j < 20; j++) {
let color = vjmap3d.randColor();
app.loadParticleFromRes(particleRes.clone(), {
onAddSystem: (obj, system) => {
// 增加前对每一个粒子效果修改属性
system.startColor.color.x = color.r;
system.startColor.color.y = color.g;
system.startColor.color.z = color.b;
},
position: [i * 5 - 50, 0, j * 5- 50],
scale: Math.random() * 2
});
}
}
粒子效果与实体对象相关联
let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
toEntity: true,
scale: [3, 3, 3]
});
entity.position.set(-4, 0, 3);
entity.addTo(app);
let particle = await app.loadParticle(env.assetsPath + "json/blue_fire.json", {
parent: entity.node, // 与实体相关联
rotation: [0, -Math.PI / 2, 0],
scale: [0.5, 0.5, 0.5]
});
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
let entity = await vjmap3d.ResManager.loadModel(env.assetsPath + "models/car.glb", {
toEntity: true,
scale: [3, 3, 3]
});
entity.position.set(-4, 0, 3);
entity.addTo(app);
let particle = await app.loadParticle(env.assetsPath + "json/blue_fire.json", {
parent: entity.node, // 与实体相关联
rotation: [0, -Math.PI / 2, 0],
scale: [0.5, 0.5, 0.5]
});
// 设置一个路径动画
let pathAnmiMod = entity.add(vjmap3d.PathAnimateModule)
pathAnmiMod.start({
paths: [[-4, 0, 3], [4, 0, 3], [4, 0, -7], [-4, 0, -7], [-4, 0, 3]],
cameraFollow: false,
offsetAngle: 90,
animation: {
duration: 15000,
repeatForever: true
}
})
文字粒子动画示例
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
let texture = await vjmap3d.ResManager.loadTextureSync(
env.assetsPath + "textures/texture1.png"
);
let logo_texture1 = await vjmap3d.ResManager.loadTextureSync(
env.assetsPath + "textures/logo_texture1.png"
);
let logo_texture2 = await vjmap3d.ResManager.loadTextureSync(
env.assetsPath + "textures/logo_texture2.png"
);
let seq = new vjmap3d.TextureSequencer(0.15, 0.15, new THREE.Vector3(-15, 1, 1));
seq.fromImage(logo_texture1.image, 0.6);
let seq2 = new vjmap3d.TextureSequencer(0.12, 0.12, new THREE.Vector3(-6, 0, 5));
seq2.fromImage(logo_texture2.image, 0.6);
let applySeq = new vjmap3d.ApplySequences(0.001);
applySeq.appendSequencer(new vjmap3d.IntervalValue(1.0, 2.0), seq);
applySeq.appendSequencer(new vjmap3d.IntervalValue(5.0, 6.0), seq2);
//
app.addParticle({
system: {
duration: 8,
looping: true,
startLife: new vjmap3d.ConstantValue(7.8),
startSpeed: new vjmap3d.ConstantValue(0),
startSize: new vjmap3d.IntervalValue(0.01, 0.1),
startColor: new vjmap3d.RandomColor(
new THREE.Vector4(0, 1, 1, 1),
new THREE.Vector4(1, 0, 1, 1)
),
worldSpace: false,
emissionOverTime: new vjmap3d.ConstantValue(0),
emissionBursts: [
{
time: 0,
count: new vjmap3d.ConstantValue(1000),
cycle: 1,
interval: 1,
probability: 1
}
],
shape: new vjmap3d.GridEmitter({ width: 15, height: 15, column: 50, row: 50 }),
material: new THREE.MeshBasicMaterial({
map: texture,
blending: THREE.NormalBlending,
transparent: true,
side: THREE.FrontSide
}),
renderMode: vjmap3d.RenderMode.BillBoard,
startTileIndex: new vjmap3d.ConstantValue(2),
uTileCount: 10,
vTileCount: 10,
renderOrder: 0
},
simulations: [applySeq]
});
粒子效果配置示例
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
container: "map", // 容器id
stat: { show: true, left: "0" },
scene: {
gridHelper: { visible: true } // 是否显示坐标网格
},
camera: { // 相机设置
viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
}
})
const texture = new THREE.TextureLoader().load(env.assetsPath + "textures/curve_triangle.png");
let options = {
duration: 5,
looping: true,
prewarm: false,
startSpeed: 1,
startLifeMin: 4,
startLifeMax: 5,
startSizeMin: 1,
startSizeMax: 2,
startColorMin: new THREE.Color(0, 1, 1),
startColorMax: new THREE.Color(1, 0, 0),
emissionOverTime: 0,
emissionBursts: {
// 粒子爆发的时间,不能超过粒子系统的Duration
time: 0,
// 发射多少个
count: 10,
// 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
cycle: 1,
// 两次循环的间隔时间(⚠️ 同上,未实现)
interval: 0.01,
// 每次爆发的可能性
probability: {
uiConfig: {
type: "slider",
label: "可能性",
value: 1,
bounds: [0, 1],
stepSize: 0.01 // 键名是 stepSize(写 step 会被忽略)
}
}
}
}
let particle;
const updateParticle = (opts) => {
if (particle) app.removeParticle(particle);
particle = app.addParticle({
system: {
// 粒子发射周期 发射5秒后进入下一个粒子发射周期。
duration: opts.duration,
// 粒子按照周期循环发射 让粒子发射时间循环起来,一直发射粒子。
looping: opts.looping,
// 预热系统 开始播放粒子是已经发射了一段时间的,只在Looping循环时才有效。
prewarm: opts.prewarm,
//instancingGeometry: new PlaneGeometry(1, 1),//.rotateX((-90 / 180) * Math.PI),
// 粒子从发生到消失的时间长短
startLife: new vjmap3d.IntervalValue(opts.startLifeMin, opts.startLifeMax),
// 粒子初始发生时候的速度
startSpeed: new vjmap3d.ConstantValue(opts.startSpeed),
// 粒子初始大小
startSize: new vjmap3d.IntervalValue(opts.startSizeMin, opts.startSizeMax),
//startRotation: new EulerGenerator(new ConstantValue(0), new ConstantValue(0), new ConstantValue(0)),
// startColor: new vjmap3d.ConstantColor(new THREE.Vector4(0, 1, 1, 1)),
// 粒子初始颜色
startColor: new vjmap3d.ColorRange(
new THREE.Vector4(opts.startColorMin.r, opts.startColorMin.g, opts.startColorMin.b, 1),
new THREE.Vector4(opts.startColorMax.r, opts.startColorMax.g, opts.startColorMax.b, 1)
),
// 世界空间坐标系
worldSpace: false,
//maxParticle: 100,
// 发射频率按时间 1秒发射的数量。
emissionOverTime: new vjmap3d.ConstantValue(opts.emissionOverTime),
// 发射爆发 从第几(Time)秒开始。发射多少个(Count)。(cycle/interval 运行时未实现,每个 burst 每个周期只发射一次)
emissionBursts: [
{
// 粒子爆发的时间,不能超过粒子系统的Duration
time: opts.emissionBursts.time,
// 发射多少个
count: new vjmap3d.ConstantValue(opts.emissionBursts.count),
// 从爆发时间开始的循环次数(⚠️ 运行时未实现,仅随 toJSON 序列化)
cycle: opts.emissionBursts.cycle,
// 两次循环的间隔时间(⚠️ 同上,未实现)
interval: opts.emissionBursts.interval,
// 每次爆发的可能性
probability: opts.emissionBursts.probability.uiConfig.value
}
],
// 发射器形状:PointEmitter / SphereEmitter / HemisphereEmitter 半球 / ConeEmitter 锥体 / CircleEmitter / DonutEmitter 甜甜圈 /
// CubeEmitter 立方体(没有 BoxEmitter)/ GridEmitter 等;Random/Loop/PingPong/Burst 是形状的 mode(vjmap3d.EmitterMode),不是形状
shape: new vjmap3d.PointEmitter(),
material: new THREE.MeshBasicMaterial({
blending: THREE.NormalBlending,
transparent: true,
map: texture
//side: DoubleSide,
}),
// 纹理总的图像数里面的位置索引
startTileIndex: new vjmap3d.ConstantValue(0),
// 纹理水平方向有几张图片 (uTileCount * vTileCount ) 表示此纹理总的图像数
uTileCount: 1,
// 纹理垂直方向有几张图片
vTileCount: 1,
renderOrder: 2,
//
// BillBoard = 0, 粒子始终面向摄像机。
// StretchedBillBoard = 1, 粒子面向摄像机,但是会应用各种缩放。
// Mesh = 2,
// Trail = 3,
// HorizontalBillBoard = 4, // 粒子平面与XZ“地板”平面平行。
// VerticalBillBoard = 5 // 粒子在世界y轴上直立,但面向摄像机。
renderMode: vjmap3d.RenderMode.VerticalBillBoard,
rendererEmitterSettings: {
// ⚠️ speedFactor/lengthFactor 只在 StretchedBillBoard 模式下生效(本例 VerticalBillBoard 下无作用);Trail 模式须改传 { startLength: new vjmap3d.ConstantValue(30) },否则发射即抛错
// 基于粒子的速度,粒子在摄影机方向上的拉伸程度。
speedFactor: 0,
// 粒子在摄影机方向上的拉伸程度取决于粒子的大小。
lengthFactor: 2
}
},
simulations: new vjmap3d.SpeedOverLife(
new vjmap3d.PiecewiseBezier([[new vjmap3d.Bezier(1, 0.75, 0.5, 0), 0]])
),
position: [-5, 0, 0]
});
}
updateParticle(options);
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px"}});
let cfg = {
type: 'folder',
expanded: true,
app: app,
label: "参数",
children: vjmap3d.generateUiConfig(options),
onChange: v => {
console.log(options)
updateParticle(options)
}
}
ui.appendChild(cfg)