隨着時間的推移技術的進步,前端越來越雜了,但是也越來越精彩了。只是會用一點
ThreeJs
,對於WebGl
的原理並沒瞭解過,這並不影響我們利用ThreeJs
去做出一個非常炫酷的項目。
開始
新世界的大門打開啦!
寫在前面
- 不要因爲不瞭解就被這種3D展示的項目給嚇到 其實實現起來很簡單 很簡單 很簡單
- 城市模型一份 最好是
gltf
模型,obj
模型也沒問題,我會介紹如何轉化與壓縮 PS:爲什麼只有這倆,因爲我寫這個項目只用到了這倆,處理的經驗也是針對這倆的,我項目中所用的模型是公司所有暫不能提供。 - 有一定
ThreeJs
的基礎 俗話說得好 萬丈高樓平地起嘛 如果沒有這方面基礎的同學也不要急 推薦一本書《THREE.JS開發指南》
,有基礎也有提高 很棒 - 本文所示代碼大部分只是思路 我也是第一次上手用
ThreeJs
處理模型並應用到項目中,可能有少許不足之處,還望各路大神指正教導 - 項目進行一半的時候,因爲沒經驗,我發現讓建模看着地圖建模的思路是不對的,應該讓他們利用
geoJson
作爲地理數據,去建模,建造出來的更精確,而且可以利用地理座標和世界座標去關聯(猜想),利於項目開發,畢竟第一次,這個鍋我背了 Threejs
的文檔是不全的,很多控制器
,loader
,後期處理
都沒有文檔,要自己多看看Threejs
的examples
,很多效果都可以基於Demo
去實現- 單頁面應用一定要清除
ThreeJs
的創建的對象,避免內存泄露,能dispose
的dispose
,多個children
的要遍歷remove
掉 而且裏面的material
和geometry
也要刪掉,最近剛知道一個取消佔用的妙招,WEBGL_lose_context - 後期處理對顯卡有一定要求
- 最好一次渲染,不要多次渲染
- 最好不要把和數據無關的掛在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>
這一步一般會出現的問題有如下
- 模型加載後,不顯示也不報錯?
檢查場景是否正常渲染了,如果正常渲染模型的位置在哪裏,攝像機在哪裏,攝像機是否對着模型,燈光是否配置,模型是否太大或者太小了,超出了攝像機的攝影範圍…… - 模型可以正常加載,但是貼圖不顯示?
首先檢查network
是否報404
錯誤,如果報錯,一般都是mtl
貼圖文件(看起來像是雪碧圖
那種)沒給你,或者路徑配置的不是相對路徑
,如果貼圖沒錯誤,模型是黑色的,在mtl
文件中可以更改ka
或kd
的三個值(對應rgb
),或者打印出模型屬性,在material.color
中更改點色值或別的屬性。黑色的時候,看不到貼圖。一般這樣一通操作之後,就能看到了模型了 - 模型文件太大了,瀏覽器在渲染的時候進程被完全卡死!要等待幾十秒之久!天吶!
這個問題看起來比較棘手,其實很好解決。ThreeJs
官方推薦gltf
格式的模型在瀏覽器中渲染,因爲它是爲瀏覽器而生的,性能好,體積小。我們項目中使用的模型文件,一開始是Obj
和Mtl
的,達到25MB大小,在vue
項目中渲染會阻塞瀏覽器46s,原生html
+js
的項目中好些,幾秒時間就行了,我懷疑是我寫法的問題,但是我測試僅僅是加載模型渲染到場景,並沒有多餘操作和數據綁定,還是一樣,阻塞進程,一度導致我懷疑人生???黑人問號臉。那麼如何將Obj模型
轉換爲gltf
模型,還能再優化嗎?進入下一章節!對了對了,Obj
模型也是可以壓縮的,而且ObjLoader2
加載會快一點
Obj模型轉Gltf模型並壓縮Gltf模型,性能爆炸提升!
真的很牛逼 模型加貼圖從 25mb 減小到了1.8mb 上效果圖
1.這是不加貼圖和mtl
的obj
文件 已經達到了22.5MB!
-
這是
obj
轉gltf
之後的文件,貼圖轉成了base64
包含在了gltf
文件中,可通過配置項提取出文件,稍後介紹
-
這是經過
gltf
壓縮處理之後的貼圖+模型的文件大小
obj2gltf —— Obj模型轉Gltf
- 用法
// 全局安裝後
obj文件所在目錄 輸出目錄
obj2gltf -i ./examples/models/obj/hanchuan/city.obj -o ./gltf/city.gltf --unlit --separate
- 介紹下爲什麼要加這兩個參數
--unlit
的作用是可以保留環境貼圖的效果,環境貼圖後面再介紹
--separate
是將貼圖文件提取出來,提出來瀏覽器可以緩存,如果你需要繼續壓縮gltf
文件,這裏不加這個參數也行,因爲壓縮的時候也能提出來
gltf-pipeline
- 用法
gltf-pipeline -i ../../../gltf/city.gltf -o ../../../examples/models/obj/hanchuan/city_small1.gltf -d --separate
- 介紹下參數
-d
是--draco.compressMeshes
的縮寫,使用draco
算法壓縮模型
--separate
就是將貼圖文件提取出來,不提可以不加
這樣,我們就完成了gltf
模型的轉化和壓縮,性能暴增!秒開!
在我們最終的模型中,obj模型297Mb,轉gltf之後還有150Mb左右,最終經過壓縮,還有7.3Mb!
Gltf模型的加載
拋棄了Obj
和Mtl
之後,我們的加載器也要做一下改變
<!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
中內置了很多有趣的控制器,用法和效果都可以從ThreeJs
的examples
中找到,記得看看。
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。
- 實驗結果是,
TextureLoader
、CubeTexture
和SphereGeometry
都可以作爲背景圖,簡單介紹下這三者。
- TextureLoader 一張圖,背景看起來是靜止不動的
- CubeTexture 立方紋理 圖片是分割成6塊 相當於攝像機和模型在一個正方體盒子中 背景隨着攝像機轉動而轉動
- SphereGeometry 一張圖 全景圖原理 相當於攝像機和模型在一個圓球盒子中 背景隨着攝像機轉動而轉動
- 不太理解可以百度下
threejs全景圖原理
,不做過多敘述
function init(){
// 省略其餘代碼
// ....
// 添加一張靜止的背景圖
scene.background = new THREE.TextureLoader().load("你的背景圖")
// ....
}
- 之後效果大概是這樣的,我們的世界裏有了天空,其實這裏用
CubeTexture
或者SphereGeometry
效果更好
設置模型環境貼圖和材質顏色
細心的同學會發現,河流和樓上會有星星點點的光,這是怎麼實現的呢?答案就是環境貼圖
。
環境貼圖
簡單的講,環境貼圖就像把物體的表面化作一面鏡子,可以反射出你爲它賦予的圖片。
如何設置環境貼圖呢?回到我們加載模型的部分。核心就是創建立方紋理
然後設置某個模型的material
的envMap
爲這個立方紋理。 環境貼圖的使用限制受紋理影響,有一部分紋理加不上環境貼圖。
<!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
,要學會使用MaskPass
和clearPass
。這一塊因爲不熟悉,我在添加效果的時候花費了很大量的時間,尤其是Threejs
內置的pass
效果沒有文檔,甚至你都不知道內置了多少種效果…《THREE.JS開發指南》
這本書介紹的比較全面,用法也很詳細。
利用EffectComposer進行後期處理——輝光(bloompass)
如何設置後期處理?
- 創建一個
EffectComposer
對象,然後在該對象上添加後期處理通道。 - 配置該對象,使它可以渲染我們的場景,並用額外的後期處理步驟
- 在
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
,讓它看起來像聚焦在中心一樣(突出中心)。
- 顏色越亮,發光效果越強
- 輝光受環境貼圖影響
- 模型可以通過
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();
區域、路線、移動等功能實現邏輯
- 不規則區域可以用
ShapeGeometry
創建,使用可以設置透明的material
比較好。material
設置transparent:true
可以支持透明 - 移動就是更改模型位置,很簡單
model.position.set(x,y,z)
- 畫線,
line
、lineLoop
、CubicBezierCurve3
等Threejs
提供的畫線方法 - 路線循環流動效果可以創建一個
管道
,然後增加一個路徑一樣的貼圖
,設置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開啓後,渲染還有鋸齒怎麼辦?
使用SSAA
、FXAA
、SMAA
等抗鋸齒後處理。任選其一即可。
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;
},
光柱效果如何實現
- 準備一張漸變灰色
png
圖片, 類似如下圖
我在這 ↑ - 代碼部分
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]) }
將數組長度存儲下來,就不會變啦!
我們項目的最終效果