阅读 344

使用three.js 画地球🌍

先看看基础效果叭

image.png image.png

demo地址

前言

本章我们将做一个可拖拽,开始时自传,点击城市可以动的地球demo

首先需要知道什么是 three.js。 简单的说,three.js 是一个非常优秀的 WebGL 开源框架, three(3d) + js(javaScript)--了解 three.js。

而 WebGL 是在浏览器中实现三维效果的一套规范。

在 three.js 中有三大大重要的概念需要先了解一下:

  • 场景(scene)
  • 相机(camera)
  • 渲染器(renderer)

1、scene

在 WebGL 世界里,场景是一个非常重要的概念,它是存放所有物体的容器。在 three.js 里面新建一个场景很简单,new THREE.Scene 实例就好了。代码如下:

const scene = new THREE.Scene();  // 场景只有一种
复制代码

2、camera

const camera = new THREE.PerspectiveCamera(75, 
window.innerWidth/window.innerHeight, 0.1, 1000)
复制代码

PerspectiveCamera( fov, aspect, near, far ) 参数解析。

  • fov(Number): 仰角的角度
  • aspect(Number): 截平面长宽比,多为画布的长宽比。
  • near(Number): 近面的距离
  • far(Number): 远面的距离

3、renderer

渲染器决定了渲染的结果应该画在页面的什么元素上面,并且以怎样的方式来绘制。

const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
复制代码

开始画地球

1.实例化一个 scence 对象, 用来存放我们的地球实体。

function initScene() {
    scene = new THREE.Scene();
    scene.opacity = 0;
    scene.transparent = true;
}
复制代码

2.准备搭建 camera 的位置,和调节角度,在 three.js 里面采用给的是右手坐标系

const camera = new THREE.PerspectiveCamera(75, 
window.innerWidth/window.innerHeight, 0.1, 1000)
复制代码

这里采用的是透视相机。视角越大,看到的场景越大,那么中间的物体相对于整个场景来说,就越小了。

3.渲染器 renderer

function initThree() {
    // 实例化 THREE.WebGLRenderer 对象。
    renderer = new THREE.WebGLRenderer({
        antialias: true, // 抗锯齿
        alpha: true, // 强度
        canvas: renderer
    });
    // 设置 renderer 的大小
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 挂载到准备的 domElement 上
    renderHelper(renderer.domElement)
    // Sets the clear color and opacity.
    renderer.setClearColor(0x000000, 1.0);
}
复制代码

4.画地球 ,这里的地球其实就是在一个球体上贴上带有地球纹路的贴纸

// 地球
function initEarth() {
    // 实例化一个半径为 200 的球体
    const earthGeo = new THREE.SphereGeometry(200, 100, 100);
    const earthMater = new THREE.MeshPhongMaterial({
        specular: 0x404040,
        shininess: 5,
        map: textureLoader.load('/earth.jpg'),
        // 镜面反射
        specularMap: textureLoader.load('/earth_spec.jpg'),
        // 用于创建凹凸贴图的纹理
        bumpMap: textureLoader.load('/earth_bump.jpg')
    });
    earthMesh = new THREE.Mesh(earthGeo, earthMater);
    earthMesh.rotation.y = -correctRotate;
    earthMesh.name = "earth"
    scene.add(earthMesh);
}
复制代码

5.地球加上云层

const cloudsMesh;
function initClouds() {
    // 实例化一个球体,半径要比地球的大一点,从而实现云飘咋地球上的感觉
    var cloudsGeo = new THREE.SphereGeometry(201, 100, 100);
    // transparent 与 opacity 搭配使用,设置材质的透明度,当 transparent 设为 true 时, 会对材质特殊处理,对性能会有些损耗。
    var cloudsMater = new THREE.MeshPhongMaterial({
        alphaMap: new THREE.TextureLoader().load('/clouds.jpg'),
        transparent: true,
        opacity: 0.2
    });
    cloudsMesh = new THREE.Mesh(cloudsGeo, cloudsMater);
    scene.add(cloudsMesh);
}
复制代码

6.给世界来点光

function initLight() {
    // 位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。
    // light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);

    // 环境光
    const allLight = new THREE.AmbientLight(0xFFFFFF);
    allLight.position.set(100, 100, 200);

    // 平行光 位置不同,方向光作用于物体的面也不同,看到的物体各个面的颜色也不一样
    light = new THREE.DirectionalLight(0xffffbb, 1);
    light.position.set(-11, 3, 1);

    const sun = new THREE.SpotLight(0x393939, 2.5);
    sun.position.set(-15, 10, 21);

    scene.add(allLight, light, sun);
}
复制代码

7.坐标

/**
 *lng:经度
 *lat:维度
 *radius:地球半径
 */
function Point(lng, lat, radius) { 
    // 将经纬度转换为three中的坐标
    const lg = THREE.Math.degToRad(lng),
        lt = THREE.Math.degToRad(lat);
    const y = radius * Math.sin(lt);
    const temp = radius * Math.cos(lt);
    const x = temp * Math.sin(lg);
    const z = temp * Math.cos(lg);
    return {x: x, y: y, z: z}
}

function createPointMesh() {
    // 打点
    LOCATIONS.forEach(location => {
            //设置坐标
            const sprite = new THREE.Sprite(new THREE.SpriteMaterial(
                {
                    map: textureLoader.load('/location.png'),
                    depthWrite: false, //禁止写入深度缓冲区数据
                }));
            const pos = Point(location.coord[1], location.coord[0], 200 * 1.05)
            sprite.coord = {lg: location.coord[1], lt: location.coord[0]}
            sprite.position.set(pos.x, pos.y, pos.z);
            sprite.scale.set(20, 20, 1);
            sprite.name = location.name
            //城市名
            const spriteText = new THREE.Sprite(new THREE.SpriteMaterial(
                {
                    color: 0x000000,
                    map: textureLoader.load(`/i_${(location.name).toLowerCase()}.png`),
                    depthWrite: false, //禁止写入深度缓冲区数据
                }));
            spriteText.position.set(pos.x, pos.y, pos.z);
            spriteText.scale.set(50, 50, 1.5);
            locationGroup.add(sprite, spriteText);
            scene.add(locationGroup);
        }
    )
}
复制代码

8.添加点击、旋转控制

function startControl() {
    document.addEventListener('mousedown', onPointClick);
    // 载入控制器
    controls = new OrbitControls(camera, renderer.domElement);
}

function onPointClick(e) {
    e.preventDefault();
    // 鼠标点击位置的屏幕坐标转换成threejs中的标准坐标-1<x<1, -1<y<1
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
    raycaster.setFromCamera(mouse, camera);
    // 获取raycaster直线和所有模型相交的数组集合
    const intersects = raycaster.intersectObjects(locationGroup.children, true);
    if (intersects.length > 0 && intersects[0].object.name) {
        // 只取第一个相交物体
        if (city) city.scale.set(20, 20, 1);
        city = intersects[0].object;
        rotateToCenter(city)
    }
}

function rotateToCenter(city) {
    // 放大
    city.scale.set(30, 30, 1);

    // 显示城市名
    const cityName = city.name;
    const cityText = document.querySelector(".showCity");
    setTimeout(function () {
        cityText.innerText = cityName;
    }, 500)

    // 旋转到中心
    const rotateRad = rotate2Center(city.coord);
    let finalY = -rotateRad.y;
    while (earthMesh.rotation.y > 0 && finalY + Math.PI * 2 < earthMesh.rotation.y) finalY += Math.PI * 2;
    while (earthMesh.rotation.y < 0 && finalY - Math.PI * 2 > earthMesh.rotation.y) finalY -= Math.PI * 2;
    if (Math.abs(finalY - earthMesh.rotation.y) > Math.PI) {
        if (finalY > earthMesh.rotation.y) finalY -= Math.PI * 2;
        else finalY += Math.PI * 2;
    }
    const needRotateX = rotateRad.x - earthMesh.rotation.x + controls.object.rotation.x
    const needRotateY = finalY - earthMesh.rotation.y - correctRotate + controls.object.rotation.y
    rotateEarth(needRotateX, needRotateY);
}

// 转换为弧度
function rotate2Center(coord) {
    return {x: degToRad(coord.lt), y: degToRad(coord.lg)};
}

// 转动操作
function rotateEarth(intervalX, intervalY) {
    if (tween) tween.stop();
    tween = new TWEEN.Tween({
        rotateY: earthMesh.rotation.y,
        rotateX: earthMesh.rotation.x,
        rotateLoc: locationGroup.rotation.y
    })
        .to({
            rotateY: earthMesh.rotation.y + intervalY,
            rotateX: earthMesh.rotation.x + intervalX,
            rotateLoc: locationGroup.rotation.y + intervalY
        }, 1000);
    tween.easing(TWEEN.Easing.Sinusoidal.InOut);
    const onUpdate = function () {
        earthMesh.rotation.y = this._object.rotateY;
        earthMesh.rotation.x = this._object.rotateX;
        locationGroup.rotation.y = this._object.rotateLoc;
        locationGroup.rotation.x = this._object.rotateX;
    }
    const onComplete = function () {
    }
    tween.onUpdate(onUpdate);
    tween.onComplete(onComplete);
    tween.start();
}

function rotateToCity(cityName) {
    city = locationGroup.children.find((location) => {
        return location.name === cityName
    })
    rotateToCenter(city)
}
复制代码

9.动画与自转

function animate() {
    stats.update();
    renderer.clear();
    
    earthMesh.rotate.y+=0.05
    locationGroup.rotate.y+=0.05
    
    renderer.render(scene, camera);
    requestAnimationFrame(() => {
        TWEEN.update();
        if (controls) {
            controls.update();
            animate()
        }
    });
}
复制代码

完整代码

city.js

export const LOCATIONS = [{
    name: 'SHANGHAI',
    coord: [30.40, 120.52] // 30° 40' N, 120° 52' E
}, {
    name: 'BEIJING',
    coord: [39.92, 116.46] // 39° 92' N, 116° 46' E
}, {
    name: 'WASHINGTON',
    coord: [38.91, -77.02] // 38° 91' N, 77° 02' W
}, {
    name: 'MELBOURNE',
    coord: [-37.50, 144.58] // 37° 50' S, 144° 58' E
}, {
    name: 'RIO',
    coord: [-22.54, -43.12] // 22° 54' S,43° 12' W
},
    {
        name: 'LONDON',
        coord: [51.30, 0.5] // 22° 54' S,43° 12' W
    },
    {
        name: 'ANTARCTICA',
        coord: [-90,360]
    },
    {
        name: 'GREENLAND',
        coord: [75.930886,-40.253906]
    },
    {
        name: 'NAMIBIA',
        coord: [-22.57,17.086117]
    }
]
复制代码

renderHelper.js

export const renderHelper = (dom) =>{
    const el = document.querySelector('.showDemos')
    if (!el)return
    el.innerHTML = ''
    el.appendChild(dom)
}
复制代码

earthScreen.js

import * as THREE from "three";
import {renderHelper} from "../renderHelper";
import Stats from "three/examples/jsm/libs/stats.module";
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls";
import {LOCATIONS} from "./city";
import {TWEEN} from "three/examples/jsm/libs/tween.module.min";
import {degToRad} from "three/src/math/MathUtils";
import {useEffect} from "react";
import './city.css'

export const EarthScreen = () => {
    let scene, camera, renderer, earthMesh, light, controls, stats,
        tween, city = null
    const mouse = new THREE.Vector2();
    const locationGroup = new THREE.Group();
    const raycaster = new THREE.Raycaster()
    const textureLoader = new THREE.TextureLoader();
    const correctRotate = (Math.PI / 2).toFixed(2)

    useEffect(() => {
        threeStart();
        rotateToCity('SHANGHAI')
    }, [])

    function initScene() {
        scene = new THREE.Scene();
        scene.opacity = 0;
        scene.transparent = true;
    }

    function initCamera() {
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.z = 500;
    }

    // 渲染器
    function initThree() {
        // 实例化 THREE.WebGLRenderer 对象。
        renderer = new THREE.WebGLRenderer({
            antialias: true,
            alpha: true,
            canvas: renderer
        });
        // 设置 renderer 的大小
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 挂载到准备的 domElement 上
        renderHelper(renderer.domElement)
        // Sets the clear color and opacity.
        renderer.setClearColor(0x000000, 1.0);
    }

    // 帧蘋
    function initStats() {
        stats = new Stats();
        const container = document.querySelector('.showDemos')
        if (!container) return
        container.appendChild(stats.dom)
    }

    // 地球
    function initEarth() {
        // 实例化一个半径为 200 的球体
        const earthGeo = new THREE.SphereGeometry(200, 100, 100);
        const earthMater = new THREE.MeshPhongMaterial({
            specular: 0x404040,
            shininess: 5,
            map: textureLoader.load('/earth.jpg'),
            specularMap: textureLoader.load('/earth_spec.jpg'),
            bumpMap: textureLoader.load('/earth_bump.jpg')
        });
        earthMesh = new THREE.Mesh(earthGeo, earthMater);
        earthMesh.rotation.y = -correctRotate;
        earthMesh.name = "earth"
        scene.add(earthMesh);
    }

    // 光源
    function initLight() {
        // 位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。
        // light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);

        // 环境光
        const allLight = new THREE.AmbientLight(0xFFFFFF);
        allLight.position.set(100, 100, 200);

        // 平行光 位置不同,方向光作用于物体的面也不同,看到的物体各个面的颜色也不一样
        light = new THREE.DirectionalLight(0xffffbb, 1);
        light.position.set(-11, 3, 1);

        const sun = new THREE.SpotLight(0x393939, 2.5);
        sun.position.set(-15, 10, 21);

        scene.add(allLight, light, sun);
    }

    /**
     *lng:经度
     *lat:维度
     *radius:地球半径
     */
    function Point(lng, lat, radius) {
        const lg = THREE.Math.degToRad(lng),
            lt = THREE.Math.degToRad(lat);
        const y = radius * Math.sin(lt);
        const temp = radius * Math.cos(lt);
        const x = temp * Math.sin(lg);
        const z = temp * Math.cos(lg);
        return {x: x, y: y, z: z}
    }

    function createPointMesh() {
        // 打点
        LOCATIONS.forEach(location => {
                //设置坐标
                const sprite = new THREE.Sprite(new THREE.SpriteMaterial(
                    {
                        map: textureLoader.load('/location.png'),
                        depthWrite: false, //禁止写入深度缓冲区数据
                    }));
                const pos = Point(location.coord[1], location.coord[0], 200 * 1.05)
                sprite.coord = {lg: location.coord[1], lt: location.coord[0]}
                sprite.position.set(pos.x, pos.y, pos.z);
                sprite.scale.set(20, 20, 1);
                sprite.name = location.name
                //城市名
                const spriteText = new THREE.Sprite(new THREE.SpriteMaterial(
                    {
                        color: 0x000000,
                        map: textureLoader.load(`/i_${(location.name).toLowerCase()}.png`),
                        depthWrite: false, //禁止写入深度缓冲区数据
                    }));
                spriteText.position.set(pos.x, pos.y, pos.z);
                spriteText.scale.set(50, 50, 1.5);
                locationGroup.add(sprite, spriteText);
                scene.add(locationGroup);
            }
        )
    }

    function rotate2Center(coord) {
        return {x: degToRad(coord.lt), y: degToRad(coord.lg)};
    }

    function rotateEarth(intervalX, intervalY) {
        if (tween) tween.stop();
        tween = new TWEEN.Tween({
            rotateY: earthMesh.rotation.y,
            rotateX: earthMesh.rotation.x,
            rotateLoc: locationGroup.rotation.y
        })
            .to({
                rotateY: earthMesh.rotation.y + intervalY,
                rotateX: earthMesh.rotation.x + intervalX,
                rotateLoc: locationGroup.rotation.y + intervalY
            }, 1000);
        tween.easing(TWEEN.Easing.Sinusoidal.InOut);
        const onUpdate = function () {
            earthMesh.rotation.y = this._object.rotateY;
            earthMesh.rotation.x = this._object.rotateX;
            locationGroup.rotation.y = this._object.rotateLoc;
            locationGroup.rotation.x = this._object.rotateX;
        }
        const onComplete = function () {
        }
        tween.onUpdate(onUpdate);
        tween.onComplete(onComplete);
        tween.start();
    }

    function onPointClick(e) {
        e.preventDefault();
        // 鼠标点击位置的屏幕坐标转换成threejs中的标准坐标-1<x<1, -1<y<1
        mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
        mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        // 获取raycaster直线和所有模型相交的数组集合
        const intersects = raycaster.intersectObjects(locationGroup.children, true);
        if (intersects.length > 0 && intersects[0].object.name) {
            // 只取第一个相交物体
            if (city) city.scale.set(20, 20, 1);
            city = intersects[0].object;
            rotateToCenter(city)
        }
    }

    function rotateToCenter(city) {
        // 放大
        city.scale.set(30, 30, 1);

        // 显示城市名
        const cityName = city.name;
        const cityText = document.querySelector(".showCity");
        setTimeout(function () {
            cityText.innerText = cityName;
        }, 500)

        // 旋转到中心
        const rotateRad = rotate2Center(city.coord);
        let finalY = -rotateRad.y;
        while (earthMesh.rotation.y > 0 && finalY + Math.PI * 2 < earthMesh.rotation.y) finalY += Math.PI * 2;
        while (earthMesh.rotation.y < 0 && finalY - Math.PI * 2 > earthMesh.rotation.y) finalY -= Math.PI * 2;
        if (Math.abs(finalY - earthMesh.rotation.y) > Math.PI) {
            if (finalY > earthMesh.rotation.y) finalY -= Math.PI * 2;
            else finalY += Math.PI * 2;
        }
        const needRotateX = rotateRad.x - earthMesh.rotation.x + controls.object.rotation.x
        const needRotateY = finalY - earthMesh.rotation.y - correctRotate + controls.object.rotation.y
        rotateEarth(needRotateX, needRotateY);
    }

    function rotateToCity(cityName) {
        city = locationGroup.children.find((location) => {
            return location.name === cityName
        })
        rotateToCenter(city)
    }

    function startControl() {
        document.addEventListener('mousedown', onPointClick);
        // 载入控制器
        controls = new OrbitControls(camera, renderer.domElement);
    }

    function threeStart() {
        initThree();
        initScene();
        initCamera();
        initStats();
        initLight();
        initEarth();
        createPointMesh()
        startControl()
        animate();
    }

    function animate() {
        stats.update();
        renderer.clear();
        renderer.render(scene, camera);
        requestAnimationFrame(() => {
            TWEEN.update();
            if (controls) {
                controls.update();
                animate()
            }
        });
    }

    return <div className='showCity'/>
}
复制代码
文章分类
前端
文章标签