关于从入门three.js到做出3d地球这件事(第四篇: 贴图地球)

lulu_up
English

关于从入门three.js到做出3d地球这件事(第四篇: 贴图地球)

相关代码可以由此github查看

本篇介绍

     通过前三篇的学习基础知识我们已经储备差不多了, 这一篇我们要做一个贴图地球, 这种地球也是不少公司现在在使用的方案, 但缺点也比较明显就是它无法精准的选中某个国家, 所以一些精细的操作它做不到, 但是学习这个技术依旧是一件令人愉快的事情, 也没准你不需要选中国家的功能, 闲言少叙我们全军出击吧。

1. 绘制一个木块

     我们这一篇只讨论规则的矩形木块, 生活中更常见的不规则木块我们在3d模型篇再聊, 绘制木块的原理就是先生成geometry, 把它的材质定义为木头图片, 使其材质均匀有规则的分布在geometry表面, 这样在我们眼里就成了木块。

下载一个木块的图片

     我是直接百度的一张木头纹理图片, 你也可以用我这张, 同时新建一个img文件夹用来存放图片。
image

新的概念 "加载器"
const loader = new THREE.TextureLoader();

     上面代码我们生成了一个加载器, 可以用这个实例进行一系列的加载操作, 内部使用ImageLoader来加载文件, 顾名思义Texture是纹理的意思所以可以叫它纹理加载器,后面章节降到加载3d模型的时候还会介绍更多的加载器
image.png

const loader = new THREE.TextureLoader();
        loader.load(
            './img/木块.jpeg',
            (texture) => {
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )
  1. 第一个参数要加载的资源的路径。
  2. 第二个参数加载成功后的回调, 会返回纹理对象。
  3. 第三个参数进度, 将在加载过程中进行调用。参数为XMLHttpRequest实例,实例包含total和loaded字节, 请注意three.js r84遗弃了TextureLoader进度事件, 我们其实可以填undefined
  4. 第四个参数错误的回调。

当前我们直接打开我们的html文件他会报如下的错误:

image.png

     每次遇到这种跨域报错, 我们第一时间应该想到把资源放在服务器上, 但是当前有更简洁的方式。

配置vscode插件

image.png
在我们的项目页面点击右下角的 Go live 启动一个服务。
image.png
此时我们就可以得到如下的效果:
image.png
image.png

完整代码:

<html>
<body>
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script src="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 20;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);

        const axisHelper = new THREE.AxisHelper(4)
        scene.add(axisHelper)

        // 为物体增加材质
        const loader = new THREE.TextureLoader();
        loader.load(
            './img/木块.jpeg',
            (texture) => {
                console.log(texture)
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度(已废弃)
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )

        const animate = function () {
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        animate();

    </script>
</body>

</html>

2. 纹理属性的详谈

     我们来谈谈纹理的几个属性吧, 木块的图片想看出差别不明显, 我们在img文件夹里面再放一张鸣人的图片。
image

代码里面我们只改路径即可。

loader.load(
    './img/螺旋丸.jpeg',
    (texture) => {
    ...//

image.png
image.png

     从上图我们可以看出, 六个面上都是完整的图片, 但是由于宽高比的不同图像被相应的压缩, 接下来我们就介绍几个比较常用的属性。

重复repeat

     我们把加载到的纹理进行处理texture.repeat.x = 0.5定义他的x轴重复值。
image.png
把它的数值调大至5。
image.png

     从上面的效果可以看得出, 这个repeat.x类似在物体x轴方向的画面个数, 也就是说0.5就是x轴方向铺满需要0.5个图片, 5就是需要5张图片才能充满, 那么与之相对的就是y轴的重复正如下图:
image.png
image.png
这看起来像个礼品盒的绳子, 那么接下来我们让这个图铺满表面。

回环wrapS wrapT

t 是图片的y轴我们设置一下:

texture.wrapT = THREE.RepeatWrapping;

image.png
同理设置x轴, 注意x轴叫s:

textureObj.wrapS = THREE.RepeatWrapping

image.png

纹理不是我们这个系列的重点就不扩展了, 有兴趣的同学自己玩一玩
完整代码如下:

<html>
<body>
    <script src="https://cdn.bootcdn.net/ajax/libs/three.js/r122/three.min.js"></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
    <script src="../utils/OrbitControls.js"></script>
    <script>
        const scene = new THREE.Scene();
        const camera = new THREE.PerspectiveCamera(35, window.innerWidth / window.innerHeight, 1, 1000);
        camera.position.z = 14;
        const renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0xffffff)
        orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
        document.body.appendChild(renderer.domElement);
        const axisHelper = new THREE.AxisHelper(4)
        scene.add(axisHelper)
        // 为物体增加材质
        let textureObj = null;
        const loader = new THREE.TextureLoader();
        loader.load(
            './img/螺旋丸.jpeg',
            (texture) => {
                textureObj = texture;
                const material = new THREE.MeshBasicMaterial({
                    map: texture
                })
                const geometry = new THREE.BoxGeometry(2, 2, 1);
                // 加入纹理
                const mesh = new THREE.Mesh(geometry, material)
                // 放入几何
                scene.add(mesh);
            },
            (xhr) => {
                // 进度(已废弃)
                console.log(`${xhr.loaded / xhr.total * 100}%`)
            },
            (err) => {
                // 错误
                console.log(err)
            }
        )
        const pames = {
            repeatx: 5,
            repeaty: 5,
        }
        function createUI() {
            const gui = new dat.GUI();
            gui.add(pames, "repeatx", 0, 5).name("repeatx")
            gui.add(pames, "repeaty", 0, 5).name("repeaty")
        }
        const animate = function () {
            if (textureObj) {
                textureObj.repeat.x = pames.repeatx
                textureObj.repeat.y = pames.repeaty
                textureObj.wrapT = THREE.RepeatWrapping;
                textureObj.wrapS = THREE.RepeatWrapping
            }
            requestAnimationFrame(animate);
            renderer.render(scene, camera);
        };
        createUI()
        animate();
    </script>
</body>
</html>

3. 搭建vue项目(主线任务终于开始)

     初始化一个干净的vue项目, 这个过程我就不在这里说了, 我们就从引入three.js开始, 这里要十分注意three.js的版本很重要, 同样的逻辑在不同版本里面效果竟然不一样, 所以想要和本篇一样编写代码的同学可以和我暂时统一版本:

yarn add three@0.123.2 

App.vue改装成如下的样子

<template>
  <div id="app">
    <cc-map id="map"></cc-map>
  </div>
</template>

<script>
import ccMap from "./components/cc_map.vue";
export default {
  name: "App",
  components: {
    ccMap,
  },
};
</script>

<style>
#app {
  overflow: hidden;
  border: 1px solid #ccc;
  width: 700px;
  height: 600px;
  margin: 20px auto;
}
</style>

从上面代码可以看出, <cc-map></cc-map>这个就是我第一篇文章里提到的专门的vue组件, 接下来的篇章里我们就都是围绕着开发这个组件的功能了, 除非零散的知识点我会单开一个html文件讲, 大部分都是主线任务了。

暂时新建这样三个文件夹与文件。
image.png

4. 要使用的贴图

思否不让上传超过4M的图, 所以下面是个模糊的截图, 想看原图的盆友可以看我项目里的, 这里的图片处于assets > images的位置。

image.png

config > earth.config.js内配置两个参数。

export default {
    r: 80, // 半径
    earthBg: require("../assets/images/地图.png"), // 贴图路径
}

当前初步components > cc_map.vue的模板结构, 注意习惯引入'three'的方式。

<template>
  <div class="map" ref="map"></div>
</template>

<script>
import * as THREE from "three";
import envConifg from "../config/earth.config";

export default {
  name: "ccMap",
  data() {
    return {
    };
  },
  methods: {
  },
  mounted() {
  },
};
</script>

<style scoped>
.map {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
}
</style>

5. 把基础环境的搭建抽成方法

都是之前篇章提到的方法, 先把data数据初始化好

data() {
    return {
      scene: null,
      camera: null,
      mapDom: null,
      renderer: null,
      orbitControls: null,
      object: new THREE.Object3D(),
      axisHelper: new THREE.AxesHelper(120),
      textureLoader: new THREE.TextureLoader(),
    };
  },
第一步: 初始场景使用initTHREE (之后基本不改)
initTHREE() {
  this.renderer = new THREE.WebGLRenderer({
    antialias: true,
  });
  this.mapDom = this.$refs.map;
  this.renderer.setSize(this.mapDom.clientWidth, this.mapDom.clientHeight);
  this.renderer.setClearColor(0xffffff, 1.0);
  this.mapDom.appendChild(this.renderer.domElement);
},
第二步: 初始相机使用initCamera(之后基本不改)
initCamera() {
  this.camera = new THREE.PerspectiveCamera(
    45,
    this.mapDom.clientWidth / this.mapDom.clientHeight,
    1,
    2000
  );
  this.camera.position.z = 300;
  this.camera.up.set(0, 1, 0);
  this.camera.lookAt(0, 0, 0);
},
第三步: 初始容器使用initScene(之后基本不改)
this.scene = new THREE.Scene();
第四步: 初始辅助线使用initAxisHelper(之后基本不改)
this.scene.add(this.axisHelper);
第五步: 初始光源使用initLight(之后基本不改)
const ambientLight = new THREE.AmbientLight(0xffffff);
this.scene.add(ambientLight);

后期可以模拟太阳光照射, 到时候我们加个平型光就很像回事了。

第六步: 初始轨道使用initOrbitControls(之后基本不改)
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// ...
initOrbitControls() {
  const os = new OrbitControls(this.camera, this.renderer.domElement);
  os.target = new THREE.Vector3(0, 0, 0); //控制焦点
  os.autoRotate = false; //将自动旋转关闭
  os.enablePan = false; // 不禁止鼠标平移, 可以用键盘来平移
  os.maxDistance = 1000; // 最大外移动
  os.minDistance = 100; // 向内最小外移动
  this.orbitControls = os;
},
第七步: 初始地球背景使用initBg
  • 之后会有一张专门讲物体的绘制的, 到时候我们再详聊圆形

    initBg() {
      // 把背景图加载过来当做纹理。
      const texture = this.textureLoader.load(envConifg.earthBg);
      // 这个绘制球体
      const geometry = new THREE.SphereGeometry(envConifg.r, 50, 50);
      // 放入纹理
      const material = new THREE.MeshLambertMaterial({
        map: texture,
      });
      const mesh = new THREE.Mesh(geometry, material);
      this.scene.add(mesh);
    },
第八步: 初始渲染函数使用glRender
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.glRender);

这里肯定不能直接叫render

end: 开关模式
  mounted() {
    this.initTHREE();
    this.initCamera();
    this.initScene();
    this.initAxisHelper();
    this.initLight();
    this.initOrbitControls();
    this.initBg();
    this.glRender();
  },

image.png
image.png

     这里的贴图地图其实已经可以满足部分的需求场景了, 不要看它简单它也可以很炫的。

6. ps加文字, 但会扭曲

     贴图地球有它的局限性, 比如上面地图上现在是空空的没有相应的国家名, 但是如果我在图片中ps上国家名, 让我们看看效果。
image.png
     ps上终究不是最灵活的办法, 而且如果你仔细看会发现文字有点向上弯曲, 因为图片是附着在球体上的, 所以越靠近南北极越会聚成一个点, 所以这样加文字的模式只针对少数面积大并在赤道附近的国家有用。

7. 有意思的球体

上面我们设置的球体我们单独拿出来玩一下, 这里我们只聊前三个参数, 后面会有专门介绍几何体的文章

![image.png](/img/bVcRbTv)
  1. r就是半径, 这个决定了球体的大小。
  2. 水平分段数(沿着经线分段),最小值为3,默认值为8, 比如说一个圆圈由100个点互相线段链接组成, 那么这参数就是这个100。
  3. 垂直分段数(沿着纬线分段),最小值为2,默认值为6。

来吧展示: 当我把水平分段数变成5new THREE.SphereGeometry(envConifg.r, 5, 50);
image.png
image.png

来吧展示: 当我把垂直分段数变成5new THREE.SphereGeometry(envConifg.r, 50, 5);
image.png
image.png

8. 贴图地球的局限性

  1. 如上面所说, 很难为国家区域加名称。
  2. 无法具体的选中某个国家。
  3. 无法让某个地区高亮或者出现红色边框。
  4. 视角拉近之后有些失真。
  5. 无法悬停显示详情信息
这里是发的对比

1x
image.png
2x
image.png
3x
image.png

end.

     下一篇开始正式绘制我们的矢量3d地球了, 会涉及一些数学知识, 比如三角函数你是否已经不会背了, 那我就带你研究?
     这次就是这样, 希望和你一起进步。

阅读 2.7k

自信自律, 终身学习.

4.6k 声望
4.6k 粉丝
0 条评论

自信自律, 终身学习.

4.6k 声望
4.6k 粉丝
文章目录
宣传栏