Community编程与开发github.com

UtkarshBirla28/threejs-geometry

创建和塑形 three.js 几何体:内置形状、3D 文字、顶点缓冲区和属性、合并、实例化、批处理、点云和线条,可在 WebGL 或 WebGPU 及 TSL 中使用。适用于构建 3D 形状或文字、自定义顶点数据、自定义网格、实例化网格或点云。

threejs-geometry 是什么?

该技能(UtkarshBirla28/threejs-geometry)旨在为 AI 助手提供创建和操作 three.js 几何体的全面指导。它涵盖了从基础形状(如 BoxGeometry、SphereGeometry)到高级形状(如胶囊、多面体)、基于路径的形状(Lathe、Extrude、Tube)以及 3D 文本几何体的生成。技能深入讲解了 BufferGeometry 的自定义顶点数据、属性类型(Float32、Uint16、Uint8)、更新几何体数据以及交错缓冲区优化。对于大规模渲染,它详细介绍了 InstancedMesh 在一次绘制调用中渲染多个实例的方法,以及通过 InstancedBufferGeometry 实现逐实例属性。技能还包括边缘几何体、线框、点云和线条的绘制,并特别指出 WebGPU 渲染器下的差异(如点的大小、LineLoop 的替代方案)。文档中还包括一个重要的本地覆盖说明,针对一个基于三.js r175 版本的动画短片管线(mromidev/claude-animated-shorts),该管线与 Remotion 和 @react-three/fiber 集成,建议在相关项目中使用此管线时忽略需要 r176+ 版本的 API。该技能适用于使用 three.js 进行 3D 图形开发,尤其在需要高效管理几何体形状、自定义顶点数据和实例化渲染时非常有用。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/UtkarshBirla28/animated-shorts-pipeline/tree/HEAD/.claude/skills/threejs-geometry

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Three.js 几何体

针对 three.js r186。示例使用 WebGLRenderer;WebGPU 和 TSL 部分使用 WebGPURenderer。

本地覆盖(2026-11-10 由 Claude 为此 PC 添加;非原始技能的一部分)。 我们的动画短片管线 (<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 的着色器块进行字符串修补。当在该管线中工作时:
  • 忽略需要 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); // 半径, 宽度分段数, 高度分段数, phi开始, phi长度, theta开始, theta长度
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); // 半径, 分段数, theta开始, theta长度
new THREE.CylinderGeometry(1, 1, 2, 32); // 顶部半径, 底部半径, 高度, 径向分段数, 高度分段数, 是否开口, theta开始, theta长度
new THREE.CylinderGeometry(1, 1, 2, 6); // 六棱柱
new THREE.ConeGeometry(1, 2, 32); // 半径, 高度, 径向分段数, 高度分段数, 是否开口, theta开始, theta长度
new THREE.TorusGeometry(1, 0.4, 16, 100); // 半径, 管道半径, 径向分段数, 管状分段数, 弧长, theta开始, theta长度
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分段数, theta开始, theta长度

高级形状

new THREE.CapsuleGeometry(0.5, 1, 4, 8, 1); // 半径, 高度(仅中间部分), 顶部和底部半圆分段数, 径向分段数, 高度分段数

// 半径, 细节级别 - 每个细节级别细分每个面
new THREE.IcosahedronGeometry(1, 0); // 20 个面(细节级别 0)
new THREE.DodecahedronGeometry(1, 0);
new THREE.OctahedronGeometry(1, 0);
new THREE.TetrahedronGeometry(1, 0);

// 多面体 - 平面顶点数组, 索引, 半径, 细节级别
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 轮廓: 点, 分段数, phi开始, phi长度
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 - 一个 2D 形状沿 Z 轴挤出
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 - 沿曲线进行圆形扫描: 路径, 管状分段数, 半径, 径向分段数, 是否闭合
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);

文本几何体

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' (从上到下)
});

geometry.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]);
geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1]);
geometry.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]);
geometry.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 和浮点颜色
geometry.setAttribute("position", new THREE.BufferAttribute(new Float32Array(count * 3), 3));
// Uint16 索引寻址顶点 0 到 65534(65535 是图元重启值);setIndex([...]) 使用普通数组会自动选择 Uint16 或 Uint32
geometry.setIndex(new THREE.BufferAttribute(new Uint16Array(count), 1));
// Uint8 颜色需要 normalized = true(第三个参数),以便字节作为 0.0-1.0 到达着色器
geometry.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)
geometry.computeVertexNormals(); // 法线必须与新的位置匹配
geometry.computeBoundingBox(); // 包围体驱动视锥体裁剪和光线投射
geometry.computeBoundingSphere();

交错缓冲区

// 一个缓冲区,每个顶点包含位置 (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, // 跨距: 每个顶点的浮点数
);
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.InterleavedBufferAttribute(interleavedBuffer, 3, 0));
geometry.setAttribute("uv", new THREE.InterleavedBufferAttribute(interleavedBuffer, 2, 3));
geometry.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();
geometry.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();
geometry.copy(new THREE.BoxGeometry(1, 1, 1)); // 复制索引和所有属性
geometry.instanceCount = count; // 必需:copy() 不携带实例计数

const offsets = new Float32Array(count * 3).map(() => Math.random() * 10);
grometry.setAttribute("offset", new THREE.InstancedBufferAttribute(offsets, 3));

// 使用 WebGLRenderer 和 ShaderMaterial,在顶点着色器中自己声明 `in vec3 offset;`
// 并将其添加到 position(参见 threejs-shaders)。在 WebGPURenderer 下,
// 逐实例数据是一个 TSL 节点 - 请参阅 WebGPU 和 TSL 部分
...

[为了翻译而截断]

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

相关技能