Three.js進階篇之9 - 紋理對映和UV對映
本文將詳細描述如何使用Three.js給3D物件新增貼圖(Texture Map,也譯作紋理對映,“貼圖”的翻譯要更直觀,而“紋理對映”更準確。)。為了能夠檢視線上演示效果,你需要有一個相容WebGL的現代瀏覽器(最好是Chrome/FireFox/Safari/Edge/IE11+)。
本文的線上演示結果和程式碼請點選這裡:Three.js貼圖例項。
什麼是貼圖(Texture Mapping)
貼圖是通過將影象應用到物件的一個或多個面,來為3D物件新增細節的一種方法。
這使我們能夠新增表面細節,而無需將這些細節建模到我們的3D物件中,從而大大精簡3D模型的多邊形邊數,提高模型渲染效能。
開始吧
這裡方便起見,我們使用踏得網線上開發工具來一步步邊學邊操作。
請點選新建作品,在第三方庫中選擇Three.js 80版本,這將自動載入對應版本的Three.js開發庫(注:你也可以直接把<script src="http://wow.techbrood.com/libs/three.r73.js"></script>拷貝到HTML程式碼皮膚中去)。
首先我們建立一個立方體,在JavaScript皮膚中編寫程式碼如下:
- var camera;
- var scene;
- var renderer;
- var mesh;
- init();
- animate();
- function init() {
- scene = new THREE.Scene();
- camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000);
- var light = new THREE.DirectionalLight( 0xffffff );
- light.position.set( 0, 1, 1 ).normalize();
- scene.add(light);
- var geometry = new THREE.CubeGeometry( 10, 10, 10);
- var material = new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );
- mesh = new THREE.Mesh(geometry, material );
- mesh.position.z = -50;
- scene.add( mesh );
- renderer = new THREE.WebGLRenderer();
- renderer.setSize( window.innerWidth, window.innerHeight );
- document.body.appendChild( renderer.domElement );
- window.addEventListener('resize', onWindowResize, false);
- render();
- }
- function animate() {
- mesh.rotation.x += .04;
- mesh.rotation.y += .02;
- render();
- requestAnimationFrame( animate );
- }
- function render() {
- renderer.render( scene, camera );
- }
- function onWindowResize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize( window.innerWidth, window.innerHeight );
- render();
- }
點選選單欄中的[執行]選單(),或者按快捷鍵:CTRL+R,來執行該程式碼,你將看到一個旋轉的藍色立方體:
我們接下來要做的就是把這個立方體變成一個遊戲裡常見的木箱子,如下圖所示:
為此我們需要一張箱子表面的影象,並用這張影象對映到立方體物件的材料中去,
這裡我們直接使用線上圖片http://wow.techbrood.com/uploads/1702/crate.jpg.
JS程式碼中修改之前的材料(material)建立程式碼:
- var material = new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } );
為使用貼圖:
- var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('http://wow.techbrood.com/uploads/1702/crate.jpg') } );
再執行下(按[執行]選單或CTRL+R快捷鍵),你會看到一個旋轉的板條箱,而不是一個普通的藍色立方體。
在構造我們的材質時,我們指定了texture屬性並將其值設定為木箱影象,Three.js然後會載入紋理影象並對映到立方體各個面上。
那麼,問題是如果我們想給不同的面新增不同的紋理貼圖,該怎麼辦呢?
一種方法是使用材料陣列,我們建立6個新材料,每一個使用不同的紋理貼圖:bricks.jpg,clouds.jpg,stone-wall.jpg,water.jpg,wood-floor.jpg以及上面的crate.jpg。
相應的,我們把材料構造程式碼修改為:
- var material1 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/crate.jpg') } );
- var material2 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/bricks.jpg') } );
- var material3 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/clouds.jpg') } );
- var material4 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/stone-wall.jpg') } );
- var material5 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/water.jpg') } );
- var material6 = new THREE.MeshPhongMaterial( {
- map: THREE.ImageUtils.loadTexture('/uploads/1702/wood-floor.jpg') } );
- var materials = [material1, material2, material3, material4, material5, material6];
- var meshFaceMaterial = new THREE.MeshFaceMaterial( materials );
上述程式碼,我們先分別建立了6個材料,組成了一個材料陣列,並使用這個陣列建立一個MeshFaceMaterial物件。
最後,我們需要告訴我們的3D模型來使用這個新的組合“面材料”,修改下面的程式碼:
- mesh = new THREE.Mesh(geometry, material );
為:
- mesh = new THREE.Mesh(geometry, meshFaceMaterial);
再執行下(按[執行]選單或CTRL+R快捷鍵),你就將看到立方體的各個表面使用了不同的貼圖。
這很酷,Three.js會自動把陣列中的這些材料應用到不同的面上去。
但問題又來了,隨著3D模型的面的增長,為每個面建立貼圖是不現實的。
這就是為什麼我們需要另外一種更為普遍的解決方法:UV對映的原因。
UV對映(UV Mapping)
UV對映最典型的例子就是把一張地圖對映到3D球體的地球儀上去。其本質上就是把平面影象的不同區塊對映到3D模型的不同面上去。我們把之前的6張圖拼裝成如下的一張圖:http://wow.techbrood.com/uploads/160801/texture-atlas.jpg.
修改如下程式碼:
- var material1 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/crate.jpg') } );
- var material2 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/bricks.jpg') } );
- var material3 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/clouds.jpg') } );
- var material4 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/stone-wall.jpg') } );
- var material5 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/water.jpg') } );
- var material6 = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/wood-floor.jpg') } );
為:
- var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture('images/texture-atlas.jpg') } );
我們又把程式碼給改回來使用一張貼圖了,接下來我們需要把貼圖的不同位置對映到立方體不同的面上去。
首先我們建立貼圖的6個子圖,在建立完材料的程式碼後面新增如下幾行:
- var bricks = [new THREE.Vector2(0, .666), new THREE.Vector2(.5, .666), new THREE.Vector2(.5, 1), new THREE.Vector2(0, 1)];
- var clouds = [new THREE.Vector2(.5, .666), new THREE.Vector2(1, .666), newTHREE.Vector2(1, 1), new THREE.Vector2(.5, 1)];
- var crate = [new THREE.Vector2(0, .333), new THREE.Vector2(.5, .333), newTHREE.Vector2(.5, .666), new THREE.Vector2(0, .666)];
- var stone = [new THREE.Vector2(.5, .333), new THREE.Vector2(1, .333), new THREE.Vector2(1, .666), new THREE.Vector2(.5, .666)];
- var water = [new THREE.Vector2(0, 0), new THREE.Vector2(.5, 0), new THREE.Vector2(.5, .333), new THREE.Vector2(0, .333)];
- var wood = [new THREE.Vector2(.5, 0), new THREE.Vector2(1, 0), new THREE.Vector2(1, .333), new THREE.Vector2(.5, .333)];
上面的程式碼建立了六個陣列,每一個對應於紋理貼圖中的每個子影象。每個陣列包含4個點,定義子影象的邊界。座標的範圍值是0到1,(0,0)表示左下角,(1,1)表示右上角。
子影象的座標是根據貼圖中百分比來定義。比如下面這個磚頭子影象:
- var bricks = [
- new THREE.Vector2(0, .666),
- new THREE.Vector2(.5, .666),
- new THREE.Vector2(.5, 1),
- new THREE.Vector2(0, 1)
- ];
在貼圖中的位置在左上角(佔據橫向1/2,豎向1/3的位置),以逆時針方向來定義頂點座標,從該子影象較低的左下角開始。
左下角:
0 - 最左邊
.666 - 底部向上2/3處
右下角:
.5 - 中間線
.666 - 底部向上2/3處
右上角:
.5 - 中間線
1 - 頂邊
右上角:
0 - 最左邊
1 - 頂邊
定義好子影象後,我們現在需要把它們對映到立方體的各個面上去。首先新增如下程式碼:
- geometry.faceVertexUvs[0] = [];
上述程式碼清除現有的UV對映,接著我們新增如下程式碼:
- geometry.faceVertexUvs[0][0] = [ bricks[0], bricks[1], bricks[3] ];
- geometry.faceVertexUvs[0][1] = [ bricks[1], bricks[2], bricks[3] ];
- geometry.faceVertexUvs[0][2] = [ clouds[0], clouds[1], clouds[3] ];
- geometry.faceVertexUvs[0][3] = [ clouds[1], clouds[2], clouds[3] ];
- geometry.faceVertexUvs[0][4] = [ crate[0], crate[1], crate[3] ];
- geometry.faceVertexUvs[0][5] = [ crate[1], crate[2], crate[3] ];
- geometry.faceVertexUvs[0][6] = [ stone[0], stone[1], stone[3] ];
- geometry.faceVertexUvs[0][7] = [ stone[1], stone[2], stone[3] ];
- geometry.faceVertexUvs[0][8] = [ water[0], water[1], water[3] ];
- geometry.faceVertexUvs[0][9] = [ water[1], water[2], water[3] ];
- geometry.faceVertexUvs[0][10] = [ wood[0], wood[1], wood[3] ];
- geometry.faceVertexUvs[0][11] = [ wood[1], wood[2], wood[3] ];
geometry物件的faceVertexUvs屬性包含該geometry各個面的座標對映。既然我們對映到一個多維資料集,你可能會疑惑為什麼陣列中有12個面。原因是在ThreeJS模型中,立方體的每個面實際上是由2個三角形組成的。所以我們必須單獨對映每個三角形。上述場景中,ThreeJS將為我們載入單一材料貼圖,自動分拆成三角形並對映到每個面。
這裡要注意每個面的頂點座標的定義順序必須遵循逆時針方向。為了對映底部三角形,我們需要使用的頂點指數0,1和3,而要對映頂部三角形,我們需要使用索引1,2,和頂點的3。
最後,我們替換如下程式碼:
- var meshFaceMaterial = new THREE.MeshFaceMaterial( materials );
- mesh = new THREE.Mesh(geometry, meshFaceMaterial);
為:
- mesh = new THREE.Mesh(geometry, material);
我們再執行下程式碼(按[執行]選單或CTRL+R快捷鍵),將看到各個面使用不同貼圖的旋轉立方體。
當然對於複雜的物件,我們還可以在建模的時候建立好模型貼圖,並匯出為ThreeJS所支援的模型格式,然後在場景中直接載入。
這個超出本文範圍,請自行搜尋本站Three.js線上例項。
參考: http://solutiondesign.com/blog/-/blogs/webgl-and-three-js-texture-mappi-1/
編注:原文線上演示和原始碼連結不可用,已重新建立在WOW上。
轉載地址:http://www.techbrood.com/zh/news/webgl/深入理解three_js紋理貼圖和uv對映.html
相關文章
- three.js UV對映簡述JS
- 頂點著色網格轉換為 UV 對映的紋理化網格
- 多視角三維模型紋理對映 01模型
- D3D中的紋理對映(2)3D
- Mybatis學習筆記(5)-高階對映之多對多對映MyBatis筆記
- JavaScript 資料處理 - 對映表篇JavaScript
- Mybatis學習筆記(4)-高階對映之一對多對映MyBatis筆記
- Mybatis學習筆記(3)—高階對映之一對一對映MyBatis筆記
- Cache與主存之間的直接對映,全相聯對映和組項聯對映以及其地址變換
- mybatis高階結果對映MyBatis
- Mybatis處理列名—欄位名對映— 駝峰式命名對映MyBatis
- TypeScript 之對映型別TypeScript型別
- MyBatis從入門到精通(九):MyBatis高階結果對映之一對一對映MyBatis
- MyBatis從入門到精通(十一):MyBatis高階結果對映之一對多對映MyBatis
- 多重對映
- mybatis入門基礎(四)----輸入對映和輸出對映MyBatis
- MyBaits | 對映檔案之引數處理AI
- MyBatis(四) 對映器配置(自動對映、resultMap手動對映、引數傳遞)MyBatis
- 思考工具之概念對映 | Untools
- MyBatis框架之SQL對映和動態SQLMyBatis框架SQL
- ElasticSearch - 基礎概念和對映Elasticsearch
- 修改對映地址
- 埠對映,內網網站對映外網訪問,透過80埠對映實現內網網站
- java高階用法之:在JNA中使用型別對映Java型別
- [非專業翻譯] Mapster - 對映前&對映後
- 如何高效的處理陣列對映陣列
- Linux埠對映是什麼?如何進行埠對映?Linux
- JPA關係對映系列四:many-to-many 關聯對映
- MongoDB、Java和物件關係對映MongoDBJava物件
- Mybatis結果對映MyBatis
- VMware Fusion 埠對映
- Docker-埠對映Docker
- Nginx埠對映配置Nginx
- ElasticSearch中的對映Elasticsearch
- TypeScript 對映型別TypeScript型別
- 記憶體對映記憶體
- hibernate 元件對映元件
- NDK java的對映Java