前言
本篇文章用threejs带大家实现一下无限隧道的效果。
实现逻辑
首先,我们要知道,如何才能无限,当然就是圆,而要实现隧道效果,自然就是需要的是立体的圆环,而我们的视角要在圆环里面。
初始化项目
ok,在知道了这个实现逻辑之后,我们先去初始化threejs项目1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80<template>
<div></div>
</template>
<script setup>
import * as THREE from "three";
// 导入轨道控制器
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
// 1.创建场景
const scene = new THREE.Scene();
// 2.创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
// 设置相机位置
camera.position.set(0, 0, 10);
// 添加相机
scene.add(camera);
// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
//设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 将webgl渲染的canvas内容添加到body上
document.body.appendChild(renderer.domElement);
//当前是平面的,让平面立体起来,使用控制器controls
// 创建轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 添加坐标轴辅助器
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 持续渲染,让相机场景动起来
function render() {
controls.update();
// 使用渲染器通过相机将场景渲染进来
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
render();
// 监听画面的变化,更新渲染的画面
window.addEventListener("resize", () => {
// 更新摄像头
camera.aspect = window.innerWidth / window.innerHeight;
// 更新摄像机的投影矩阵
camera.updateProjectionMatrix();
// 更新渲染器
renderer.setSize(window.innerWidth, window.innerHeight);
// 设置渲染器的像素比
renderer.setPixelRatio(window.devicePixelRatio);
});
</script>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
}
</style>
添加管道
我们可以看到,threejs中的管道比较符合我们的需求。
那我们先添加一个管道,下面这段是官网示例1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 添加管道
class CustomSinCurve extends THREE.Curve {
constructor(scale = 1) {
super();
this.scale = scale;
}
getPoint(t, optionalTarget = new THREE.Vector3()) {
const tx = t * 3 - 1.5;
const ty = Math.sin(2 * Math.PI * t);
const tz = 0;
return optionalTarget.set(tx, ty, tz).multiplyScalar(this.scale);
}
}
const path = new CustomSinCurve(10);
const geometry = new THREE.TubeGeometry(path, 20, 2, 8, false);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
我们可以看到,管道已经被我添加进来了
将管道变成圆环
我们可以看到管道的获取点的函数,我们完全可以把这个getPoint改成我们自己的方法
原来的点位布置可以想象为,x方向一直变大,y方向按照sin曲线忽高忽低,z轴一直为0。可以简单理解为xy平面上的正弦运动。
我们将getPoint修改成为这样1
2
3const tx = Math.cos(2 * Math.PI * t);
const ty = Math.sin(2 * Math.PI * t);
const tz = 0.1 * Math.sin(8 * Math.PI * t);
这样的点位布置,xy平面上是一个圆,因为cos和sin的平方加起来是一个1,你可以理解,sin函数为1的时候cos为0,cos为1的时候sin为0,z方向设置了一个值,用来添加抖动。
然后我就得到了这样的一个立体圆环带一点点弧度的管道。这里我旋转了一下,为了大家方便看到z方向的变化。
添加一张贴图
这里添加完成纹理之后设置了wrapS和wrapT,让纹理在水平和垂直方向上进行重复,水平重复10次,垂直1次1
2
3
4
5
6
7const material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
map: new THREE.TextureLoader().load("/texture/texture.png"),
});
material.map.wrapS = THREE.RepeatWrapping
material.map.wrapT= THREE.RepeatWrapping
material.map.repeat.set(10, 1)
这样就得到了这样的一个管道
将视角移动到管道内
要实现穿越效果,我们要将摄像机放在模型内部,并沿着隧道路径运动
我们先实现视角放进去1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 获得初始点位的三维向量
const startPoint = path.getPoint(0);
// 根据实际情况调整偏移量
const offsetVector = new THREE.Vector3(0,4, 1);
camera.position.copy(startPoint).add(offsetVector);
// 持续渲染,让相机场景动起来
function render() {
controls.update();
// 让相机始终看向隧道路径上的当前位置
camera.lookAt(startPoint);
// 使用渲染器通过相机将场景渲染进来
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
这里我的偏移三维向量(0,4,1)是基于我设置的参数进行设置的,你可以慢慢调整,直到你的视角在隧道内部就行。
添加运动效果
然后,我们添加运动效果,我们这时候就需要思考一下了,这个该如何实现,很明显就是我们需要根据t计算当前需要看的点以及不停调整偏移量
得到看的点
这个是简单的,我们设置time和speed1
2
3
4
5
6
7
8
9
10
11
12
13
14
15let time=0;
let speed=0.02;
function render() {
controls.update();
time+=speed
// 获得初始点位的三维向量
const startPoint = path.getPoint(time);
camera.position.copy(startPoint)
camera.lookAt(startPoint);
// 使用渲染器通过相机将场景渲染进来
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
这时候得到的效果肯定是有问题的,因为我们没设置偏移量,因为我们把相机放到了构建管道的点上面,又让它看向这个点,也就是自己看到自己,这显然是不现实的。
偏移量思路分析
那这个偏移量该如何计算呢,其实我们可以换一个思路,我们其实就是想一直看到下一个点而已,那我们完全可以把相机放到前一个点位,然后看向下一个点位
实现代码
代码里面我都加了注释,大家自己理解一下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46let time = 0;
let binormal = new THREE.Vector3();
let normal = new THREE.Vector3();
let speed =10;
function render() {
controls.update();
time += speed;
// 定义了一个循环一圈所需的时间,这里设置为 20 秒(20 * 1000 毫秒)。
let looptime = 20 * 1000;
// 计算当前时间在一个循环周期中的比例。通过对总时间取模looptime,确保时间始终在一个循环周期内,然后除以looptime得到比例值t,这个值在 0 到 1 之间变化。
let t = (time % looptime) / looptime;
// 根据时间比例t,从管道的路径上获取当前时间对应的位置点。这样随着时间的变化,位置点也会在管道路径上不断移动。
let pos = geometry.parameters.path.getPointAt(t);
// 获取管道的切线数量,这与管道的复杂度或细分程度有关。
let segments = geometry.tangents.length;
// 通过时间比例t乘以切线数量得到一个值pickt,然后取整得到当前时间对应的切线索引pick
let pickt = t * segments;
let pick = Math.floor(pickt);
// 计算下一个切线索引,通过对当前切线索引加 1 并取模切线数量,确保索引在合法范围内循环。
let pickNext = (pick + 1) % segments;
// 计算下一个点的法向向量和当前点法向向量的差值,存储在binormal中。
binormal.subVectors(geometry.binormals[pickNext], geometry.binormals[pick]);
// 处理小数部分的误差。将差值向量binormal乘以小数部分(pickt - pick),然后加上当前点的法向向量,得到更准确的法向向量。
binormal.multiplyScalar(pickt - pick).add(geometry.binormals[pick]);
// 获取当前时间点在管道路径上的切线向量。
let dir = geometry.parameters.path.getTangentAt(t);
// 通过当前和下一个点位差值的法向向量(binormal)与切线向量dir的叉乘,得到相机的上方向向量normal。
normal.copy(binormal).cross(dir);
// 设置相机的位置为当前时间在管道路径上的位置点。
camera.position.copy(pos);
// 计算相机的目标点,通过在当前时间比例的基础上稍微增加一点(这里是加上1 / geometry.parameters.path.getLength),并取模 1,确保目标点在管道路径上循环。
let lookAt = geometry.parameters.path.getPointAt(
(t + 1 / geometry.parameters.path.getLength()) % 1
);
// 设置相机的矩阵,使其从当前位置看向目标点,并使用计算得到的上方向向量。
// 视点、目标点、上方向得到旋转矩阵
camera.matrix.lookAt(camera.position, lookAt, normal);
// 然后,根据矩阵设置相机的旋转,确保相机正确地指向目标点并具有正确的方向。
camera.rotation.setFromRotationMatrix(camera.matrix, camera.rotation.order);
// 使用渲染器通过相机将场景渲染进来
renderer.render(scene, camera);
// 渲染下一帧的时候就会调用render函数
requestAnimationFrame(render);
}
这个效果当然你可以用threejs自带的沿曲线运动实现,但是在弯道的部分效果过渡没有现在按照自己实现的丝滑。
给大家看看实现的效果
实现滚轮控制速度
这里我们不是有个speed吗,我们完全可以添加一个滚轮事件1
2
3document.addEventListener('wheel', e => {
speed += e.deltaY * 0.02
})
完整代码
1 | <template> |
结语
通过这个管道,我们可以实现其他很多类似的效果,比如血管等等之类的。本篇文章就到这里了,更多内容敬请期待,债见~