上节我们实现了一个隧道效果:

但是隧道是有尽头的,到了尽头就得从头开始:

如何实现没有尽头的无限隧道呢?
这就得用前面学过的 uv 动画了。
我们可以相机位置不变,只改变隧道的 texture.offset,用 uv 动画来实现这种效果。
创建项目:
npx create-vite infinite-tunnel
这次用 create-vite 创建 vite 项目。

进入项目,安装依赖:
npm install
npm install --save three
npm install --save-dev @types/three
然后改下 src/main.js
import './style.css';
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000);
camera.position.set(300, 300, 300);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
创建 Scene、Camera、Renderer。
改下 style.css
body {
margin: 0;
}
然后来写下 mesh.js
import * as THREE from 'three';
const geometry = new THREE.CylinderGeometry( 30, 50, 1000);
const material = new THREE.MeshBasicMaterial({
color: 'orange'
});
const tunnel = new THREE.Mesh(geometry, material);
export default tunnel;
跑一下:
npm run dev

可以看到,它是一个实心的圆柱:

我们把它设置成空心:

32 是分段数,第六个参数是是否空心,设置为 true。
然后材质设置双面可见。
const geometry = new THREE.CylinderGeometry( 30, 50, 1000, 32, 32, true);
const material = new THREE.MeshBasicMaterial({
color: 'orange',
side: THREE.DoubleSide
});
看下效果:

这样就可以了。
然后我们贴上颜色贴图:

保存在 public 目录下。

const loader = new THREE.TextureLoader();
const texture = loader.load('./storm.png');
texture.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshBasicMaterial({
map: texture,
side: THREE.DoubleSide
});
加载纹理图片,设置颜色空间,然后设置到材质的颜色贴图。
看下效果:

现在就有时空隧道的感觉了。
其实并不需要双面可见,因为相机只需要看到内部。
我们设置为反面可见,然后改下相机位置:

相机位置还是通过 OrbitControls 来调试:

controls.addEventListener('change', () => {
console.log(camera.position);
});

调好之后设置到代码里:
camera.position.set(0.9, -520, 6.5);
现在一刷新就是在隧道内部了:

并且只设置了内部一个面的纹理:

设置竖直方向重复两次:

竖直方向重复,就要设置 wrapT。

这次我们不改变相机位置 camera.position 了,而是让纹理 offset 动。
用 uv 动画来实现穿梭隧道的效果。

mesh.material.map.offset.y += 0.01;
看下效果:

这样就有无限隧道的感觉了。
其实每次改变的数值还有另一种写法:

const clock = new THREE.Clock();
function render() {
const delta = clock.getDelta();
mesh.material.map.offset.y += delta * 0.5;
mesh.rotation.y += delta * 0.5;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
用 clock.getDelta 来拿到每次渲染的时间间隔,用它作为改变的数值。
这里顺便再让圆柱体旋转下。

不过现在还是太单调了,我们可以让它定期改变颜色。
首先,我们不把它设置为颜色贴图了,而是作为 alpha 贴图:

它是用贴图的颜色来控制不透明度的。

const material = new THREE.MeshBasicMaterial({
transparent: true,
alphaMap: texture,
side: THREE.BackSide
});
这里也要改一下:

‘
可以看到,现在颜色就消失了,只剩下了不透明度的改变。
然后我们改变下 color 的色相就好了

这里用 HSL 来改变颜色。
let H = 0;
H += 0.002;
if (H > 1) { H = 0; }
mesh.material.color.setHSL(H, 0.5, 0.5);
hsl 就是色相、饱和度、亮度,它是颜色的一种表示方法。
这个是色相环:

美术生应该很熟悉这种色相环,什么相差 60 度是邻近色、相差 180 度是互补色等等。
所以取值范围是 0 到 360 也就是共 360 度。
有 RGB 不就好了么?为啥还要搞个 HSL?
红绿蓝是计算机存储颜色的方式,它喜欢这种表示法,可以直接用来显示颜色。
但是对人来说,是不是还是明暗关系、色彩饱和度更容易理解一点?
所以选色的时候都是基于色相、饱和度、亮度这些东西,但存储的时候使用 RGB,最后屏幕显示颜色也是基于 RGB的。
我们用 HSL 标识颜色,然后改变色相的位置就好了。
看下效果:

现在就更有无限隧道的感觉了。
案例代码上传了小册仓库。
总结
这节我们用 uv 动画实现了无限隧道的效果。
首先画了一个圆柱体,内部空心,设置反面的透明度贴图 alphaMap。
透明度贴图是用贴图的颜色来设置透明度的,需要同时设置材质的 transparent 为 true。
通过改变 texture.offset.y 来实现 uv 动画,用 clock.getDelata 来计算每次 offset.y 改变的数值。
然后通过 hsl 标识颜色,并且改变色相 h 的方式来实现了颜色的改变。
这算是一个比较综合的小实战,用到了 alphaMap + HSL + uv 动画。