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

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

相关代码能够由此github查看html

本篇介绍

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

1. 绘制一个木块

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

下载一个木块的图片

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

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

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

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.pngapp

完整代码:dom

<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地球了, 会涉及一些数学知识, 好比三角函数你是否已经不会背了, 那我就带你研究?     此次就是这样, 但愿和你一块儿进步。

相关文章
相关标签/搜索