本文將詳細描述如何使用Three.js給3D對象添加貼圖(Texture Map,也譯做紋理映射,「貼圖」的翻譯要更直觀,而「紋理映射」更準確。)。爲了可以查看在線演示效果,你須要有一個兼容WebGL的現代瀏覽器(最好是Chrome/FireFox/Safari/Edge/IE11+)。web
本文的在線演示結果和代碼請點擊這裏:Three.js貼圖實例。數組
貼圖是經過將圖像應用到對象的一個或多個面,來爲3D對象添加細節的一種方法。瀏覽器
這使咱們可以添加表面細節,而無需將這些細節建模到咱們的3D對象中,從而大大精簡3D模型的多邊形邊數,提升模型渲染性能。app
這裏方便起見,咱們使用踏得網在線開發工具來一步步邊學邊操做。dom
請點擊新建做品,在第三方庫中選擇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,來運行該代碼,你將看到一個旋轉的藍色立方體:開發工具
咱們接下來要作的就是把這個立方體變成一個遊戲裏常見的木箱子,以下圖所示:webgl
爲此咱們須要一張箱子表面的圖像,並用這張圖像映射到立方體對象的材料中去,spa
這裏咱們直接使用在線圖片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映射最典型的例子就是把一張地圖映射到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上。