Three.js 幾何圖形 (Geometry)
目標版本為 three.js r186。範例使用 WebGLRenderer;WebGPU 與 TSL 部分則使用 WebGPURenderer。
在地端覆寫 (於 2026-11-10 由 Claude 為此電腦新增;非原始技能的一部分)。 我們的動畫短片管線 (
<ROOT>\studio,源自 mromidev/claude-animated-shorts) 鎖定在 three 0.175 (r175)
- Remotion 4.0.290 + @react-three/fiber 9.1.2 + pmndrs postprocessing 6.37.2。請鎖定 three 版本:shade.ts/cardshade.ts 會對 three 的 shader 區塊進行字串修補。當在此管線上工作時:
- 忽略需要 r176+ 的「常見錯誤」/ API 建議(例如 HDRLoader, Timer, PCFSoftShadowMap 棄用, setEffects, RenderPipeline, outputBufferType, mesh.pivot, Capsule
height, TSL/WebGPU)。請先確認該 API 存在於 r175 中。- 絕對不要使用 useFrame、Clock、Timer、delta time、setAnimationLoop 或 requestAnimationFrame 來製作動畫。Remotion 會逐幀渲染:每個值都應從
useCurrentFrame()/ fps 推導得出(對於動畫片段則使用mixer.setTime(frame / fps))。- 後處理使用 pmndrs
postprocessing,而非 three/addons 的 EffectComposer:請勿新增 OutputPass(會導致雙重 sRGB 編碼)。- 卡通顏色:我們合成器中的色調映射是 NEUTRAL;請小心 ACES 預設值與
tonemapping_fragment的色調影響。- 對於其他 three.js 專案,以下原始的 r186 指南仍然適用。
快速開始
import * as THREE from "three";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
// 幾何圖形即形狀;多個網格可以共享同一個幾何實例
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial());
scene.add(mesh);
renderer.setAnimationLoop(animate);
function animate() {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
內建幾何圖形
基本形狀
new THREE.BoxGeometry(1, 1, 1); // 寬度, 高度, 深度, 寬度分段數, 高度分段數, 深度分段數
new THREE.SphereGeometry(1, 32, 16); // 半徑, 寬度分段數, 高度分段數, phiStart, phiLength, thetaStart, thetaLength
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI); // 半球 (phi 掃掠,穿過極點切割)
new THREE.SphereGeometry(1, 32, 16, 0, Math.PI * 2, 0, Math.PI / 2); // 圓頂 (在赤道處切割)
new THREE.PlaneGeometry(10, 10, 1, 1); // 寬度, 高度, 寬度分段數, 高度分段數
new THREE.CircleGeometry(1, 32); // 半徑, 分段數, thetaStart, thetaLength
new THREE.CylinderGeometry(1, 1, 2, 32); // 上半徑, 下半徑, 高度, 徑向分段數, 高度分段數, openEnded, thetaStart, thetaLength
new THREE.CylinderGeometry(1, 1, 2, 6); // 六角棱柱
new THREE.ConeGeometry(1, 2, 32); // 半徑, 高度, 徑向分段數, 高度分段數, openEnded, thetaStart, thetaLength
new THREE.TorusGeometry(1, 0.4, 16, 100); // 半徑, 管徑, 徑向分段數, 管狀分段數, arc, thetaStart, thetaLength
new THREE.TorusGeometry(1, 0.4, 16, 100, Math.PI); // 半弧
new THREE.TorusKnotGeometry(1, 0.4, 100, 16, 2, 3); // 半徑, 管徑, 管狀分段數, 徑向分段數, p, q
new THREE.RingGeometry(0.5, 1, 32, 1); // 內半徑, 外半徑, theta 分段數, phi 分段數, thetaStart, thetaLength
進階形狀
new THREE.CapsuleGeometry(0.5, 1, 4, 8, 1); // 半徑, 高度 (僅中間部分), 頂蓋分段數, 徑向分段數, 高度分段數
// 半徑, 細節層級 - 每個細節層級會細分每個面
new THREE.IcosahedronGeometry(1, 0); // 細節 0 時有 20 個面
new THREE.DodecahedronGeometry(1, 0);
new THREE.OctahedronGeometry(1, 0);
new THREE.TetrahedronGeometry(1, 0);
// Polyhedron - 平面頂點陣列, 索引, 半徑, 細節
const vertices = [1, 1, 1, -1, -1, 1, -1, 1, -1, 1, -1, -1];
const indices = [2, 1, 0, 0, 3, 2, 1, 3, 0, 2, 3, 1];
new THREE.PolyhedronGeometry(vertices, indices, 1, 0);
基於路徑的形狀
// Lathe - 圍繞 Y 軸旋轉的 Vector2 輪廓:點, 分段數, phiStart, phiLength
const profile = [
new THREE.Vector2(0, 0), new THREE.Vector2(0.5, 0), new THREE.Vector2(0.5, 1), new THREE.Vector2(0, 1),
];
new THREE.LatheGeometry(profile, 32);
// Extrude - 沿 Z 軸擠出的 2D 形狀
const shape = new THREE.Shape();
shape.moveTo(0, 0);
shape.lineTo(1, 0);
shape.lineTo(1, 1);
shape.lineTo(0, 1);
new THREE.ExtrudeGeometry(shape, {
steps: 1, depth: 1, bevelEnabled: true,
bevelThickness: 0.1, bevelSize: 0.1, bevelSegments: 3,
});
// Tube - 沿曲線的圓形掃掠:path, tubularSegments, radius, radialSegments, closed
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 0, 0),
]);
new THREE.TubeGeometry(curve, 64, 0.2, 8, false);
文字幾何 (Text Geometry)
import { FontLoader } from "three/addons/loaders/FontLoader.js";
import { TextGeometry } from "three/addons/geometries/TextGeometry.js";
// 一個 .typeface.json 字型,來自 three.js 倉儲的 examples/fonts
const font = await new FontLoader().loadAsync("fonts/helvetiker_regular.typeface.json");
const geometry = new TextGeometry("Hello", {
font: font,
size: 1, // 世界單位中的文字大小
depth: 0.2, // 擠出深度
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.03,
bevelSize: 0.02,
direction: "ltr", // 'ltr', 'rtl' (右到左) 或 'tb' (上到下)
});
gometry.center(); // 將字形重新置中於原點
scene.add(new THREE.Mesh(geometry, new THREE.MeshNormalMaterial()));
BufferGeometry
所有幾何圖形的基礎類別:頂點資料的類型化陣列加上一個可選的索引。屬性是每個頂點包含 itemSize 個值的命名緩衝區;position、normal 和 uv 是材質預設讀取的屬性。
自訂 BufferGeometry
const geometry = new THREE.BufferGeometry();
// 四個頂點,每個三個浮點數 (x, y, z)
const vertices = new Float32Array([-1, -1, 0, 1, -1, 0, 1, 1, 0, -1, 1, 0]);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
// 索引讓兩個三角形共用頂點
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
geometry.setIndex(new THREE.BufferAttribute(indices, 1));
// 法線驅動光照,UV 定址紋理
const normals = new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
gometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
gometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
// 可選的每個頂點顏色,需設定 material.vertexColors = true 來渲染
const colors = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0]);
gometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
scene.add(new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({ vertexColors: true })));
屬性類型
const count = 4;
const geometry = new THREE.BufferGeometry();
// Float32 用於位置、法線、UV 和浮點顏色
gometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(count * 3), 3));
// Uint16 索引可定址頂點 0 到 65534(65535 是圖元重啟值);使用純陣列的 setIndex([...]) 會自行選擇 Uint16 或 Uint32
gometry.setIndex(new THREE.BufferAttribute(new Uint16Array(count), 1));
// Uint8 顏色需要設定 normalized = true(第三個引數),這樣位元組才能以 0.0-1.0 的範圍到達著色器
gometry.setAttribute("color", new THREE.BufferAttribute(new Uint8Array(count * 3), 3, true));
更新 BufferGeometry
const geometry = new THREE.PlaneGeometry(2, 2);
const positions = geometry.attributes.position;
positions.setXYZ(0, -2, -2, 0); // 覆蓋頂點 0
positions.getX(0); // 使用 getX / getY / getZ 讀取單個分量
positions.needsUpdate = true; // 上傳變更;陣列在首次渲染後無法增長,因此請分配最大值並使用 geometry.setDrawRange(0, n)
gometry.computeVertexNormals(); // 法線必須與新位置匹配
gometry.computeBoundingBox(); // 包圍體積驅動視錐體剔除與光線投射
gometry.computeBoundingSphere();
交錯緩衝區 (Interleaved Buffers)
// 一個緩衝區,每個頂點包含位置(3 個浮點數)和 UV(2 個浮點數)
const interleavedBuffer = new THREE.InterleavedBuffer(
new Float32Array([
-1, -1, 0, 0, 0, 1, -1, 0, 1, 0, 1, 1, 0, 1, 1, -1, 1, 0, 0, 1, // 四個頂點:xyz + uv
]),
5, // stride: 每個頂點的浮點數
);
const geometry = new THREE.BufferGeometry();
gometry.setAttribute("position", new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0));
gometry.setAttribute("uv", new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 3));
gometry.setIndex([0, 1, 2, 0, 2, 3]); // 四個頂點形成兩個三角形
// BufferGeometryUtils.interleaveAttributes([position, normal, uv]) 以這種方式打包現有屬性
邊緣與線框
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
// 兩個面的法線夾角至少為 thresholdAngle(度數;此處為 15)的邊緣,加上開放的邊界邊緣
const edges = new THREE.EdgesGeometry(boxGeometry, 15);
const edgeLines = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff }));
// 每個三角形邊緣,包含對角線;被兩個三角形共用的邊緣只會繪製一次
const wireframe = new THREE.WireframeGeometry(boxGeometry);
const wireLines = new THREE.LineSegments(wireframe, new THREE.LineBasicMaterial({ color: 0xffffff }));
scene.add(edgeLines, wireLines);
點
const count = 1000;
const positions = new Float32Array(count * 3).map(() => (Math.random() - 0.5) * 10);
const geometry = new THREE.BufferGeometry();
gometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// sizeAttenuation(預設為 true)會使點在透視相機下隨深度縮小:一個點佔據 size * (canvas CSS height / 2) / depth CSS 像素
const material = new THREE.PointsMaterial({ size: 0.1, color: 0xffffff });
const points = new THREE.Points(geometry, material);
scene.add(points);
在 WebGPURenderer 下,WebGPU 後端無論材質大小如何,都只將點繪製為一個像素;請參閱 WebGPU 與 TSL 章節以了解可恢復大小的實例化 Sprite。
線條
const linePoints = [
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(0, 1, 0), new THREE.Vector3(1, 0, 0),
];
const geometry = new THREE.BufferGeometry().setFromPoints(linePoints);
const material = new THREE.LineBasicMaterial({ color: 0xff0000 });
// Line 按順序連接點;LineLoop 會閉合回第一個點
const line = new THREE.Line(geometry, material);
const loop = new THREE.LineLoop(geometry, material);
scene.add(line, loop);
// LineSegments 繪製獨立的點對:頂點 0-1, 2-3, ...
const segmentsGeometry = new THREE.BufferGeometry().setFromPoints([linePoints[0], linePoints[1], linePoints[1], linePoints[2]]);
const segments = new THREE.LineSegments(segmentsGeometry, material);
scene.add(segments);
LineLoop 在 WebGPURenderer 下不受支援:它會記錄錯誤並且不渲染任何內容。在那裡要閉合迴圈,請重複第一個點並使用 Line。
InstancedMesh
一次繪製呼叫繪製多個相同幾何體的副本,每個副本都有其自己的轉換。
const geometry = new THREE.BoxGeometry(0.2, 0.2, 0.2);
const material = new THREE.MeshBasicMaterial(); // 會乘以實例顏色;MeshNormalMaterial 忽略它們
const count = 1000;
const instancedMesh = new THREE.InstancedMesh(geometry, material, count);
const dummy = new THREE.Object3D();
for (let i = 0; i < count; i++) {
dummy.position.set((Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20, (Math.random() - 0.5) * 20);
dummy.rotation.set(Math.random() * Math.PI, Math.random() * Math.PI, 0);
dummy.scale.setScalar(0.5 + Math.random());
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true; // 在網格渲染後進行變更後需要;在之前是沒有影響的
// setColorAt 在首次使用時建立 instanceColor;在迴圈後標記它
for (let i = 0; i < count; i++) {
instancedMesh.setColorAt(i, new THREE.Color(Math.random(), Math.random(), Math.random()));
}
instancedMesh.instanceColor.needsUpdate = true;
scene.add(instancedMesh);
在運行時,更新和光線投射實例的方法如下:
// 此處的 mesh 是一個 InstancedMesh
const matrix = new THREE.Matrix4();
const index = 7;
mesh.getMatrixAt(index, matrix);
matrix.setPosition(0, 2, 0); // 僅移動此實例
mesh.setMatrixAt(index, matrix);
mesh.instanceMatrix.needsUpdate = true; // 在任何一批 setMatrixAt 呼叫後標記
mesh.computeBoundingSphere(); // 實例移動了:刷新用於視錐體剔除和光線投射的邊界
// 光線投射會報告擊中了哪個實例
const raycaster = new THREE.Raycaster();
const hits = raycaster.intersectObject(mesh);
if (hits.length > 0) {
const instanceId = hits[0].instanceId; // 非實例化物件時為 undefined
}
InstancedBufferGeometry
每個實例的屬性,超出變換和顏色之外。
const count = 1000;
const geometry = new THREE.InstancedBufferGeometry();
gometry.copy(new THREE.BoxGeometry(1, 1, 1)); // 複製索引和所有屬性
gometry.instanceCount = count; // 必須:copy() 不攜帶實例計數
const offsets = new Float32Array(count * 3).map(() => Math.random() * 10);
gometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));
// 使用 WebGLRenderer 和 ShaderMaterial,在頂點著色器中宣告 `in vec3 offset;`
// 並將其加到 position(請參閱 threejs-shaders)。在 WebGPURenderer 下,
// 每個實例的資料是一個 TSL 節點 - 請參閱 WebGPU 與 TSL 章節