2天賺了4個W,手把手教你用Threejs搭建一個Web3D汽車展廳!

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!javascript


前言

事情是這樣的,前段時間外包工頭老楊又來找我了,說某汽車大品牌要開發一個網頁展廳,但願能夠在網頁裏360度展現它家新款汽車的3d模型,還要可讓用戶DIY汽車部件的顏色。html

image.png

可能不少朋友看完此文後會以爲兩週時間還挺充裕,但其實不是,做爲丙方沒什麼話語權,常常要配合甲方反覆修改,不少時候改來改去最後拖到上線前一晚沒辦法了直接上,一個campaign site的生命週期也不長,最長也就在線上待1-3個月。前端

嘿嘿,時間緊,預算多!java

我心想報價四個W,再給他留點砍價空間,後端

誰知道老楊一口答應,還說完事要請我去XX人間api

我猜他起碼要從客戶那賺10個W微信

image.png

互動話題

先看最終效果,大家以爲值四個W嗎?markdown

BMW在線展廳

也就是以前的文章《三種前端實現VR全景看房的方案!說不定哪天就用得上!》裏提到的用threejs來實現的app

3D引擎的基本知識

本文的目標是讓你們看完以後能夠馬上上手用起來,既然要用3d引擎,那咱們理解了一些3d的基本知識後,再看threejs的API文檔效率就會很高。不管什麼3d引擎,都不外乎由如下幾種基本元素構成dom

場景(scene

一個容器,容納着除渲染器之外的三維世界裏的一切。場景的元素採用右手笛卡爾座標系,x軸正方向向右,y軸正方向向上,z軸由屏幕從裏向外

image.png

攝像機(camera

就像人的眼睛,在一個空間裏能夠看向任意方向,能夠經過參數調節可視角度和可視距離。

通常咱們使用符合物理世界近大遠小真實狀況的透視相機PerspectiveCamera,還有一些特殊狀況,須要遠近大小是同樣的,那就要用正交相機OrthographicCamera

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
//構造函數參數
//fov:視場角
//aspect:視場寬高比(通常用 畫布寬/畫布 高便可)
//near:能看多近
//far:能看多遠
//這幾個參數決定了哪些scene裏的三維頂點會被渲染/繪製出來
複製代碼

image.png

渲染器(renderer

camerascene裏看到的內容渲染/繪製到畫布上

幾何體(geometry

3D世界裏的全部物體都是點組成面面組成幾何體。相信你們對如下標準的幾何體比較熟悉

  • 球體
  • 立方體
  • 圓錐體
  • 圓柱體
  • ...

是由點構成的,又能夠組成各式各樣的幾何體。以球體舉例,球體面上的點越多,球就越圓。但點越多,運算量也會越大...

image.png

另外咱們通常說的3d模型就是一個或多個幾何體,只是有的3d模型文件裏除了包含幾何體還能夠包含一些額外的信息,好比貼圖,材質等...須要在讀取模型文件時解析出來

燈光(light

3d引擎在沒有手動建立光的狀況下會默認有個環境光,否則你什麼都看不到。常見的燈光有如下幾種類型

  • AmbientLight(環境光,沒有方向全局打亮,不會產生明暗)
  • DirectionLight(平行光,參考日光來理解)

image.png

  • PointLight(點光源,參考燈泡來理解)

image.png

  • SpotLight(聚光燈,參考舞臺聚光燈)

image.png

貼圖(texture

想象一下你手裏有一個立方體,你用一張A4紙包裹上立方體的全部面,並在上面畫畫。你畫的內容就是貼圖

image.png

有一些類型的貼圖會和光照發生反應...後面咱們用到的時候再說

材質(material

延續貼圖裏的想象,你用白卡紙畫畫,仍是用油紙畫畫,呈現出來的質感是不一樣的對不對,這就是材質!下面五個球的顏色都是同樣的,而材質從左至右分別是

  • MeshBasicMaterial(基礎材質,不受光照影響)
  • MeshStandardMaterial(PBR標準材質)
  • MeshPhongMaterial(高光材質,適用於陶瓷,烤漆類質感)
  • MeshToonMaterial(卡通材質,俗稱三渲二)
  • MeshStandardMaterial(PBR標準材質模擬金屬反射)

image.png

來實戰吧!

有了這些基礎知識,再來使用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 );
複製代碼

2021-06-28 14_14_45.gif

很顯然,場景是生效的...你們注意看源碼塊中的註釋

汽車模型

回到我們的項目上來,品牌方給的是一個很是精細的模型,文件量有好幾百兆,數百萬面(triangles)。

Snipaste_2021-07-01_11-02-49.jpg

我說這可用不了,你得減面還得給我轉成引擎能支持的格式gltfobj

根據個人評估,要想在移動端網頁裏流暢運行,最多不能超過10萬面

外包工頭老楊說,你也別讓客戶給你弄了,他們都不會

我知道你懂,你就給弄了算了,我給你加【5K】

加5K你讓我怎麼好意思拒絕呢...

image.png

而後,我花25美刀巨資在sketchfab上購買了一個模型

再稍微改改就能知足要求,固然sketchfab也有免費模型

但畢竟收了老楊5K,不花點錢我內心略感不安吶 :p

優化模型結構

image.png

根據實際的需求,好比車窗要透明能夠看到內飾,因此車窗就得單獨給有透明屬性的材質。車輪,燈罩,車網,車架,車身等等都要拆成獨立的幾何體才能獨立配置材質

image.png

梳理好模型結構後,咱們就要準備模型文件了

加載模型

3d模型的文件格式有不少,但threejs裏經常使用的基本是

  • OBJ格式

老牌通用3d模型文件,不包含貼圖,材質,動畫等信息。

  • GLTF格式(圖形語言傳輸格式)

由OpenGL官方維護團隊推出的現代3d模型通用格式,能夠包含幾何體、材質、動畫及場景、攝影機等信息,而且文件量還小。有3D模型界的JPEG之稱。

原項目中我使用的是OBJ格式,本文裏咱們使用GLTF格式。利用threejs提供的editor,咱們能夠將模型的格式進行轉換並導出。

image.png

經過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' );
    }
);
複製代碼

2021-07-01 14_22_13.gif

經過這個代碼能夠遍歷查看模型裏的幾何體列表

console.log(gltf.scene.children);

//能夠用for,也能夠用traverse api
//gltf.scene.children.traverse((child){});
複製代碼

貼圖和材質

如今咱們來給幾何體添加貼圖,貼圖怎麼作是設計師的專業。這裏不過多的說,咱們只須要知道,這些貼圖如何使用便可。

image.png

  • 普通貼圖(_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);
}
複製代碼

而後根據名稱手動一一對應,好比咱們先把車輪轂的貼圖給加上

image.png

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;
    }
}
複製代碼

咱們繼續把車輪的貼圖給加上

image.png

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;
}
複製代碼

仔細看看動圖裏前擋風和天窗透明度的差別

2021-07-02 09_50_58.gif

  • 玻璃的反射

想真的去反射真實的環境?你別想多了,用envMap作個假的看起來就很能夠了...

child.material.envMap=allTexture["skymap"].texture;
//環境反射貼圖envMap的映射方式,這裏用的是一個叫等量矩形投影的映射方法
child.material.envMap.mapping = THREE.EquirectangularReflectionMapping;
//環境反射貼圖的強度
child.material.envMapIntensity=1;
複製代碼

2021-07-02 10_02_08.gif

仔細看動圖裏的前擋風玻璃,是否是反射了什麼東西?看過《三種前端實現VR全景看房的方案!說不定哪天就用得上!》的小夥伴們,記得這張圖麼?

  • 車身漆面質感

使用MeshStandardMaterial材質,經過調節metalnessroughness的值來調節金屬的質感

child.material = new THREE.MeshStandardMaterial();
                    
child.material.color=new THREE.Color(0x70631B);
child.material.metalness = 0.44;
child.material.roughness = 0;
複製代碼

image.png

信息點

畢竟是個在線展廳,在車身周圍得呈現一些信息點,點擊後能夠彈窗顯示更多信息對吧。實現方式一樣在VR全景的文章中提到過了,就是Sprite+Raycast

2021-07-02 11_01_36.gif

//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);
        }
    });
}
複製代碼

UI怎麼作

image.png

既然咱們用了threejs,因此咱們就要在threejs裏把UI作出來嗎?這麼想的話,會把本身累死。要知道在3d場景裏作2d的UI可不算是一件容易的事,還要實現UI的一些用戶行爲(點擊,拖動等)的話就更麻煩了...因此咱們直接用html來作UI就好啦~

2021-07-02 11_20_56.gif

到這裏,這個3D汽車展廳的核心部分你已經學會(fei)了吧!

結語

以上只是對threejs一個很是粗淺的使用,threejs能實現的酷炫效果遠遠不止於此,但願本文能讓你開始對Web3D開發產生興趣,若是以爲本文還不錯,請點贊收藏關注吧~

BTW:明明是個笨馳的smart,怎麼說是BMW呢?由於本故事純屬虛構撒,請勿對號入座。本文中全部聊天記錄均爲使用微信對話生成器僞造。

查看本文配套視頻教程

源碼下載

微信搜索並關注公衆號「大帥老猿」,回覆「smart3d」得到本文所有源碼

相關文章
相關標籤/搜索