cesium+three,火焰粒子效果,消防员喷水灭火。

老规矩:直接放效果图,符合就往下看,不符合出门右转。
在这里插入图片描述
在这里插入图片描述

文章内容

主要应用了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')

欢迎评论区交流沟通

<think>好的,我现在需要帮助用户解决如何在Cesium中实现林火蔓延模拟的问题。首先,我要回忆用户提到的引用内容,看看有没有相关的实现方法。用户提供了四个引用,其中引用1提到了使用元胞自动机算法模拟林火蔓延,结合气象、地形、植被等因素。引用3讨论了Cesium中的可视化方法,如GeoJSON数据展示,但用户提到其他方法有问题。引用4提到了粒子系统,用于模拟火、烟雾等效果。引用2则涉及在Cesium上叠加Three.js进行火焰粒子效果渲染,但存在遮挡问题。 用户的需求是使用Cesium来实现林火蔓延模拟,所以我需要结合这些引用中的信息,给出一个可行的方案。首先,林火蔓延的核心算法可能需要元胞自动机,如引用1所述。然后,如何在Cesium中可视化这个过程呢?引用3提到可以使用GeoJSON展示矢量数据,但用户可能需要动态效果,所以可能需要结合粒子系统(引用4)或者Three.js叠加(引用2)。 考虑到Cesium本身的粒子系统可能更适合动态火焰效果,但引用4指出粒子系统的参数需要调整方向、速度等属性,这可能比较复杂。另外,引用2中提到使用Three.jsCesium结合,但存在遮挡问题,可能影响视觉效果。因此,可能需要权衡使用Cesium原生粒子系统还是结合其他库。 接下来,我需要分步骤回答用户的问题。首先,算法部分采用元胞自动机模型,处理地形、气象等数据。然后,可视化部分,使用Cesium粒子系统模拟火焰,或者用GeoJSON动态更新火场边界。此外,三维效果可能需要调整粒子参数,如大小、颜色、生命周期,以模拟火势变化。同时,引用3提到的初始化地球步骤也是基础,需要确保地形服务正确加载。 用户可能需要具体代码示例,比如如何创建粒子系统,如何更新元胞状态。此外,性能优化也是关键,因为动态更新大量粒子或面数据可能导致卡顿。可能需要分块处理或LOD优化。 最后,生成相关问题时,要围绕用户可能进一步询问的点,比如如何集成气象数据、性能优化方法、粒子系统参数调整等。需要确保回答结构清晰,步骤明确,引用相关文献中的方法,如引用1的元胞自动机和引用4的粒子系统参数。</think>在Cesium中实现林火蔓延模拟需要结合地理空间数据可视化与动态效果控制,以下是分步实现方法: ### 一、基础环境搭建 1. **地形数据加载** 使用Cesium TerrainProvider加载高精度地形数据,确保地形起伏与真实场景匹配: ```javascript viewer.terrainProvider = Cesium.createWorldTerrain(); ``` 2. **植被图层叠加** 通过GeoJSON或CZML格式加载植被分布数据(引用[3]),使用PolygonGraphics定义不同可燃物类型区域: ```javascript viewer.entities.add({ polygon: { hierarchy: Cesium.Cartesian3.fromDegreesArray([...]), material: new Cesium.ColorMaterialProperty(Cesium.Color.GREEN.withAlpha(0.5)) } }); ``` ### 二、火势蔓延算法实现 1. **元胞自动机模型**(引用[1]) 基于王正非模型设计蔓延规则: ```javascript class FireSpread { constructor(cells) { this.grid = cells; // 元胞网格 } update(windSpeed, humidity) { // 根据风速、湿度计算蔓延概率 return this.grid.map(cell => this.calculateNextState(cell)); } } ``` 2. **动态参数绑定** 将气象数据与算法关联,通过Cesium Clock实现时间轴控制: ```javascript viewer.clock.onTick.addEventListener(clock => { const wind = getWindData(clock.currentTime); // 实时获取风速 fireModel.update(wind.speed, wind.humidity); }); ``` ### 三、可视化实现 1. **粒子系统模拟火焰**(引用[4]) 创建可调节方向的火焰粒子系统: ```javascript const fireEmitter = viewer.scene.primitives.add(new Cesium.ParticleSystem({ image: 'flame.png', emissionRate: 100, startScale: 0.5, endScale: 2.0, particleLife: 1.5, speed: 10.0, lifetime: 16.67 })); ``` 2. **动态边界渲染** 使用CallbackProperty实时更新火场边界: ```javascript entity.polygon.hierarchy = new Cesium.CallbackProperty(time => { return new Cesium.PolygonHierarchy(currentFireEdge); }, false); ``` ### 四、三维交互优化 1. **多层可视化融合** 将粒子系统(火焰)、多边形(火场)和地形图层叠加,通过depthTestAgainstTerrain: true确保地形遮挡关系正确(引用[2]) 2. **性能优化技巧** - 采用LOD技术分级渲染火场细节 - 使用Web Worker处理蔓延计算 - 通过Cesium3DTileset加载高密度植被模型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值