持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第30天,点击查看活动详情
效果
前言
以上是使用vue+three.js实现的双摄相机,小视图相机显示第三人称视角的演示gif,以下是该效果的实现逻辑和完整代码,说的都很详细。
实现逻辑
首先,需要创建正常视图的场景和相机,我使用new THREE.Scene()方法创建场景。
使用new THREE.PerspectiveCamera()方法创建透视相机,在initCamera()方法中,我创建了2个相机,分别是正常视图用的camera相机和小窗口第三人称视角用的canera2相机。
在initLight()方法中,我创建了两个灯光,一个是使用new THREE.DirectionalLight()方法创建的平行光,另一个是使用new THREE.AmbientLight()方法创建的环境光。
接着是场景渲染的创建和插入,我使用new THREE.WebGLRenderer()方法创建一个渲染器,并将其dom节点插入到html中。
然后是轨道控制器的创建new OrbitControls(),加了这个,就可以通过鼠标控制相机的位置和视角,从而实现鼠标控制场景的旋转缩放平移。
再然后是地面、地面上的运动轨迹线、小机器人模型的创建,这里就不再细讲,前面的文章中都有过详细说明,而且代码中也进行了详细地注释。在引入小机器人模型后,我们需要制定并激活小机器人走路的动画。
最后是场景的渲染,先是正常视图的场景渲染this.renderer.render(this.scene, this.camera),使用的是camera相机,然后是小窗口第三人称视角的场景渲染,使用的是camera2相机,我们将该相机的位置移到小机器人的后面,并且让其始终朝向小机器人。
具体每个方法的意义在代码中都有进行注释,有兴趣可以查看完整代码。
完整代码
<template>
<div class="item">
<div id="THREE70"></div>
<div class="inset_box"></div>
</div>
</template>
<script>
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
export default {
data() {
return {
camera: null,
camera2: null,
scene: null,
renderer: null,
gltfLoader: null,
controls: null,
manager: null,
clock: null,
mixer: null, // 混合动画器
Robot: null, // 机器人模型
curve: null, // 运动轨迹
insetWidth: 200,
insetHeight: 200,
};
},
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(); // 使用Catmull-Rom算法, 从一系列的点创建一条平滑的三维曲线
this.initModel(); // 加载模型
this.manager.onLoad = () => {
this.animate();
console.log("Loading complete!");
};
},
methods: {
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.camera2 = new THREE.PerspectiveCamera(50, 1, 0.1, 1000);
this.camera2.position.set(0, 2, 4); // 设置相机位置
this.camera2.lookAt(0, 0, 0);
},
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);
},
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("THREE70").appendChild(this.renderer.domElement);
},
initControls() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
},
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);
},
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); // 将曲线添加到场景中
},
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() {
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); // 参数t表示当前点在线条上的位置百分比,该方法根据传入的百分比返回在曲线上的位置
this.Robot.position.copy(position); // 更新机器人的位置
const tangent = this.curve.getTangentAt(t); // 该方法根据传入的百分比返回在曲线上的位置的切线
const lookAtVec = tangent.add(this.Robot.position); // 位置向量和切线向量相加即为所需朝向的点向量
this.Robot.lookAt(lookAtVec); // 更新机器人朝向
// 创建双摄相机
this.renderer.setClearColor(0x000000, 1); // 设置颜色及其透明度
this.renderer.setViewport(
0,
0,
window.innerWidth - 201,
window.innerHeight
); // 设置视口大小
this.renderer.render(this.scene, this.camera);
this.renderer.setClearColor(0x444444, 1); // 设置颜色及其透明度
this.renderer.clearDepth(); // 清除深度缓存。相当于调用.clear( false, true, false )
this.renderer.setScissorTest(true); // 启用或禁用剪裁检测. 若启用,则只有在所定义的裁剪区域内的像素才会受之后的渲染器影响。
this.renderer.setScissor(
window.innerWidth - 416,
window.innerHeight - 215,
this.insetWidth,
this.insetHeight
); // 设置裁剪区域
this.renderer.setViewport(
window.innerWidth - 416,
window.innerHeight - 215,
this.insetWidth,
this.insetHeight
); // 设置视口大小
let time2 = time - 2 * 1000;
let t2 = (time2 % loopTime) / loopTime; // 计算当前时间进度百分比
const position2 = this.curve.getPointAt(t2); // 比上面模型在曲线上的的位置晚2s的位置
this.camera2.position.set(position2.x, position2.y + 0.5, position2.z); // 更新小视图相机的位置,这里让它向上偏移了0.5
this.camera2.lookAt(position.x, position.y + 0.2, position.z); // 小视图相机朝向机器人的位置
this.renderer.render(this.scene, this.camera2);
this.renderer.setScissorTest(false);
},
},
};
</script>
<style lang="less" scoped>
.inset_box {
position: absolute;
top: 10px;
right: 10px;
width: 200px;
height: 200px;
overflow: hidden;
border-radius: 3px;
border: 5px solid rgba(255, 255, 255, 0.8);
}
</style>
结语
以上即是完整代码和实现逻辑说明。