零基础也能玩转3D!手把手教你用Three.js打造会转的地球

570 阅读10分钟

引言:

Three.js 是一个用于在网页浏览器中创建和显示3D图形的JavaScript库。它基于WebGL技术,简化了复杂的3D渲染过程,使得开发者不需要深入了解WebGL的底层API就能快速上手创建丰富的3D内容。Three.js提供了大量的功能,包括但不限于几何体、材质、光源、相机、动画和加载器等,这些功能被封装成易于使用的接口,大大降低了3D图形编程的门槛。

本文就带着小白一步步来实现创造一个3D地球,并且可以自旋转和鼠标控制水平旋转。

效果如图:

2024-12-24-2.gif

实战:

而要实现这个页面效果必不可少的就是Three.js库。

导入Three.js:

首先我们使用CDN来导入three.js,在<head></head>标签中添加以下代码:

<script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>

或者去官方网站去查询如何从CDN导入,也可直接安装Three.js包:安装 – three.js docs

创建画布:

首先我们要创建一个用于绘制3D图形的画布(即:3D的绘制容器),而这个画布就是<canvas>元素,并且设置其id属性来方便引用。所以在 <body>的第一行添加如下

<canvas id="webglcanvas"></canvas>

脚本部分:

我们要展现一个3D地球,但是在我们页面显示的其实相当于一个镜头拍摄一个立体图形得到的效果。既然我们要拍摄,那当然少不了一些必要的设备与环境了。

let canvas,
    camera,
    scene,
    renderer,
    group;

这里我们一一介绍实例化了哪些对象:

  • canvas:绘制图形的容器,Three.js 将在这个元素上渲染3D场景。
  • camera:Three.js 中的相机对象,定义了观察者的位置和视角,它决定了场景中哪些部分是可见的。
  • scene:Three.js 中的场景对象,包含了所有的3D模型、光源、组等。
  • renderer:Three.js 中的渲染器对象,负责将场景的3D对象转换为2D图像,并渲染到canvas元素上。
  • group:Three.js 中的组对象,组对象是一个容器,可以用来组织和管理多个3D对象,可以将多个3D对象添加到一个组中进行操作。

上述操作完成后我们还需要再实例化一些对象,在后续中会用上。

let mouseX = 0,// 存储鼠标坐标
    mouseY = 0;

let windowHalfX = window.innerWidth / 2,// 计算窗口的中心点坐标,x、y轴都是窗口的一半
    windowHalfY = window.innerHeight / 2;

接下来就是创建3D场景了。其实这个过程就像拍电影一样,而我们就是那个导演,现在我们就开启拍摄计划,创建一个init()函数来存储详细代码。

首先先获取canvas元素。

canvas = document.getElementById('webglcanvas');

摄影组 OK?

我们页面展现的效果主要是由相机来"拍照"呈现的,所以我们需要确定相机的位置,这里我们需要选择合适的位置,要不然给大明星拍的不好看可是会摆架子哦~。毕竟不能随便选个位置就开拍了,所以实例化相机我们需要一些参数,让摄影师知道如何调整相机的位置,例如:

// camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 2000);
// 相机离场景的距离,越大我们里场景中的物体越远
camera.position.z = 500;
  • 视野角度(FOV) :其就是无论在什么时候我们能在显示器上看到的场景的范围的角度。
  • 长宽比(aspect ratio):就是你用一个物体的宽除以它的高的值。(即:横向视场 / 纵向视场)
  • 近截面(near):最近的渲染区域,当物体在比近界面更近时将不再被渲染到场景中。
  • 远截面(far):最远的渲染区域,当物体在比远界面更远时将不再被渲染到场景中。

这样讲想必各位可能还没有一个更直观的概念,不妨来看看下面的示例:

8e07824e424be0839a3227b5b2277157.png

而我们相机呈现的效果可以在官方的示例里直观的看到(three.js examples),各位不妨将上面的图片结合来观看,说不定一下就大彻大悟了:

2024-12-24.gif

场景 OK?

设置完相机后我们就要来看看拍摄场景如何安排了,先实例化场景,也调节场景的一些样式。

scene = new THREE.Scene();// 实例化场景
scene.background = new THREE.Color(0xffffff);// 将背景色改为白色

经纪人 OK?

场景和相机都安排好了,那我们也是时候请"演员团队的经纪人"入场了,实例化用于管理3D对象的组,并且将其添加到场景中。

group = new THREE.Group();// 实例化组对象
scene.add(group);// 将组对象添加到场景中

美工组 OK?

但是这样演员就可以入场了吗?当然不行,我们的化妆师和服装师还没来呢,所以我们需要实例化纹理加载器,要不然怎么给大明星化美美的妆!既然要手搓地球,没有地球的图片怎么行呢。将下面的图片存入项目文件夹中,会用上的哦~😘

lQDPJwNaaT1wC3nNBADNCACwjtceHAzRxRMHT8GrIzXUAA_2048_1024.jpg

let loader = new THREE.TextureLoader();// 实例化纹理加载器
loader.load('图片文件名', function(texture) {}) // texture就是纹理(传入的图片)

有请大明星入场!!

现在万事俱备,那么就让我们掌声欢迎我们的"大明星😎"入场了,在function(texture) {}内我们就需要创建3D对象了。首先需要创建一个几何球体,并且设置其纹理(化妆),这样就完成了实例化材质。

let geometry = new THREE.SphereGeometry(200, 20, 20);// 实例化球体几何
let material = new THREE.MeshBasicMaterial({
    map: texture,// 设置纹理
});// 实例化材质

但是这样完全不够,我们需要将对应几何体与纹理结合,并且统一交给各自经纪人管理,所以要实例化一个可以绘制的3D网格来结合,并且添加到组中。

let mesh = new THREE.Mesh(geometry, material);// 实例化可绘制的3D网格
group.add(mesh);// 将网格添加到组对象中

那么接下来就可以开拍了,但是这是我们好不容易请到的大明星呀!怎么说也要拍的该帅气帅气,该漂亮漂亮,那怎么办呢?哎😏,咱美化一下拍摄出来的2D图像不就好了,这里我们就需要使用渲染器了,通过其将场景中的3D对象(如几何体、材质、光源等)转换为2D图像,并在浏览器中显示出来。

// 渲染器
renderer = new THREE.WebGLRenderer({
    canvas: canvas,// 指定渲染器的目标元素 <canvas>
    antialias: true// 启动抗锯齿
});// 实例化渲染器
renderer.setSize(window.innerWidth, window.innerHeight);// 设置渲染器的大小
// 将渲染器的大小为与窗口相同,就可以确保渲染的图像能够填满整个窗口

调整好渲染器后当然要进行场景渲染了。

renderer.render(scene, camera);

这样我们就写完了init()函数的内容,最后别忘了调用init()函数,看看效果。

image.png

不错是不错,但是这个地球它不动呀,这怎么行!哪个观众来看电影是来看一个照片放几个小时吗?哎~接下来我们就要让它动起来。让我们把前面的renderer.render(scene, camera);注释掉。

动起来?转起来!

我们要创造一个动画,而动画可以理解为什么,是一帧一帧的图片替换而形成的动画效果,那么我们是不是能使用一个回调函数来调用动画函数从而实现平滑的动画效果,而恰好我们就有这样的一个方法能实现这样的效果---requestAnimationFrame,其会根据屏幕的刷新率来调用回调函数,通常每秒调用60次(对于60Hz的显示器)。

function animate() {
    // 递归调用 按照屏幕的刷新率同步,例如,60Hz的显示器每秒会调用60次)
    requestAnimationFrame(animate);
    render()// 更新场景并渲染到画布上(即:更新图片)
}

显而易见,接下来肯定是来写render()函数来渲染场景了,首先我们要让相机始终看向场景中心。那我们要实现怎样的效果呢?不如让地球转起来,就像一个地球仪一样。

function render() {
    camera.lookAt(scene.position); // 相机始终看向场景的中心点
    group.rotation.y += 0.005; // 使场景中的物体(地球)绕y轴旋转,而调整0.005越大越快
    renderer.render(scene, camera); // 渲染场景
}

并且在init()后添加animate()函数,这样我们的页面中的地球就转起来了呀。

2024-12-24.gif

但是这就满足了吗?有哪个观众不想操控电影中的情节(经典我上早干嘛干嘛了),所以我们要实现让地球不仅能自己转,还能让我们通过鼠标操控着转!

转起来?用起来!

首先我们是不是需要让页面知道我们什么时候按下了鼠标,什么时候松开了鼠标,所以先创建两个函数分别为按下和松开鼠标的情况。

function onDocumentMouseDown(event) { // 按下时被调用
    isMouseDown = true; // isMouseDown 变量被设置为 true,表示鼠标当前处于按下状态。
}

function onDocumentMouseUp(event) { // 松开时被调用
    isMouseDown = false; // isMouseDown 变量被设置为 false,表示鼠标当前处于松开状态。
}

我们按下鼠标是为了做什么,是不是要移动鼠标并且实现我们视角的移动(相当于变相移动图形),那我们就需要知道按下鼠标后,鼠标当前的位置,所以在onDocumentMouseDown()函数中添加一个对象来监控鼠标当前的位置。

function onDocumentMouseDown(event) {
    isMouseDown = true;
    previousMousePosition = {
        x: event.clientX,
        y: event.clientY
    };
}

并且我们需要一个函数来实现鼠标移动时所需要实现的效果,所以创建一个onDocumentMouseMove()函数,而且需要注意的时只有当鼠标按下时发生移动才会发生改变,所以我们需要判断鼠标是否按下。

function onDocumentMouseMove(event) {
    if (isMouseDown) { // 前面函数中使用isMouseDown来监控鼠标是否按下
        
    }
}

接下来就是写按下且移动后发生的改变了,我们要让相机发生移动从而达到效果,那么相机移动的距离是我们不可或缺的数据,通过onDocumentMouseDown()函数中的previousMousePosition对象是不是可以知道鼠标按下时的位置,那么我们就可以将移动的距离设置为按下鼠标时的位置到移动鼠标后的位置吗,并且实时更新位置信息。

let deltaMove = { // x、y轴上的移动距离移动
    x: event.clientX - previousMousePosition.x,
    y: event.clientY - previousMousePosition.y
};

previousMousePosition = {
    x: event.clientX,
    y: event.clientY
};

在得到了具体数据后就可以来操控相机的旋转角度了,并且我们需要确保相机与地球的位置不变,这样就可以有更好的体验。

 // 根据鼠标移动的距离更新相机的旋转角度
 camera.rotation.y += deltaMove.x * 0.005; // 水平旋转

 // 确保相机与地球的距离保持不变(通过三角函数)
 camera.position.x = Math.sin(camera.rotation.y) * 500;
 camera.position.z = Math.cos(camera.rotation.y) * 500;

最后我们需要在init()函数的末尾处添加鼠标事件监听器。

canvas.addEventListener('mousedown', onDocumentMouseDown, false);
canvas.addEventListener('mouseup', onDocumentMouseUp, false);
canvas.addEventListener('mousemove', onDocumentMouseMove, false);

恭喜您,完成了小白入门的第一个项目🤓

奉上源码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>3D 地球</title>
    <script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
</head>
<body>
    <canvas id="webglcanvas"></canvas>
    <script>
        let canvas, 
            camera,
            scene,
            renderer,
            group;

        let mouseX = 0,
            mouseY = 0;

        let windowHalfX = window.innerWidth / 2, 
            windowHalfY = window.innerHeight / 2;

        let isMouseDown = false;
        let previousMousePosition = {
            x: 0,
            y: 0
        };

        function init() {
            canvas = document.getElementById('webglcanvas');
            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 2000);
            camera.position.z = 500;

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0xffffff);

            group = new THREE.Group();
            scene.add(group);

            let loader = new THREE.TextureLoader();
            loader.load('land_ocean_ice_cloud_2048.jpg', function(texture) {
                let geometry = new THREE.SphereGeometry(200, 20, 20);
                let material = new THREE.MeshBasicMaterial({
                    map: texture,
                });

                let mesh = new THREE.Mesh(geometry, material);
                group.add(mesh);

                renderer = new THREE.WebGLRenderer({
                    canvas: canvas,
                    antialias: true
                });
                renderer.setSize(window.innerWidth, window.innerHeight);

                canvas.addEventListener('mousedown', onDocumentMouseDown, false);
                canvas.addEventListener('mouseup', onDocumentMouseUp, false);
                canvas.addEventListener('mousemove', onDocumentMouseMove, false);
            });
        }

        function onDocumentMouseDown(event) {
            isMouseDown = true;
            previousMousePosition = {
                x: event.clientX,
                y: event.clientY
            };
        }

        function onDocumentMouseUp(event) {
            isMouseDown = false;
        }

        function onDocumentMouseMove(event) {
            if (isMouseDown) {
                let deltaMove = {
                    x: event.clientX - previousMousePosition.x,
                    y: event.clientY - previousMousePosition.y
                };

                previousMousePosition = {
                    x: event.clientX,
                    y: event.clientY
                };

                camera.rotation.y += deltaMove.x * 0.005;

                camera.position.x = Math.sin(camera.rotation.y) * 500;
                camera.position.z = Math.cos(camera.rotation.y) * 500;
            }
        }
        function render() {
            camera.lookAt(scene.position);
            group.rotation.y += 0.005;
            renderer.render(scene, camera);
        }

        function animate() {
            requestAnimationFrame(animate);
            render();
        }

        init();
        animate();
    </script>
</body>
</html>

---欢迎各位点赞、收藏、关注,如果觉得有收获或者需要改进的地方,希望评论在下方,不定期更新

0bae-hcffhsw0416753.gif