场景
作为刚刚接触 Three.js 的小白,在工作中遇到下面的需求:
- 加载一个 3D 模型
- 通过代码切换预设的任意模型的视角
最终效果(在线示例):
👆基于官方示例增加的控制代码
我们通过官方示例可以知道,只要使用 OrbitControls 就可以通过鼠标调整模型的视角。可是,能不能通过代码,切换特定的视角呢?有没有官方的 API 可以实现这个交互呢?小白暂时未能找到拿来即用的示例代码。
基本原理
通过 AI 可以得知相关矩阵变化的公式原理:
通过 AI,我们还能得到上面原理的核心算法代码:
// Function to multiply matrix and point
function multiplyMatrixAndPoint(matrix, point) {
let result = []
for (let i = 0; i < matrix.length; i++) {
result[i] =
matrix[i][0] * point[0] +
matrix[i][1] * point[1] +
matrix[i][2] * point[2]
}
return result
}
function rotatePoint(x, y, z, rotateZ, rotateY) {
// Convert degrees to radians
const radZ = (rotateZ * Math.PI) / 180
const radY = (rotateY * Math.PI) / 180
// Rotation matrix around Z-axis
const Rz = [
[Math.cos(radZ), -Math.sin(radZ), 0],
[Math.sin(radZ), Math.cos(radZ), 0],
[0, 0, 1],
]
// Rotation matrix around Y-axis
const Ry = [
[Math.cos(radY), 0, Math.sin(radY)],
[0, 1, 0],
[-Math.sin(radY), 0, Math.cos(radY)],
]
// Apply rotation matrices
let pointAfterZ = this.multiplyMatrixAndPoint(Rz, [x, y, z])
let finalPoint = this.multiplyMatrixAndPoint(Ry, pointAfterZ)
return finalPoint
}
输入:起始点 x, y, z;绕着 Z 轴旋转的角度;绕着 Y 轴旋转的角度;
输出:旋转后的新坐标 [x,y,z];
动画
基于上面坐标的计算,还要实现新旧坐标变化的过渡动画,这里可以用浏览器 API 的 requestAnimationFrame 实现:
/**
* 淡入淡出
* https://www.cnblogs.com/cloudgamer/archive/2009/01/06/Tween.html
* @param t 当前时间
* @param b 初始值
* @param c 增/减量
* @param d 持续时间
*/
cubicEaseInOut(t, b, c, d) {
if ((t /= d / 2) < 1) return (c / 2) * t * t * t + b
return (c / 2) * ((t -= 2) * t * t + 2) + b
}
rotateAnimate(fromZ, toZ, fromY, toY, fromDistance, toDistance, duration) {
return new Promise((resolve) => {
let time = 0
cancelAnimationFrame(this.timer)
const fn = () => {
let degZ = this.cubicEaseInOut(time, fromZ, toZ - fromZ, duration)
let degY = this.cubicEaseInOut(time, fromY, toY - fromY, duration)
let distance =
this.cubicEaseInOut(
time,
fromDistance * 1000,
toDistance * 1000 - fromDistance * 1000,
duration,
) / 1000
if (time < duration) {
time += 10
}
if (time >= duration) {
degZ = toZ
degY = toY
}
if (camera) {
// 旋转后
const posRotated = this.rotatePoint(
this.cameraPos.x,
this.cameraPos.y,
this.cameraPos.z,
degZ,
degY,
)
// 缩放后
const posZoomed = this.zoom(
posRotated[0],
posRotated[1],
posRotated[2],
distance,
)
// 偏移后
this.camera.position.x = posZoomed[0]
this.camera.position.y = posZoomed[1]
this.camera.position.z = posZoomed[2]
camera.lookAt(scene.position)
}
if (time < duration) {
this.timer = requestAnimationFrame(fn)
} else {
cancelAnimationFrame(this.timer)
resolve(true)
}
}
this.timer = requestAnimationFrame(fn)
})
}
以 Z 轴旋转为例,主要逻辑就是 fromZ, toZ,代表从原来的角度 fromZ 渐变成新的角度 toZ,通过缓动函数 cubicEaseInOut,得出每一帧动画需要变化的角度,该角度通过上面的矩阵变换旋转,就可以得出该帧的新坐标。
把这些逻辑包装一下:
// 切换镜头视角工具
class ViewAnimate {
scene
camera
timer = 0
lastRotateZ = 0
lastRotateY = 0
lastDistance = 1
cameraPos = {
x: 0,
y: 0,
z: 0,
}
radius = 0
constructor(scene, camera, cameraPos) {
this.scene = scene
this.camera = camera
this.cameraPos = cameraPos
this.radius = Math.abs(cameraPos.x)
}
/**
* 淡入淡出
* https://www.cnblogs.com/cloudgamer/archive/2009/01/06/Tween.html
* @param t 当前时间
* @param b 初始值
* @param c 增/减量
* @param d 持续时间
*/
cubicEaseInOut(t, b, c, d) {
if ((t /= d / 2) < 1) return (c / 2) * t * t * t + b
return (c / 2) * ((t -= 2) * t * t + 2) + b
}
// Function to multiply matrix and point
multiplyMatrixAndPoint(matrix, point) {
let result = []
for (let i = 0; i < matrix.length; i++) {
result[i] =
matrix[i][0] * point[0] +
matrix[i][1] * point[1] +
matrix[i][2] * point[2]
}
return result
}
rotatePoint(x, y, z, rotateZ, rotateY) {
// Convert degrees to radians
const radZ = (rotateZ * Math.PI) / 180
const radY = (rotateY * Math.PI) / 180
// Rotation matrix around Z-axis
const Rz = [
[Math.cos(radZ), -Math.sin(radZ), 0],
[Math.sin(radZ), Math.cos(radZ), 0],
[0, 0, 1],
]
// Rotation matrix around Y-axis
const Ry = [
[Math.cos(radY), 0, Math.sin(radY)],
[0, 1, 0],
[-Math.sin(radY), 0, Math.cos(radY)],
]
// Apply rotation matrices
let pointAfterZ = this.multiplyMatrixAndPoint(Rz, [x, y, z])
let finalPoint = this.multiplyMatrixAndPoint(Ry, pointAfterZ)
return finalPoint
}
zoom(x, y, z, distance) {
const m = [
[distance, 0, 0],
[0, distance, 0],
[0, 0, distance],
]
const pointAfterZ = this.multiplyMatrixAndPoint(m, [x, y, z])
return pointAfterZ
}
rotateAnimate(fromZ, toZ, fromY, toY, fromDistance, toDistance, duration) {
return new Promise((resolve) => {
let time = 0
cancelAnimationFrame(this.timer)
const fn = () => {
let degZ = this.cubicEaseInOut(time, fromZ, toZ - fromZ, duration)
let degY = this.cubicEaseInOut(time, fromY, toY - fromY, duration)
let distance =
this.cubicEaseInOut(
time,
fromDistance * 1000,
toDistance * 1000 - fromDistance * 1000,
duration,
) / 1000
if (time < duration) {
time += 10
}
if (time >= duration) {
degZ = toZ
degY = toY
}
if (camera) {
// 旋转后
const posRotated = this.rotatePoint(
this.cameraPos.x,
this.cameraPos.y,
this.cameraPos.z,
degZ,
degY,
)
// 缩放后
const posZoomed = this.zoom(
posRotated[0],
posRotated[1],
posRotated[2],
distance,
)
// 偏移后
this.camera.position.x = posZoomed[0]
this.camera.position.y = posZoomed[1]
this.camera.position.z = posZoomed[2]
camera.lookAt(scene.position)
}
if (time < duration) {
this.timer = requestAnimationFrame(fn)
} else {
cancelAnimationFrame(this.timer)
resolve(true)
}
}
this.timer = requestAnimationFrame(fn)
})
}
async view(config) {
if (camera) {
await this.rotateAnimate(
this.lastRotateZ,
config.rotateZ,
this.lastRotateY,
config.rotateY,
this.lastDistance,
config.distance,
config.duration,
)
this.lastRotateZ = config.rotateZ
this.lastRotateY = config.rotateY
this.lastDistance = config.distance
}
}
}
使用的时候:
const viewAnimate = new ViewAnimate(scene, camera, cameraPosition)
// 视角切换序列,可以自行添加更多
viewAnimate
.view({
rotateZ: 90,
rotateY: 180,
distance: 0.5,
duration: 1000,
})
.then(() =>
viewAnimate.view({
rotateZ: 0,
rotateY: 0,
distance: 1,
duration: 1000,
}),
)
.then(() =>
viewAnimate.view({
rotateZ: 30,
rotateY: -120,
distance: 2,
duration: 1000,
}),
)
.then(() =>
viewAnimate.view({
rotateZ: 0,
rotateY: 0,
distance: 1,
duration: 1000,
}),
)
如果内容对您有一点点帮助,请给我一个赞吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。