這裡是「Three.js學習日誌」的第10篇,本篇的主旨是要介紹紋理與材質的關係,這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。
其實我們在前幾天的內容有大概給大家展示過紋理(Texture)的使用,但沒有講得很Detail,所以今天我特別規劃了一篇文章要用來講解這個部分。
在3D建模的知識圈中,通常會有紋理(Texture)和材質(Material)這兩個專有名詞交互穿插於文獻之中。
通常紋理(Texture)這個詞指的是:
一組圖像數據,用來表示3D模型表面上的細節。
而材質(Material)這個詞則指的是:
一組定義光照模型如何與表面交互的係數。例如決定物體表面的反射率/漫反射率,...etc.
儘管概念不相似,但這兩者其實常常被混為一談,尤其是有些人已經習慣把紋理(Texture)稱作材質貼圖,導致兩者之間的界線更加模糊。
在Three.js
中,若想要導入紋理,例如我想要給一個方塊表面具有磚塊的圖樣,那就會需要用到TextureLoader
。
通常我們會在專案裡面建立一個TextureLoader
的實例。然後反覆地用TextureLoader.load
這個方法去讀入材質。
const tl = new TextureLoader();
const brickTexture = tl.load('../img/brick.png');
TextureLoader.load
這個方法其實有點像是jquery
的$.ajax
,他也一樣可以傳入載入目標的url
,還有onLoad/onError時的callback
;
有些人可能會覺得好奇為什麼TextureLoader.load
這邊不設計成回傳Promise
。
畢竟大家都愛Promise,Promise讚!。
但其實官方有說因為這樣改下去會牽涉到大幅度改動,所以暫時沒有規劃。
對這個話題有興趣的人可以看這篇文章
如果想要讓紋理載入可以採Promise
機制,這邊會需要自己做包裝。
import { TextureLoader } from "https://cdn.skypack.dev/three";
const tl = new TextureLoader();
const getTexture = (url) => {
return new Promise((res, rej) => {
tl.load(
url,
(texture) => {
res(texture);
},
null //因為TextureLoader目前不支援OnProgress,但是卻又留了一個空的參數欄位,所以必須給null
,
rej
);
});
};
async function main() {
const someTexture = await getTexture("https://picsum.photos/id/237/200/300");
console.log(someTexture);
//接著就可以在這邊取用someTexture
}
main();
接著我們順便介紹另外一個Class
,他的名字叫做LoadingManager
。
我們可以把LoadingManager
的實例傳進去TextureLoader.constructor
裡面。
而每當被植入這個LoadingManager
實例的Loader系函數進入onLoad
/onProgress
/OnError
階段的時候,LoadingManager
就會通報我們階段的發生。
這個功能通常適用在網頁剛載入,但資源還沒有完全載入,因此需要顯示一個載入條UI的狀況。
我們把上面的Promise
包裝範例稍作改造,用來示範如何使用LoadingManager
import { TextureLoader, LoadingManager } from "https://cdn.skypack.dev/three";
const lm = new LoadingManager();
lm.onStart = (url, itemsLoaded, itemsTotal) => {
//onStart會在每一項資源開始載入的時候被執行
//可以選擇用console.log的方式去顯示url, itemsLoaded, itemsTotal的狀況
//url是該項資源的位址
//itemsLoaded是目前一共已經有多少資源完成載入
//itemsTotal是當前所有需要載入的資源數量
console.log(
"開始載入: " +
url +
".\nLoaded " +
itemsLoaded +
" of " +
itemsTotal +
" files."
);
};
lm.onProgress = (url, itemsLoaded, itemsTotal) => {
console.log( '已載入: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
lm.onError = (url) => {
//onError會在每一項資源載入失敗的時候被執行
};
const tl = new TextureLoader(lm);
const getTexture = (url) => {
return new Promise((res, rej) => {
tl.load(
url,
(texture) => {
res(texture);
},
null,
rej
);
});
};
//以上部分其實可以包裝成es6 module,這樣用起來就更優雅了。
async function main() {
const someTexture1 = await getTexture("https://picsum.photos/id/237/200/300");
const someTexture2 = await getTexture("https://picsum.photos/id/238/200/300");
}
main();
codepen連結:點我
透過TextureLoader
拿到紋理之後,要怎麼使用呢?
其實我們前幾天已經有示範過了,這邊就再讓我示範一次~
最簡單的方式其實就是把紋理填入Material
的map
屬性。
假設我們要用的Material是MeshStandardMaterial
async function main() {
const someTexture1 = await getTexture("https://picsum.photos/id/237/200/300");
const mat = new MeshStandardMaterial({
map:someTexture1
})
}
就這麼簡單。
常常有種狀況就是,當我們把紋理貼到模型上面的時候,才發現紋理貼圖的位置好像對不上模型。 又或者是有時候我們會需要去Repeat一個紋理,就像css
的background-repeat
一樣。
這種時候就需要調整Texture
本身的屬性。
平移可以透過Texture.offset
這個屬性來完成。
Texture.offset
的型別會是Vector2
。
texture.offset.x = 0.5;
texture.offset.y = 0.5;
旋轉的話就是Texture.rotation
。他的型別是number
,所以我們這邊必須要帶入Radian
的數值。
預設情況下他是以材質貼圖的左下角做為旋轉中心,我們可以透過改變Texture.center
這個Vector2
的X/Y值來改變旋轉中心。
texture.rotation = 0.5 * Math.PI;
我們之前有講解過uv
是什麼,但是卻沒有解釋uv
這個名稱的由來。
uv
其實代表的是材質貼圖的X軸(又稱U軸),和Y軸(又稱V軸),之所以這樣命名是因為要避免跟3D物體的XY軸混淆。
這邊如果我們想要讓材質沿著U軸重複,首先我們必須要先把Texture.wrapS
設置為RepeatWrapping
,而如果是要沿著V軸重複材質,則是要先把Texture.wrapT
設置為RepeatWrapping
。
這邊要注意
RepeatWrapping
是一個常數,必須要從Three.js
的module中引入。
設置完wrapS
/wrapT
之後,接著就是設置Texture.repeat
,他也一樣是一個Vector2
。
texture.wrapS = RepeatWrapping;
texture.wrapT = RepeatWrapping;
texture.repeat.x = 2;
texture.repeat.y = 3;
其實Three.js
並沒有提供原生的材質貼圖縮放功能。
但我們可以透過調整Texture.repeat
的值來實現這件事。
這邊記得不要去設定
Texture.wrapS
/Texture.wrapT
,除非你除了放大縮小,還想要有重複
texture.repeat.x = 0.5; //小於1的值會造成放大
texture.repeat.y = 2; //大於1的值會造成縮小
通常我們在實作大量的紋理重複時,我們會碰到一個狀況。這邊我用圖片展示給大家看一下。
這張圖的左右兩邊圖樣都是透過大量重複同樣的紋理才形成的。
而當我們把攝影機往下移動。
可以發現右半邊的圖形,好像看起來有種不自然的抖動感。
其實這是一種GPU成像的特性,有興趣的人可以看看這篇文
而如果想要消除這種不自然的感覺,讓右邊的圖變得跟左邊一樣,那就需要使用Mipmapping。
在Three.js
中,與Mipmapping相關的設置是Texture.minFilter
。在預設之下,Texture.minFilter
的值是開放Mipmapping的,但如果碰到不需要使用Mipmapping的材質(例如材質沒有像上圖一樣有大量重複的紋樣),我們可以把它換成別的。
例如:
texture.minFilter = LinearFilter; //注意LinearFilter也是個需要引入的常數
另外,因為在Three.js
中,Mipmapping的原理是透過預先渲染出一系列低畫素模糊版本的材質貼圖,才達成的。
就像這樣。
所以如果真的要關閉Mipmapping,我們還需要停止預渲染上圖材質的動作。
texture.generateMipmaps = false;
今天我們講解了Texture
的一些基本操作,但這還只是學習Material
的第一步而已。
接著我預期至少也還要2天以上的時間才可以結束掉這個章節。
還請大家繼續追蹤~