老规矩:直接放效果图,符合就往下看,不符合出门右转。
文章内容
主要应用了three.js的SpriteNodeMaterial与cesium中的ParticleSystem效果。
第一步three+cesium结合
参照cesium官方的示例,渲染两个canvas,three.js的canvas画布在cesium之上,相机根据cesium相机更改,类似与图层叠加。
- 问题
遮挡:three.js的图层是最高的,所以当倾斜视角时,会发现很多cesium中的元素杯火焰遮挡。
three.js 火焰粒子官方示例
cesium.js粒子官方示例
火焰源码
import { Mesh, AxesHelper, Scene, PointLight, PointLightHelper, TextureLoader, PerspectiveCamera, Object3DEventMap, AdditiveBlending, Vector3, PlaneGeometry, Group } from 'three'
import WebGPURenderer from 'three/src/renderers/webgpu/WebGPURenderer';
import SpriteNodeMaterial from 'three/src/materials/nodes/SpriteNodeMaterial';
import { range, texture, mix, uv, color, rotateUV, uniform, positionLocal, ShaderNodeObject, TimerNode, timerLocal } from 'three/src/nodes/TSL';
import {
Viewer, Math as CesiumMath, Cartesian3, Matrix4, Transforms, Primitive, GeometryInstance, ColorGeometryInstanceAttribute,
PerInstanceColorAppearance, EllipsoidGeometry, Color
} from 'cesium'
import { creatGuid } from '@/utils/tool';
interface Object3DOpt {
lon: number,
lat: number,
alt: number,
id: string,
type: string,
visible: boolean
timerLocal: ShaderNodeObject<TimerNode>
}
export enum WIND_DIRECTION {
NORTH = 'n', //北
SOUTH = 's', //南
EAST = 'e', //东
WEST = 'w', //西
NORTH_EAST = 'ne', //东北
SOUTH_EAST = 'se', //东南
NORTH_WEST = 'nw', //西北
SOUTH_WEST = 'sw' //西南
}
interface Object3DConfig {
rotateX: number, rotateY: number, rotateZ: number,
speed: number,
smokeCount: number, fireCount: number,
fireScale: number, smokeScale: number, fireOffestY: number, isChange: boolean,
}
class Object3D {
threeMesh: Mesh<PlaneGeometry, SpriteNodeMaterial, Object3DEventMap>
lon: number
lat: number
alt: number
id: string = ''
type: string = ''
config: Object3DConfig
visible: boolean
timerLocal: ShaderNodeObject<TimerNode>
constructor(mesh: Mesh<PlaneGeometry, SpriteNodeMaterial, Object3DEventMap>, opt: Object3DOpt) {
this.threeMesh = mesh
this.lon = opt.lon
this.lat = opt.lat
this.alt = opt.alt
this.id = opt.id
this.type = opt.type
this.timerLocal = opt.timerLocal
this.visible = opt.visible
this.config = {
rotateX: 0, rotateY: 0, rotateZ: 0,
isChange: false,
speed: 0.2,
smokeCount: 2000,
fireCount: 100,
fireScale: 100,
smokeScale: 100,
fireOffestY: -100
}
}
}
interface TCOption {
imgUrl?: string
domId: string
}
export class TC {
viewer: Viewer
threeScene: Scene | undefined
threeCamera: PerspectiveCamera | undefined
threeRenderer: WebGPURenderer | undefined
imgUrl: string = ''
objects3D: Object3D[] = []
isDestory: boolean = false
CesiumContainer: HTMLElement
STATIC_SCALE = 100
MaskBoxPrimitives: Map<string, Primitive> = new Map<string, Primitive>();
constructor(viewer: Viewer, opt?: TCOption) {
this.viewer = viewer
this.imgUrl = opt?.imgUrl || '/images/smoke1.png'
const domId = opt?.domId || 'CesiumViewer'
this.CesiumContainer = document.getElementById(domId)!
this.initThree()
this.loop()
}
initThree() {
// 设置相机配置
let fov = 45;//视角
let aspect = window.innerWidth / window.innerHeight;//宽高比例
let near = 0.1;
let far = 10 * 1000 * 1000;//视域范围
// 初始化场景
this.threeScene = new Scene();
const axesHelper = new AxesHelper(100); // 三维坐标辅助线
this.threeScene.add(axesHelper);
//点光源 助手 第二个参数为大小
const pointLigth = new PointLight("#fff", 1);
pointLigth.position.set(2, 2, 2);
this.threeScene.add(new PointLightHelper(pointLigth, 0.1))
this.threeCamera = new PerspectiveCamera(fov, aspect, near, far);
this.threeRenderer = new WebGPURenderer({
antialias: true,//抗锯齿
alpha: true,
});
// 设置渲染器大小
// three.renderer.setSize(window.innerWidth, window.innerHeight);
// 添加环境光
// let ambientLight = new THREE.AmbientLight(0xffffff, 1);
// three.scene.add(ambientLight);
//将three.js canvas添加在cesium容器中
if (this.CesiumContainer) this.CesiumContainer.appendChild(this.threeRenderer.domElement);
// this.viewer.scene.canvas.appendChild(this.threeRenderer.domElement)
}
createFire(lon: number, lat: number, alt: number, id?: string, visible?: boolean) {
// textures
const textureLoader = new TextureLoader();
const map = textureLoader.load(this.imgUrl);
// create nodes
const lifeRange = range(.1, 1);
const offsetRange = range(new Vector3(-2, 3, -2), new Vector3(2, 5, 2));
const timer = timerLocal(.2, 1/*100000*/); // @TODO: need to work with 64-bit precision
const lifeTime = timer.mul(lifeRange).mod(1);
const scaleRange = range(.3, 2);
const rotateRange = range(.1, 4);
const life = lifeTime.div(lifeRange);
const fakeLightEffect = positionLocal.y.oneMinus().max(0.2);
const textureNode = texture(map, rotateUV(uv(), timer.mul(rotateRange)));
const opacityNode = textureNode.a.mul(life.oneMinus());
//0x2c1501 0x222222 0xB8B8B8 0x969696
const smokeColor = mix(color(0x2c1501), color(0x222222), positionLocal.y.mul(3).clamp());
// create particles
const smokeNodeMaterial = new SpriteNodeMaterial();
smokeNodeMaterial.colorNode = mix(color(0xf27d0c), smokeColor, life.mul(2.5).min(1)).mul(fakeLightEffect);
smokeNodeMaterial.opacityNode = opacityNode;
smokeNodeMaterial.positionNode = offsetRange.mul(lifeTime);
smokeNodeMaterial.scaleNode = scaleRange.mul(lifeTime.max(0.3));
smokeNodeMaterial.depthWrite = false;
smokeNodeMaterial.transparent = true;
const smokeInstancedSprite = new Mesh(new PlaneGeometry(1, 1), smokeNodeMaterial);
smokeInstancedSprite.scale.setScalar(this.STATIC_SCALE);
(smokeInstancedSprite as any).count = 2000;
this.threeScene?.add(smokeInstancedSprite)
const uuid = id || creatGuid()
let OB3d1 = new Object3D(
smokeInstancedSprite,
{ lon, lat, alt, id: uuid, type: 'smoke', timerLocal: timer, visible: visible === undefined ? true : visible }
);
this.objects3D.push(OB3d1);
const fireNodeMaterial = new SpriteNodeMaterial();
fireNodeMaterial.colorNode = mix(color(0xb72f17), color(0xb72f17), life);
fireNodeMaterial.positionNode = range(new Vector3(- 1, 1, - 1), new Vector3(1, 2, 1)).mul(lifeTime);
fireNodeMaterial.scaleNode = smokeNodeMaterial.scaleNode;
fireNodeMaterial.opacityNode = opacityNode;
fireNodeMaterial.blending = AdditiveBlending;
fireNodeMaterial.transparent = true;
fireNodeMaterial.depthWrite = false;
const fireInstancedSprite = new Mesh(new PlaneGeometry(1, 1), fireNodeMaterial);
fireInstancedSprite.scale.setScalar(this.STATIC_SCALE);
(fireInstancedSprite as any).count = 100;
fireInstancedSprite.position.y = - 100;
fireInstancedSprite.renderOrder = 1;
this.threeScene?.add(fireInstancedSprite)
let OB3d2 = new Object3D(
fireInstancedSprite,
{ lon, lat, alt, id: uuid, type: 'fire', timerLocal: timer, visible: visible === undefined ? true : visible }
);
this.objects3D.push(OB3d2);
}
toggleVisible(id: string, visible: boolean) {
this.objects3D.forEach(i => {
if (i.id === id) {
i.visible = visible
}
})
}
renderThree() {
if (this.threeCamera && this.threeRenderer && this.threeScene) {
this.threeCamera.fov = CesiumMath.toDegrees((this.viewer.camera.frustum as any).fovy);
this.threeCamera.updateProjectionMatrix();
this.objects3D.forEach(i => {
//模型中心位置
const center = Cartesian3.fromDegrees(i.lon, i.lat, i.alt);
//设置实体位置
i.threeMesh.position.copy(this.cartToVec(center));
//计算朝向(切面方向-切线向量)
//中心高度点
// const centerHeight = Cartesian3.fromDegrees(i.lon + i.config.rotateX, i.lat + i.config.rotateY, i.alt);
// const centerHeight = Cartesian3.fromDegrees(i.lon, i.lat, i.alt);
// i.threeMesh.lookAt(this.cartToVec(centerHeight));
if (i.config.isChange) {
i.timerLocal.scale = i.config.speed
if (i.type === 'smoke') {
i.threeMesh.scale.setScalar(i.config.smokeScale);
(i.threeMesh as any).count = i.config.smokeCount;
} else {
i.threeMesh.scale.setScalar(i.config.fireScale);
(i.threeMesh as any).count = i.config.fireCount;
i.threeMesh.position.y = i.config.fireOffestY;
}
i.config.isChange = false
}
//朝向
// let latDir = new Vector3().subVectors(bottomLeft, topLeft).normalize();
// let latDir = new Vector3(i.config.rotateX,i.config.rotateY,i.config.rotateZ) //.normalize();
// i.threeMesh.up.copy(latDir);
i.threeMesh.rotation.x = i.config.rotateX
i.threeMesh.rotation.y = i.config.rotateY
i.threeMesh.rotation.z = i.config.rotateZ
//控制显示隐藏
i.threeMesh.visible = i.visible
//设置实体方向
})
//克隆Cesium机投影位置
//Three.js对象将显示在铯球体上方的同一位置
this.threeCamera.matrixAutoUpdate = false;
const cvm = this.viewer.camera.viewMatrix;
const civm = this.viewer.camera.inverseViewMatrix;
this.threeCamera.lookAt(0, 0, 0);
// 设置threejs相机矩阵 4*4矩阵
this.threeCamera.matrixWorld.set(
civm[0], civm[4], civm[8], civm[12],
civm[1], civm[5], civm[9], civm[13],
civm[2], civm[6], civm[10], civm[14],
civm[3], civm[7], civm[11], civm[15]
)
//这是相机矩阵的逆。
this.threeCamera.matrixWorldInverse.set(
cvm[0], cvm[4], cvm[8], cvm[12],
cvm[1], cvm[5], cvm[9], cvm[13],
cvm[2], cvm[6], cvm[10], cvm[14],
cvm[3], cvm[7], cvm[11], cvm[15]
)
//设置宽高比例
let width = this.CesiumContainer.clientWidth
let height = this.CesiumContainer.clientHeight
//更新相机矩阵
this.threeCamera.aspect = width / height;
this.threeCamera.updateProjectionMatrix();
// this.threeRenderer.clearAsync();
this.threeRenderer.setSize(width, height);
this.threeRenderer.renderAsync(this.threeScene, this.threeCamera);
}
}
removeAll() {
this.objects3D.forEach(i => {
this.threeScene?.remove(i.threeMesh)
})
}
cartToVec(cart: Cartesian3) {
return new Vector3(cart.x, cart.y, cart.z);
};
loop() {
if (this.isDestory) return
requestAnimationFrame(this.loop.bind(this));
this.renderThree();
}
destory() {
this.isDestory = true
this.removeAll()
}
}
调用: tc = new TC(viewer!, { domId: 'CesiumViewer' }) tc.createFire(116.162941, 40.262713, 228, '123')
欢迎评论区交流沟通