想必大家都看过哆啦A梦,时光机是里面的常用道具。
那坐时光机是什么样的体验呢?
我用 Three.js 写了一下,应该是这种感觉:
我们一起来实现一下。
首先,我们过一下 Three.js 的基础:
在二维屏幕上渲染三维物体,得有个坐标轴。
在 three.js 里以向右的方向为 x 轴,向上的方向为 y 轴,向前的方向为 z 轴:
然后管理在三维坐标系里的物体得有个对象体系。
Three.js 的对象体系是这样的:
image.png
所有三维场景中的东西都加到 scene 里来管理。
三维世界本来是黑的,有了 light 之后才能看到东西,有点光源、环境光等不同的光源。
三维世界中的物体,可以从不同角度去观察,改变位置就可以看到不同的风景,这就是相机 camera 的事情。
三维世界中的物体叫做 mesh,任何一个物体都有一个形状,比如圆柱、立方体等,也就是 geometry,然后还得有材质 material,比如金属材质可以反光、普通材质不能。材质可以指定颜色、还可以指定图片作为纹理 texture。
场景中的所有物体,会由渲染器 WebGLRenderer 渲染出来。
场景、物体、灯光、相机、渲染器,这就是 three.js 的核心概念。
每一个物体都可以设置位置 position、缩放 scale、旋转 rotation。
每一帧渲染的时候,改变物体的位置、颜色、旋转角度等就可以实现动画效果了。
大家想一下,时空隧道用什么几何体比较合适呢?
很明显,是圆柱,也就是 CylinderGeometry
在 three.js 文档中可以看到预览大概是这样样子:
示例代码是这样的:
创建一个圆柱几何体 CylinderGeometry ,传入上圆半径、下圆半径,高度,分段数量(分的多了就是圆了)。
指定材质,用 MeshBasicMaterial,指定蓝色。
然后就可以创建一个物体 Mesh,把它加到场景 scene 里。
我们可以创建一个圆柱,内部贴上图,然后相机放在圆柱内部,是不是看到的就是一个隧道了?
圆柱体的材质我们用纹理贴图,比如这种:
这个纹理是可以设置重复 repeat 和偏移 offset 的。
用 TextureLoader 加载图片作为纹理,设置 wrapS 为 repeat,也就是水平重复、wrapT 为竖直重复。
T 是 vertical 的缩写,而 S 就是 horizontal 了。
然后 repeat.set(4, 4) 每个单位内水平方向重复 4 次、竖直方向重复 4 次。
这样就完成了纹理贴图。
然后每一帧渲染的时候,让纹理的 offset 不断增加或减少,再让圆柱不断旋转,不就实现了时空隧道效果么?
我们来写下试试:
先写个 html,引入 three.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>时光机</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="https://www.unpkg.com/three@0.154.0/build/three.js"></script>
</head>
<body>
<script>
</script>
</body>
</html>
我们先创建场景 scene 和 renderer:
const width = window.innerWidth;
const height = window.innerHeight;
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
document.body.appendChild(renderer.domElement);
调整画布大小为窗口宽高。
然后创建相机:
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
camera.position.set(0,0, 500);
camera.lookAt(scene.position);
这里创建的是透视相机,也就是近大远小那种。
它有 4 个参数:
从相机往前看,会有个角度 fov,这是第一个参数。
然后视野范围的矩形会有个宽高比 aspect,这是第二个参数。
视野范围会形成一个椎体,叫做视椎体,三四个参数是指定视椎体的范围,从哪里看到哪里。
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
所以 PerspectiveCamera 的这 4个参数分别制定了 45 度的观察角度,宽高比和窗口宽高比一样。视野范围为 0.1 到 10000。
然后就可以一帧帧渲染了:
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
只不过现在还没有物体,我们添加个立方体 BoxGeometry 试试:
function create() {
const geometry = new THREE.BoxGeometry( 100, 100, 100 );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const cube = new THREE.Mesh( geometry, material );
cube.rotation.y = 0.5;
cube.rotation.x = 0.5;
scene.add( cube );
}
create();
添加一个 Mesh,用 BoxGeometry 创建立方体,长宽高为 100,用 MeshBasicMaterial 指定材质,颜色为绿色。
让这个 mesh 绕 y 和 x 旋转 0.5 的角度。
渲染出来的是这样的:
确实是个立方体,只不过没有明暗变化。
我们加个点光源就好了:
const pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set(0, 0, 500);
scene.add(pointLight);
创建白色的灯光,放在和相机同一个位置,来照向场景中心的位置。
但是你刷新页面会发现没有变化,因为 MeshBasicMaterial 的材质是不反光的。
换成 MeshPhongMaterial 的材质,这种材质有金属质感,会反光。
就这样了:
然后我们来创建圆柱体:
let tunnel;
function create() {
const geometry = new THREE.CylinderGeometry( 30, 50, 100, 32, 32, true);
const material = new THREE.MeshPhongMaterial({
color: 0x0000ff
});
tunnel = new THREE.Mesh(geometry, material);
scene.add(tunnel);
}
上圆半径 30、下圆半径 50,高 100,分 32 段(差不多就是圆了)。
用 MeshPhongMaterial 指定金属材质为 蓝色。
渲染出来的是这样的:
我们让它绕 z 轴旋转下:
function render() {
renderer.render(scene, camera);
tunnel.rotation.z = tunnel.rotation.z + 0.01;
requestAnimationFrame(render);
}
很明显,我们观察的方向不对,应该是看到下圆的底才对。所以让它绕 x 轴逆时针旋转 90 度。
然后去掉底,这个是在创建圆柱体的时候指定:
再让圆柱绕 x 轴旋转下看看:
tunnel.rotation.x = tunnel.rotation.x + 0.01;
确实,没有底了。
然后我们来处理下贴图。
let tunnel;
function create() {
const textureLoader = new THREE.TextureLoader();
textureLoader.load('./storm.jpg', function(texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 1, 2 );
const geometry = new THREE.CylinderGeometry( 30, 50, 100, 32, 32, true);
const material = new THREE.MeshPhongMaterial({
map: texture,
side: THREE.DoubleSide
});
tunnel = new THREE.Mesh(geometry, material);
tunnel.rotation.x = -Math.PI / 2;
scene.add(tunnel);
});
}
用 TextureLoader 加载纹理图片,设置水平和竖直方向重复。
然后把这个 texture 设置为纹理。
就是这样的:
把圆柱体高度改为 1000。
看到的就像是一个隧道了:
让 tunnel 绕 y 轴转起来:
function render() {
renderer.render(scene, camera);
if(tunnel) {
tunnel.rotation.y += 0.01;
}
requestAnimationFrame(render);
}
我们不是在 z 轴的方向看向中心点么?为什么不是绕 z 轴转?
因为这个圆柱已经绕 x 轴顺时针转了 90 度,所以是绕 y 轴转,看到的是绕 z 轴转的效果。
然后我们再让纹理的 offset 也动起来:
就有穿梭隧道的感觉了:
不过如果我们想实现变色,最好不要直接把贴图作为纹理,而是用它来做透明通道,也就是这样:
const material = new THREE.MeshPhongMaterial({
transparent: true,
alphaMap: texture,
side: THREE.BackSide
});
设置透明,然后图片作为透明通道,就会根据不同像素的颜色来设置不同的透明度。
也就是这样的效果:
然后只要设置不同的颜色,并且不断地变色就好了。
let H = 0;
function render() {
renderer.render(scene, camera);
H += 0.001;
if (H > 1) { H = 0; }
if(tunnel) {
tunnel.material.color.setHSL(H, 0.5, 0.5);
tunnel.rotation.y += 0.01;
stormTexture.offset.y += 0.01;
}
requestAnimationFrame(render);
}
我们用 HSL 颜色标识法,也就是色相、饱和度、明度。
色相是从 0 到 1 的数值,我们在每一帧改变色相的值。
效果是这样的:
隧道完成了,我们再加个时光机的底座。
这个比较简单,就是一个立方体,调整下位置:
const geometry = new THREE.BoxGeometry( 30, 2, 30 );
const material = new THREE.MeshPhongMaterial( {
color: 0x0000ff
});
const cube = new THREE.Mesh( geometry, material );
cube.position.z = 460;
cube.position.y = -20;
scene.add( cube );
相机在 0, 0, 500,那底座就在 460 就好了,再沿着 y 轴往下一点。
这个颜色不大好,我们还是换成贴图。
找个金属的纹理图片,比如这个:
用 TextureLoader 把纹理图片加载进来,设置水平、竖直的重复。
textureLoader.load('./metal.png', function(texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 10, 10 );
const geometry = new THREE.BoxGeometry( 30, 2, 30 );
const material = new THREE.MeshPhongMaterial( {
map: texture
});
const cube = new THREE.Mesh( geometry, material );
cube.position.z = 460;
cube.position.y = -20;
scene.add( cube );
});
这样,我们的时光机效果完成了!
全部代码上传了 github:https://github.com/QuarkGluonPlasma/threejs-exercize
不到 100 行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>时光机</title>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
<script src="https://www.unpkg.com/three@0.154.0/build/three.js"></script>
</head>
<body>
<script>
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
camera.position.set(0, 0, 500);
camera.lookAt(scene.position);
const pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set(0, 0, 500);
scene.add(pointLight);
document.body.appendChild(renderer.domElement)
let tunnel;
let stormTexture;
function create() {
const textureLoader = new THREE.TextureLoader();
textureLoader.load('storm.jpg', function(texture) {
stormTexture = texture;
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 1, 2 );
const geometry = new THREE.CylinderGeometry( 30, 50, 1000, 32, 32, true);
const material = new THREE.MeshPhongMaterial({
transparent: true,
alphaMap: texture,
side: THREE.BackSide
});
tunnel = new THREE.Mesh(geometry, material);
tunnel.rotation.x = -Math.PI / 2;
scene.add(tunnel);
});
textureLoader.load('metal.png', function(texture) {
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set( 10, 10 );
const geometry = new THREE.BoxGeometry( 30, 2, 30 );
const material = new THREE.MeshPhongMaterial( {
map: texture
});
const cube = new THREE.Mesh( geometry, material );
cube.position.z = 460;
cube.position.y = -20;
scene.add( cube );
});
}
let H = 0;
function render() {
renderer.render(scene, camera);
H += 0.002;
if (H > 1) { H = 0; }
if(tunnel) {
tunnel.material.color.setHSL(H, 0.5, 0.5);
tunnel.rotation.y += 0.01;
stormTexture.offset.y += 0.01;
}
requestAnimationFrame(render);
}
create();
render();
</script>
</body>
</html>
今天我们用 Three.js 实现了时光机的效果。
首先,过了下 Three.js 的基础:
实现时空隧道的效果,就是创建了一个圆柱体,贴上纹理图片,然后把相机放到圆柱体内。
每帧渲染不断改变纹理的 offset 和圆柱体的 rotation。
此外,我们不是直接贴的图,而是把它作为透明度通道,这样可以实现变色效果,结合 HSL 改变色相的方式来变色。
最后,还加了一个立方体的几何体作为时光机底座。
用 Three.js 画一个时光机,一起穿越时空隧道吧。