這裡是「Three.js學習日誌」的第8篇,本篇的主旨是要介紹Geometry的概念,還有一些常用的Geometry子類的使用方法。這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。
我們昨天提到了一些關於BufferGeometry
的基礎面知識,也介紹了一些BufferGeometry
底下的屬性用途,而接著主要是想講講在客製理想幾何結構時可以用的一些方法,或是一些必要的知識。
「假如說,我想要做一個正四面體,而這個正四面體,四個面要可以填充不同的Matatrial,要怎麼做?」
其實這是我年初的時候在寫一個Side Project時碰到的狀況。
我當時一開始想得很簡單,因為平常使用BoxGeometry
的時候,我們其實可以在生成Mesh
的時候,傳入一系列的Material
(要以陣列的方式傳入),這樣就可以讓方塊的6個面個別使用不同的材質,像這樣:
const geo = new BoxGeometry(1,1,1,10,10,10);
const mats = [
new MeshBasicMaterial({color:0xff0000}),
new MeshBasicMaterial({color:0x00ff00}),
new MeshBasicMaterial({color:0x0000ff}),
new MeshBasicMaterial({color:0xf0f0f0}),
new MeshBasicMaterial({color:0x0f0f0f}),
new MeshBasicMaterial({color:0xfff000})
]
const mesh = new Mesh(geo,mats);
Scene.add(mesh)
codepen連結:點我
順帶一提,有些比較早期的Three.js
版本(其實也沒很早,大概是r124
以前的版本),多數Geometry
子類底下是有faces
這個屬性的。
faces是一個陣列,裡面會有一個多面體每個面的實例
不過後來因為r125
做出了一個重大改變,官方決定要改革幾何結構的底層邏輯,最後導致了很多開箱即用的Geometry
子類連帶受到影響,有興趣的可以看看這篇文。
為了避免偏離主題太多,講古就先講到這邊~ 讓我們重新回到剛剛的正四面體。
我一開始的想法想得很簡單,畢竟既然要做四個面有不同材質的正四面體,那當然首先就是先建立一個TetrahedronGeometry
實例吧~ 然後接著應該就是如法炮製,傳入4個面的材質就好。
TetrahedronGeometry就是three.js提供的正四面體(開箱即用的)幾何結構
所以我就這樣做:
// TetrahedronGeometry的第一個參數是他的外接球半徑
const geo = new TetrahedronGeometry(1);
const mats = [
new MeshBasicMaterial({color:0xff0000}),
new MeshBasicMaterial({color:0x00ff00}),
new MeshBasicMaterial({color:0x0000ff}),
new MeshBasicMaterial({color:0xf0f0f0})
]
const mesh = new Mesh(geo,mats);
Scene.add(mesh)
結果是和我預期的完全不一樣,什麼都沒有長出來 = =
我當時看了半天看不明白為什麼啥都沒跑出來,於是就先把多重材質取消,改成引入單一材質
// TetrahedronGeometry的第一個參數是他的外接球半徑
const geo = new TetrahedronGeometry(1);
const mat = new MeshBasicMaterial({color:0xff0000}),
const mesh = new Mesh(geo,mat);
Scene.add(mesh)
這次倒是很老實地跑出來了
在了解問題出在多重材質之後,我就開始查找資料,上網發問。最後我在比較BoxGeometry
和TetrahedronGeometry
兩者物件結構時發現了groups
這個屬性。
BoxGeometry
的groups屬性是一個長度為6的陣列:
TetrahedronGeometry
的groups屬性是一個長度為0的陣列:
所以我就直接找到官方文件上面關於bufferGeometry.groups
的解釋。
這裡就有提到了,groups
就是一個geometry
底下的分組,每一組會構成一個獨立draw call,而且同時會附帶一個Material
的slot。
這時我才理解到,原來TetrahedronGeometry
沒有辦法填入多重材質是因為它底下並沒有去定義groups
(陣列是空的)。
所以這邊如果我們要讓TetrahedronGeometry
能夠填入四種材質,那就是得用addGroup
去給內部頂點做分組。
const geo = new TetrahedronGeometry(1);
//這邊addGroup 第一個參數是代表從哪一個頂點起算,第二個參數則是該組一共多少頂點,接著最後參數則是給定一個數字做為該組材質的編號
geo.addGroup(0, 3, 0);
geo.addGroup(3, 3, 1);
geo.addGroup(6, 3, 2);
geo.addGroup(9, 3, 3);
const mats = [
new MeshBasicMaterial({color:0xff0000}),
new MeshBasicMaterial({color:0x00ff00}),
new MeshBasicMaterial({color:0x0000ff}),
new MeshBasicMaterial({color:0xf0f0f0})
]
const mesh = new Mesh(geo,mats);
Scene.add(mesh)
codepen 連結:點我
終於弄出來四個面不同材質了!菜鳥小弟我當時感覺很開心,於是就想說那就來試著把MeshBasicMaterial
加上圖片紋理看看好了~
這邊我們先稍微超前一下進度。
Three.js
的圖片紋理基本上要用TextureLoader
- 也就是紋理載入器,來讀取,我們可以使用.load
這個方法,透過填入不同的url
,這樣就可以返回對應的texture
,然後再把他賦值給MeshBasicMaterial
的map
這一屬性。
把每個面都填入一張300*300的圖片。
const tl = new TextureLoader();
const mats = [
new MeshBasicMaterial({map: tl.load( 'https://picsum.photos/seed/123/300/300')}),
new MeshBasicMaterial({map: tl.load( 'https://picsum.photos/seed/456/300/300')}),
new MeshBasicMaterial({map: tl.load( 'https://picsum.photos/seed/789/300/300')}),
new MeshBasicMaterial({map: tl.load( 'https://picsum.photos/seed/012/300/300')})
]
codepen連結:點我
結果。
圖片確實有跑出來了,但我們可以看到其中有幾個面,圖片的定位好像被放得不太對,而且好像有被拉長的狀況,這是為什麼呢?
其實這個就是跟我們前面提到的uv
有關係,也就是TetrahedronGeometry
預設提供的uv
跟我們理想的不一樣。
這邊再複習一次,uv
的定義就是:
代表一張材質貼圖上面,每一塊小部位,實際是要映射到模型上的「哪個位置」
所以這邊如果我們想要弄出理想中的正三角形,我們必須要重新定義TetrahedronGeometry
的uv
。
geo.setAttribute("uv", new Float32BufferAttribute([
//這些代表每個頂點實際上是映射到該面圖片紋理的哪一個位置
//第一個頂點映射到原點
0,0,
1, 0, // 第二個頂點映射到(1,0),也就是圖片的右下角
0.5, 1, // 第三個頂點映射到(0.5,1),也就是圖片的上緣中點
//
1, 0,
0.5, 1,
0,0,
//
1, 0,
0.5, 1,
0,0,
//
0,0,
1, 0,
0.5, 1
], 2)); // 這邊的2也是類似stride的意思,代表每個頂點持有兩組數值
codepen連結:點我
是不是看起來好些了呢?
今天我們講了關於Geometry
的面的相關知識,包括怎麼樣把BufferGeometry
所產生的模型來分面,還有如何調整Geometry
的uv
,有興趣的讀者不妨自己拿其他的內建Geometry
來試著客製化作為練習~
Geometry
的部分依然還沒有結束,敬請各位讀者期待。