WebGL three.js學習筆記 6種型別的紋理介紹及應用

nsytsqdtn發表於2019-05-15

WebGL three.js學習筆記 6種型別的紋理介紹及應用

本文所使用到的demo演示:

高光貼圖Demo演示

反光效果Demo演示(因為是載入的模型,所以速度會慢)

(一)普通紋理

計算機圖形學中的紋理既包括通常意義上物體表面的紋理即使物體表面呈現凹凸不平的溝紋,同時也包括在物體的光滑表面上的彩色圖案,所謂的紋理對映就是在物體的表面上繪製彩色的圖案。

在three.js中使用紋理可以實現很多不同的效果,但是最基本的就是為網格體的每個畫素指定顏色。等同於將一張紋理圖片應用在一個幾何體的材質上。

使用的方式很簡單,只需要設定
material.map = 需要設定的紋理物件
紋理物件的獲得方式也很簡單,只需要使用THREE.TextureLoader().load(url)函式就可以為url指定路徑的紋理圖片建立一個物件。具體的使用方式如下:

        let texture = new THREE.TextureLoader().load("../../../Image/metal-rust.jpg");
        let material = new THREE.MeshBasicMaterial();
        material.map = texture;
        let geometry = new THREE.BoxGeometry(10,10,10);
        let cube = new THREE.Mesh(geometry,material);
        scene.add(cube);

其中,"../../../Image/metal-rust.jpg"是我使用的紋理的路徑,圖片就是下面這一張
紋理貼圖1
建立出來的帶有上圖紋理的cube就是這樣的
普通紋理
除了THREE.TextureLoader()這個載入器以為,three.js還為我們提供了其他自定義的載入器,如dds格式,pvr格式,tga格式。

就拿tga格式舉例,我們要載入tga格式的紋理,首先需要引用TGALoader.js這個檔案,然後建立一個tga格式的載入器
let loader = new THREE.TGALoader();
我們就可以使用loader這個載入器,像上面一樣的載入tga格式的紋理了。
具體程式碼如下:

        let loader = new THREE.TGALoader();
        let texture = loader.load("../../../Image/crate_color8.tga");
        let material = new THREE.MeshBasicMaterial();
        material.map = texture;
        let geometry = new THREE.BoxGeometry(10,10,10);
        let cube = new THREE.Mesh(geometry,material);
        scene.add(cube);

下面是我使用的tga格式的紋理圖片(只能上傳截圖,tga格式圖片的這裡上傳不了)
tga紋理圖片
執行出來是這個樣子的
tga紋理
其他格式的載入也是和tga格式載入方法一樣的,只需要引入相應的js檔案就可以使用了。

(二)凹凸貼圖

凹凸紋理用於為材質新增厚度與深度,如字面意思一樣,可以讓材質看上去是凹凸不平的。凹凸貼圖只包含畫素的相對高度,畫素的密集程度定義凹凸的高度,所以想要讓物體好看,首先還是應該設定一個普通的紋理,再在這個基礎上新增一個凹凸紋理,就可以實現凹凸不平的物體效果。
凹凸貼圖的建立方法很簡單,和普通紋理類似,只是我們設定的不是map,而是bumpMap
material.bumpMap = 需要設定的紋理物件

特別需要注意的是,這裡的材質只能使用MeshPhongMaterial,凹凸貼圖才會有效果。
具體的設定方法如下:

        let geom = new THREE.BoxGeometry(10, 10, 10);
        
        //建立普通紋理材質
        let texture = new THREE.TextureLoader().load("../../../Image/stone.jpg");
        let material = new THREE.MeshPhongMaterial({
            map:texture
        });
        cube = new THREE.Mesh(geom,material);
        cube.position.set(-7,0,0);
        scene.add(cube);

        //建立凹凸紋理材質
        let bumpTexture = new THREE.TextureLoader().load("../../../Image/stone-bump.jpg");
        let bumpMaterial = new THREE.MeshPhongMaterial({
            map:texture,
            bumpMap:bumpTexture,
            bumpScale:2
        });
        bumpCube = new THREE.Mesh(geom,bumpMaterial);
        bumpCube.position.set(7,0,0);
        scene.add(bumpCube);

其中material.bumpScale可以設定凹凸的高度,如果為負值,則表示的是深度。

執行程式截圖如下:
左邊材質的是普通的紋理貼圖,右邊的材質是帶有凹凸紋理的,當前bumpScale設定的是2,兩者看上去有比較明顯的不同
凹凸紋理
使用的紋理圖片如下:
紋理2
凹凸紋理圖片:
凹凸紋理

我們可以發現,凹凸圖只包含了畫素的相對高度,沒有任何的傾斜的方向資訊,所以使用凹凸紋理能表達的深度資訊有限,如果想用實現更多的細節可以使用下面介紹的法向貼圖。

(三)法向貼圖

法向貼圖儲存的不是高度的資訊,而是法向量的資訊,我們使用法向貼圖,只需要很少的頂點和麵就可以實現很豐富的細節。
同樣的,實現法向貼圖和凹凸貼圖也很類似,只需要設定
material.normalMap = 需要設定的紋理物件

同樣也是在MeshPhongMaterial材質中才有效果,還要注意的一點是設定normalScale指定材質的凹凸程度時,normalScale需要接受的是一個THREE.Vector2型別

具體的程式碼如下:

        let geom = new THREE.BoxGeometry(10, 10, 10);

        //建立普通紋理材質
        let texture = new THREE.TextureLoader().load("../../../Image/plaster.jpg");
        let material = new THREE.MeshPhongMaterial({
            map:texture
        });
        cube = new THREE.Mesh(geom,material);
        cube.position.set(-7,0,0);
        scene.add(cube);

        //建立凹凸紋理材質
        let normalTexture = new THREE.TextureLoader().load("../../../Image/plaster-normal.jpg");
        let normalMaterial = new THREE.MeshPhongMaterial({
            map:texture,
            normalMap:normalTexture,
            normalScale:new THREE.Vector2(1,1)
        });
        normalCube = new THREE.Mesh(geom,normalMaterial);
        normalCube.position.set(7,0,0);
        scene.add(normalCube);

場景如下圖,右邊的是帶有法向紋理的物體,明顯感覺出材質的細節多出來了很多。
法向貼圖
用到的紋理圖
plaster
法向紋理圖:
plaster-normal
雖然法向紋理能帶給物體更逼真的效果,但是想要建立法向紋理圖,本身就比較困難,需要ps或者blender這樣的特殊工具。

(四)光照貼圖

如果我們想在場景中新增陰影,three.js給我們提供了renderer.shadowMapEnabled = true這個辦法,但是這對於資源的消耗是很大的。如果我們只是需要對靜態的物體新增陰影效果,我們就有一種開銷很小的辦法,那就是光照貼圖。
光照貼圖是預先渲染好的陰影貼圖,可以用來模擬真實的陰影。我們能使用這種技術建立出解析度很高的陰影,並且不會損耗渲染的效能。因為是提前根據場景渲染好的,所以只對靜態的場景有效。

比如下面這張光照貼圖:
光照貼圖
設定光照貼圖的方式很簡單,只需要設定
material.lightMap = 需要設定的紋理物件
和前面兩個沒什麼太大的區別。當紋理設定好以後,我們還需要把我們的物體擺放在正確的位置,這樣陰影效果才會真實的顯現出來。

        let lightMap = new THREE.TextureLoader().load("../../../Image/lm-1.png");
        let map =  new THREE.TextureLoader().load("../../../Image/floor-wood.jpg");
        //建立地板
        let planeGeo = new THREE.PlaneGeometry(95,95,1,1);
        planeGeo.faceVertexUvs[1] = planeGeo.faceVertexUvs[0];
        let planeMat = new THREE.MeshBasicMaterial({
            color:0x999999,
            lightMap:lightMap,//在地板的材質上新增光照貼圖
            map:map//地板的普通紋理材質
        });
        let plane = new THREE.Mesh(planeGeo,planeMat);
        plane.rotation.x = -Math.PI / 2;
        plane.position.y = 0;
        scene.add(plane);

        //建立大的cube
        var boxGeo = new THREE.BoxGeometry(12,12,12);
        var material = new THREE.MeshBasicMaterial();
        material.map = new THREE.TextureLoader().load("../../../Image/stone.jpg");
        var box = new THREE.Mesh(boxGeo,material);
        box.position.set(0.9,6,-12);
        scene.add(box);

        //建立小的cube
        var boxGeo = new THREE.BoxGeometry(6, 6, 6);
        var material = new THREE.MeshBasicMaterial();
        material.map = new THREE.TextureLoader().load("../../../Image/stone.jpg");
        var box = new THREE.Mesh(boxGeo,material);
        box.position.set(-13.2, 3, -6);
        scene.add(box);

其中,planeGeo.faceVertexUvs[1] = planeGeo.faceVertexUvs[0] 這句話是我們需要明確的指定光照貼圖的uv對映(將紋理的哪一部分應用在物體表面)這樣才能將光照貼圖的使用和其他的紋理分別開來。
planeGeo.faceVertexUvs儲存的就是幾何體面的uv對映資訊,我們將faceVertexUvs[0]層的資訊儲存到faceVertexUvs[1]層

faceVertexUvs的官方文件解釋:

.faceVertexUvs : Array
Array of face UV layers, used for mapping
textures onto the geometry. Each UV layer is an array of UVs matching
the order and number of vertices in faces.

執行結果如圖:
光照貼圖

(五)高光貼圖

高光是光源照射到物體然後反射到人的眼睛裡時,物體上最亮的那個點就是高光,高光不是光,而是物體上最亮的部分。
而高光貼圖就是高光貼圖是反應光線照射在物體表面的高光區域時所產生的環境反射,它的作用是反映物體高光區域效果。

通過高光貼圖,我們可以為材質建立一個閃亮的、色彩明快的貼圖。高光貼圖的黑色部分會暗淡,而白色的部分會比較的亮。
建立高光貼圖的方法也和前面差不多
material.specularMap= 需要設定的紋理物件

具體的程式碼如下:

        let map = new THREE.TextureLoader().load("../../../Image/Earth.png");
        let specularMap = new THREE.TextureLoader().load("../../../Image/EarthSpec.png");
        let normalMap = new THREE.TextureLoader().load("../../../Image/EarthNormal.png");
        let sphereMaterial = new THREE.MeshPhongMaterial({
            map:map,
            specularMap:specularMap,
            normalMap:normalMap,
            normalScale:THREE.Vector2(2,2),
            specular:0x0000ff,
            shininess:2
        });
        let sphereGeometry = new THREE.SphereGeometry(30,30,30);
        let sphere = new THREE.Mesh(sphereGeometry,sphereMaterial);
        scene.add(sphere);

這段程式碼建立了一個球體,併為球體的材質貼上了普通紋理,法向紋理和高光紋理,其中specular屬性可以決定反光的顏色,shininess可以決定發光的亮度。

執行出來的樣子如下:
可以看到,海洋的地方比較亮,而大陸的的顏色相對較暗。
高光貼圖

高光貼圖Demo演示

用到的幾張紋理圖:
紋理
高光紋理:
高光紋理
法向紋理:
法向紋理

(六)環境貼圖

如果我們想要在場景中建立反光的物體,通常會使用光線追蹤的演算法,但是這對cpu的消耗是巨大的,但是環境貼圖就給我們創造了更容易的方法,我們只需要使用給物體的材質貼上環境貼圖,就可以模擬反光的效果。

首先我們的場景需要有一個環境,這個環境我們可以使用CubeTextureLoader()來建立。在前面的文章裡曾經介紹過如何建立360度全景的環境,這個CubeTextureLoader()和那裡面用到的其實是一樣的,只是版本的更替,現在更多使用這個函式。
具體用法是:

        let cubeMap = new THREE.CubeTextureLoader().setPath(
        "../../../Image/MapCube/Bridge2/").load(
            [
                'posx.jpg',
                'negx.jpg',
                'posy.jpg',
                'negy.jpg',
                'posz.jpg',
                'negz.jpg'
            ]);
        scene = new THREE.Scene();
        scene.background = cubeMap;

在前面的文章已經介紹過,這裡就不再贅述。
建立cubeMap所用到的圖片在http://www.humus.name/index.php?page=Textures可以直接下載。

我們有了一個可以反射的環境以後,就可以開始為我們的物體建立材質貼圖了。
建立材質貼圖的方式和前面還是差不多
material.envMap = scene.background;

scene.background就是我們剛剛所建立的場景的背景,這樣材質的環境貼圖就相當於貼上了周圍環境,從攝像機去看物體的話,看上去就是對環境有一個反射的效果了。

建立的程式碼如下:

       function initObject()
       {
       let material = new THREE.MeshPhongMaterial();
       material.envMap = scene.background;
       let boxGeometry = new THREE.BoxGeometry(5,50,50);
       let box = new THREE.Mesh(boxGeometry,material);
       box.position.set(-70,0,-10);
       box.rotation.y-=Math.PI/2;
       scene.add(box);
       let sphereGeometry = new THREE.SphereGeometry(30,30,30);
       let sphere = new THREE.Mesh(sphereGeometry,material);
       sphere.position.set(70,0,-10);
       scene.add(sphere);
       }

和前面的程式碼沒有太大的區別,這裡主要是建立了兩個物體,都使用的相同環境貼圖的材質。

執行的結果:
環境貼圖
可以看到,這兩個物體都對環境有反射的效果。

值得注意的是,我們使用環境貼圖建立的材質僅僅靜態的環境貼圖。我們只能看到物體上面有周圍環境的反射,看不到物體對其他物體的反射。

如果我們要看到物體對其他物體的反射,我們可以使用一個新的物件——cubeCamera
建立cubeCamera的方法很簡單.
let cubeCamera = new THREE.CubeCamera(0.1, 2000, 2048);
scene.add(cubeCamera);

其中:
第一個引數0.1是相機的近裁剪距離
第二個引數2000是相機遠裁剪距離
第三個引數2048是相機解析度

使用THREE.CubeCamera可以為場景中所要渲染的物體建立快照,並使用這些快照建立CubeMap物件。但是需要確保攝像機被放置在THREE.Mesh網格上你所想顯示反射的位置上。例如,我們想在球體的中心顯示反射,由於球體所處的位置是(0, 0, 0),所以我們沒有顯示的指定THREE.CubeCamera的位置。我們只是將動態反射應用於球體上,所以把它的envMap設定為cubeCamera.renderTarget
即material.envMap = cubeCamera.renderTarget;

簡單來說,就是把我們所要顯示反射的“鏡子”的material.envMap設定為cubeCamera.renderTarget,同時還要把cubeCamera的位置設定到鏡子的位置,cubeCamera.position.copy(鏡子.position);

程式碼如下:

        let loader = new THREE.STLLoader();
        loader.load("../../../asset/LibertStatue.obj.stl",function (bufferGeometry)
        {
            let material = new THREE.MeshBasicMaterial();
            material.envMap=scene.background;
            obj = new THREE.Mesh(bufferGeometry,material);
            obj.scale.set(50,50,50);
            scene.add(obj);
        });//載入stl模型
       
        let cubeMaterial = new THREE.MeshPhongMaterial();
        cubeMaterial.envMap = cubeCamera.renderTarget;
        let boxGeometry = new THREE.BoxGeometry(3, 400, 400);
        let box = new THREE.Mesh(boxGeometry, cubeMaterial);
        box.position.set(0, 0, -300);
        box.rotation.y -= Math.PI / 2;
        scene.add(box);
        cubeCamera.position.copy(box.position);

這段程式碼中,我們從外部載入了一個stl格式的模型,也可以就使用簡單的幾何體來演示。下面的一部分程式碼就建立了可以反射的鏡子。

最後,我們還需要在render()中新增cubeCamera.update(renderer, scene)用cubeCamera進行渲染

function render()
    {
        if(obj) obj.rotation.y+=0.02;
        cubeCamera.update(renderer, scene);
        stats.update();
        renderer.clear();
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }

執行後的情況如下:
cubeCamera
我們可以看到我們載入的stl模型在我們建立的鏡子中反射出來了,並且會根據模型的移動,鏡子的反射也會自動變化。

反光效果Demo演示

以上就是介紹的全部型別的紋理。

反光效果demo的完整程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Environment Map</title>
    <script src="../../../import/three.js"></script>
    <script src="../../../import/stats.js"></script>
    <script src="../../../import/Setting.js"></script>
    <script src="../../../import/OrbitControls.js"></script>
    <script src="../../../import/STLLoader.js"></script>
    <style type="text/css">
        body {
            border: none;
            cursor: pointer;
            width: 100%;
            height: 1000px;
            /*全屏顯示的設定*/
            margin: 0;
            overflow: hidden; /*消除瀏覽器的滾動條*/

        }

        /*載入動畫*/
        #loading {
            width: 100%;
            height: 850px;
            background-color: #333333;
        }

        #spinner {
            width: 100px;
            height: 100px;
            position: fixed;
            top: 50%;
            left: 50%;
        }

        .double-bounce1, .double-bounce2 {
            width: 100%;
            height: 100%;
            border-radius: 50%;
            background-color: #67CF22;
            opacity: 0.6;
            position: absolute;
            top: 0;
            left: 0;
            -webkit-animation: bounce 2.0s infinite ease-in-out;
            animation: bounce 2.0s infinite ease-in-out;
        }

        .double-bounce2 {
            -webkit-animation-delay: -1.0s;
            animation-delay: -1.0s;
        }

        @-webkit-keyframes bounce {
            0%, 100% {
                -webkit-transform: scale(0.0)
            }
            50% {
                -webkit-transform: scale(1.0)
            }
        }

        @keyframes bounce {
            0%, 100% {
                transform: scale(0.0);
                -webkit-transform: scale(0.0);
            }
            50% {
                transform: scale(1.0);
                -webkit-transform: scale(1.0);
            }
        }
    </style>
</head>
<body onload="Start()">
<!--載入動畫的div-->
<div id="loading">
    <div id="spinner">
        <div class="double-bounce1"></div>
        <div class="double-bounce2"></div>
    </div>
</div>
<script>
    let camera, renderer, scene, cubeCamera, light;
    let controller;

    function initThree()
    {
        //渲染器初始化
        renderer = new THREE.WebGLRenderer({
            antialias: true
        });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setClearColor(0x333333);
        document.body.appendChild(renderer.domElement);//將渲染新增到body中
        //初始化攝像機,這裡使用透視投影攝像機
        camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 10000);
        camera.position.set(20, 15, 200);
        camera.up.x = 0;//設定攝像機的上方向為哪個方向,這裡定義攝像的上方為Y軸正方向
        camera.up.y = 1;
        camera.up.z = 0;
        camera.lookAt(0, 0, 0);

        let cubeMap = new THREE.CubeTextureLoader().setPath("../../../Image/MapCube/Bridge2/").load(
            [
                'posx.jpg',
                'negx.jpg',
                'posy.jpg',
                'negy.jpg',
                'posz.jpg',
                'negz.jpg'
            ]);
        scene = new THREE.Scene();
        scene.background = cubeMap;

        cubeCamera = new THREE.CubeCamera(0.1, 1000, 2048);
        scene.add(cubeCamera);
        //相機的移動
        controller = new THREE.OrbitControls(camera, renderer.domElement);
        controller.target = new THREE.Vector3(0, 0, 0);

        light = new THREE.AmbientLight(0xffffff);
        light.position.set(-50, -50, -50);
        scene.add(light);
    }
    
     let obj;
    function initObject()
    {
        let loader = new THREE.STLLoader();
        loader.load("../../../asset/LibertStatue.obj.stl",function (bufferGeometry)
        {
            let material = new THREE.MeshBasicMaterial();
            material.envMap=scene.background;
            obj = new THREE.Mesh(bufferGeometry,material);
            obj.scale.set(50,50,50);
            scene.add(obj);
            console.log(obj);
        });
        let cubeMaterial = new THREE.MeshPhongMaterial();
        cubeMaterial.envMap = cubeCamera.renderTarget;
        let boxGeometry = new THREE.BoxGeometry(3, 400, 400);
        let box = new THREE.Mesh(boxGeometry, cubeMaterial);
        box.position.set(0, 0, -300);
        box.rotation.y -= Math.PI / 2;
        scene.add(box);
        cubeCamera.position.copy(box.position);
        document.getElementById('loading').style.display = 'none';
    }
    //渲染函式
    function render()
    {
        if(obj) obj.rotation.y+=0.02;
        cubeCamera.update(renderer, scene);
        stats.update();
        renderer.clear();
        requestAnimationFrame(render);
        renderer.render(scene, camera);
    }

    //功能函式
    function setting()
    {
        loadFullScreen();
        loadAutoScreen(camera, renderer);
        loadStats();
    }

    //執行主函式
    function Start()
    {
        initThree();
        initObject();
        setting();
        render();
    }
</script>
</body>
</html>

相關文章