ThreeJs做智慧城市項目後記

demo展示效果

隨着時間的推移技術的進步,前端越來越雜了,但是也越來越精彩了。只是會用一點ThreeJs,對於WebGl的原理並沒瞭解過,這並不影響我們利用ThreeJs去做出一個非常炫酷的項目。

開始

新世界的大門打開啦!

寫在前面

  1. 不要因爲不瞭解就被這種3D展示的項目給嚇到 其實實現起來很簡單 很簡單 很簡單
  2. 城市模型一份 最好是gltf模型,obj模型也沒問題,我會介紹如何轉化與壓縮 PS:爲什麼只有這倆,因爲我寫這個項目只用到了這倆,處理的經驗也是針對這倆的,我項目中所用的模型是公司所有暫不能提供。
  3. 有一定ThreeJs的基礎 俗話說得好 萬丈高樓平地起嘛 如果沒有這方面基礎的同學也不要急 推薦一本書《THREE.JS開發指南》,有基礎也有提高 很棒
  4. 本文所示代碼大部分只是思路 我也是第一次上手用ThreeJs處理模型並應用到項目中,可能有少許不足之處,還望各路大神指正教導
  5. 項目進行一半的時候,因爲沒經驗,我發現讓建模看着地圖建模的思路是不對的,應該讓他們利用geoJson作爲地理數據,去建模,建造出來的更精確,而且可以利用地理座標和世界座標去關聯(猜想),利於項目開發,畢竟第一次,這個鍋我背了
  6. Threejs的文檔是不全的,很多控制器loader後期處理都沒有文檔,要自己多看看Threejsexamples,很多效果都可以基於Demo去實現
  7. 單頁面應用一定要清除ThreeJs 的創建的對象,避免內存泄露,能disposedispose,多個children的要遍歷remove掉 而且裏面的 materialgeometry也要刪掉,最近剛知道一個取消佔用的妙招,WEBGL_lose_context
  8. 後期處理對顯卡有一定要求
  9. 最好一次渲染,不要多次渲染
  10. 最好不要把和數據無關的掛在vue的data上,會造成不必要的性能浪費,因爲vue會深度遍歷給每個對象加getter和setter,不過在這個項目裏,我沒發現啥差別。

HTML部分

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <script src="../build/three.min.js"></script>
  </body>
</html> 

創建場景

首先,我們要祭出ThreeJs的最重要的幾大組件——scene(場景)camera(相機)renderer(渲染器)light(燈光),以及渲染的目標——container(就是DOM結構),老生常談,不多說

打個比方,scene就是舞臺,camera就是拍攝舞臺的攝像機,它能決定觀衆看到什麼,而一個舞臺沒有燈光的話它就是黑乎乎的,所以light就是舞臺上的各種燈光,所以舞臺上表演什麼,就是舞臺中有什麼,所以要加入到scene中 scene.add(“演員們(模型)”)

var camera, scene, renderer;
var container;
var ambientLight, pointLight;

// 初始化
init()
// 循環渲染每一幀  一幀一幀的 就是你打遊戲時的FPS
animate()

function init(){
	// 初始化相機 
	// 這裏使用的是透視相機來模擬人眼看到的效果 近大遠小
	camera = new THREE.PerspectiveCamera(
      45,
      window.innerWidth / window.innerHeight,
      1,
      2000
    );
    camera.position.z = 70;
    camera.position.x = 50;
    camera.position.y = 10;
	
	// 初始化場景
	scene = new THREE.Scene();

	// 初始化燈光
	// 環境光 能保持整體都是亮點
	ambientLight = new THREE.AmbientLight(0x404040)
	// 點光源 就像燈泡一樣的效果  白色燈光 亮度0.6
	pointLight = new THREE.PointLight(0xffffff, 0.6);

	// 將燈光加入到場景中
	scene.add(ambientLight)
	// 將燈光加到攝像機中 點光源跟隨攝像機移動
	// 爲什麼這樣做  因爲這樣可以讓後期處理時的輝光效果更漂亮 
	camera.add(pointLight);

	// 我們將攝像機加入到場景中
    scene.add(camera);

	// 初始化渲染器
	renderer = new THREE.WebGLRenderer({
	  // 開啓抗鋸齒
      antialias: true,
      // 開啓背景透明
      alpha: true
    });
    // 把自動清除顏色緩存關閉 這個如果不關閉 後期處理這塊會不能有效顯示
    // 書上的描述是 如果不這樣做,每次調用效果組合器的render()函數時,之前渲染的場景會被清理掉。通過這種方法,我們只會在render循環開始時,把所有東西清理一遍。
    renderer.autoClear = false;
    // 背景透明 配合 alpha
    renderer.setClearColor(0xffffff, 0);
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 伽馬值啓動 更像人眼觀察的場景
    renderer.gammaInput = true;
    renderer.gammaOutput = true;
	
	// 渲染到DOM中去
	container = document.createElement("div");
    container.appendChild(renderer.domElement);
    document.body.appendChild(container);
}
// 這樣一來,基礎場景創建就完成了,接下來我們來讓它循環渲染起來

function animate() {
   // 這個方法低版本瀏覽器兼容不好 可以從github上找些兼容庫 如果要兼容低版本瀏覽器
   requestAnimationFrame(animate);
   // 渲染我們的場景  攝像機啪啪啪的拍和錄
   // 由於把renderer autoClear  關閉了 所以我們要在渲染函數中手動清除
   renderer.clear();
   renderer.render(scene, camera);
 }
// ok 基礎部分完成 接下來我們來加載模型

加載城市模型

限於經驗和技術等各種外力因素影響,項目最開始時編寫demo使用的是Obj模型Mtl貼圖文件(不太確定貼圖文件的叫法是否準確),使用起來也很簡單(ThreeJs倉庫裏的webgl_loader_obj_mtl.html拿來改下就行了)

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <script src="../build/three.min.js"></script>
    <!-- 引入我們可愛的加載器 -->
    <script src="js/loaders/MTLLoader.js"></script>
    <script src="js/loaders/OBJLoader.js"></script>
    <script>
	  /* 省略創建場景部分的代碼 */

	  // 加載的過程 
	  var onProgress = function(xhr) {
          if (xhr.lengthComputable) {
            var percentComplete = (xhr.loaded / xhr.total) * 100;
            console.log(Math.round(percentComplete, 2) + "% downloaded");
          }
        };

      var onError = function() {
 		// 載入出錯時候
	  };

	  // 加載Mtl貼圖文件
      new THREE.MTLLoader()
        // 貼圖文件的路徑 
        .setPath("models/obj/male02/")
        .load("male02_dds.mtl", function(materials) {
          // 看代碼意思是預加載
          materials.preload();

		  // 加載OBJ模型
          new THREE.OBJLoader()
            // 設置OBJ模型的材質貼圖
            .setMaterials(materials)
            .setPath("models/obj/male02/")
            .load(
              "male02.obj",
              function(object) {
                object.position.y = -95;
                scene.add(object);
              },
              onProgress,
              onError
            );
        });
	</script>
  </body>
</html> 

這一步一般會出現的問題有如下

  1. 模型加載後,不顯示也不報錯?
    檢查場景是否正常渲染了,如果正常渲染模型的位置在哪裏,攝像機在哪裏,攝像機是否對着模型,燈光是否配置,模型是否太大或者太小了,超出了攝像機的攝影範圍……
  2. 模型可以正常加載,但是貼圖不顯示?
    首先檢查network是否報404錯誤,如果報錯,一般都是mtl貼圖文件(看起來像是雪碧圖那種)沒給你,或者路徑配置的不是相對路徑,如果貼圖沒錯誤,模型是黑色的,在mtl文件中可以更改kakd的三個值(對應rgb),或者打印出模型屬性,在material.color中更改點色值或別的屬性。黑色的時候,看不到貼圖。一般這樣一通操作之後,就能看到了模型了
  3. 模型文件太大了,瀏覽器在渲染的時候進程被完全卡死!要等待幾十秒之久!天吶!
    這個問題看起來比較棘手,其實很好解決。ThreeJs官方推薦gltf格式的模型在瀏覽器中渲染,因爲它是爲瀏覽器而生的,性能好,體積小。我們項目中使用的模型文件,一開始是ObjMtl的,達到25MB大小,在vue項目中渲染會阻塞瀏覽器46s,原生html+js的項目中好些,幾秒時間就行了,我懷疑是我寫法的問題,但是我測試僅僅是加載模型渲染到場景,並沒有多餘操作和數據綁定,還是一樣,阻塞進程,一度導致我懷疑人生???黑人問號臉。那麼如何將Obj模型轉換爲gltf模型,還能再優化嗎?進入下一章節!對了對了,Obj模型也是可以壓縮的,而且ObjLoader2加載會快一點

Obj模型轉Gltf模型並壓縮Gltf模型,性能爆炸提升!

真的很牛逼 模型加貼圖從 25mb 減小到了1.8mb 上效果圖

1.這是不加貼圖和mtlobj文件 已經達到了22.5MB在這裏插入圖片描述

  1. 這是objgltf之後的文件,貼圖轉成了base64包含在了gltf文件中,可通過配置項提取出文件,稍後介紹
    在這裏插入圖片描述

  2. 這是經過gltf壓縮處理之後的貼圖+模型的文件大小在這裏插入圖片描述

obj2gltf —— Obj模型轉Gltf

obj2gltf-github

  1. 用法
// 全局安裝後
             obj文件所在目錄                             輸出目錄 
obj2gltf  -i ./examples/models/obj/hanchuan/city.obj -o ./gltf/city.gltf --unlit --separate
  1. 介紹下爲什麼要加這兩個參數
    --unlit的作用是可以保留環境貼圖的效果,環境貼圖後面再介紹
    --separate是將貼圖文件提取出來,提出來瀏覽器可以緩存,如果你需要繼續壓縮gltf文件,這裏不加這個參數也行,因爲壓縮的時候也能提出來

gltf-pipeline

gltf-pipeline-github

  1. 用法
gltf-pipeline -i  ../../../gltf/city.gltf  -o  ../../../examples/models/obj/hanchuan/city_small1.gltf -d --separate
  1. 介紹下參數
    -d--draco.compressMeshes的縮寫,使用draco算法壓縮模型
    --separate就是將貼圖文件提取出來,不提可以不加

這樣,我們就完成了gltf模型的轉化和壓縮,性能暴增!秒開!
在我們最終的模型中,obj模型297Mb,轉gltf之後還有150Mb左右,最終經過壓縮,還有7.3Mb!

Gltf模型的加載

拋棄了ObjMtl之後,我們的加載器也要做一下改變

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <script src="../build/three.min.js"></script>
    <!-- 引入我們可愛的加載器 -->
    <script src="js/loaders/GLTFLoader.js"></script>
    <script src="js/loaders/DRACOLoader.js"></script>
    <script>
	  /* 省略創建場景部分的代碼 */

	  // 加載的過程 
	  var onProgress = function(xhr) {
          if (xhr.lengthComputable) {
            var percentComplete = (xhr.loaded / xhr.total) * 100;
            console.log(Math.round(percentComplete, 2) + "% downloaded");
          }
        };

      var onError = function() {
 		// 載入出錯時候
	  };

      var loader = new THREE.GLTFLoader();
      // 這個是Threejs解析draco壓縮之後的解析器 
      // 它從這裏讀取解析器JS
      THREE.DRACOLoader.setDecoderPath("js/libs/draco/gltf/");
      // 將Draco解析器和GltfLoader綁定在一起
      loader.setDRACOLoader(new THREE.DRACOLoader());
      loader.load(
        "models/obj/hanchuan/city_small1.gltf",
        function(gltf) {
         // gltf.scene 拿到這個可以處理模型
         scene.add(gltf.scene)
        },
        onProgress,
        onError
      );
	</script>
  </body>
</html> 

這時候的場景,應該是這樣的,很醜吧哈哈哈,沒關係沒關係,我們可以爲它美容,不過在此之前,我們先來試着轉動這個模型,看看性能怎麼樣。
在這裏插入圖片描述

OrbitControls——軌道控制器

var controls

function init(){
	// 省略創建場景部分
	controls = new THREE.OrbitControls(camera, renderer.domElement);
}

它的常用參數在源碼中可以找到,也可以百度/goggle一下中文翻譯的,不做太多介紹,這是其中一段源碼。

// Set to false to disable this control
	this.enabled = true;

	// "target" sets the location of focus, where the object orbits around
	this.target = new THREE.Vector3();

	// How far you can dolly in and out ( PerspectiveCamera only )
	this.minDistance = 0;
	this.maxDistance = Infinity;

	// How far you can zoom in and out ( OrthographicCamera only )
	this.minZoom = 0;
	this.maxZoom = Infinity;

	// How far you can orbit vertically, upper and lower limits.
	// Range is 0 to Math.PI radians.
	this.minPolarAngle = 0; // radians
	this.maxPolarAngle = Math.PI; // radians

	// How far you can orbit horizontally, upper and lower limits.
	// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
	this.minAzimuthAngle = - Infinity; // radians
	this.maxAzimuthAngle = Infinity; // radians

	// Set to true to enable damping (inertia)
	// If damping is enabled, you must call controls.update() in your animation loop
	this.enableDamping = false;
	this.dampingFactor = 0.25;

	// This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
	// Set to false to disable zooming
	this.enableZoom = true;
	this.zoomSpeed = 1.0;

	// Set to false to disable rotating
	this.enableRotate = true;
	this.rotateSpeed = 1.0;

	// Set to false to disable panning
	this.enablePan = true;
	this.panSpeed = 1.0;
	this.screenSpacePanning = false; // if true, pan in screen-space
	this.keyPanSpeed = 7.0;	// pixels moved per arrow key push

	// Set to true to automatically rotate around the target
	// If auto-rotate is enabled, you must call controls.update() in your animation loop
	this.autoRotate = false;
	this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60

	// Set to false to disable use of the keys
	this.enableKeys = true;

	// The four arrow keys
	this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 };

	// Mouse buttons
	this.mouseButtons = { LEFT: THREE.MOUSE.LEFT, MIDDLE: THREE.MOUSE.MIDDLE, RIGHT: THREE.MOUSE.RIGHT };

	// for reset
	this.target0 = this.target.clone();
	this.position0 = this.object.position.clone();
	this.zoom0 = this.object.zoom;

	//
	// public methods
	//

	this.getPolarAngle = function () {
  • 初始化這個控制器之後,就可以操作模型旋轉放大縮小了。它的原理就是控制攝像機和模型的距離,同理也可以控制模型與攝像機的距離去實現移動放大縮小等功能,可以自己嘗試一下。一個比較有趣的操作是在function animate(){}中,設置camera.lookAt=scene.position效果也很不錯。
  • ThreeJs中內置了很多有趣的控制器,用法和效果都可以從ThreeJsexamples中找到,記得看看。

Stats

玩過LOL,大型單機遊戲的同學都知道,如果幀率不好,畫面看起來就會卡頓,影響體驗,這也爲什麼用requestAnimationFrame去作爲渲染調用的原因之一,它的性能比函數遞歸setInterval實現渲染調用好很多。那麼我們如何去檢測我們的場景渲染的性能怎麼樣呢?就可以使用Stats

// <script src="js/libs/stats.min.js"></script> 不要忘了引入進來
var stats;

function init(){
	// 省略創建場景部分
	stats = new Stats();
	container.appendChild(stats.dom);
}

function animatie(){
	stats.update();
	// 省略renderer
}
  • 初始化之後在頁面左上角會看到,這個原理還沒研究過,有機會翻翻源碼看看。
  • 在這裏插入圖片描述
  • 如果實在vue/react等單頁面環境中,可以通過process.env.NODE_ENV控制開發環境再顯示這個。
  • 這樣一來,我們在開發調試的時候,就能很直觀的看出效果了。

給scene添加自定義背景

若不爲空,在渲染場景的時候將設置背景,且背景總是首先被渲染的。 可以設置一個用於的“clear”的Color(顏色)、一個覆蓋canvas的Texture(紋理),或是一個CubeTexture。默認值爲null。

  • 實驗結果是,TextureLoaderCubeTextureSphereGeometry都可以作爲背景圖,簡單介紹下這三者。
  1. TextureLoader 一張圖,背景看起來是靜止不動的
  2. CubeTexture 立方紋理 圖片是分割成6塊 相當於攝像機和模型在一個正方體盒子中 背景隨着攝像機轉動而轉動
  3. SphereGeometry 一張圖 全景圖原理 相當於攝像機和模型在一個圓球盒子中 背景隨着攝像機轉動而轉動
  4. 不太理解可以百度下threejs全景圖原理,不做過多敘述
function init(){
	// 省略其餘代碼
	// ....
	// 添加一張靜止的背景圖
	scene.background = new THREE.TextureLoader().load("你的背景圖")
	// ....
}
  1. 之後效果大概是這樣的,我們的世界裏有了天空,其實這裏用CubeTexture或者SphereGeometry效果更好
  2. 在這裏插入圖片描述

設置模型環境貼圖和材質顏色

細心的同學會發現,河流和樓上會有星星點點的光,這是怎麼實現的呢?答案就是環境貼圖

環境貼圖
簡單的講,環境貼圖就像把物體的表面化作一面鏡子,可以反射出你爲它賦予的圖片。

如何設置環境貼圖呢?回到我們加載模型的部分。核心就是創建立方紋理然後設置某個模型的materialenvMap爲這個立方紋理。 環境貼圖的使用限制受紋理影響,有一部分紋理加不上環境貼圖。

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <script src="../build/three.min.js"></script>
    <!-- 引入我們可愛的加載器 -->
    <script src="js/loaders/GLTFLoader.js"></script>
    <script src="js/loaders/DRACOLoader.js"></script>
    <script>
	  /* 省略創建場景部分的代碼 */

	 // 創建一個立方紋理
	 var envMap = new THREE.CubeTextureLoader()
            .setPath("textures/")
            .load(new Array(6).fill("start.jpg"));

      var loader = new THREE.GLTFLoader();
      // 這個是Threejs解析draco壓縮之後的解析器 
      // 它從這裏讀取解析器JS
      THREE.DRACOLoader.setDecoderPath("js/libs/draco/gltf/");
      // 將Draco解析器和GltfLoader綁定在一起
      loader.setDRACOLoader(new THREE.DRACOLoader());
      loader.load(
        "models/obj/hanchuan/city_small1.gltf",
        function(gltf) {
         // gltf.scene 拿到這個可以處理模型
         gltf.scene.traverse(function(child) {
            if (child.isMesh) {
              /* 這些都是DEMO  具體看你模型調整 下節介紹通過鼠標點擊確定模型所屬對象 然後去調試模型 */
			  // 這些名稱都可以通過打印看出 console.log(child)

			  // 比如我想給這些加上環境貼圖 就可以這樣寫
              /hai|city|liubianxing/i.test(child.name) &&
                (child.material.envMap = envMap);
              
              if (/city/i.test(child.name)) {
                // 更改模型顏色
                child.material.color = new THREE.Color(6, 6, 5);
                // 更改模型環境貼圖影響  0-1
                child.material.reflectivity = 0.9;
              }
              
			  // 更改模型位置
              /lumian|hai/i.test(child.name) && (child.position.y = 0.5);
              
              // ...
            }
          });
          
          scene.add(gltf.scene)
        },
        onProgress,
        onError
      );
      
	</script>
  </body>
</html> 

Raycaster 光線投射

光線投射用於進行鼠標拾取(在三維空間中計算出鼠標移過了什麼物體)。

  • 打印出所有的child不好定位是哪塊模型,有沒有更快的方法?
  • 您好,有的。
  • 通過 THREE.Raycaster 實現模型選中與信息顯示,點擊打印出當前點擊的模型,在它的屬性中修改顏色,位置等,可以直接更新效果,調試更方便
  • 到此,經過我們的美化之後,效果就是這樣了。還缺了點什麼,道路咋不發光啊,看着沒光效,不炫酷!
  • 在這裏插入圖片描述

利用EffectComposer(效果組合器)進行後期處理

這一塊的基礎建議好好看看《THREE.JS開發指南》這本書。如果需要多個pass,要學會使用MaskPassclearPass。這一塊因爲不熟悉,我在添加效果的時候花費了很大量的時間,尤其是Threejs內置的pass效果沒有文檔,甚至你都不知道內置了多少種效果…《THREE.JS開發指南》這本書介紹的比較全面,用法也很詳細。

利用EffectComposer進行後期處理——輝光(bloompass)

如何設置後期處理?

  1. 創建一個EffectComposer對象,然後在該對象上添加後期處理通道。
  2. 配置該對象,使它可以渲染我們的場景,並用額外的後期處理步驟
  3. render循環中,使用EffectComposer渲染場景、應用通道,並輸出結果

幾個引用介紹

  • EffectComposer效果組合器,每個通道會按照其加入EffectComposer的順序執行。
  • RenderPass該通道在指定的場景和相機的基礎上渲染出一個新的場景。一般在第一個加入到Composer中,它會渲染場景,但是不會將渲染結果輸出到屏幕上。
  • ShaderPass使用該通道可以傳入一個自定義的着色器,用來生成高級的、自定義的後期處理通道
  • BloomPass該通道會使明亮區域滲入較暗的區域,模擬相機照到過多亮光的情形
  • CopyShader它不會添加任何特殊效果,只是將最後一個通道的結果複製到屏幕上,BloomPass無法直接添加到屏幕上,需要藉助這個Shader,其實使用bloompass.renderToScreen = true是可以添加的,但是後續再加處理效果會無效,所以一定要借用這個Shader
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <!-- 省略其他引入的 -->
    <!-- 引入Effect -->
    <script src="js/postprocessing/EffectComposer.js"></script>
    <!-- 引入Effect配套的render -->
    <script src="js/postprocessing/RenderPass.js"></script>
    <script src="js/postprocessing/ShaderPass.js"></script>
    <!-- 引入各種需要的shader -->
    <script src="js/shaders/CopyShader.js"></script>
    <script src="js/shaders/LuminosityHighPassShader.js"></script>
    <script src="js/postprocessing/UnrealBloomPass.js"></script>
    <script>
      var clock;
	  /* 省略創建場景部分的代碼 */
	  // 初始化renderPass
	  var renderScene = new THREE.RenderPass(scene, camera);
	
	  // 初始化bloomPass 
	  var bloomPass = new THREE.UnrealBloomPass(
	    // 沒研究過這些參數的意義 會提上日程
        new THREE.Vector2(window.innerWidth, window.innerHeight),
        1.5,
        0.4,
        0.85
      );
      // 一些參數 可以調整看效果
      bloomPass.threshold = 0.36;
      bloomPass.strength = 0.6;
      bloomPass.radius = 0;

	  // effectCopy
      var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
      // 讓effectCopy渲染到屏幕上 沒這句不會再屏幕上渲染
      effectCopy.renderToScreen = true;
	  
	  // 初始化 composer
	  var composer = new THREE.EffectComposer(renderer);
	  // 模版緩衝(stencil buffer) https://blog.csdn.net/silangquan/article/details/46608915
      composer.renderTarget1.stencilBuffer = true;
      composer.renderTarget2.stencilBuffer = true;
      composer.setSize(window.innerWidth, window.innerHeight);
      composer.addPass(renderScene);
	  composer.addPass(bloomPass);
      composer.addPass(effectCopy);

	  // 修改animate
	  function animate() {
        requestAnimationFrame(animate);
        var delt = clock.getDelta();
        stats.update();
        renderer.clear();
        // 刪除renderer使用composerrender去渲染
        // renderer.render(scene, camera);
        
		// 沒理解透這個delt的作用 ???
        composer.render(delt);
      }
	</script>
  </body>
</html> 

在這裏插入圖片描述這樣 輝光效果就出來了。還不夠還不夠,讓我們加上FocusShaper,讓它看起來像聚焦在中心一樣(突出中心)。

  1. 顏色越亮,發光效果越強
  2. 輝光受環境貼圖影響
  3. 模型可以通過map貼圖來更改亮度,比如暗色的貼圖,它反光就會很軟

爲場景添加聚焦效果——FocusShader

我們要引入FocusShader

  • FocusShader是一個簡單的着色器,其結果是中央區域渲染的比較銳利,單週圍比較模糊。
  • 在這裏插入圖片描述
<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Threejs-city-model-show</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        color: #fff;
        margin: 0px;
        overflow: hidden;
      }
    </style>
  </head>

  <body>
    <!-- 省略其他引入的 -->
    <!-- 引入Effect -->
    <script src="js/postprocessing/EffectComposer.js"></script>
    <!-- 引入Effect配套的render -->
    <script src="js/postprocessing/RenderPass.js"></script>
    <script src="js/postprocessing/ShaderPass.js"></script>
    <!-- 引入各種需要的shader -->
    <script src="js/shaders/CopyShader.js"></script>
    <script src="js/shaders/LuminosityHighPassShader.js"></script>
    <script src="js/postprocessing/UnrealBloomPass.js"></script>
    <!-- focusShader 相對於bloompass新加的 -->
    <script src="js/shaders/FocusShader.js"></script>
    <script>
      var clock;
	  /* 省略創建場景部分的代碼 */
	
	 // 創建focusShader 相對於bloompass新加的
	 var focusShader = new THREE.ShaderPass(THREE.FocusShader);
     focusShader.uniforms["screenWidth"].value = window.innerWidth;
     focusShader.uniforms["screenHeight"].value = window.innerHeight;
     focusShader.uniforms["sampleDistance"].value = 1.07;

	  // 初始化renderPass
	  var renderScene = new THREE.RenderPass(scene, camera);
	
	  // 初始化bloomPass 
	  var bloomPass = new THREE.UnrealBloomPass(
	    // 沒研究過這些參數的意義 會提上日程
        new THREE.Vector2(window.innerWidth, window.innerHeight),
        1.5,
        0.4,
        0.85
      );
      // 一些參數 可以調整看效果
      bloomPass.threshold = 0.36;
      bloomPass.strength = 0.6;
      bloomPass.radius = 0;

	  // effectCopy
      var effectCopy = new THREE.ShaderPass(THREE.CopyShader);
      // 讓effectCopy渲染到屏幕上 沒這句不會再屏幕上渲染
      effectCopy.renderToScreen = true;
	  
	  // 初始化 composer
	  var composer = new THREE.EffectComposer(renderer);
	  // 模版緩衝(stencil buffer) https://blog.csdn.net/silangquan/article/details/46608915
      composer.renderTarget1.stencilBuffer = true;
      composer.renderTarget2.stencilBuffer = true;
      composer.setSize(window.innerWidth, window.innerHeight);
      composer.addPass(renderScene);
	  composer.addPass(bloomPass);
	  // 相對於bloompass新加的
	  composer.addPass(focusShader);
      composer.addPass(effectCopy);

	  // 修改animate
	  function animate() {
        requestAnimationFrame(animate);
        var delt = clock.getDelta();
        stats.update();
        renderer.clear();
        // 刪除renderer使用composerrender去渲染
        // renderer.render(scene, camera);
        
		// 沒理解透這個delt的作用 ???
        composer.render(delt);
      }
	</script>
  </body>
</html> 

模型的渲染和後期處理就到此就全部結束了。

Sprite精靈的應用

精靈是一個總是面朝着攝像機的平面,通常含有使用一個半透明的紋理。

在這裏插入圖片描述

 var textured = new THREE.TextureLoader().load("textures/warning.png");
 var spriteMaterial = new THREE.SpriteMaterial({
   // color: 0xffffff,
   map: textured
 });
 var sprite = new THREE.Sprite(spriteMaterial);
 sprite.position.set(
   25.729931791092394,
   10.179400757773436,
   36.07142388020101
 );
 // console.log(sprite);
 sprite.scale.x = 10;
 sprite.scale.y = 5;

 scene.add(sprite);

這張圖火災預警的圖其實就是一張透明的png圖片,精靈可以用canvas貼圖,你可以自己編寫canvas渲染在指定點上,也可以使用CSS3DRenderer去實現。

Group

通常的情況下Threejs裏的模型是要分組的。在處理交互起來,有分組會更加清晰明瞭,就像模塊拆分一樣。

var group = new THREE.Group();

區域、路線、移動等功能實現邏輯

  1. 不規則區域可以用ShapeGeometry創建,使用可以設置透明的material比較好。material設置transparent:true可以支持透明
  2. 移動就是更改模型位置,很簡單model.position.set(x,y,z)
  3. 畫線,linelineLoopCubicBezierCurve3Threejs提供的畫線方法
  4. 路線循環流動效果可以創建一個管道,然後增加一個路徑一樣的貼圖,設置wrap爲重複,在animate中不斷更改texture.offset即可

VUE/React等單頁面注意點

由於單頁面中,Threejs創建的任何材質,模型,貼圖……只要含有dispose方法的,你在頁面組件即將銷燬的週期中,都要調用下dispose方法清除,不然可能內存泄漏。剛學會一個妙招,利用WEBGL_lose_context這個API 可以讓當前的webgl環境失效,達到取消佔用的目的。

beforeDestory(){
	this.bloomPass.dispose();
    this.envMap.dispose();
    this.skymap.dispose();
    this.dracoLoader.dispose();
    this.spriteMaterial.dispose();
    this.sphereGeometry.dispose();
    this.meshBasicMaterial.dispose();
    this.scene.dispose();
    this.controls.dispose();
	
	/*
	const data = this.$data;
    for (let i in data) {
      if (data.hasOwnProperty(i)) {
        if (data[i] && typeof data[i].dispose == "function") {
          data[i].dispose();
        }
      }
    }
	*/
	// this.renderer.domElement 就是你的threejs的canvas Dom
	let gl = this.renderer.domElement.getContext("webgl");

    gl && gl.getExtension("WEBGL_lose_context").loseContext();
}

模型發光還帶線的效果怎麼做?

在這裏插入圖片描述


var lineMaterial = new THREE.LineBasicMaterial({
  // 線的顏色
  color: "blue",
  transparent: true,
  opacity: 0.8,
  depthFunc: THREE.AlwaysDepth
});
模型.add(
  new THREE.LineSegments(模型geometry, lineMaterial)
);
// 之後把模型設置下透明度就成了

座標轉換 經緯度轉墨卡託

  • 先把經緯度轉墨卡託座標 然後由於墨卡託座標比較大,找到地圖模型的中心點,墨卡託轉Threejs的座標時,減去這個中心點,之後就能畫出一樣的點或區域,之後再將z軸(y)取反
  • x+對應東,z+對應南
  • z算出來還得取個反
  • 根據座標系適當調整
function lonlatToMercator(lon, lat, height) {
        var z = height ? height : 0;
        var x = (lon / 180.0) * 20037508.3427892;
        var y = (Math.PI / 180.0) * lat;
        var tmp = Math.PI / 4.0 + y / 2.0;
        y = (20037508.3427892 * Math.log(Math.tan(tmp))) / Math.PI;
        return { x: x, y: y, z: z };
      }

// 找到地圖的中心對應的經緯度座標
var center = lonlatToMercator(113.82909, 30.6549, 1);

function lonlatToThree(lon, lat, height) {
  var z = height ? height : 0;
  var x = (lon / 180.0) * 20037508.3427892;
  var y = (Math.PI / 180.0) * lat;
  var tmp = Math.PI / 4.0 + y / 2.0;
  y = (20037508.3427892 * Math.log(Math.tan(tmp))) / Math.PI;
  var result = {
    x: x - center.x,
    y: y - center.y,
    z: z - center.z
  };
  // x 越大越遠
  // 因爲比地圖大了 可以讓地圖整體放大或縮小 然後偏移到大概位置
  return [result.x / 100 + 17, -result.y / 100 + 33];
  // [-result.x / 100 - 14, -result.y / 100 - 35];
}
console.log(lonlatToThree(113.84411, 30.65231));

antialias開啓後,渲染還有鋸齒怎麼辦?

使用SSAAFXAASMAA等抗鋸齒後處理。任選其一即可。

initFxaaPass() {
	let fxaaPass = new ShaderPass(FXAAShader);
	const pixelRatio = this.renderer.getPixelRatio();
	fxaaPass.material.uniforms["resolution"].value.x =
	  1 / (this.width * pixelRatio);
	fxaaPass.material.uniforms["resolution"].value.y =
	  1 / (this.height * pixelRatio);
	fxaaPass.renderToScreen = true;
	this.fxaaPass= fxaaPass;
},
initSmaaShader() {
	const pixelRatio = this.renderer.getPixelRatio();
	this.smaaPass = new SMAAPass(
	  this.width * pixelRatio,
	  this.height * pixelRatio
	);
	this.smaaShader.renderToScreen = true;
},
initSsaaShader() {
	this.ssaaRenderPass = new SSAARenderPass(this.scene, this.camera);
	this.ssaaRenderPass.unbiased = false;
	this.ssaaRenderPass.sampleLevel = 2;
},

利用EffectComposer應用某個效果

initEffectComposer() {
	const composer = new EffectComposer(this.renderer);
	composer.setSize(this.width, this.height);
	composer.addPass(this.renderScene);
	composer.addPass(this.ssaaRenderPass);
	composer.addPass(this.bloomPass);
	composer.addPass(this.focusShader);
	composer.addPass(this.effectCopy);
	
	this.composer = composer;
},

光柱效果如何實現

在這裏插入圖片描述

  1. 準備一張漸變灰色png圖片, 類似如下圖
    在這裏插入圖片描述我在這 ↑
  2. 代碼部分
import * as THREE from "three";

const scaleSpeed = 0.01;

export default {
  data(){
    return {
      // ...  
    }
  },
  created(){
    this.loadRangeMap()
  },
  beforeDestory(){
      // ...
  },
  methods: {
    initRingAnimate() {
      Array.isArray(this.gatewayGroup.children) &&
        this.gatewayGroup.children.forEach(v => {
          Array.isArray(v.children) &&
            v.children.forEach(item => {
              if (item.userData.type === "ring") {
                item.rotation.z = item.rotation.z + scaleSpeed;
              }
            });
        });
    },
    loadRangeMap() {
      this.rangeMap = this.textureLoader.load(require("../images/range.png"));
    },
    initOctahedronBufferGeometry() {
      this.octahedronBufferGeometry = new THREE.OctahedronBufferGeometry();
    },
    initCylinderBufferGeometry() {
      this.cylinderBufferGeometry = new THREE.CylinderBufferGeometry(
        2,
        2,
        14,
        12,
        1,
        true
      );
    },
    initOctahedron(color) {
      let geometry = this.octahedronBufferGeometry;
      let material = new THREE.MeshBasicMaterial({
        color,
        transparent: true,
        opacity: 0.3
      });
      let lineMaterial = new THREE.LineBasicMaterial({
        color,
        depthFunc: THREE.AlwaysDepth
      });
      let octahedron = new THREE.Mesh(geometry, material);
      let line = new THREE.LineSegments(geometry, lineMaterial);
      octahedron.add(line);
      octahedron.position.z = -8;
      return octahedron;
    },
    initRing(color) {
      let geometry = this.cylinderBufferGeometry;
      let material = new THREE.MeshBasicMaterial({
        color,
        map: this.rangeMap,
        side: THREE.DoubleSide,
        transparent: true,
        depthWrite: false
      });
      let cylinder = new THREE.Mesh(geometry, material);
      cylinder.rotation.x = (Math.PI / 180) * -90;
      cylinder.position.z = -2;
      return cylinder;
    },
    initGateway(data = { color: "#54C41D",x: 0, z: 0 }) {
      let group = new THREE.Group();
      let octahedron = this.initOctahedron(data.color);
      let ring = this.initRing(data.color);
      group.add(ring);
      group.add(octahedron);
      group.rotation.x = (Math.PI / 180) * 90;
      group.position.y = 0.2;
      group.position.x = data.x;
      group.position.z = data.z;
      this.gatewayGroup.add(group);
    }
  }
};

刪除子對象時,用forEach等高階循環刪不乾淨?

  • 因爲group.children是個數組,每次刪除的時候,數組都會變動,比如長度是5,你刪了第一個,下次循環你要刪除第二個,但是數組長度變了,第二次刪除的時候其實刪的是第三個了。
  • 解決方案1 children.map(v=>{group.remove(children[0])}) 一直刪除第一個
  • 解決方案2 for(let i = 0, l = children.length; i < l; i++){ group.remove(children[i]) } 將數組長度存儲下來,就不會變啦!

我們項目的最終效果

在這裏插入圖片描述
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章