GLTF模型讲解
前言
在繁忙的業務中,為了縮短設計和開發的周期,我們的 H5 小游戲更多的會采用 2D 的視覺風格,但總是一個風格是很無趣的,所以最近搞了一個 3D 物理游戲的需求,在開發的過程中遇到了不少問題,希望通過這篇文章將關于 Three.js、Cannon.js、模型、工具等基礎知識、問題總結分享給大家。
開始 3D 項目之前,首先從選擇 3D 框架開始,老牌引擎 Three.js 和微軟的 Babylon.js 都不錯,針對自己的項目需求選擇一款即可,這次我主要針對更熟悉的 Three.js 來講。
Three.js 基礎概念
使用 Three.js 前,首先要理解以下幾個核心概念:
Sence 場景
在 Three.js 中首先需要創建一個三維空間,我們稱之為場景。場景可以想象成是一個容器,里面存放著所有渲染的物體和使用的光源。
Axes 坐標軸
Three.js 采用的是右手坐標系,拇指、食指、中指分別表示 X、Y、Z 軸的方向。
Camera 攝像機
攝像機就相當于我們的雙眼,決定了能夠在場景中的所見所得。
Three.js 中提供以下幾種攝像機類型,最為常用的是透視攝像機,其他了解下即可。
ArrayCamera 陣列攝像機
一個 ArrayCamera 會包含多個子攝像機,通過這一組子攝像機渲染出實際效果,適用于 VR 場景。
CubeCamera 立方攝像機
創建六個 PerspectiveCamera(透視攝像機),適用于鏡面場景。
StereoCamera 立體相機
雙透視攝像機適用于 3D 影片、視差效果。
OrthographicCamera 正交攝像機
OrthographicCamera(正交攝像機)定義了一個矩形可視區域,物體只有在這個區域內才是可見的,另外物體無論距離攝像機是遠或事近,物體都會被渲染成一個大小,所以這種攝像機類型適用于 2.5D 場景(例如斜 45 度游戲)。
PerspectiveCamera 透視攝像機
最為常用的攝像機類型,模擬人眼的視覺,根據物體距離攝像機的距離,近大遠小。默認情況下,攝像機的初始位置 X、Y、Z 都為 0,攝像機方向是從正 Z 軸向負 Z 軸看去。通過 Near和 Far 定義最近和最遠的可視距離,Fov 定義可視的角度。
Mesh 網格
有了場景和攝像頭就可以看到 3D 場景中的物體,場景中的我們最為常用的物體稱為網格。
網格由兩部分組成:幾何體和材質
Geometry 幾何體
記錄了渲染一個 3D 物體所需要的基本數據:Face 面、Vertex 頂點等信息。
例如下面這個網格是由三角形組成,組成三角形的點稱為頂點,組成的三角形稱為面。
Material 材質
材質就像是物體的皮膚,決定了幾何體的外表。
外表的定義可以讓一個物體看起來是否有鏡面金屬感、暗淡、純色、或是透明與否等效果。
Light 光源
光源相當于在密閉空間里的一盞燈,對于場景是必不可少的
在 Three.js 常用的有這幾種光源:
AmbientLight 環境光源
屬于基礎光源,為場景中的所有物體提供一個基礎亮度。
DirectionalLight 平行光源
效果類似太陽光,發出的光源都是平行的。
HemisphereLight 半球光
只有圓球的半邊會發出光源。
PointLight 點光源
一個點向四周發出光源,一般用于燈泡。
SpotLight 聚光燈光源
一個圓錐體的燈光。
Shadow 陰影
另外要注意并不是每一種光源都能產生陰影,目前只有三種光源可以:
- DirectionalLight 平行光源
- PointLight 點光源
- SpotLight 聚光燈光源
另外如果要開啟模型的陰影的話,模型是由多個 Mesh 組成的,只開啟父的 Mesh 的陰影是不行的,還需要遍歷父 Mesh 下所有的子 Mesh 為其開啟投射陰影 castShadow 和接收投射陰影 receiveShadow。
// 遍歷子 Mesh 開啟陰影 object.traverse(function(child) {if (child instanceof THREE.Mesh) {child.castShadow = truechild.receiveShadow = true} })glTF 模型格式
前面提到 Three.js 引擎支持的格式非常的多,我們最為常見的格式有 .obj + .mtl +.jpg/.png,但使用這種模型格式存在一個問題,.obj 是靜態模型,不支持動畫數據存儲,無法使用模型的動畫,所以我建議使用 glTF 這種模型格式。
glTF 模型格式介紹
傳統的 3D 模型格式的設計理念更多是針對本地離線使用,所以這類 3D 模型格式沒有針對下載速度或加載速度進行優化,文件大小往往會非常的大,隨著 Web 端的興起,對文件大小更為敏感的今天,我們該嘗試別的模型格式了。
glTF 是由 Khronos Group 開發的 3D 模型文件格式,該格式的特點是最大程度的減少了 3D 模型文件的大小,提高了傳輸、加載以及解析 3D 模型文件的效率,并且它可擴展,可互操作。
第一版 glTF 1.0 于 2015 年 10 月 19 日發布,2017 年 6 月 5 日的 Web 3D 2017 大會發布了最終版本 glTF 2.0。
glTF 模型格式文件組成
模型文件 .gltf
包含場景中節點層次結構、攝像機、網格、材質以及動畫等描述信息。
二進制文件 .bin
包含幾何、動畫的數據以及其他基于緩沖區的數據,.bin 文件可以直接加載到 GPU 的緩沖區中從而不需要額外的解析,因此能夠高效傳輸和快速加載。
材質貼圖文件 .png / .jpg
3D 模型做凹凸貼圖或普通貼圖上所使用到文件。
glTF 模型格式導出
官方在 glTF 格式導出上提供了多種建模軟件的導出插件,比如有:
- 3DS Max Exporter
- Maya Exporter
- Blender glTF 2.0 Exporter
- ...
正巧我們常用的 C4D 建模軟件官方沒有提供 C4D 的導出插件,所以我們使用 C4D 導出后再導入 Blender,通過 Blender 作為中轉站導出 glTF 格式文件。
但由于兩個建模軟件之間的材質并不能相通,導出后的模型文件材質效果表現不佳,這是因為 Blender 有自己的一套材質流程系統,例如有 glTF Metallic Roughness 和 glTF Specular Glossiness,需在此基礎之上重新貼材質后導出解決。
另外注意的一點 Blender 的坐標系與 Three.js 是不同的,Blender 會將 Z 和 Y 對調位置,在導出時要選擇 Convert Z up to Y up 進行對調。
Three.js 使用 glTF 模型
Three.js 中使用 glTF 格式需額外引入 GLTFLoader.js 加載器。
var gltfLoader = new THREE.gltfLoader()gltfLoader.load(’./assets/box.gltf’, function(sence) {
var object = scene.gltf // 模型對象
scene.add(object) // 將模型添加到場景中
})
glTF 模型動畫
Animation Clip 動畫片段
前面提到 glTF 模型格式支持動畫,模型動畫可以使用 Blender 建模軟件制作,通過 Blender 提供的時間軸編輯變形動畫或者骨骼動畫,每個動畫可以編輯為一個 Action 動作,導出后使用 GLTFLoader 加載到 Three.js 中,可以拿到一個 animations 數組,animations 里包含了模型的每個動畫 Action 動作。
let gltfLoader = new THREE.gltfLoader()let mixer = null
gltfLoader.load(’./assets/box.gltf’, function(sence) {
let object = scene.gltf
let animations = sence.animations // 動畫數據
if (animations && animations.length) {
mixer = new THREE.AnimationMixer(object) // 對動畫進行控制
for (let i = 0; i < animations.length; i++) {
mixer.clipAction(animations[i]).play() // 播放所有動畫
}
}
scene.add(object)
})
function update() {
let delta = clock.getDelta(mixer)
mixer.update(delta) // 更新動畫片段
}
Tween 動畫
對模型實現淡入淡出、縮放、位移、旋轉等動畫推薦使用 GSAP 來實現更為簡便。
let tween = new TimelineMax()tween.to(box.scale, 1, { // 從 1 縮放至 2,花費 1 秒
x: 2,
y: 2,
z: 2,
ease: Power0.easeInOut, // 速度曲線
onStart: function() { // 監聽動畫開始 },
onUpdate: function() { // 監聽動畫過程 },
onComplete: function() { // 監聽動畫結束 }
}).to(box.position, 1, { // 縮放結束后,位移 x 至 10,花費 1 秒
x: 10,
y: 0,
z: 0
})
Draco 3D 模型壓縮工具
Draco 是一個用于壓縮、解壓縮 3D 幾何網格和點云的開源庫,為改善 3D 圖形存儲和傳輸而設計。
使用該工具可以對 glTF 格式進一步的壓縮,會將 glTF 格式轉為 .glb 格式,并且 .bin 壓縮效果拔群,但是在 Three.js 中使用.glb 格式需要引入額外的解析庫,解析庫文件包括 draco_decoder.js(791KB)、draco_decoder.wasm(323 KB)、draco_wasm_wrapper.js(64.3 KB)。所以更推薦當模型文件數量多,且文件較大時使用,否則得不償失。
壓縮使用 glTF Pipeline 工具,需要將三個種類的文件放在一起,執行命令行進行轉換。
$ npm install -g gltf-pipeline // 安裝 gltf-pipeline 工具$ gltf-pipeline -i model.gltf -o model.glb // 指定某個 .gltf 文件轉為 .glb 格式
Three.js 使用 .glb 格式引入 Draco 解碼庫
// 實例化 loaderlet loader = new THREE.GLTFLoader()
// Draco 解碼庫
THREE.DRACOLoader.setDecoderPath(’/examples/js/libs/draco’)
loader.setDRACOLoader(new THREE.DRACOLoader())
// 加載 glTF 模型
loader.load(‘models/gltf/box.gltf’, function(gltf) {
scene.add(gltf.scene)
})
Cannon.js 3D 物理引擎
目前在 Github 上搜索到的 3D 物理引擎庫有 Cannon.js、Oimo.js、Ammo.js、Energy.js、Physijs 等等,大部分都已許久沒有更新迭代了(長達好幾年),項目的 Star 數量和 Issues 數量也不多,我們該如何選擇?
Cannon.js、Oimo.js 和 Energy.js 作為 Babylon.js 的內置物理引擎,我們試著從這三個下手。
Energy.js:使用 C++ 編寫轉 JavaScript 的 3D 物理引擎,源碼不可讀,目前 Github 比較冷清。
Oimo.js:一款輕量級的 3D 物理引擎,文件大小 153 KB。
Cannon.js:完全使用 JavaScript 編寫的優秀 3D 物理引擎,包含簡單的碰撞檢測、各種形狀的摩擦力、彈力、約束等功能。
從綜合性來看,我更偏向于 Cannon.js ,所以下面主要講講 Cannon.js。
Cannon.js 的特性
以下是 Cannon.js 的特性,基本可以滿足大部分的 3D 物理開發場景。
使用 Cannon.js
我們以官方的一個平面加球剛體的例子來快速上手 Cannon.js。在線例子
1、初始化物理世界
使用 Cannon.js 前需要創建 CANNON.World 對象,CANNON.World 對象是負責管理對象和模擬物理的中心。
創建完 CANNON.World 對象后,接著設置物理世界的重力,這里設置了負 Y 軸為 10 m/s2。
let world = new CANNON.World()world.gravity.set(0, -10, 0)
Cannon.js 提供了 Broadphase、NaiveBroadphase 兩種碰撞檢測階段,默認是 NaiveBroadphase。
world.broadphase = new CANNON.NaiveBroadphase()2、創建動態球體
創建 Body 分三個步驟:
- 創建形狀
- 為形狀添加剛體
- 將剛體添加到世界
let sphereBody = new CANNON.Body({ // Step 2
mass: 5,
position: new CANNON.Vec3(0, 10, 0),
shape: sphereShape
})
world.add(sphereBody) // Step 3
第一步創建了半徑為 1 的球形,第二步創建球的剛體,如果剛體的 mass 屬性設置為 0,剛體則會處于靜止狀態,靜止的物體不會和其他靜止的物體發生碰撞,我們這里為球的剛體設置了 5kg,球會處于動態的狀態,會受的重力的影響而移動,會與其他物體發生碰撞。
3、創建靜態平面和動態球體
// 平面 Bodylet groundShape = new CANNON.Plane()
let groundBody = new CANNON.Body({
mass: 0,
shape: groundShape
})
// setFromAxisAngle 旋轉 X 軸 -90 度
groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -1.5707963267948966)
world.add(groundBody)
創建平面形狀,接著是剛體,這里設置了平面剛體的 mass 為 0,保證剛體處于靜止狀態。默認情況下平面的方向是朝向 Z 方向的(豎立著),可以通過 Body.quaternion.setFromAxisAngle 對平面進行旋轉。
4、創建平面和球的網格
前面創建的剛體在場景中并沒有實際的視覺效果,這一步創建平面、球的網格。
// 平面網格let groundGeometry = new THREE.PlaneGeometry(20, 20, 32)
let groundMaterial = new THREE.MeshStandardMaterial({
color: 0x7f7f7f,
side: THREE.DoubleSide
})
let ground = new THREE.Mesh(groundGeometry, groundMaterial)
scene.add(ground)
// 球網格
let sphereGeometry = new THREE.SphereGeometry(1, 32, 32)
let sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00 })
let sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
scene.add(sphere)
5、模擬世界
接著我們為物理世界開啟持續更新,并且將創建的球剛體與球網格關聯起來。
function update() {requestAnimationFrame(update)
world.step(1 / 60)
if (sphere) {
sphere.position.copy(sphereBody.position)
sphere.quaternion.copy(sphereBody.quaternion)
}
}
通過這幾步,一個簡單的物理場景就完成了,另外更多官方例子可以點擊這里,可以查看到 Cannon.js 各個約束、摩擦力、模擬汽車等特性的例子。
其他:
1、自定義物理材質需關聯
還是上面的例子,現在場景中剛體的物理特性都為默認的,我希望球的恢復系數高一點,即掉落時彈跳的更高。首先需要通過 CANNON.Material 實例物理材質,剛體使用該物理材質,最后通過 CANNON.ContactMaterial 來定義兩個剛體相遇后會發生什么。
// 平面let ground_cm = new CANNON.Material() // Step 1 : 實例 CANNON.Material
let groundBody = new CANNON.Body({
…
material: groundMaterial // Step 2 : 使用該物理材質
…
})
// 球
let sphere_cm = new CANNON.Material()
let sphereBody = new CANNON.Body({
…
material: sphere_cm
…
})
let sphere_ground = new CANNON.ContactMaterial(ground_cm, sphere_cm, { // Step 3 : 定義兩個剛體相遇后會發生什么
friction: 1,
restitution: 0.4
})
world.addContactMaterial(sphere_ground) // Step 4 : 添加到世界中
2、剛體添加位移動畫時需取消速度值
比如我使用 GSAP 庫對某個剛體進行 Y 軸向上移動,在 update 階段需要將剛體的重力加速度設置為 0,否則動畫結束后剛體會出現向下砸的效果。
let tween = new TimelineMax()tween.to(boxBody.position, 2, {
x: 0,
y: 10,
z: 0,
update: function() {
// 歸 0 設置
boxBody.velocity.setZero()
boxBody.initVelocity.setZero()
boxBody.angularVelocity.setZero()
boxBody.initAngularVelocity.setZero()
}
})
3、只檢測碰撞,不發生物理效果
允許只檢測是否碰撞,實際不發生物理效果,需要為剛體添加以下屬性:
boxBody.collisionResponse = false4、縮放剛體
如果剛體需要縮放,則需要為剛體添加此屬性,來更新剛體大小。
boxBody.updateMassProperties()let tween = new TimelineMax()
tween.to(sphereBody.shapes[0], 2, {
radius: 0.2 // 縮放至 0.2
})
點擊交互
在 3D 的世界中不能像我們在 DOM 中為一個節點綁定點擊事件那么容易,在 Three.js 中提供了 THREE.Raycaster 方法處理點擊交互,使用鼠標或者手指點擊屏幕時,會將二維坐標進行轉換,發射一條射線判斷與哪個物體發生了碰撞,由此得知點擊了哪個物體。點擊這里官方例子
let raycaster = new THREE.Raycaster()let mouse = new THREE.Vector2()
function onTouchEnd(ev) {
// 點擊獲取屏幕坐標
var event = ev.changedTouches[0]
mouse.x = (event.clientX / window.innerWidth) 2 - 1
mouse.y = -(event.clientY / window.innerHeight) 2 + 1
raycaster.setFromCamera(mouse, camera)
let intersects = raycaster.intersectObjects(scene, true)
for (let i = 0; i < intersects.length; i++) {
console.log(intersects[i]) // 與射線發生碰撞的物體
}
}
性能方面
模型精細程度
在 Web 端由于性能的限制,在開發過程中要盡量避免做一些損耗性能較大的事情。
首先是模型的精細程度,在保證效果的前提下,盡量降低模型面的數量,也就是說采用低模模型,一些模型的凹凸褶皺感也可以通過凹凸貼圖的方式去實現,越是復雜的模型在實時渲染的過程中就越占用手機性能。
光源與陰影
另外一方面光源、陰影也是占性能,尤其是陰影。光源一般會使用平行光或者聚光燈,這種光源照射在物體上更為真實,使用半球光會稍微提升幀數,但效果略差些,陰影效果前面提到過要遍歷每一個子 Mesh 接收產生陰影 castShadow 和接收陰影 receiveShadow,這相當耗費性能,開啟后對陰影的精細程度以及陰影類型進行參數優化,在 Android 系統性能不太好,iOS 系統基本能保證流暢運行,所以建議根據設備系統優化。
var n = navigator.userAgentif (/iPad|iPhone|iPod/.test(n) && !window.MSStream) { } // 針對 iOS 系統使用陰影
抗鋸齒與像素比
抗鋸齒是讓模型的邊緣效果更加圓滑不粗糙,也會占用一些性能,默認是關閉的,視情況開啟。
renderer.antialias = true // 開啟抗鋸齒另外像素比 setPixelRatio,移動端由于 Retina 屏的緣故,一般會設置為 2,所以使用window.devicePixelRatio 獲取實際設備像素比動態設置的話,部分大屏手機的像素比有 3 的情況,所有會因為像素比過高造成性能問題。
renderer.setPixelRatio(2) // 推薦renderer.setPixelRatio(window.devicePixelRatio) // 不推薦
工具推薦
最后推薦一些在開發過程中常用的工具:
OrbitControls 軌道控制器
OrbitControls 是用于調試 Camera 的方法,實例化后可以通過鼠標拖拽來旋轉 Camera 鏡頭的角度,鼠標滾輪可以控制 Camera 鏡頭的遠近距離,旋轉和遠近都會基于場景的中心點,在調試預覽則會輕松許多。
new THREE.OrbitControls(camera, renderer.domElement)glTF Viewer 模型快速預覽工具
在設計師建模完成導出后,設計師并不知道在 Three.js 最終會呈現一個什么效果或者開發者也想快速的查看模型是否存在問題,glTF 官方貼心的提供了一款快速預覽的工具,提供了兩個版本:Web 版本和 Desktop 版本。
將 .gltf、.bin、.jpg/.png 文件拖拽到工具中,可以調試預覽到模型的動畫、變形目標、背景、線框模式、自動旋轉、光源等功能。
Camera Helper 攝像機調試模式
開啟 Camera Helper 調試模式后,可以直觀的看到 Camera 的 Fov、 Nera、Far的參數效果。
let camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000)let helper = new THREE.CameraHelper(camera)
scene.add(helper)
Light Helper 光源調試模式
聚光燈開啟 Light Helper 調試模式后,可以直觀的看到 distance、angle 的參數效果。
let light = new THREE.DirectionalLight(0xffffff)let helper = new THREE.DirectionalLightHelper(0xffffff)
scene.add(helper)
AxesHelper 坐標軸調試模式
AxesHelper 是在場景的中心點,添加一個坐標軸(紅色:X 軸、綠色:Y 軸、藍色:Z 軸),方便辨別方向。
let axesHelper = new THREE.AxesHelper(10)scene.add(axesHelper)
Cannon.js 3D 物理引擎調試模式
Cannon.js 3D 物理引擎提供的調試模式需引入 Debug renderer for Three.js,可以將創建的物理盒子、球、平面等顯示線框,便于在使用過程中實時查看效果。
let cannonDebugRenderer = new THREE.CannonDebugRenderer(scene, world)function render() {
requestAnimationFrame(render)
cannonDebugRenderer.update() // Update the debug renderer
}
dat.GUI 圖形用戶界面調試工具
在開發過程中,常常需要對參數變量進行微調,針對這個 Three.js 提供了 dat.GUI,dat.GUI 是一個輕量級的圖形用戶界面調試工具,使用后在右上角會出現一個 GUI 可視化參數配置區域,通過修改數值來實時查看結果。
let opts = {x: 0,
y: 0,
scale: 1
}
let gui = new dat.GUI()
gui.add(opts, ‘x’, -3, 3)
gui.add(opts, ‘y’, -3, 3)
gui.add(opts, ‘scale’, 1, 3)
function loop() {
cube.position.x = opt.x
cube.position.y = opt.y
cube.scale.set(opts.scale, opts.scale, opts.scale)
requestAnimationFrame()
}
Stats 調試工具
Stats 工具可以實時查看:
FPS:最后一秒的幀數,越大越流暢
MS:渲染一幀需要的時間(毫秒),越低越好
MB:占用的內存信息
CUSTOM:自定義面板
var stats = new Stats()stats.showPanel(1)
document.body.appendChild(stats.dom)
function animate() {
requestAnimationFrame(animate)
}
requestAnimationFrame(animate)
尾巴
最后,希望本篇文章所講到的內容能幫助你更好的開發 3D 項目。另外,如果你有更好的建議或意見,也歡迎你在下方評論區留言,感謝您的閱讀。
我們會定期更新關于「H5游戲開發」的文章,歡迎關注我們的知乎專欄。
總結
- 上一篇: WinPE安装64位Win7的方法
- 下一篇: 不用U盘从linux重装win系统,不用