Cesium实现模型拆解-从底层拓展强行让它和Threejs一样拆解模型

0 阅读4分钟

背景:

在Cesium中,加载模型只能用entity来加载,想对这个模型进行拆分查看内部接口,只能用ClippingPlane来笨笨的切割模型。想让他它能和threejs那样通过mesh来拆解模型是不行的,Cesium中的模型加载器没有mesh这个概念。在补充Webgl的相关知识时,给了我一个思路,Cesium的上下文 (gl) 肯定是和Webgl的gl是一个东西,因为Cesium源自于Webgl,那Threejs的gl也是和Webgl的gl一致的。此时我意识到Threejs和Cesium可以作用于一个gl,渲染还是交给Cesium来执行,Threejs有强大的插件库,他能帮我解析模型。

解决思路:

  1. 找出Cesium的上下文,这很好找,直接通过viewer就可以获取viewer.scene.context._gl
  2. 通过Threejs的插件 GLTFLoader.js解析模型,模型不能用threejs加载,也不能用cesium加载,用最原始的webgl加载,但是写webgl需要太长的代码,所以引入另一个库,twgl.js,这个库是对webgl的简单抽象,使用它可以简化我们的代码长度。插一句(在webgl中是没有相机、场景、渲染器等等这些概念的,这些概念是threejs之类的高级框架添加的。它只有投影矩阵、缓冲区、纹理等概念)。总之我们通过Threejs的插件 GLTFLoader.js解析模型,这让我们少写非常多代码。
  3. 把读取到的模型的geometry和material解析出来,存储到一个对象中。
  4. 把这个对象利用twgl绘制到Cesium中的gl上。
  5. 在Cesium的viewer.scene.postRender.addEventListener中运行一个twgl绘制模型的回调函数,每次都会执行这个回调函数Cesium就带着这个已经被twgl绘制过的gl绘制到最终的canvas中被用户看见。

开始实施:

我把主要代码展示一下,完整代码放到github了

git地址是github.com/yuwoniu03/c…

  1. 先用GLTFLoader.js把模型解析出来,然后threejs的作用就没了(或者说他们来就没用,只是用到了他的插件)
loadGlbWithTwgl() {
        const { modelUrl } = this.options;
        try {
            const gltfLoader = new GLTFLoader();
            const dracoDecoderPath =
                this.options?.dracoDecoderPath ??
                '/model/draco/';
            const dracoLoader = new DRACOLoader();
            dracoLoader.setDecoderPath(dracoDecoderPath);
            dracoLoader.setDecoderConfig({ type: 'wasm' });
            dracoLoader.preload();
            gltfLoader.setDRACOLoader(dracoLoader);
            gltfLoader.load(modelUrl, (gltf) => {
                const modelData = gltf.scene;
                modelData.updateMatrixWorld(true);
                this.sceneGraph = this.buildSceneGraph(modelData);
                this.twglSceneGraph = this.prepareTwglResources(this.sceneGraph);
                this.setupTwglRendering();
                if (typeof this.options?.onLoaded === 'function') {
                    this.options.onLoaded({
                        gltf,
                        modelData,
                        instance: this,
                    });
                }
            }, undefined, (error) => {
                console.error('GLB 加载出错:', error);
            });
        } catch (error) {
            console.error('GLB 加载出错:', error);
        }
    }
    
  1. twgl登场
render(frameState) {
        if (!this.programInfo || !this.drawables.length || typeof Cesium === 'undefined') {
            return;
        }
        const gl = this.gl;
        const camera = frameState.camera;
        const viewMatrix = camera.viewMatrix;
        const projectionMatrix = camera.frustum.projectionMatrix;
        // ---- VP 缓存:逐帧按值比对相机矩阵(Cesium 会原地改写矩阵,不能用引用比较)----
        // camera.changed 有百分比阈值,微移不触发会导致模型与地球错帧抖动。
        // camera.viewMatrix/projectionMatrix 是稳定引用但内容会被原地改写,
        // 故用 Matrix4.equals 比内容,与 Cesium 内部脏判定方式一致。
        const viewChanged = !this._lastViewMatrix
            || !Cesium.Matrix4.equals(this._lastViewMatrix, viewMatrix);
        const projChanged = !this._lastProjectionMatrix
            || !Cesium.Matrix4.equals(this._lastProjectionMatrix, projectionMatrix);
        const vpChanged = viewChanged || projChanged || !this._cachedVP;
        let vpMatrix;
        if (vpChanged) {
            vpMatrix = Cesium.Matrix4.multiply(
                projectionMatrix,
                viewMatrix,
                this._cachedVP || (this._cachedVP = new Cesium.Matrix4())
            );
            // 用 clone 存上一帧的值,因为 viewMatrix/projectionMatrix 内容会被原地改写
            this._lastViewMatrix = Cesium.Matrix4.clone(viewMatrix, this._lastViewMatrix);
            this._lastProjectionMatrix = Cesium.Matrix4.clone(projectionMatrix, this._lastProjectionMatrix);
            // VP 变了 → 所有 drawable 的 MVP 都得重算
            this.invalidateMvpCache();
        } else {
            vpMatrix = this._cachedVP;
        }
        gl.useProgram(this.programInfo.program);
        gl.enable(gl.DEPTH_TEST);
        gl.depthFunc(gl.LEQUAL);
        gl.depthMask(true);
        gl.disable(gl.BLEND);
        gl.disable(gl.SCISSOR_TEST);
        gl.enable(gl.CULL_FACE);
        gl.cullFace(gl.BACK);
        gl.frontFace(gl.CCW);
        const rootMatrix = this.modelMatrix || Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
        const axisMatrix = this.yUpToZUpMatrix || Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
        // rootWithAxis 不依赖相机/offset,常量,复用 scratch
        const rootWithAxis = this._scratch?.rootWithAxis || new Cesium.Matrix4();
        const scratchModelMatrix = new Cesium.Matrix4();
        const scratchMvpMatrix = new Cesium.Matrix4();
        const scratchMeshMatrix = this._scratch?.meshMatrix || new Cesium.Matrix4();
        const scratchTranslationMatrix = this._scratch?.translationMatrix || new Cesium.Matrix4();
        const scratchOffsetTranslation = this._scratch?.offsetTranslation || new Cesium.Cartesian3();
        let rootWithAxisValid = this._scratch?.rootWithAxisValid === true;
        if (!rootWithAxisValid) {
            Cesium.Matrix4.multiply(rootMatrix, axisMatrix, rootWithAxis);
            if (this._scratch) this._scratch.rootWithAxisValid = true;
        }
        for (let i = 0; i < this.drawables.length; i++) {
            const drawable = this.drawables[i];
            const node = drawable.node;
            const offset = node.offset || { x: 0, y: 0, z: 0 };
            // ---- MVP 缓存:offset 没变 + VP 没变(drawable.cachedMvpValid 仍为 true)→ 直接复用 
            const offsetKey = `${offset.x},${offset.y},${offset.z}`;
            const offsetChanged = offsetKey !== drawable.lastOffsetKey;
            let mvpArray = drawable.cachedMvp;
            if (offsetChanged || !drawable.cachedMvpValid) {
                const meshMatrix = Cesium.Matrix4.fromArray(node.matrixWorld, 0, scratchMeshMatrix);
                if (offset.x !== 0 || offset.y !== 0 || offset.z !== 0) {
                    Cesium.Cartesian3.fromElements(offset.x, offset.y, offset.z, scratchOffsetTranslation);
                    Cesium.Matrix4.fromTranslation(scratchOffsetTranslation, scratchTranslationMatrix);
                    Cesium.Matrix4.multiply(scratchTranslationMatrix, meshMatrix, scratchMeshMatrix);
                } else {
                    // offset 为 0 时 meshMatrix 即最终局部矩阵,避免一次乘法
                    Cesium.Matrix4.clone(meshMatrix, scratchMeshMatrix);
                }
                Cesium.Matrix4.multiply(rootWithAxis, scratchMeshMatrix, scratchModelMatrix);
                Cesium.Matrix4.multiply(vpMatrix, scratchModelMatrix, scratchMvpMatrix);
                Cesium.Matrix4.pack(scratchMvpMatrix, mvpArray, 0);
                drawable.lastOffsetKey = offsetKey;
                drawable.cachedMvpValid = true;
            }
            gl.bindVertexArray(drawable.vao);
            twgl.setUniforms(this.programInfo, {
                u_mvpMatrix: mvpArray,
                u_color: node.materialColor || this.meshColor,
                u_useTexture: Boolean(drawable.texture && drawable.hasTexcoord),
                u_diffuseMap: drawable.texture || this.defaultWhiteTexture,
            });
            const drawInfo = drawable.drawInfo;
            if (drawInfo) {
                const mode = drawInfo.mode || gl.TRIANGLES;
                twgl.drawBufferInfo(gl, drawable.bufferInfo, mode, drawInfo.count, drawInfo.start);
            } else {
                twgl.drawBufferInfo(gl, drawable.bufferInfo);
            }
        }
    }
  1. 看射线和模型的mesh的AABB是否相交
pickMeshAtScreenPosition(windowPosition) {
        if (!this.viewer?.camera || typeof Cesium === 'undefined') {
            return null;
        }
        const ray = this.viewer.camera.getPickRay(windowPosition);
        if (!ray) {
            return null;
        }
        const list = this.getMeshList();
        if (!list.length) {
            return null;
        }
        const rootMatrix = this.modelMatrix || Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
        const axisMatrix = this.yUpToZUpMatrix || Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY);
        const rootWithAxis = new Cesium.Matrix4();
        Cesium.Matrix4.multiply(rootMatrix, axisMatrix, rootWithAxis);
        const scratch = {
            mesh: new Cesium.Matrix4(),
            translation: new Cesium.Matrix4(),
            worldMatrix: new Cesium.Matrix4(),
            invWorld: new Cesium.Matrix4(),
            offset: new Cesium.Cartesian3(),
            localOrigin: new Cesium.Cartesian3(),
            localDir: new Cesium.Cartesian3(),
        };
        let nearest = null;
        let nearestT = Infinity;
        for (let i = 0; i < list.length; i++) {
            const item = list[i];
            const meshes = item.meshes || [];
            for (let j = 0; j < meshes.length; j++) {
                const t = this.intersectRayMeshAabb(ray, meshes[j], rootWithAxis, scratch);
                if (t !== null && t < nearestT) {
                    nearestT = t;
                    nearest = item;
                }
            }
        }
        return nearest;
    }

5. 拆解模型,我这里用的是动画库gsap

disassembleMesh(partNode, options = {}) {
        if (!partNode) {
            return null;
        }
        const meshes = this.collectMeshNodes(partNode, []);
        if (!meshes.length) {
            return null;
        }
        if (!partNode.offset) {
            partNode.offset = { x: 0, y: 0, z: 0 };
        }
        const duration = options.duration ?? 2;
        const ease = options.ease ?? 'power1.inOut';
        const axis = options.axis ?? 'x';
        const distance = options.distance ?? 80;
        const restore = options.restore ?? (Math.abs(partNode.offset[axis] || 0) > 0.001);
        const target = restore ? 0 : (partNode.offset[axis] || 0) + distance;
        const onUpdate = () => {
            // 父节点 offset 作为状态源,同步到下属可画 mesh
            for (let i = 0; i < meshes.length; i++) {
                const mesh = meshes[i];
                if (!mesh.offset) {
                    mesh.offset = { x: 0, y: 0, z: 0 };
                }
                mesh.offset.x = partNode.offset.x;
                mesh.offset.y = partNode.offset.y;
                mesh.offset.z = partNode.offset.z;
            }
            this.invalidateMvpCache();
            this.viewer?.scene?.requestRender?.();
        };
        return gsap.to(partNode.offset, {
            [axis]: target,
            duration,
            ease,
            onUpdate,
        });
    }

最终实现的效果是这样的

model拆解.gif