本文 AI 產出,尚未審核

Three.js 課程 – 貼圖 (Textures)

主題:UV Mapping 基礎


簡介

在 3D 渲染中,貼圖是賦予模型表面顏色、細節與材質感的關鍵技術。沒有貼圖的模型只能靠純色或光照來呈現,往往缺乏真實感。
UV Mapping 則是把 2D 圖像(U、V 坐標)正確對應到 3D 物件表面的過程,決定了圖像的哪一塊會映射到模型的哪一個面。掌握 UV 的概念,才能在 Three.js 中靈活運用各種貼圖、重複、旋轉與變形效果,讓作品更具視覺衝擊力。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步建立對 UV Mapping 的完整認知,適合 初學者 也能為 中階開發者 提供深入的技巧。


核心概念

1. UV 坐標系統

  • UV 分別對應 2D 圖像的 水平垂直 軸,範圍通常是 [0, 1]
  • 在 Three.js 中,每個頂點都會儲存一組 (u, v),渲染器會根據這組座標在貼圖上取樣顏色。
  • 左上角(0, 0)右下角(1, 1),但有些圖像格式(如 WebGL)會把原點放在左下角,需注意 Y 軸翻轉 的情況。

2. 預設 UV 與自訂 UV

  • 大多數內建幾何體(BoxGeometrySphereGeometry…)已內建合理的 UV,直接套用貼圖即可。
  • 當使用 自訂幾何BufferGeometry)或需要特殊映射時,必須手動建立或修改 uv 屬性。

3. 貼圖的重複、偏移與旋轉

Three.js 的貼圖物件 (THREE.Texture) 提供 repeatoffsetrotation 等屬性,配合 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);

提示wrapSwrapT 必須先設為 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;

最佳實踐

  1. 統一 UV 範圍:所有貼圖使用 [0,1] 範圍,避免意外的拉伸。
  2. 使用 TextureLoaderonLoad 回呼,確保貼圖載入完成後再建立材質。
  3. 將重複與鏡射設定寫在貼圖載入後,避免在渲染循環中不斷重設。
  4. 對於大型貼圖,考慮啟用 MIPMAP(預設開啟),提升遠距離渲染品質與效能。
  5. 在開發階段使用 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)。
  • 透過 repeatoffsetrotationwrap 等屬性,可快速產生瓦片、鏡射、流動等多樣效果。
  • 常見的倒置、重複失效、光照貼圖全黑等問題,多半與 UV 設定或貼圖屬性 有關,依照本文的檢查表即可快速定位與解決。

掌握了上述概念與實作技巧,你就能在 Three.js 專案中 靈活運用貼圖,打造出既美觀又高效的 3D 互動體驗。祝開發順利,玩得開心!