關於從入門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地球了, 會涉及一些數學知識, 好比三角函數你是否已經不會背了, 那我就帶你研究?     此次就是這樣, 但願和你一塊兒進步。

相關文章
相關標籤/搜索