本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!javascript
事情是這樣的,前段時間外包工頭
老楊又來找我了,說某汽車大品牌要開發一個網頁展廳,但願能夠在網頁裏360度展現它家新款汽車的3d模型,還要可讓用戶DIY汽車部件的顏色。html
可能不少朋友看完此文後會以爲兩週時間還挺充裕,但其實不是,做爲丙方沒什麼話語權,常常要配合甲方反覆修改,不少時候改來改去最後拖到上線前一晚沒辦法了直接上,一個campaign site的生命週期也不長,最長也就在線上待1-3個月。前端
嘿嘿,時間緊,預算多!java
我心想報價四個W,再給他留點砍價空間,後端
誰知道老楊一口答應,還說完事要請我去XX人間api
我猜他起碼要從客戶那賺10個W微信
先看最終效果,大家以爲值四個W嗎?markdown
也就是以前的文章《三種前端實現VR全景看房的方案!說不定哪天就用得上!》裏提到的用
threejs
來實現的app
本文的目標是讓你們看完以後能夠馬上上手用起來,既然要用3d引擎,那咱們理解了一些3d的基本知識後,再看threejs
的API文檔效率就會很高。不管什麼3d引擎,都不外乎由如下幾種基本元素構成dom
scene
)一個容器,容納着除渲染器之外的三維世界裏的一切。場景的元素採用右手笛卡爾座標系,x軸正方向向右,y軸正方向向上,z軸由屏幕從裏向外
camera
)就像人的眼睛,在一個空間裏能夠看向任意方向,能夠經過參數調節可視角度和可視距離。
通常咱們使用符合物理世界近大遠小真實狀況的透視相機PerspectiveCamera
,還有一些特殊狀況,須要遠近大小是同樣的,那就要用正交相機OrthographicCamera
PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
//構造函數參數
//fov:視場角
//aspect:視場寬高比(通常用 畫布寬/畫布 高便可)
//near:能看多近
//far:能看多遠
//這幾個參數決定了哪些scene裏的三維頂點會被渲染/繪製出來
複製代碼
renderer
)將
camera
在scene
裏看到的內容渲染/繪製到畫布上
geometry
)3D世界裏的全部物體都是
點組成面
,面組成幾何體
。相信你們對如下標準的幾何體比較熟悉
面
是由點構成的,面
又能夠組成各式各樣的幾何體。以球體舉例,球體面上的點越多,球就越圓。但點越多,運算量也會越大...
另外咱們通常說的3d模型
就是一個或多個幾何體,只是有的3d模型文件裏除了包含幾何體還能夠包含一些額外的信息,好比貼圖,材質等...須要在讀取模型文件時解析出來
light
)3d引擎在沒有手動建立光的狀況下會默認有個
環境光
,否則你什麼都看不到。常見的燈光有如下幾種類型
texture
)想象一下你手裏有一個立方體,你用一張A4紙包裹上立方體的全部面,並在上面畫畫。你畫的內容就是
貼圖
。
有一些類型的貼圖會和光照發生反應...後面咱們用到的時候再說
material
)延續貼圖裏的想象,你用白卡紙畫畫,仍是用油紙畫畫,呈現出來的質感是不一樣的對不對,這就是
材質
!下面五個球的顏色都是同樣的,而材質從左至右分別是
有了這些基礎知識,再來使用threejs就很容易上手了。能夠說在3dmax等軟件中調出來的90%的效果,用threejs都能找到對應的配置參數。
//<div id='container' style="width:100%;height: 100%;"></div>
var scene, camera, renderer;
function init(){
scene = new THREE.Scene();
//這裏參數不懂的同窗回去看基本知識裏的camera部分
camera = new THREE.PerspectiveCamera(90, document.body.clientWidth / document.body.clientHeight, 0.1, 100);
//camera的位置在x0,y0,z3,還記得迪爾卡右手座標系嗎?
camera.position.set(0, 0, 3);
renderer = new THREE.WebGLRenderer();
renderer.setSize(document.body.clientWidth, document.body.clientHeight);
document.getElementById("container").appendChild(renderer.domElement);
var controls = new THREE.OrbitControls(camera, renderer.domElement);
//等待添加模型
loop();
}
function loop() {
requestAnimationFrame(loop);
renderer.render(scene, camera);
}
window.onload = init;
複製代碼
如今咱們能夠先添加一個標準幾何體來試試看,好比咱們添加一個立方體來試試看
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( {color: 0x00ff00} );
const cube = new THREE.Mesh( geometry, material );
scene.add( cube );
複製代碼
很顯然,場景是生效的...你們注意看源碼塊中的註釋
回到我們的項目上來,品牌方給的是一個很是精細的模型,文件量有好幾百兆,數百萬面(triangles
)。
我說這可用不了,你得減面
還得給我轉成引擎能支持的格式gltf
或obj
根據個人評估,要想在移動端網頁裏流暢運行,最多不能超過10萬面
外包工頭老楊說,你也別讓客戶給你弄了,他們都不會
我知道你懂,你就給弄了算了,我給你加【5K】
加5K你讓我怎麼好意思拒絕呢...
而後,我花25美刀
巨資在sketchfab
上購買了一個模型
再稍微改改就能知足要求,固然sketchfab也有免費模型
但畢竟收了老楊5K,不花點錢我內心略感不安
吶 :p
根據實際的需求,好比車窗要透明能夠看到內飾
,因此車窗就得單獨給有透明屬性的材質。車輪,燈罩,車網,車架,車身等等都要拆成獨立的幾何體
才能獨立配置材質
。
梳理好模型結構後,咱們就要準備模型文件了
3d模型的文件格式有不少,但threejs
裏經常使用的基本是
老牌通用3d模型文件,不包含貼圖,材質,動畫等信息。
由OpenGL官方維護團隊推出的現代3d模型通用格式,能夠包含幾何體、材質、動畫及場景、攝影機等信息,而且文件量還小。有3D模型界的JPEG之稱。
原項目中我使用的是OBJ格式,本文裏咱們使用GLTF格式。利用threejs提供的editor,咱們能夠將模型的格式進行轉換並導出。
經過GLTFLoader,咱們能夠加載一個.gltf
格式的3d模型文件。須要注意的是,這些Loader都以插件的形式存在,須要引入相應的XXXLoader.js
才能使用
//<script src="js/GLTFLoader.js"></script>
//放到以前添加立方體的代碼處
const loader = new THREE.GLTFLoader();
loader.load(
'images/model.gltf',
function ( gltf ) {
scene.add( gltf.scene );
},
function ( xhr ) {
//偵聽模型加載進度
console.log( ( xhr.loaded / xhr.total * 100 ) + '% loaded' );
},
function ( error ) {
//加載出錯時的回調
console.log( 'An error happened' );
}
);
複製代碼
經過這個代碼能夠遍歷查看模型裏的幾何體列表
console.log(gltf.scene.children);
//能夠用for,也能夠用traverse api
//gltf.scene.children.traverse((child){});
複製代碼
如今咱們來給幾何體添加貼圖,貼圖怎麼作是設計師的專業。這裏不過多的說,咱們只須要知道,這些貼圖如何使用便可。
_col
)material.map,替代顏色
_nor
)material.normalMap,讓細節程度較低的表面生成高細節程度的精確光照方向和反射效果
_occ
)material.aoMap,用來描繪物體和物體相交或靠近的時候遮擋周圍漫反射光線的效果
material.envMap,用於模擬材質反射周圍環境的效果
咱們如今先把這些貼圖文件統一加載到內存裏
var allTexture;
function loadAllTexture(cb){
allTexture = {};
var loadIndex = 0;
var textures = [
"skymap",
"shache_occ",
"shache_nor",
"shache_col",
"neishi_occ",
"neishi_nor",
"mennei_col",
"luntai_nor",
"luntai_col",
"lungu_occ",
"lungu_nor",
"lungu_col",
"linjian_occ",
"linjian_nor",
"linjian_col",
"floor",
"deng_occ",
"deng_nor",
"deng_col",
"cheshen_occ",
"cheshen_nor",
"chejia_occ",
"chejia_nor",
"chedengzhao_nor"
];
function loadNextTexture(){
var textureName = textures[loadIndex];
loadTexture("images/textures/"+textureName+".jpg",function(texture){
if(loadIndex<textures.length-1){
allTexture[textureName] = {
texture:texture
};
loadIndex++;
loadNextTexture();
}else{
if(cb)cb();
}
});
}
loadNextTexture();
}
function loadTexture(filepath,cb){
const textureLoader = new THREE.TextureLoader();
textureLoader.load(filepath,cb);
}
複製代碼
而後根據名稱手動一一對應,好比咱們先把車輪轂的貼圖給加上
for(var i=0;i<gltf.scene.children[0].children.length;i++){
var modelObj = gltf.scene.children[0].children[i];
if(modelObj.name=="smart_lungu0"||modelObj.name=="smart_lungu1"||modelObj.name=="smart_lungu2"||modelObj.name=="smart_lungu3"){
modelObj.material = new THREE.MeshStandardMaterial();
modelObj.material.map = allTexture["lungu_col"].texture;
modelObj.material.normalMap = allTexture["lungu_nor"].texture;
modelObj.material.aoMap = allTexture["lungu_occ"].texture;
}
}
複製代碼
咱們繼續把車輪的貼圖給加上
else if(modelObj.name=="smart_chelun0"||modelObj.name=="smart_chelun1"||modelObj.name=="smart_chelun2"||modelObj.name=="smart_chelun3"){
modelObj.material = new THREE.MeshStandardMaterial();
modelObj.material.map = allTexture["luntai_col"].texture;
modelObj.material.normalMap = allTexture["luntai_nor"].texture;
}
複製代碼
其他的材質貼圖都如此添加上,後續固然還有不少材質的細節是能夠去調整的,但這是個細活兒,這裏主要重點分享下玻璃的反射和透明
,金屬漆的反光
天窗和前擋風玻璃的透明度以及基底顏色是不一樣的
else if(child.name=="smart_boli"){
child.material=new THREE.MeshPhongMaterial();
child.material.color = new THREE.Color( 0x333333 );
child.material.transparent=true;
child.material.opacity=.2;
}else if(child.name=="smart_tianchuang"){
child.material=new THREE.MeshPhongMaterial();
child.material.color = new THREE.Color( 0x000 );
child.material.transparent=true;
child.material.opacity=.5;
}
複製代碼
仔細看看動圖裏前擋風和天窗透明度的差別
想真的去反射真實的環境?你別想多了,用envMap作個假的看起來就很能夠了...
child.material.envMap=allTexture["skymap"].texture;
//環境反射貼圖envMap的映射方式,這裏用的是一個叫等量矩形投影的映射方法
child.material.envMap.mapping = THREE.EquirectangularReflectionMapping;
//環境反射貼圖的強度
child.material.envMapIntensity=1;
複製代碼
仔細看動圖裏的前擋風玻璃,是否是反射了什麼東西?看過《三種前端實現VR全景看房的方案!說不定哪天就用得上!》的小夥伴們,記得這張圖麼?
使用
MeshStandardMaterial
材質,經過調節metalness
,roughness
的值來調節金屬的質感
child.material = new THREE.MeshStandardMaterial();
child.material.color=new THREE.Color(0x70631B);
child.material.metalness = 0.44;
child.material.roughness = 0;
複製代碼
畢竟是個在線展廳,在車身周圍得呈現一些信息點,點擊後能夠彈窗顯示更多信息對吧。實現方式一樣在VR全景的文章中提到過了,就是
Sprite
+Raycast
//frame只是一個標記,叫什麼都行
var poiPosArray=[
{x:-1.47,y:0.87,z:-0.36,frame:1},
{x:-1.46,y:0.49,z:-0.69,frame:2},
{x:1.5,y:.7,z:0,frame:8},
{x:0.33,y:1.79,z:0,frame:3},
{x:0,y:0.23,z:0.96,frame:4},
{x:0.73,y:1.38,z:-0.8,frame:5},
{x:-.1,y:1.17,z:0.88,frame:6},
{x:-1.16,y:0.16,z:0.89,frame:7}
],poiObjects=[];
function setupInfoPoint(){
const pointTexture = new THREE.TextureLoader().load("images/point.png");
var group = new THREE.Group();
var materialC = new THREE.SpriteMaterial( { map: pointTexture, color: 0xffffff, fog: false } );
for ( var a = 0; a < poiPosArray.length; a ++ ) {
var x = poiPosArray[a].x;
var y = poiPosArray[a].y-.5;
var z = poiPosArray[a].z;
var sprite = new THREE.Sprite( materialC );
sprite.scale.set( .15, .15, 1 );
sprite.position.set( x, y, z );
sprite.idstr="popup_"+poiPosArray[a].frame;
group.add( sprite );
poiObjects.push(sprite);
}
scene.add( group );
document.body.addEventListener("click",function (event) {
event.preventDefault();
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera( mouse, camera );
var intersects = raycaster.intersectObjects( poiObjects );
if(intersects.length>0){
var popIndex=parseInt(intersects[ 0 ].object.idstr.substr(6,1));
console.log(popIndex);
}
});
}
複製代碼
既然咱們用了threejs
,因此咱們就要在threejs
裏把UI作出來嗎?這麼想的話,會把本身累死。要知道在3d場景裏作2d的UI可不算是一件容易的事,還要實現UI的一些用戶行爲(點擊,拖動等)的話就更麻煩了...因此咱們直接用html
來作UI就好啦~
到這裏,這個3D汽車展廳的核心部分你已經學會(fei)了吧!
以上只是對threejs
一個很是粗淺的使用,threejs
能實現的酷炫效果遠遠不止於此,但願本文能讓你開始對Web3D開發產生興趣,若是以爲本文還不錯,請點贊收藏關注吧~
BTW:明明是個笨馳
的smart,怎麼說是BMW呢?由於本故事純屬虛構撒,請勿對號入座。本文中全部聊天記錄均爲使用微信對話生成器僞造。
微信搜索並關注公衆號「大帥老猿
」,回覆「smart3d
」得到本文所有源碼