Three.js 課程 – 貼圖 (Textures)
主題:UV Mapping 基礎
簡介
在 3D 渲染中,貼圖是賦予模型表面顏色、細節與材質感的關鍵技術。沒有貼圖的模型只能靠純色或光照來呈現,往往缺乏真實感。
而 UV Mapping 則是把 2D 圖像(U、V 坐標)正確對應到 3D 物件表面的過程,決定了圖像的哪一塊會映射到模型的哪一個面。掌握 UV 的概念,才能在 Three.js 中靈活運用各種貼圖、重複、旋轉與變形效果,讓作品更具視覺衝擊力。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立對 UV Mapping 的完整認知,適合 初學者 也能為 中階開發者 提供深入的技巧。
核心概念
1. UV 坐標系統
- U 與 V 分別對應 2D 圖像的 水平 與 垂直 軸,範圍通常是
[0, 1]。 - 在 Three.js 中,每個頂點都會儲存一組
(u, v),渲染器會根據這組座標在貼圖上取樣顏色。 - 左上角 為
(0, 0)、右下角 為(1, 1),但有些圖像格式(如 WebGL)會把原點放在左下角,需注意 Y 軸翻轉 的情況。
2. 預設 UV 與自訂 UV
- 大多數內建幾何體(
BoxGeometry、SphereGeometry…)已內建合理的 UV,直接套用貼圖即可。 - 當使用 自訂幾何(
BufferGeometry)或需要特殊映射時,必須手動建立或修改uv屬性。
3. 貼圖的重複、偏移與旋轉
Three.js 的貼圖物件 (THREE.Texture) 提供 repeat、offset、rotation 等屬性,配合 UV 繪製 可以產生瓦片、鏡射或動態流動的效果。
程式碼範例
以下示範 5 個常見且實用的 UV 操作範例,皆以 ES6 模組 方式撰寫,適用於最新的 Three.js 版本(r158 以上)。
範例 1:最簡單的貼圖與預設 UV
import * as THREE from 'three';
// 建立場景、相機與渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(2, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 載入貼圖
const texture = new THREE.TextureLoader().load('textures/brick_diffuse.jpg');
// 建立盒子,使用預設 UV
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ map: texture });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 簡單光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(5, 5, 5);
scene.add(light);
// 渲染迴圈
function animate() {
requestAnimationFrame(animate);
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
說明:
BoxGeometry內建的 UV 讓每個面都正確映射貼圖,適合作為入門範例。
範例 2:自訂 UV – 把同一張貼圖映射到平面兩次
import * as THREE from 'three';
// 建立平面幾何(兩個三角形)
// 頂點位置 (x, y, z) 與對應的 UV (u, v)
const vertices = new Float32Array([
// 第 1 個三角形
-1, 1, 0, 0, 1, // 左上
1, 1, 0, 1, 1, // 右上
-1, -1, 0, 0, 0, // 左下
// 第 2 個三角形(同樣的貼圖區域)
1, 1, 0, 1, 1, // 右上
1, -1, 0, 1, 0, // 右下
-1, -1, 0, 0, 0 // 左下
]);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3, false, 5));
geometry.setAttribute('uv', new THREE.BufferAttribute(vertices, 2, false, 5, 3));
const texture = new THREE.TextureLoader().load('textures/wood.jpg');
const material = new THREE.MeshBasicMaterial({ map: texture });
const plane = new THREE.Mesh(geometry, material);
scene.add(plane);
重點:在
BufferGeometry中,uv必須與position使用相同的緩衝區,並透過stride(每個頂點的總長度)與offset(UV 在緩衝區的起始位置)來分離。
範例 3:貼圖重複 (Tile) 與鏡射 (Repeat & Wrap)
const texture = new THREE.TextureLoader().load('textures/grass.jpg');
texture.wrapS = THREE.RepeatWrapping; // 水平重複
texture.wrapT = THREE.MirroredRepeatWrapping; // 垂直鏡射重複
texture.repeat.set(4, 2); // 在 U 方向重複 4 次、V 方向 2 次
const material = new THREE.MeshStandardMaterial({ map: texture });
const plane = new THREE.Mesh(new THREE.PlaneGeometry(10, 5), material);
scene.add(plane);
提示:
wrapS與wrapT必須先設為RepeatWrapping(或MirroredRepeatWrapping)才會生效,否則repeat會被忽略。
範例 4:貼圖偏移與旋轉(製作流動水面)
const waterTex = new THREE.TextureLoader().load('textures/water.png');
waterTex.wrapS = waterTex.wrapT = THREE.RepeatWrapping;
waterTex.repeat.set(2, 2);
waterTex.offset.set(0, 0);
waterTex.rotation = Math.PI / 4; // 45 度旋轉
waterTex.center.set(0.5, 0.5); // 以中心點為旋轉基準
const waterMat = new THREE.MeshStandardMaterial({
map: waterTex,
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
const water = new THREE.Mesh(new THREE.PlaneGeometry(8, 8), waterMat);
water.rotation.x = -Math.PI / 2;
scene.add(water);
// 動態改變 offset 產生流動感
function animate() {
requestAnimationFrame(animate);
waterTex.offset.x += 0.001;
waterTex.offset.y += 0.001;
renderer.render(scene, camera);
}
animate();
說明:
center必須先設定,旋轉才會圍繞貼圖中心;配合offset的逐幀變化,可模擬水流或雲層的移動。
範例 5:使用 UV2 做光照貼圖 (AO、光照貼圖)
const loader = new THREE.TextureLoader();
const diffuse = loader.load('textures/stone_diffuse.jpg');
const aoMap = loader.load('textures/stone_ao.jpg');
// 需要在幾何體上建立第二組 UV 座標 (uv2)
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.setAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2));
const material = new THREE.MeshStandardMaterial({
map: diffuse,
aoMap: aoMap,
aoMapIntensity: 1.5
});
const box = new THREE.Mesh(geometry, material);
scene.add(box);
關鍵:光照貼圖(AO、光照貼圖)必須使用
uv2,若忘記設定會出現 全黑 或 貼圖不顯示 的情況。
常見陷阱與最佳實踐
| 陷阱 | 可能原因 | 解決方式 |
|---|---|---|
| 貼圖顯示倒置 | 圖片原點在左上角、WebGL 以左下角為原點 | 設 texture.flipY = false; 或在 UV 中手動翻轉 V 坐標 |
| 貼圖不重複 | wrapS / wrapT 仍為預設 ClampToEdgeWrapping |
設 texture.wrapS = texture.wrapT = THREE.RepeatWrapping; |
| 自訂 BufferGeometry 沒有 UV | 忘記建立 uv 屬性或 stride 設定錯誤 |
使用 geometry.setAttribute('uv', ...),確認每個頂點都有對應的 (u, v) |
| 光照貼圖全黑 | 未設定 uv2,或 aoMap 解析度不符合 diffuse |
geometry.setAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2)); |
| 貼圖過度模糊 | 采樣方式預設為 LinearFilter,放大時失真 |
設 texture.minFilter = THREE.LinearMipmapLinearFilter; texture.magFilter = THREE.NearestFilter; |
最佳實踐
- 統一 UV 範圍:所有貼圖使用
[0,1]範圍,避免意外的拉伸。 - 使用
TextureLoader的onLoad回呼,確保貼圖載入完成後再建立材質。 - 將重複與鏡射設定寫在貼圖載入後,避免在渲染循環中不斷重設。
- 對於大型貼圖,考慮啟用 MIPMAP(預設開啟),提升遠距離渲染品質與效能。
- 在開發階段使用
THREE.MeshBasicMaterial觀測 UV 分布,確認貼圖座標正確,再切換到StandardMaterial進行光照測試。
實際應用場景
| 場景 | 使用的 UV 技巧 | 效果 |
|---|---|---|
| 城市街道 | 重複磚牆、道路貼圖 (repeat + wrap) |
渲染大量建築時保持高效、避免單張貼圖過大 |
| 角色服裝 | 自訂 UV 展開(在外部 3D 軟體) + uv2 AO 貼圖 |
讓角色在光照下呈現細緻陰影與凹凸感 |
| 水面或雲層 | 動態 offset + rotation |
創造自然流動感,提升沉浸感 |
| 地形 | 多層貼圖(diffuse + AO + normal)+ uv2 |
結合多種貼圖產生真實的山丘、岩石質感 |
| UI 介面(3D 按鈕) | 單一貼圖的 UV 裁切(只取圖集中的一格) | 減少 HTTP 請求,提升載入速度與效能 |
總結
- UV Mapping 是將 2D 圖像映射到 3D 模型的基礎,掌握它可以讓你在 Three.js 中自由控制貼圖的顯示方式。
- 內建幾何體提供預設 UV,對於自訂幾何或特殊需求時,需要手動建立或調整
uv(甚至uv2)。 - 透過
repeat、offset、rotation、wrap等屬性,可快速產生瓦片、鏡射、流動等多樣效果。 - 常見的倒置、重複失效、光照貼圖全黑等問題,多半與 UV 設定或貼圖屬性 有關,依照本文的檢查表即可快速定位與解決。
掌握了上述概念與實作技巧,你就能在 Three.js 專案中 靈活運用貼圖,打造出既美觀又高效的 3D 互動體驗。祝開發順利,玩得開心!