前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用 Three.js 画一个哆啦A梦的时光机

用 Three.js 画一个哆啦A梦的时光机

作者头像
神说要有光zxg
发布2023-08-28 19:37:06
3370
发布2023-08-28 19:37:06
举报

想必大家都看过哆啦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

代码语言:javascript
复制
<!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:

代码语言:javascript
复制
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);

调整画布大小为窗口宽高。

然后创建相机:

代码语言:javascript
复制
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

camera.position.set(0,0, 500);
camera.lookAt(scene.position);

这里创建的是透视相机,也就是近大远小那种。

它有 4 个参数:

从相机往前看,会有个角度 fov,这是第一个参数。

然后视野范围的矩形会有个宽高比 aspect,这是第二个参数。

视野范围会形成一个椎体,叫做视椎体,三四个参数是指定视椎体的范围,从哪里看到哪里。

代码语言:javascript
复制
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);

所以 PerspectiveCamera 的这 4个参数分别制定了 45 度的观察角度,宽高比和窗口宽高比一样。视野范围为 0.1 到 10000。

然后就可以一帧帧渲染了:

代码语言:javascript
复制
function render() {
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

render();

只不过现在还没有物体,我们添加个立方体 BoxGeometry 试试:

代码语言:javascript
复制
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 的角度。

渲染出来的是这样的:

确实是个立方体,只不过没有明暗变化。

我们加个点光源就好了:

代码语言:javascript
复制
const pointLight = new THREE.PointLight( 0xffffff );
pointLight.position.set(0, 0, 500);
scene.add(pointLight);

创建白色的灯光,放在和相机同一个位置,来照向场景中心的位置。

但是你刷新页面会发现没有变化,因为 MeshBasicMaterial 的材质是不反光的。

换成 MeshPhongMaterial 的材质,这种材质有金属质感,会反光。

就这样了:

然后我们来创建圆柱体:

代码语言:javascript
复制
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 轴旋转下:

代码语言:javascript
复制
function render() {
    renderer.render(scene, camera);

    tunnel.rotation.z = tunnel.rotation.z + 0.01;
    requestAnimationFrame(render);
}

很明显,我们观察的方向不对,应该是看到下圆的底才对。所以让它绕 x 轴逆时针旋转 90 度。

然后去掉底,这个是在创建圆柱体的时候指定:

再让圆柱绕 x 轴旋转下看看:

代码语言:javascript
复制
tunnel.rotation.x = tunnel.rotation.x + 0.01;

确实,没有底了。

然后我们来处理下贴图。

代码语言:javascript
复制
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 轴转起来:

代码语言:javascript
复制
function render() {
    renderer.render(scene, camera);

    if(tunnel) {
        tunnel.rotation.y += 0.01;
    }
    requestAnimationFrame(render);
}

我们不是在 z 轴的方向看向中心点么?为什么不是绕 z 轴转?

因为这个圆柱已经绕 x 轴顺时针转了 90 度,所以是绕 y 轴转,看到的是绕 z 轴转的效果。

然后我们再让纹理的 offset 也动起来:

就有穿梭隧道的感觉了:

不过如果我们想实现变色,最好不要直接把贴图作为纹理,而是用它来做透明通道,也就是这样:

代码语言:javascript
复制
const material = new THREE.MeshPhongMaterial({ 
    transparent: true,
    alphaMap: texture,
    side: THREE.BackSide
});

设置透明,然后图片作为透明通道,就会根据不同像素的颜色来设置不同的透明度。

也就是这样的效果:

然后只要设置不同的颜色,并且不断地变色就好了。

代码语言:javascript
复制
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 的数值,我们在每一帧改变色相的值。

效果是这样的:

隧道完成了,我们再加个时光机的底座。

这个比较简单,就是一个立方体,调整下位置:

代码语言:javascript
复制
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 把纹理图片加载进来,设置水平、竖直的重复。

代码语言:javascript
复制
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 行:

代码语言:javascript
复制
<!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 的基础:

  • 向右为 x 轴、向上为 y 轴,向前为 z 轴
  • 场景 scene 管理灯光 light、相机 camera、物体mesh 等,然后通过渲染器 renderer 渲染出来
  • 物体 mesh 由几何体 geometry 和材质 material 构成,material 可以指定 color 或者纹理 texture
  • camera 一般用透视相机,也就是近大远小的效果

实现时空隧道的效果,就是创建了一个圆柱体,贴上纹理图片,然后把相机放到圆柱体内。

每帧渲染不断改变纹理的 offset 和圆柱体的 rotation。

此外,我们不是直接贴的图,而是把它作为透明度通道,这样可以实现变色效果,结合 HSL 改变色相的方式来变色。

最后,还加了一个立方体的几何体作为时光机底座。

用 Three.js 画一个时光机,一起穿越时空隧道吧。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-07-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 神光的编程秘籍 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档