持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情
写在前面
本文用vue+threejs控制相机跟随物体。
有时候,我们想要展示场景中某个物体的视野,这时我们就需要用相机跟随物体。
以下是演示gif,用相机跟随移动的机器人,展示其移动的视野:
完整代码说明
- html和css
html中包含一个id容器用于插入three.js的渲染器dom节点,一个按钮,用来控制是否跟随
<template>
<div class="item">
<div id="THREE53"></div>
<div class="btn_box">
<el-button @click="followRobot">{{
isFollow ? "取消跟随" : "跟随机器人"
}}</el-button>
</div>
</div>
</template>
<style lang="less" scoped>
.btn_box {
position: absolute;
top: 10px;
left: 210px;
}
</style>
- 引入threejs和需要的模块
OrbitControls轨道控制器,GLTFLoader模型加载器
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
- data()中的变量
camera相机,cameraPerspective跟随用的相机,scene场景,renderer场景渲染器,gltfLoader模型加载器,controls控制器,manager模型加载进度管理,clock时间,mixer混合动画器,Robot机器人模型,curve运动轨迹,isFollow是否跟随
data() {
return {
camera: null,
cameraPerspective: null,
scene: null,
renderer: null,
gltfLoader: null,
controls: null,
manager: null,
clock: null,
mixer: null,
Robot: null,
curve: null,
isFollow: false,
};
},
- mounted()中调用的方法
initScene创建场景,initCamera创建相机,initLight创建灯光,initRenderer创建渲染器,initControls创建轨道控制器,initGround创建地面,initCurve使用Catmull-Rom算法从一系列的点创建一条平滑的三维曲线,initModel加载模型,然后在模型加载完成后onLoad执行animate()方法
mounted() {
this.manager = new THREE.LoadingManager();
this.gltfLoader = new GLTFLoader(this.manager);
this.clock = new THREE.Clock();
this.initScene();
this.initCamera();
this.initLight();
this.initRenderer();
this.initControls();
this.initGround();
this.initCurve();
this.initModel();
this.manager.onLoad = () => {
this.animate();
console.log("Loading complete!");
};
},
- 创建场景
initScene() {
this.scene = new THREE.Scene();
},
- 创建相机
创建两个相机,一个是用来显示正常视野,一个用来显示跟随视野
initCamera() {
this.camera = new THREE.PerspectiveCamera(
50,
(window.innerWidth - 201) / window.innerHeight,
1,
1000
); // 透视相机
this.camera.position.set(2, 2, 4); // 设置相机位置
// 创建透视相机
this.cameraPerspective = new THREE.PerspectiveCamera(
75,
(window.innerWidth - 201) / window.innerHeight,
0.1,
6
);
},
- 创建灯光
创建一个平行光和一个环境光
initLight() {
const light = new THREE.DirectionalLight(0xffffff); // 平行光
light.position.set(0.5, 1.0, 0.5).normalize(); // 设置平行光的方向,从(0.5, 1.0, 0.5)->target一般(0, 0, 0)
this.scene.add(light); // 将灯光添加到场景中
const ambLight = new THREE.AmbientLight(0xf0f0f0, 0.1); // 环境光
this.scene.add(ambLight);
},
- 创建渲染器
创建渲染器并且将渲染器dom节点添加到html元素中
initRenderer() {
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.outputEncoding = THREE.sRGBEncoding;
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.setSize(window.innerWidth - 201, window.innerHeight);
document.getElementById("THREE53").appendChild(this.renderer.domElement);
},
- 创建轨道控制器
加了这个可以通过鼠标控制界面的缩放平移旋转
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.addEventListener("change", this.render);
},
- 创建地面
创建一个灰色的地面
initGround() {
const ground = new THREE.Mesh(
new THREE.BoxGeometry(4, 0.0015, 4),
new THREE.MeshPhongMaterial({
color: 0x999999,
depthWrite: false,
transparent: true,
opacity: 1,
})
);
ground.receiveShadow = true;
ground.position.y = -0.0015;
this.scene.add(ground);
},
- 创建运动轨迹
使用Catmull-Rom算法,从一系列的点创建一条平滑的三维曲线,这里设置了四个点,分别是(1, 0, -1)、(1, 0, 1)、(-1, 0, 1)、(-1, 0, -1),并且设置曲线的类型为"centripetal",设置曲线闭合.closed = true
initCurve() {
this.curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(1, 0, -1),
new THREE.Vector3(1, 0, 1),
new THREE.Vector3(-1, 0, 1),
new THREE.Vector3(-1, 0, -1),
]);
this.curve.curveType = "centripetal";
this.curve.closed = true;
const points = this.curve.getPoints(50); // 获取点列表,50为要将曲线划分为的分段数
const line = new THREE.LineLoop(
new THREE.BufferGeometry().setFromPoints(points),
new THREE.LineBasicMaterial({ color: 0x0000ff })
); // 一条头尾相接的连续的线(参数说明:顶点列表,材质)
this.scene.add(line); // 将曲线添加到场景中
},
- 加载模型(机器人)
使用gltfLoader加载器加载机器人模型,并激活机器人模型的动画
initModel() {
this.gltfLoader.load(
"/models/models/gltf/RobotExpressive/RobotExpressive.glb",
(gltf) => {
gltf.scene.scale.set(0.1, 0.1, 0.1);
this.Robot = gltf.scene;
this.scene.add(gltf.scene);
const animations = gltf.animations;
this.mixer = new THREE.AnimationMixer(this.Robot); // 动画混合器
let actions = {};
for (let i = 0; i < animations.length; i++) {
const clip = animations[i];
const action = this.mixer.clipAction(clip);
actions[clip.name] = action;
}
// 制定动画
actions["Walking"]
.reset() // 重置动作
.play(); // 让混合器激活动作
}
);
},
- 跟随代码和渲染场景
在animate()方法中更新机器人走路的动画,并且调整机器人走路的位置和朝向,通过曲线curve的.getPointAt(t)方法:参数t表示当前点在线条上的位置百分比,该方法根据传入的百分比返回在曲线上的位置;和.getTangentAt(t)方法:该方法根据传入的百分比返回在曲线上的位置的切线。
根据上面两个方法得到的结果更新机器人的位置和朝向,以及更新跟随相机的位置和朝向。
代码中的每一步都有详细说明。
最后render()方法渲染场景,如果开启了跟随,就用跟随相机,如果没开启跟随,就用平常的相机
animate() {
requestAnimationFrame(this.animate);
// 执行动画
const dt = this.clock.getDelta();
if (this.mixer) {
this.mixer.update(dt);
}
// 调整走路位置和朝向
const loopTime = 25 * 1000; // 定义循环一圈所需的时间
let time = Date.now();
let t = (time % loopTime) / loopTime; // 计算当前时间进度百分比
const position = this.curve.getPointAt(t);
this.Robot.position.copy(position); // 更新机器人的位置
const tangent = this.curve.getTangentAt(t);
const lookAtVec = tangent.add(this.Robot.position); // 位置向量和切线向量相加即为所需朝向的点向量
this.Robot.lookAt(lookAtVec); // 更新机器人朝向
let time2 = time - 2 * 1000;
let t2 = (time2 % loopTime) / loopTime; // 计算当前时间进度百分比
const position2 = this.curve.getPointAt(t2); // 比上面模型在曲线上的的位置晚2s的位置
this.cameraPerspective.position.set(
position2.x,
position2.y + 0.5,
position2.z
); // 更新跟随用的相机的位置,这里让它向上偏移了0.5
this.cameraPerspective.lookAt(position); // 跟随用的相机朝向机器人的位置
this.render();
},
render() {
if (this.isFollow) {
this.renderer.render(this.scene, this.cameraPerspective);
} else {
this.renderer.render(this.scene, this.camera);
}
},
- 按钮事件
点击按钮切换是否跟随
followRobot() {
this.isFollow = !this.isFollow;
},
写在最后
以上就是所有的代码和说明。