起因:最近想做模型的动画,结果上网查资料,看到网上好多对于模型控制的文章都有限制。决定还是自己研究下。欢迎大家一起探讨,评论留言。
效果
火箭
全部代码在最后
起步
-
模型控制,第一步当然是需要一个合适的模型,去cesium官网实例中找到了一个合适的模型,并且还顺带了一些模型操作方法。
-
搜索关键字applyArticulations;模型地址;
-
拿到模型迫不及待的想在自己自己的项目中加载出来
-
加载方式有两种entity和Primitive, 我个人更喜欢第二种Primitive方式。
-
对应的参数就不做解释了,文档中都有
let rocketPrimitive: Cesium.Model
let position = Cesium.Cartesian3.fromDegrees(104.200403, 30.396231, 600.0);
const hpRoll = new Cesium.HeadingPitchRoll();
const fixedFrameTransform = Cesium.Transforms.localFrameToFixedFrameGenerator("north", "west");
const rocketPrimitive = viewer.scene.primitives.add(
Cesium.Model.fromGltf({
url: "https://assets.agi.com/models/launchvehicle.glb",
modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
position,
hpRoll,
Cesium.Ellipsoid.WGS84,
fixedFrameTransform
),
minimumPixelSize: 128,
})
);
模型的组成
const articulations = model.sceneGraph._runtimeArticulations;官方的这段代码我用着报错,文档里面没有。也不知道啥问题
- 直接将加载的模型打印出来看里面具体的结构
发现了nodes,模型里面的关节点,就是模型由哪些部分组成,官网文档中也明确的说了。画红线的那段话翻译出来就是"返回glTF中具有给定名称的节点。这用于修改用户定义动画的节点变换"
模型操作
- 点火
rocketPrimitive.setArticulationStage( //对应属性改变参数值
'SRBFlames Size',
1
);
rocketPrimitive.applyArticulations(); //使得修改的属性生效
- 火箭点火后自然就要移动,添加模型的平滑移动,模型的平滑移动我之前的文章有。
- 原理就是不停的移动模型位置,以及模型的姿态,只要移动的距离足够小看起来就是平滑的。
Cesium.Transforms.headingPitchRollToFixedFrame(
showPath[activeIndex], //当前坐标点Cesium.Cartesian3
hpRoll,//姿态
Cesium.Ellipsoid.WGS84,
fixedFrameTransform,
rocketPrimitive.modelMatrix //模型当前的世界矩阵
);
- 利用viewer.scene.preUpdate.addEventListener //下一帧渲染前回调
viewer.scene.preUpdate.addEventListener(keepRun)
- keepRun 就是我们移动的函数
const keepRun = (scene: Cesium.Scene, time: number) => {
if (activeIndex >= maxIndex) return
if (autoDirection && activeIndex > 0 && !showPath[activeIndex - 1].equals(showPath[activeIndex])) { //判断前后两个点是否一样,不一样就调整姿态
const heading = Helper.getHeading(
showPath[activeIndex - 1],
showPath[activeIndex],
);
if (heading) hpRoll.heading = heading
const pitch = Helper.getPitch(
showPath[activeIndex - 1],
showPath[activeIndex])
if (pitch) hpRoll.pitch = pitch
}
Cesium.Transforms.headingPitchRollToFixedFrame(
showPath[activeIndex],
hpRoll,
Cesium.Ellipsoid.WGS84,
fixedFrameTransform,
rocketPrimitive.modelMatrix
);
activeIndex += 1
}
-
可以平滑的移动了。
-
当然火焰的生起也应当平滑,控制模型都需要平滑的操作,直接写个函数控制
function modelAnimationController(controller: typeModelAnimationController) {
const { type, initVal, maxVal, fn, step, minVal } = controller
let num = initVal
let stopFrame: number
const max = maxVal || 1
const min = minVal || -99999
const duration = step || 0.1
const render = () => {
num += duration
rocketPrimitive.setArticulationStage(
type,
num
);
rocketPrimitive.applyArticulations();
stopFrame = requestAnimationFrame(render)
if (num > max || num <= min) {
window.cancelAnimationFrame(stopFrame)
fn && fn()
}
}
render()
}
modelAnimationController({
type: 'SRBFlames Size', initVal: 0, maxVal: 1, step: 0.05, fn: () => {
viewer.scene.preUpdate.addEventListener(keepRun)
}
})
剩下的都是重复的操作,以及反复调试修改达到最佳
- 比如火焰喷射要有真实感
- 火箭转向时 喷射头偏转,等等
<template>
<div class="btn-box">
</div>
<Map @onViewerLoaded="onViewerLoaded" :options="options">
</Map>>
</template>
<script lang="ts" setup>
import Map from "@/components/Cesium/lib/Map.vue";
import * as Cesium from "cesium";
import { GetPosition } from "@/components/Cesium/utils";
import { initLayerPromise } from '@/components/Cesium/utils/initLayer'
import { Helper } from "@/components/Cesium/lib/helper";
let viewer: Cesium.Viewer
const options = {}
let handler: Cesium.ScreenSpaceEventHandler
const onViewerLoaded = (Viewer: Cesium.Viewer) => {
viewer = Viewer
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
initLayerPromise(Viewer, true).then(() => {
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(104.200403,
30.396231, 2000),
// destination: Cesium.Cartesian3.fromDegrees(120.38105869, 31.10115627, 3000),
complete: () => { init() }
});
// init()
})
const getP = new GetPosition(Viewer);
getP.getPositionByClick((position: any) => {
console.log(position);
});
};
let planePrimitive: Cesium.Model
let position = Cesium.Cartesian3.fromDegrees(104.200403, 30.396231, 600.0);
const hpRoll = new Cesium.HeadingPitchRoll();
const fixedFrameTransform = Cesium.Transforms.localFrameToFixedFrameGenerator("north", "west");
let activeIndex = 0 //插值经纬度索引
let maxIndex = 0// 最大插值经纬度数组索引
let autoDirection = true; //自动调整方向
let path: [number, number, number][] = [] //存在路线数组
let showPath: Cesium.Cartesian3[] = [] //插值数组
let camera: Cesium.Camera
let controller: Cesium.ScreenSpaceCameraController
let r: number
const hpRange = new Cesium.HeadingPitchRange();
let nodes: any[] = []
const init = () => {
hpRoll.pitch = 90 * Math.PI / 180;
planePrimitive = viewer.scene.primitives.add(
Cesium.Model.fromGltf({
url: "models/launchvehicle.glb",
modelMatrix: Cesium.Transforms.headingPitchRollToFixedFrame(
position,
hpRoll,
Cesium.Ellipsoid.WGS84,
fixedFrameTransform
),
minimumPixelSize: 128,
})
);
const scene = viewer.scene;
planePrimitive.readyPromise.then((model) => {
camera = viewer.camera;
controller = scene.screenSpaceCameraController;
r =
2.0 * Math.max(model.boundingSphere.radius, camera.frustum.near);
controller.minimumZoomDistance = r * 0.2;
/**
modelAnimationController({
type: 'SRBFlames Size', initVal: 0, maxVal: 1, step: 0.05, fn: () => {
modelAnimationController({ type: 'SRBFlames Size', initVal: 1, minVal: 0, step: -0.5 })
modelAnimationController({
type: 'SRBs Separate', initVal: 0, maxVal: 10, step: 0.5, fn: () => {//一级脱落
modelAnimationController({ type: 'SRBs Drop', initVal: 0, minVal: -50, step: -0.5 })
}
})
// modelAnimationController({ type: 'BoosterEngines Yaw', initVal: 0, maxVal: 1, step: 0.1 }) //左右
// modelAnimationController({ type: 'BoosterEngines Pitch', initVal: 0, maxVal: 1, step: 0.1 }) //上下
modelAnimationController({
type: 'BoosterFlames Size', initVal: 0, maxVal: 1, step: 0.1, fn: () => {
modelAnimationController({ type: 'Fairing Open', initVal: 0, maxVal: 45, step: 0.5 })
modelAnimationController({ type: 'Fairing Separate', initVal: 0, minVal: -10, step: -0.1 })
modelAnimationController({ type: 'Fairing Drop', initVal: 0, minVal: -50, step: -0.5, fn: ()=> {
//主推进器脱落
modelAnimationController({ type: 'Booster MoveZ', initVal: 0, minVal: -50, step: -0.5})
modelAnimationController({ type: 'UpperStageFlames Size', initVal: 0, maxVal: 1, step: 0.05, fn:()=> {
modelAnimationController({ type: 'InterstageAdapter MoveZ', initVal: 0, minVal: -50, step: -0.5})
}})
// modelAnimationController({ type: 'UpperStageEngines Yaw', initVal: 0, maxVal: 1, step: 0.05,})//左右
// modelAnimationController({ type: 'UpperStageEngines Pitch', initVal: 0, maxVal: 1, step: 0.05,}) //上下
} })
}
})
}
})
*/
lookAt()
pickup()
nodes = planePrimitive.gltf.nodes
// nodes.forEach(i => {
// if (new RegExp(/InterstageAdapter/).test(i.name)) {
// planePrimitive.getNode(i.name).show = false
// }
// })
crateLine().then(() => {
modelAnimationController({
type: 'SRBFlames Size', initVal: 0, maxVal: 1, step: 0.05, fn: () => {
viewer.scene.preUpdate.addEventListener(keepRun)
}
})
})
})
}
function pickup() {
handler.setInputAction(function (movement) {
const pickedObject = viewer.scene.pick(movement.position);
if (Cesium.defined(pickedObject)) {
console.log(pickedObject)
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);
}
const lookAt = () => {
const center = Cesium.Matrix4.multiplyByPoint(
planePrimitive.modelMatrix,
Cesium.Cartesian3.ZERO,
new Cesium.Cartesian3()
);
const heading = Cesium.Math.toRadians(10.0);
const pitch = Cesium.Math.toRadians(-5.0);
camera.lookAt(
center,
new Cesium.HeadingPitchRange(heading, pitch, r * 2)
);
}
type typeModelAnimationController = {
type: string;
initVal: number;
maxVal?: number;
minVal?: number;
fn?: Function;
step?: number,
}
function modelAnimationController(controller: typeModelAnimationController) {
const { type, initVal, maxVal, fn, step, minVal } = controller
let num = initVal
let stopFrame: number
const max = maxVal || 1
const min = minVal || -99999
const duration = step || 0.1
const render = () => {
num += duration
planePrimitive.setArticulationStage(
type,
num
);
planePrimitive.applyArticulations();
stopFrame = requestAnimationFrame(render)
if (num > max || num <= min) {
window.cancelAnimationFrame(stopFrame)
fn && fn()
}
}
render()
}
const crateLine = () => {
const lon = 104.200403, lat = 30.396231, alt = 20600
for (let index = 1; index < 20; index++) {
path.push([lon, lat, 600 + 1000 * index])
}
const len = 1000
let lastLat = 0, lastLon = 0, lastAlt = 0, activeLon, activeLat, activeAlt
for (let index = 0; index < len; index++) {
activeLon = Number((lon + index * 0.01).toFixed(6))
activeLat = Number((lat + index * 0.02).toFixed(6))
activeAlt = alt + index * 1000
path.push([activeLon, activeLat, activeAlt])
if (index === len - 1) {
lastLon = activeLon
lastLat = activeLat
lastAlt = activeAlt
}
}
for (let i = 0; i <= 360; i += 1) {
path.push([lastLon + i, lastLat, lastAlt])
}
return new Promise(resolve => {
getPosition().then(res => {
showPath = res
maxIndex = res.length
const line = viewer.scene.primitives.add(
new Cesium.Primitive({
geometryInstances: new Cesium.GeometryInstance({
geometry: new Cesium.PolylineGeometry({
positions: res,
width: 3.0,
vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT,
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.BLUE.withAlpha(.7)),
},
}),
appearance: new Cesium.PolylineColorAppearance(),
})
);
line.readyPromise.then(() => {
resolve('')
})
})
})
}
const keepRun = (scene: Cesium.Scene, time: number) => {
if (activeIndex >= maxIndex) return
console.log(activeIndex)
if (activeIndex === 1000) {
modelAnimationController({ type: 'SRBFlames Size', initVal: 1, minVal: 0, step: -0.5 })
modelAnimationController({
type: 'SRBs Separate', initVal: 0, maxVal: 10, step: 0.5, fn: () => {//一级脱落
modelAnimationController({
type: 'SRBs Drop', initVal: 0, minVal: -100, step: -1, fn: () => nodes.forEach(i => {
if (new RegExp(/SRB\d/).test(i.name)) {
planePrimitive.getNode(i.name).show = false
}
})
})
}
})
modelAnimationController({ type: 'BoosterFlames Size', initVal: 0, maxVal: 1, step: 0.1 }) //主推期开始点火
}
if (activeIndex === 3000) {
modelAnimationController({ type: 'Fairing Open', initVal: 0, maxVal: 45, step: 0.5 })
modelAnimationController({ type: 'Fairing Separate', initVal: 0, minVal: -10, step: -0.1 })
modelAnimationController({
type: 'Fairing Drop', initVal: 0, minVal: -150, step: -1, fn: () => {
//主推进器脱落
modelAnimationController({ type: 'BoosterFlames Size', initVal: 1, minVal: 0, step: -0.05 })
modelAnimationController({
type: 'Booster MoveZ', initVal: 0, minVal: -150, step: -1, fn: () => {
nodes.forEach(i => {
if (new RegExp(/Fairing\d/).test(i.name) || new RegExp(/Booster/).test(i.name)) {
planePrimitive.getNode(i.name).show = false
}
})
}
})
modelAnimationController({ type: 'UpperStageFlames Size', initVal: 0, maxVal: 1, step: 0.05 })
}
})
}
if (activeIndex === 3600) {
modelAnimationController({
type: 'InterstageAdapter MoveZ', initVal: 0, minVal: -150, step: -1, fn: () => {
nodes.forEach(i => {
if (new RegExp(/InterstageAdapter/).test(i.name)) {
planePrimitive.getNode(i.name).show = false
}
})
}
})
}
lookAt()
if (autoDirection && activeIndex > 0 && !showPath[activeIndex - 1].equals(showPath[activeIndex])) {
const heading = Helper.getHeading(
showPath[activeIndex - 1],
showPath[activeIndex],
);
if (heading) hpRoll.heading = heading
const pitch = Helper.getPitch(
showPath[activeIndex - 1],
showPath[activeIndex])
if (pitch) hpRoll.pitch = pitch
}
Cesium.Transforms.headingPitchRollToFixedFrame(
showPath[activeIndex],
hpRoll,
Cesium.Ellipsoid.WGS84,
fixedFrameTransform,
planePrimitive.modelMatrix
);
activeIndex += 1
}
const getPosition = () => {
//插值 new Cesium.LinearSpline new Cesium.CatmullRomSpline esium.HermiteSpline.createNaturalCubic
//let pos = Cesium.Cartesian3.lerp(startP, endP, i / duration, new Cesium.Cartesian3());
return new Promise((resolve: (value: Cesium.Cartesian3[]) => void) => {
const points = path.map(i => Cesium.Cartesian3.fromDegrees(...i))
let times: number[] = []
for (let index = 0; index < points.length; index++) {
times.push(index)
}
const spline = new Cesium.CatmullRomSpline({
points,
times,
});
const positions: Cesium.Cartesian3[] = [];
for (let i = 1; i < times.length; i++) {
for (let j = 0; j < 100; j++) {
const cartesian3 = spline.evaluate(i - 1 + j * 0.01);
positions.push(cartesian3);
}
}
resolve(positions)
})
}
</script>
<style lang="less" scoped>
.btn-box {
position: absolute;
top: 10px;
z-index: 10;
width: 500px;
margin-left: 20px;
}
</style>