第一次使用threejs到實際項目中,開始的時候心情有點小激動,畢竟是第一次嘛,然而做着做着就感受到這玩意水好深,滿滿的都是坑,填都填不過來。經過老板20天慘無人道的摧殘,終於小有成就。
因為第一次搞這玩意,相對的遇到的問題也是大把的,讓我來一一訴說一路上遇到的各種問題。
開發使用: C4D、Blender2.75、[threejs-r72](http://threejs.org/)
萬事開頭難,第一個問題就是怎么才能把3d軟件中做好的模型顯示在瀏覽器中。
一、模型在軟件中的導入與導出。
這個項目中涉及到單個模型和動畫模型,而不同模型的導入導出有差異,下面就告訴大家我是如何將坑填平的。
1、單個模型:
因為自己不會使用3D軟件建模,只能求助公司大神設計師來一起搞。剛開始的想法是直接用3d軟件建模然后直接導出obj格式來用,然后設計師用C4D做好了一個測試模型,發現模型數量少的話網頁的大小還可以接受,但是由於項目的模型數量比較多,然后粗算了一下模型總的大小,發現超出了預想,所以得另尋它法。
接着在網上搜索發現Blender這玩意,由於設計師對C4D情有獨鍾不會Blender軟件,所以決定用C4D做好模型然后導出obj格式接着再導入到Blender里面,再經由Blender導出需要的格式。
因為是第一次倒騰這個軟件,所以並不會導出。然后就在網上搜素了怎么用Blender導出的json、js文件。經過測試js文件導出比較大,最后果斷選擇json。
在軟件中如何導入導出如圖所示:
2、動畫模型:
由於設計師出一個動畫模型也沒有這么快,就沒法進行導出測試。於是看到threejs官網里有demo中使用的動畫模型,我就拿過來進行測試,發現動畫模型跟單個模型導出選擇有差異然后發現更單個模型的導出有出入,經過反復的測試,得到導動畫模型需要注意的幾點,
(1)選擇好動畫的幀數,如果沒選擇,導出的json文件會有空幀。並且文件也會相對增大。
(2)選擇好導出選項中Animation,一般就選擇Morph Animation、Embed Animation選項。
單個模型以及動畫模型導出選項如下圖所示:
圖二 (左邊為導出單個模型,右邊導出動畫模型)
注:導出單個帶材質模型需要在導出選項的時候需要在shading選項中選擇Face Materials。
拿着設計師做好的動畫模型導出json格式后碰到了一些問題,雖然json格式的大小相比obj格式的要小一點,不過項目中有人物的動畫模型導出的json格式大小還是太大。然后為了解決這個問題,跟設計師進行討論,然后得到以下解決方案:
(1)將模型的面和頂點在不影響正常顯示的情況下進行刪減
(2)對動畫模型的幀數、面、頂點也進行刪減
經過反復的修改和測試終於將動畫模型控制在500-1000KB左右,單個模型控制在100K左右。
模型的導出問題解決了然后是對模型進行導入到頁面中去。
二、模型加載到頁面
在threejs官網上看到利用obj格式加載的demo比較多,所以就直接使用的是obj的格式模型進行加載,根據demo利用THREE.OBJLoader()、THREE.OBJMTLLoader()進行加載。然后設計大神給了我一個帶材質的模型讓我進行測試,發現兩問題:
(1)模型材質丟失
(2)模型的大小太大(模型量少大小還可以接受,考慮到此次項目中的模型量多,估算了一下大概有70-80M左右)
由於模型大小太大所以放棄。所以改選用THREE.JSONLoader()進行加載。
在這一步由於是直接導出帶材質的json格式,材質對應到模型的各個面是一個問題。然后在官網的demo上看到THREE.MeshFaceMaterial()方法,查看了一下文檔,然后迅速解決這個了這個問題。
另外threejs提供了各種模型的加載方法具體可去threejs.org查詢。
雖然解決了模型加載和面的問題,但是模型在網頁的表現與軟件中渲染的差別太大。剛開始以為是模型方面以及導入導出的方式不對,於是和設計師進行各種修改然后反復的測試,發現沒什么變化。然后求助stackoverflow找到了答案。是由於模型的shading的原因造成的,然后根據網上提供的解決方案在材質中加上materials.shading = THREE.FlatShading來解決。不過有些Android手機上會出現材質無法解析的錯誤。而且在r72的版本中MeshLambertMaterial已經移除了shading這個屬性。
下圖就是在網頁中渲染的結果對比。
圖三 (左邊為未加shading,右邊加了shading)
靜態模型的導入沒有問題了,然后是動畫模型的導入,參考官網demo,直接套用基本的動畫沒有太大問題,只是項目中有一個人物運球的動畫模型比較難折騰,剛開始的時候直接是按照動畫模型的導出直接導,然后同步到頁面中發現只能導出一個動畫,另外一個丟失了,定位了一下問題好像是Blender不能同時導出多個動畫(具體是不是待研究)。最后想了一個辦法就是采取分開導出再創建一個obj包裹兩個動畫,操作這個obj。來解決多個動畫問題(如有更好的辦法求大神指導)。
至此將模型放到頁面中的准備工作都做完了。接着就是模型上的事件與動畫,模型上的各種事件整的頭都大了,然而到現在我還是有一些東西沒有弄清楚原理還得繼續研究。
三、模型的圓周運動
剛開始項目中有個需求就是進入頁面中模型需要做一個圓周運動,圓周運動以前在數學中學過,但是一直沒用所以就忘了,然后就在網上找有關圓周運動計算的方法。這里不做過多的解釋,用下面一張圖來完整解釋怎么來計算圓周運動。
圖四 (圓周運動計算)
在頁面中測試的結果如下圖所示:
圖五 (圓周運動測試結果)
功能代碼如下:
var clock = new THREE.Clock(); //時間跟蹤 //圓周運動 var time = clock.getElapsedTime() * 1; loadMesh.position.x = Math.cos( time ) * 10; loadMesh.position.y = Math.sin( time ) * 10;
老板看了一下,然后腦補一下整個頁面的效果說還是不要這樣子,很多模型都這樣運動的話畫面太亂了,最后決定的是簡單點直接把所有模型擺放成一個球體形狀,然后模型不單獨運動,而是整體繞中心轉,這個實現起來比較簡單思路是直接設置外層模型y軸旋轉就可以了。
四、所有模型在空間里的位置
整體的運動效果描繪出來了,接着就是開始實施了。接着遇到了一個算是比較坑的問題。那就是模型在空間位置的確認了,由於對3D場景的不熟悉,將所有模型擺出一個球體就有點困難了。只能求助設計大神了,然后他在C4D中將所有模型擺成一個球體之后,然后像操作模型一樣導了一份obj給我。然后我利用Blender打開(如下圖六所示),然后我看了下,每個模型在軟件中都存在一個x,y,z值,我抱着僥幸的心里把所有模型的x,y,z記錄下來然后填到頁面中。最后發現球體的形狀出來了,只是距離有點差異,接着想了個投機取巧的辦法把所有的x,y,z進行等比的放大縮小,改完效果還不錯。最后就是拉着設計師瘋狂調整細節方面的問題。效果如下圖七所示。
圖六 (球體模型)
圖七 (頁面中渲染效果)
頁面的基本樣子出來了,剩下的就是頁面的交互了,整個難點基本都在這里了。
項目需求:在頁面中選中模型,然后選擇模型現在在屏幕中間,然后用手指進行360度滑動,點擊關閉按鈕回到原型原有的位置。
思路:選中模型-->移動某個東西-->綁定旋轉事件-->回位。
可以說項目大部分的時間都花在實現這個操作過程中。下面就簡單說一下我是怎么去填這些坑的。
五、在頁面中選中模型
之后的所有操作都要基於這個模型去做,所有第一步就需要選中這個模型。這個跟以前做的完全不同,然后在官網demo和stackoverflow游盪,因為涉及到屏幕坐標和世界坐標這個概念有種完全懵逼的感覺。還好官網上有demo的支持,參考了demo之后發現,首先獲取屏幕坐標的x,y然后想辦法轉換成向量,接着標准化向量,通過raycaster.intersectObjects的檢測來獲取選中的模型。功能代碼如下:
mouse.x = ( e.clientX / window.innerWidth ) * 2 - 1; //鼠標的x到屏幕y軸的距離與屏幕寬的一半的比值 絕對值不超過1 mouse.y = - ( e.clientY / window.innerHeight ) * 2 + 1; //鼠標的y到屏幕x軸的距離與屏幕寬的一半的比值 絕對值不超過1 //新建一個三維變換半單位向量 假設z方向就是0.5,這樣左右移的時候,還會有前后移的效果 //屏幕和場景轉換工具根據照相機,把這個向量從屏幕轉化為場景中的向量 var vector = new THREE.Vector3( mouse.x, mouse.y, 0.5 ).unproject( camera ); //變換過后的向量vector減去相機的位置向量后標准化 var raycaster = new THREE.Raycaster( camera.position, vector.sub( camera.position ).normalize() ); //新建一條從相機的位置到vector向量的一道光線 var intersects = raycaster.intersectObjects( objects ); if ( intersects.length > 0 ) { //把選中的對象放到全局變量SELECTED中 SELECTED = intersects[ 0 ].object; }
參考: http://threejs.org/examples/webgl_octree_raycasting.html
模型選中之后然后開始下一步。
六、相機的移動
因為要讓選中的模型顯示在屏幕中間,於是想了兩種方案:
(1)改變選中物體的x,y,z值
經過反復的測試發現改變x,y,z值,模型會在空間中亂竄,把握不好位置。於是就思考其它的方案。
(2)移動相機
移動相機這一塊被坑了無數次,因為剛開始對這個相機的原理不是很清楚,就隨意試了幾個值(可以利用threejs提供的相機輔助線來操作具體參考:http://threejs.org/docs/index.html#Reference/Extras.Helpers/CameraHelper),來證明自己的方向是正確的,發現模型確實出現在不同的位置了,然后就繼續往下深挖。首先在google中尋找有關threejs中相機的原理,具體參考:http://www.flowers1225.com/lessons/2015/12/08/1,對原理有一定了解之后,然后就想怎么讓相機出現在自己想要的位置上。
首先獲取到選中模型的世界坐標,然后再根據當前的坐標值改變相機的x,y,z讓相機直接照在當前模型上。測試了一下發現選中模型出現在屏幕中間,不過根據模型的不同位置上有偏差,這個后來設置了默認值來修正偏差。效果下圖所示:
圖八 (相機移動)
功能實現了,然后發現選擇之后出現的太突然了,沒有體現相機移動的效果,然后就想到使用TweenMax這個動畫庫來實現平滑過渡的動畫。代碼如下:
TweenMax.to(camera.position, 1, { x: x, y: y, z: z, ease:Expo.easeInOut, onComplete: function (){} })
加上之后效果如下圖所示:
圖九 (相機平滑移動)
模型已經放大顯示在屏幕中,然后點擊關閉按鈕讓模型回到原位這個就直接用TweenMax將相機的position值設置為初始值就可以了。
接下來就是開發用手指操作模型旋轉的功能了。
七、操作模型旋轉
這個功能我卡了好久,里面涉及到數學中的矩陣、四元數、歐拉角、向量乘積、軸-角啥的,完全都忘了。在二維中旋轉可以通過角度來控制,而在三維空間中需要通過四元數或者矩陣來實現,萬般無奈只能求助萬能的google來了解怎么在三維空間中對物體進行旋轉操作。 通過了解在3D中表現旋轉有三種方法,矩陣、歐拉角、四元組。 最后選用四元組來實現旋轉的方法,簡單點說原因就是四元組的是圍繞一個軸來做旋轉,而且在threejs中也提供了THREE.Quaternion()方法,然后在threejs的包中找到了一些寫好的鼠標控制的類(TrackballControls.js、OrbitControls.js等),然后參考着源碼,將方法雖然寫出來了,但是有時候操作起來會有意向不到的bug,所以里面的細節還有待深挖。下面簡述一下旋轉的思路。
首先四元數控制旋轉需要的是一個旋轉軸和一個旋轉弧度,直接上圖清楚明了
圖十
然后就想辦法得到這個兩個東西,接着開始想怎么弄到旋轉弧度,首先獲取到點擊開始和結束的x,y值,然后得到兩個向量之間的夾角,得到一個弧度,然后在設置一個默認的旋轉系數,兩者相乘得到弧度。接着通過開始和結束的向量乘積得到旋轉軸,最后通過setFromAxisAngle(axis, angle)得到旋轉四元數。
核心代碼如下:
function rotateMatrix(rotateStart, rotateEnd){ var axis = new THREE.Vector3(), quaternion = new THREE.Quaternion(); //得到開始和結束向量間的夾角 var angle = Math.acos(rotateStart.dot(rotateEnd) / rotateStart.length() / rotateEnd.length()); if (angle){ //如果夾角等於0, 說明物體沒有旋轉 axis.crossVectors(rotateStart, rotateEnd).normalize(); //rotateStart,rotateEnd向量乘積 標准化 得到旋轉軸 angle *= _that.rotationSpeed; //rotationSpeed旋轉系數 得到旋轉弧度 quaternion.setFromAxisAngle(axis, angle); //從一個旋轉軸和旋轉弧度得到四元組, 如果要讓物體相反方向旋轉 設置angle為負 } return quaternion; //返回一個旋轉的四元數 } this.handleRotation = function(object){ _that.rotateEndPoint = _that.projectOnTrackball(_that.deltaX, _that.deltaY); var rotateQuaternion = rotateMatrix(_that.rotateStartPoint, _that.rotateEndPoint); var curQuaternion = object.quaternion; curQuaternion.multiplyQuaternions(rotateQuaternion, curQuaternion); //設置四元組 a x b curQuaternion.normalize(); object.setRotationFromQuaternion(curQuaternion); //方法通過規范化的旋轉四元數直接應用旋轉 參數必須normalize() };
在這里有個小坑,就是所有模型的外觀大小不同,當旋轉的時候,可能會出現誤操作,然后用了一個小技巧就是用一個透明的方體包裹模型,這樣做就相當於旋轉一個cube了,而且設置方體有網格時對排除bug有幫助。如下圖所示:
圖十一 (外層包裹框)
雖然旋轉的效果做出來了,但是旋轉里面涉及的東西還有一些理解的不是很清楚,還需要繼續深入研究,等我研究透徹了再重新整理一下(還望大神指點一下)。
參考:四元數旋轉、cube旋轉、TrackballControls.js源碼
八、 燈光
最后就是燈光的控制,因為NBA的主色調是紅藍色,設計大神就想模型在頁面中有紅藍光打在模型上的感覺,於是照着這個方向,他開始在軟件中調試燈光,調整好之后我按照設計師在軟件中調整好的位置擺放燈光,發現跟預想的有點差異,頁面中的顏色顯的太深了,於是在整個空間中加上了一個白色的全局光來提亮整體的亮度,然后對燈光進行反復的調整,就到了現在頁面中呈現的樣子了。燈光調整的過程如圖所示:
圖十二 (燈光調整過程)
注: threejs提供很多種燈光具體可以在threejs文檔中查看http://threejs.org/docs/index.html#Reference/Lights,而燈光調整可以借助threejs提供的輔助線來調整,THREE.PointLightHelper()、THREE.SpotLightHelper()等。這樣有助於迅速定位到問題。
最后就是手機兼容性的測試了(因為是微信的活動頁,所以其它瀏覽器未測試)。在iPhone下整體體驗較好,在Android下使用r71版本發現模型會出現菱角不分明的情況如圖三所示,之后改用r72版本,用高版本的Android測試發現問題解決了,然后拿我自己的mx2測試時出現另外一個問題,直接卡在加載頁面進不去頁面,然后通過調試工具發現在控制台中有方法報錯(小米2也是同等情況),倒騰了好久,然而並沒有什么卵用,因為是threejs內部報錯,無奈只能放大招,做一個Android版本來解決這個問題。