什么是缓冲几何体
假设我们要在电脑屏幕上画一个 3D 的立方体。这个立方体由很多个小的三角形面片组成(这是 WebGL 绘制的基础)。每个三角形的角叫做顶点。每个顶点至少需要知道:
-
位置信息:它在 3D 空间中的坐标 (x, y, z)。
-
朝向信息 :这个点所面对的方向(法向量),用于计算光照。
-
贴图坐标 :告诉电脑这个点对应到图片(纹理)上的哪个位置,以便给立方体贴上图案。
在three.js中使用BufferGeometry
来构建缓冲几何体
它是高效描述 3D 形状的数据结构
它把顶点数据(位置、法线、UV等)按类型分离,各自打包成连续的大数组(缓冲区)。
我们需要通过一系列的练习来熟悉掌握缓冲几何体相关概念
案例 - 顶点构建三角形
通过设置一组顶点坐标,来构建一个三角形

const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1, 0, 0,
1, 0, 0,
1, 1, 0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
const mesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'deepskyblue', side: THREE.DoubleSide}));
scene.add(mesh);
案例 - 设置法线

const data_normal = [
0, -0, 1,
0, -0, 1,
0, -0, 1
];
geometry.setAttribute('normal', new THREE.BufferAttribute( new Float32Array(data_normal), 3 ));
// 显示法线
const helper = new VertexNormalsHelper( mesh1, 0.5, 'yellow' );
scene.add( helper );
案例 - UV贴图
这个案例在之前的章节里有写过,这里就当是复习一下,下面是其核心代码

const width = 4,
height = 4;
const size = width * height;
const data = new Uint8Array( 4 * size );
for (let i = 0; i < size; i++) {
const stride = i * 4;
const v = Math.floor(THREE.MathUtils.seededRandom() * 255);
data[stride] = v-15;
data[stride + 1] = v-40;
data[stride + 2] = v-170;
data[stride + 3] = 155;
}
const texture = new THREE.DataTexture(data, width, height);
texture.needsUpdate = true;
const mesh1 = new THREE.Mesh(
geometry,
new THREE.MeshStandardMaterial({
map: texture,
side: THREE.FrontSide
}));
案例 - groups组合
先看效果

这个案例用到了缓冲几何体的分组功能,它可以让我们将一个几何体划分为多个部分,每个部分使用不同的材质渲染
const materials = [
new THREE.MeshBasicMaterial({color: 'deepskyblue', side: THREE.DoubleSide}),
new THREE.MeshBasicMaterial({color: 'deeppink', side: THREE.DoubleSide}),
]
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0, 0, 0, // triangle 1
1, 0, 0,
1, 1, 0,
0, 0, 0, // triangle 2
0, 1, 0,
1, 1, 0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.addGroup(0, 3, 1);
geometry.addGroup(3, 3, 0);
scene.add(new THREE.Mesh(geometry, materials));
代码中materials
数组中定义了两种不同颜色的基础材质,用于后续分组渲染,vertices
定义了6个顶点数据,需要注意的是分组规则中的addGroup
,其参数分别指顶点数据开始索引,顶点数,以及材质索引
案例 - Index索引
这个案例主要是练习如何通过索引渲染,减少顶点数据的重复存储

通过索引渲染,我们只需要存储4个顶点,6个索引值便可实现该效果
const geometry = new THREE.BufferGeometry();
const pos = new THREE.BufferAttribute(
new Float32Array([
0,-3, 0, // 0
0, 3, 0, // 1
-5, 0, 0, // 2
0, 0,-5 // 3
]), 3
);
geometry.setAttribute('position', pos);
geometry.computeVertexNormals(); // 计算顶点法线
geometry.setIndex([0, 1, 2, 0, 1, 3]); // 定义索引
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshNormalMaterial({ side: THREE.DoubleSide})
);
scene.add(mesh);
案例 - triangles三角
先看效果

从图中可以看到,我们创建了两个相连的三角形,形成了一个具有立体感的几何体
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0.00, 0.00, 0.00,
1.00, 0.00, 0.00,
0.50, 1.00, -0.50,
1.00, 0.00, 0.00,
0.75, 0.00, -1.00,
0.50, 1.00, -0.50
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.computeVertexNormals(); // 计算顶点法线
const mesh = new THREE.Mesh(geometry, materials);
scene.add(mesh);
案例 - unIndexed贴图索引
这是一个对比案例,左侧蓝色三角形设置uv属性以及进行了法线贴图使得平面看起来更具立体感,右侧红色三角形被转换为非索引几何体与蓝色三角形产生区别

const texture_normal = new THREE.DataTexture( new Uint8Array( data_normalmap ), 4, 4 );
texture_normal.needsUpdate = true;
const material_nm = new THREE.MeshPhongMaterial({
color: 'deepskyblue',
normalMap: texture_normal,
side: THREE.FrontSide
});
const material = new THREE.MeshPhongMaterial({
color: 'deeppink',
side: THREE.FrontSide
});
const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0.00, 0.00, 0.00,
2.00, 0.00, 0.00,
0.00, 2.00, 0.00,
0.00, 0.00, -3.50
]);
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex([0,1,2,1,3,2]);
geometry.computeVertexNormals();
const data_uv = new Float32Array([
0.00, 1.00,
0.00, 0.00,
1.00, 0.00,
1.00, 1.00,
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(data_uv, 2));
geometry.computeTangents(); // 计算切线向量,用于光照计算。
const geometry_ni = geometry.toNonIndexed(); // 转换为非索引几何体,以便后续操作。
geometry_ni.computeVertexNormals(); // 计算顶点法线,用于光照计算。
案例 - rotation旋转
先看效果

如图所示,左侧椎体是以几何体级别进行旋转,而右侧椎体则是以对象级别进行旋转的,两者的旋转方式如下
mesh1.geometry.copy( new THREE.ConeGeometry(0.25, 2, 32, 32) );
mesh1.geometry.rotateX( rx );
mesh1.geometry.rotateZ( rz );
mesh2.rotation.set(rx ,0, rz)
案例 - json转换
这个案例演示了Three.js中3D几何体的序列化和反序列化流程,通过创建THREE.BufferGeometryLoader
实例,调佣parse
方法将JSON对象解析为BufferGeometry
实例, 这个案例对于需要保存、传输或动态生成3D模型数据的开发工作特别有用,感兴趣的童鞋可以尝试载入个模型转换看看

const geo = new THREE.SphereGeometry(1,8,8);
const buffObj = geo.toNonIndexed().toJSON(); // 转为非索引几何体
console.log(buffObj);
const text = JSON.stringify(buffObj);
const loader = new THREE.BufferGeometryLoader();
const obj = JSON.parse(text); // 解析json
const geo2 = loader.parse(obj);
const mesh = new THREE.Mesh(geo2)
scene.add(mesh);
案例 - 四元数
这里通过四元数来对几何体进行旋转,并以点云形式展现该几何体的顶点,效果如下

const geometry = new THREE.CylinderGeometry(0, 2, 6, 32, 32);
const q = new THREE.Quaternion(); // 创建一个四元数
q.setFromAxisAngle(new THREE.Vector3(0, 0, 1), Math.PI / 4); // 将四元数设置为绕Z轴旋转45度的四元数
geometry.applyQuaternion(q); // 应用四元数到几何体上
const material = new THREE.PointsMaterial({ color: 'deepskyblue', size: 0.1 }); // 点的材质
const points = new THREE.Points(geometry, material); // 创建点云
scene.add(points); // 将点云添加到场景中
案例 - 中心点
这个案例主要使用到 center
方法,该方法有以下几个功能
- 计算几何体的边界盒(bounding box)
- 计算几何体的中心点坐标
- 将几何体平移,使其中心点位于原点(0,0,0)

const geometry = new THREE.BufferGeometry();
const vertices = new Float32Array([
0,0,0,
2,0,0,
0,2,0,
2,2,0
])
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
geometry.setIndex([0, 1, 2, 1, 3, 2 ]); // 定义索引
geometry.computeVertexNormals(); // 计算顶点法线
geometry.center(); // 计算几何体的中心
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshNormalMaterial({ side: THREE.DoubleSide})
);
scene.add(mesh);
案例 - 平移
练习一下几何体的平移 translate
很简单,效果如下

[ [0,1,0], [1,0,-1], [0,1,-4], ].forEach( (pos, i, arr) => {
const geometry = geometry_source.clone().translate( pos[0], pos[1], pos[2]);
const mesh = new THREE.Mesh(
geometry,
new THREE.MeshBasicMaterial({
color: 'deepskyblue',
side: THREE.FrontSide,
transparent: true,
opacity: 0.5
})
);
mesh.renderOrder = arr.length - i; // 控制渲染顺序,后添加的在前显示
scene.add(mesh);
});
案例 - setfrompoints
这个案例主要是练习使用setfrompoints
自动设置geometry
的position
属性

const points_array = [
new THREE.Vector3( -1, -1, 1),
new THREE.Vector3( -1, -1, -1),
new THREE.Vector3( 1, -1, 1),
new THREE.Vector3( 1, -1, -1),
new THREE.Vector3( -1, 1, 1),
new THREE.Vector3( -1, 1, -1),
new THREE.Vector3( 1, 1, 1),
new THREE.Vector3( 1, 1, -1),
];
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(points_array);
const material = new THREE.PointsMaterial( { color: 'deepskyblue', size: 0.15 } );
scene.add( new THREE.Points( geometry, material ) );
案例 - 克隆
练习克隆几何体,clone
方法会创建当前几何体的完整副本,对于需要基于同一几何体创建多个不同变体的场景非常有用

const geo_source = new THREE.ConeGeometry( 0.5, 1, 24, 24 ); // 圆锥体
const geo1 = geo_source.clone().rotateX(Math.PI / 180 * 45).translate(-2,0,0); // 旋转45度,然后平移到(-2,0,0)
const geo2 = geo_source.clone();
const material = new THREE.MeshNormalMaterial();
const mesh1 = new THREE.Mesh(geo1, material);
const mesh2 = new THREE.Mesh(geo2, material);
scene.add(mesh1, mesh2);
案例 - builtinBox
这个案例在之前的章节里有介绍过了,实现方式也很简单,这里就贴代码看看就好了

const w=2, h=4,d=2;
const ws=8, hs=20, ds=32;
const geometry = new THREE.BoxGeometry(w, h, d, ws, hs, ds); // 立方体
const material = new THREE.PointsMaterial({ size: 0.05, color: 'deepskyblue' }); // 点材质
const points = new THREE.Points(geometry, material); // 点
scene.add(points);
案例 - 胶囊
该案例演示了如何创建一个胶囊几何体

const radius = 1;
const length = 2;
const capsubs = 8;
const radsegs = 16;
const geometry = new THREE.CapsuleGeometry(radius, length, capsubs, radsegs); // 胶囊
const material = new THREE.MeshNormalMaterial({wireframe: true});
const mesh = new THREE.Mesh(geometry, material); // 网格
scene.add(mesh);
案例 - builtinEdges
这个案例使用到EdgesGeometry
,它接收一个已有的几何体作为输入,提取该几何体的边缘线,根据角度阈值过滤边缘(只保留两个相邻面之间夹角大于指定阈值的边缘),最后生成一个质保函这些边缘线的新几何体

const geo_source = new THREE.BoxGeometry();
const threshold_angle = 1;
const geometry = new THREE.EdgesGeometry(geo_source, threshold_angle); // 边
const material = new THREE.LineBasicMaterial({color: 'deeppink', linewidth: 5}); // 线
const line = new THREE.LineSegments(geometry, material); // 线段
scene.add(line);
案例 - 挤出
先看效果

该案例中将使用ExtrudeGeometry
对2D形状进行挤出,从而实现如图效果, 该方法的配置参数说明如下
-
depth
: 挤出的深度(默认为1)
-
bevelEnabled
: 是否启用斜面(默认为true)
-
bevelThickness
: 斜面厚度
-
bevelSize
: 斜面大小
-
bevelSegments
: 斜面的分段数
-
curveSegments
: 曲线的分段数
-
steps
: 沿挤出深度的分段数
const shape = new THREE.Shape(); // 定义形状
shape.moveTo( 2,-1 );
shape.bezierCurveTo( 0.45,-0.25, 0.25,0, 1,0 );
shape.lineTo( 1,1 );
shape.lineTo( -1,2 );
shape.bezierCurveTo(-2,0, -2,-1, 0,-1 );
const geometry = new THREE.ExtrudeGeometry( shape );
const material = new THREE.MeshNormalMaterial();
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
案例 - 旋转成型
这个案例中用到了LatheGeometry
,它一系列二维点(Vector2数组)作为输入,将这些点围绕Y轴旋转一周(或指定角度),生成一个旋转体(车削体)几何形状

const v1 = new THREE.Vector2( 0, 0 );
const v2 = new THREE.Vector2( 0.5, 0 );
const v3 = new THREE.Vector2( 0.5, 0.5);
const v4 = new THREE.Vector2( 0.4, 0.5);
const v5 = new THREE.Vector2( 0.2, 0.1);
const v6 = new THREE.Vector2( 0, 0.1);
const vc1 = v2.clone().lerp(v3, 0.5).add( new THREE.Vector2(0.25,-0.1) );
const vc2 = v4.clone().lerp(v5, 0.5).add( new THREE.Vector2(0.25, 0) );
const curve = new THREE.CurvePath();
curve.add( new THREE.LineCurve( v1, v2 ) );
curve.add( new THREE.QuadraticBezierCurve(v2, vc1, v3) );
curve.add( new THREE.LineCurve( v3, v4 ) );
curve.add( new THREE.QuadraticBezierCurve( v4, vc2, v5 ) );
curve.add( new THREE.LineCurve( v5, v6 ) );
const v2array = curve.getPoints(20);
const segments_lathe = 80;
const phi_start = 0;
const phi_length = 2*Math.PI;
const geometry = new THREE.LatheGeometry( v2array, segments_lathe, phi_start, phi_length );
案例 - ring环
这个案例利用了three.js 内置的RingGeometry
几何体创建一个圆环

const radius1 = 1.5;
const radius2 = 1;
const segments = 80;
const geometry = new THREE.RingGeometry( radius1, radius2, segments );
const material = new THREE.LineBasicMaterial({ linewidth: 3, color: 'deepskyblue'});
const line = new THREE.LineSegments( geometry, material );
scene.add( line );
案例 - shape形状
这个案例使用了three.js内置的ShapeGeometry
几何体实现

const heartShape = new THREE.Shape();
heartShape.moveTo( 2.5, 2.5 );
heartShape.bezierCurveTo( 2.5, 2.5, 2.0, 0, 0, 0 );
heartShape.bezierCurveTo( - 3.0, 0, - 3.0, 3.5, - 3.0, 3.5 );
heartShape.bezierCurveTo( - 3.0, 5.5, - 1.0, 7.7, 2.5, 9.5 );
heartShape.bezierCurveTo( 6.0, 7.7, 8.0, 5.5, 8.0, 3.5 );
heartShape.bezierCurveTo( 8.0, 3.5, 8.0, 0, 5.0, 0 );
heartShape.bezierCurveTo( 3.5, 0, 2.5, 2.5, 2.5, 2.5 );
const geometry = new THREE.ShapeGeometry( heartShape );
geometry.rotateX(Math.PI);
geometry.rotateY(Math.PI);
geometry.scale(0.3, 0.3, 0.3);
geometry.center();
const material = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide});
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
案例 - 环形体
与上面那个案例不同,这里用到了内置的TorusGeometry
创建了一个环形体,而不是环面,效果中是以点云形式呈现

const geometry = new THREE.TorusGeometry( 1, 0.3, 26, 80 );
const material = new THREE.PointsMaterial( { color: 'cyan', size: 0.05 } );
const points = new THREE.Points( geometry, material );
scene.add( points );
案例 - loadPromise
先看效果

这个案例演示了如何处理多个模型加载的方案,首先我们创建一个能够加载多个BufferGeometry JSON
const loadBufferGeometryJSON = (urls = [], w = 2, scale = 5, material = new THREE.MeshNormalMaterial()) => {
const onBuffLoad = (geometry, i) => {
const x = i % w;
const z = Math.floor(i / w);
const mesh = new THREE.Mesh(geometry, material);
mesh.name = `mesh_${i}`;
mesh.position.set(x,0, z).multiplyScalar(scale);
scene.add(mesh);
}
const onBuffProgress = (geometry) => {};
return new Promise((resolve, reject) => {
const manager = new THREE.LoadingManager();
manager.onLoad = () => {
resolve(scene);
};
const onBuffError = (error) => {
reject(error);
}
const loader = new THREE.BufferGeometryLoader(manager);
urls.forEach((url,index)=> {
loader.load(url,(geometry)=> {
onBuffLoad(geometry, index);
}, onBuffProgress, onBuffError);
})
});
}
加载多个json格式的geometry
const URLS = [
'/json/vertcolor-trees/6tri/0.json',
'/json/vertcolor-trees/6tri/1.json',
'/json/vertcolor-trees/6tri/2.json',
'/json/vertcolor-trees/6tri/3.json',
'/json/vertcolor-trees/6tri/4.json',
'/json/vertcolor-trees/6tri/5.json'
];
const material = new THREE.MeshBasicMaterial({vertexColors: true, side: THREE.DoubleSide });
loadBufferGeometryJSON(URLS, 2, 4, material)
.then( (scene_source) => {
console.log('JSON files are loaded!');
scene.add( scene_source );
renderer.render(scene, camera);
})
.catch( (e) => {
console.warn('No Good.');
console.warn(e);
});
案例 - morphattributes
先看效果

这里用到了morphTargetInfluences
,它用于在不同的几何形状之间实现平滑过渡,常用于角色动画、面部表情或物体变形等效果
const geo = new THREE.BoxGeometry(2, 2, 2, 32, 32, 32);
geo.morphAttributes.position = [];
const pos = geo.attributes.position;
const data_pos = [];
for ( let i = 0; i < pos.count; i ++ ) {
const x = pos.getX( i );
const y = pos.getY( i );
const z = pos.getZ( i );
data_pos.push(
x * Math.sqrt( 1 - ( y * y / 2 ) - ( z * z / 2 ) + ( y * y * z * z / 3 ) ),
y * Math.sqrt( 1 - ( z * z / 2 ) - ( x * x / 2 ) + ( z * z * x * x / 3 ) ),
z * Math.sqrt( 1 - ( x * x / 2 ) - ( y * y / 2 ) + ( x * x * y * y / 3 ) )
);
}
geo.morphAttributes.position[ 0 ] = new THREE.Float32BufferAttribute( data_pos, 3 );
const material = new THREE.MeshNormalMaterial({ side: THREE.DoubleSide });
const mesh = new THREE.Mesh(geo, material);
scene.add(mesh);
orbitControls.update();
let count = 0.01;
function animation() {
// 渲染场景和相机
renderer.render( scene, camera );
requestAnimationFrame( animation );
count += 0.01;
if ( count > 10 ) count = 0;
mesh.morphTargetInfluences[ 0 ] = ( Math.cos( count * Math.PI ) + 1 ) / 0.5;
}
animation();