3D 时空隧道效果
基于 Three.js 和 GSAP 的动态“时空隧道”效果。
整个“时空隧道”的核心是通过动画、纹理和相机运动的配合,让用户产生一种不断向前、且管道随视觉变化而扭曲延伸的沉浸感。
一、项目准备
在开始前,你需要确保已经在网页环境中引用了以下库:
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/",
"gsap": "https://esm.sh/[email protected]"
}
}
</script>
示例中可以使用 <script type="importmap">
来设置 importmap,也可以直接通过传统的 <script>
标签在页面中引入这些脚本资源。关键是要保证环境能正确导入 THREE
和 gsap
。
同时,你需要准备好一张纹理图片(示例中命名为 img.jpg
),也要确保这张图片能被正确加载到场景中。
二、主要实现步骤
1. 初始化场景、相机、渲染器
- Scene(场景):三维世界的容器,所有物体都将加入到其中。
- Camera(相机):决定了你从哪个角度和位置去观察场景。这里使用
PerspectiveCamera
(透视相机)。 - Renderer(渲染器):用来将三维场景渲染到屏幕(Canvas)上,这里用
WebGLRenderer
。
// 创建场景
const scene = new THREE.Scene();
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(ww, wh);
document.body.appendChild(renderer.domElement);
// 创建相机
const camera = new THREE.PerspectiveCamera(15, ww / wh, 0.01, 1000);
camera.rotation.y = Math.PI;
camera.position.z = 0.35;
小提示:
camera.rotation.y = Math.PI;
让相机向反方向看,结合后面管道的朝向,可以让我们更直观地在“管道”内前进。
2. 创建 CatmullRomCurve3 曲线
为了生成一个可以动态改变形状的管道,我们使用 Three.js 提供的 CatmullRomCurve3
曲线。该曲线可以基于多个点自动生成平滑的曲线。
const points = [];
for (let i = 0; i < 5; i++) {
points.push(new THREE.Vector3(0, 0, 3 * (i / 4)));
}
points[4].y = -0.06;
const curve = new THREE.CatmullRomCurve3(points);
curve.type = "catmullrom";
我们这里给定了 5 个点,每个点在 z 轴上做了步进,这样就形成了一条大体朝前方(z 轴正方向)的曲线。
3. 将曲线生成管道
TubeGeometry 可以把一条三维曲线“挤”成一根可指定半径的管道。我们只需要把曲线 curve
和一些额外参数(分段数、管道半径、管道横截面边数等)传进去即可。
const tubeGeometry = new THREE.TubeGeometry(curve, 70, 0.02, 30, false);
const tubeMaterial = new THREE.MeshBasicMaterial({
side: THREE.BackSide,
map: texture, // 后续会在这里加载纹理
});
const tubeMesh = new THREE.Mesh(tubeGeometry, tubeMaterial);
scene.add(tubeMesh);
注意这里指定了 side: THREE.BackSide
,让我们可以“站在管道里面”观察到管道的纹理。
4. 加载并设置纹理
我们通过 TextureLoader
异步加载本地图片,然后指定给 tubeMaterial.map
来给管道贴图。并且通过 wrapS
和 wrapT
设置成 THREE.MirroredRepeatWrapping
,可让纹理在重复时镜像翻转,避免在重复拼接时出现明显的缝隙;repeat.set(textureParams.repeatX, textureParams.repeatY)
则决定了在 S 和 T 方向上的重复次数。
const loader = new THREE.TextureLoader();
loader.load("./img.jpg", function (texture) {
const tubeMaterial = new THREE.MeshBasicMaterial({
side: THREE.BackSide,
map: texture,
});
tubeMaterial.map.wrapS = THREE.MirroredRepeatWrapping;
tubeMaterial.map.wrapT = THREE.MirroredRepeatWrapping;
tubeMaterial.map.repeat.set(10, 4); // textureParams.repeatX, textureParams.repeatY
...
});
5. 通过 GSAP 给纹理和相机增加动画
- 纹理动画:使用了一个
timeline
来反复改变textureParams.offsetX
、textureParams.repeatX
,并在渲染循环里根据这些参数更新纹理贴图的偏移量和重复值。这样就可以让纹理“滚动”或“变速”。 - 相机抖动:同样通过 GSAP 的
timeline
,不断改变一个名为cameraShake.x
的数值,并将这个数值加到相机的position.x
上,以模拟相机随机抖动的效果。
// 纹理动画
const hyperSpace = gsap.timeline({ repeat: -1 });
hyperSpace.to(textureParams, {
duration: 4,
repeatX: 0.3,
ease: "power1.inOut",
});
hyperSpace.to(
textureParams,
{
duration: 12,
offsetX: 8,
ease: "power2.inOut",
},
0
);
hyperSpace.to(
textureParams,
{
duration: 6,
repeatX: 10,
ease: "power2.inOut",
},
"-=5"
);
// 相机抖动动画
const shake = gsap.timeline({ repeat: -1, repeatDelay: 5 });
shake.to(
cameraShake,
{
duration: 2,
x: -0.01,
ease: "rough({ ... })",
},
4
);
shake.to(cameraShake, {
duration: 2,
x: 0,
ease: "rough({ ... })",
});
6. 鼠标移动与相机位置联动
在文章开头我们定义了一个 mouse
对象,用来追踪鼠标位置(target
)和实际需要缓动到的位置(position
)。在每一帧中,我们使用缓动公式将鼠标的当前位置和相机位置对应起来,得到一个 ratio
(相对屏幕宽高的比例),并把这个比例转化成相机偏移位置。
document.body.addEventListener("mousemove", function (e) {
mouse.target.x = e.clientX;
mouse.target.y = e.clientY;
});
// 在渲染循环中
mouse.position.x += (mouse.target.x - mouse.position.x) / 50;
mouse.position.y += (mouse.target.y - mouse.position.y) / 50;
mouse.ratio.x = mouse.position.x / ww;
mouse.ratio.y = mouse.position.y / wh;
camera.position.x = mouse.ratio.x * 0.044 - 0.025 + cameraShake.x;
camera.position.y = mouse.ratio.y * 0.044 - 0.025;
这样,我们就能通过移动鼠标来“左右上下”微调相机位置,配合管道本身的动画,就会给人一种沉浸式在隧道中穿梭的感觉。
7. 动态更新管道几何
代码中还有一个关键点:在每一帧中,会根据 mouse.ratio.x
和 mouse.ratio.y
重新调整 CatmullRomCurve3 的某些控制点,从而使管道本身也在动态弯曲变化。接着,会用新的曲线更新 TubeGeometry。为了节省开销,先尝试直接复制新的几何顶点到现有几何体上,只有在顶点数量不一致(极少见)时才替换整个几何体。
// 在渲染循环里,根据鼠标移动来调整曲线点
if (curve && curve.points) {
curve.points[2].x = 0.6 * (1 - mouse.ratio.x) - 0.3;
...
}
// 创建新几何,再将新的 positions 拷贝到旧几何
if (curve) {
const newTubeGeometry = new THREE.TubeGeometry(curve, 70, 0.02, 30, false);
...
if (oldPositions.count === newPositions.count) {
oldPositions.copy(newPositions);
oldPositions.needsUpdate = true;
} else {
tubeMesh.geometry = newTubeGeometry;
currentTubeGeometry = newTubeGeometry;
}
}
这样做的好处是可以在不销毁或重建 Mesh 的情况下,让管道边绕曲线动态发生形变,更加平滑且节省性能。
四、总结
- 三维场景的基础搭建:Scene + Camera + Renderer。
- 使用管道几何:Three.js 提供的
TubeGeometry
可以很方便地将任意曲线变成一个可视化的三维管道。 - 纹理加载与动画:通过
TextureLoader
获取纹理,并结合 GSAP 的timeline
可以轻松实现各种循环动画效果。 - 鼠标交互与相机运动:将鼠标位置转化为一个归一化的比例,通过缓动公式更新相机位置,从而让用户在移动鼠标时感觉到隧道的动态移动。
- 曲线更新:可以在每一帧中根据鼠标等输入实时更新 CatmullRomCurve3 的控制点,然后更新
TubeGeometry
,从而实现管道的“弯曲”效果。
代码
https://github.com/calmound/threejs-demo/tree/main/cubeliu (opens in a new tab)
参考
https://github.com/Mamboleoo/InfiniteTubes/blob/master/index3.html (opens in a new tab)