本文將詳細描述如何使用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面板中編寫代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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)創建代碼:
1 | var material = new THREE.MeshPhongMaterial( { ambient: 0x050505, color: 0x0033ff, specular: 0x555555, shininess: 30 } ); |
為使用貼圖:
1 | 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。
相應的,我們把材料構造代碼修改為:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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模型來使用這個新的組合“面材料”,修改下面的代碼:
1 | mesh = new THREE.Mesh(geometry, material ); |
為:
1 | 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.
修改如下代碼:
1 2 3 4 5 6 | 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' ) } ); |
為:
1 | var material = new THREE.MeshPhongMaterial( { map: THREE.ImageUtils.loadTexture( 'images/texture-atlas.jpg' ) } ); |
我們又把代碼給改回來使用一張貼圖了,接下來我們需要把貼圖的不同位置映射到立方體不同的面上去。
首先我們創建貼圖的6個子圖,在創建完材料的代碼后面添加如下幾行:
1 2 3 4 5 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), new THREE.Vector2(1, 1), new THREE.Vector2(.5, 1)]; var crate = [ new THREE.Vector2(0, .333), new THREE.Vector2(.5, .333), new THREE.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)表示右上角。
子圖像的坐標是根據貼圖中百分比來定義。比如下面這個磚頭子圖像:
1 2 3 4 5 6 | 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 - 頂邊
定義好子圖像后,我們現在需要把它們映射到立方體的各個面上去。首先添加如下代碼:
1 | geometry.faceVertexUvs[0] = []; |
上述代碼清除現有的UV映射,接著我們添加如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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。
最后,我們替換如下代碼:
1 2 | var meshFaceMaterial = new THREE.MeshFaceMaterial( materials ); mesh = new THREE.Mesh(geometry,? meshFaceMaterial); |
為:
1 | mesh = new THREE.Mesh(geometry,? material); |
我們再運行下代碼(按[運行]菜單或CTRL+R快捷鍵),將看到各個面使用不同貼圖的旋轉立方體。
當然對于復雜的對象,我們還可以在建模的時候建立好模型貼圖,并導出為ThreeJS所支持的模型格式,然后在場景中直接加載。
這個超出本文范圍,請自行搜索本站Three.js在線實例。
?
參考:?http://solutiondesign.com/blog/-/blogs/webgl-and-three-js-texture-mappi-1/
編注:原文在線演示和源代碼鏈接不可用,已重新建立在WOW上。