背景:
在Cesium中,加载模型只能用entity来加载,想对这个模型进行拆分查看内部接口,只能用ClippingPlane来笨笨的切割模型。想让他它能和threejs那样通过mesh来拆解模型是不行的,Cesium中的模型加载器没有mesh这个概念。在补充Webgl的相关知识时,给了我一个思路,Cesium的上下文 (gl) 肯定是和Webgl的gl是一个东西,因为Cesium源自于Webgl,那Threejs的gl也是和Webgl的gl一致的。此时我意识到Threejs和Cesium可以作用于一个gl,渲染还是交给Cesium来执行,Threejs有强大的插件库,他能帮我解析模型。
解决思路:
- 找出Cesium的上下文,这很好找,直接通过viewer就可以获取
viewer.scene.context._gl; - 通过Threejs的插件
GLTFLoader.js解析模型,模型不能用threejs加载,也不能用cesium加载,用最原始的webgl加载,但是写webgl需要太长的代码,所以引入另一个库,twgl.js,这个库是对webgl的简单抽象,使用它可以简化我们的代码长度。插一句(在webgl中是没有相机、场景、渲染器等等这些概念的,这些概念是threejs之类的高级框架添加的。它只有投影矩阵、缓冲区、纹理等概念)。总之我们通过Threejs的插件GLTFLoader.js解析模型,这让我们少写非常多代码。 - 把读取到的模型的geometry和material解析出来,存储到一个对象中。
- 把这个对象利用twgl绘制到Cesium中的gl上。
- 在Cesium的
viewer.scene.postRender.addEventListener中运行一个twgl绘制模型的回调函数,每次都会执行这个回调函数Cesium就带着这个已经被twgl绘制过的gl绘制到最终的canvas中被用户看见。
开始实施:
我把主要代码展示一下,完整代码放到github了
git地址是github.com/yuwoniu03/c…
- 先用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);
}
}
- 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);
}
}
}
- 看射线和模型的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,
});
}
最终实现的效果是这样的