Three.js 如何为不同几何体填充等数量点的Float32Array
问题根因
当前逻辑存在两个核心问题:
- 预先分配固定长度
1024*1024*3的positionData数组,但合并后的模型顶点总长度大概率小于这个固定值,索引超出combinedBuffer长度时读取值为undefined,写入Float32Array会被自动转成0,就出现了空值 - 直接拼接所有几何体的顶点数据,没有按单个几何体做数量对齐,根本无法保证不同几何体生成的粒子数完全相等
实现方案
核心逻辑是先给每个有效几何体分配固定的粒子配额,再对单个几何体独立做顶点重采样,保证每个几何体输出的顶点数完全等于配额,从根源上避免数组越界,同时满足粒子数均等的要求。
具体执行步骤:
- 先过滤出所有有效的Mesh对象,根据总目标粒子数计算单个Mesh需要生成的固定粒子数,即
单Mesh粒子数 = 总目标粒子数 / 有效Mesh总数,向下取整保证数值对齐 - 对每个几何体单独做采样处理,不提前合并所有顶点:
- 原几何体顶点数大于配额时,按均匀步长采样原顶点,或者随机无重复抽取顶点凑够配额
- 原几何体顶点数小于配额时,先写入全部原有顶点,剩余配额通过三角面表面随机采样生成新点填充;如果不需要粒子严格贴合模型表面,也可以随机重复原有顶点补位,性能更高
- 所有几何体采样完成后,将固定长度的顶点块按顺序拼接,最终得到的数组长度刚好匹配总粒子数,不会出现索引越界问题
- 注意采样完成后要对每个顶点应用对应Mesh的世界矩阵,避免粒子停留在模型局部坐标导致位置不对
参考实现代码:
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; import { Mesh, Vector3, Triangle, Float32BufferAttribute } from 'three'; const totalTargetParticles = 1024 * 1024; // 固定总粒子数 const modelLoader = new OBJLoader(); const modelObject = await modelLoader.loadAsync('/path/to/model.obj'); const modelChildren = modelObject.children as Mesh[]; // 过滤有效mesh const validMeshes = modelChildren.filter(({ isMesh }) => isMesh); // 计算单个mesh固定分配的粒子数 const perMeshParticleCount = Math.floor(totalTargetParticles / validMeshes.length); // 复用临时对象减少GC const tempVec = new Vector3(); const tempTri = new Triangle(); /** * 从单个几何体采样固定数量的粒子顶点 * @param {BufferGeometry} geometry 原几何体 * @param {number} targetCount 目标输出顶点数 * @returns {Float32Array} 固定长度顶点数组 */ function sampleGeometryVertices(geometry, targetCount) { const result = new Float32Array(targetCount * 3); const posAttr = geometry.attributes.position; const originalVertexCount = posAttr.count; // 原顶点数多于目标值:均匀步长采样 if (originalVertexCount >= targetCount) { const step = originalVertexCount / targetCount; for (let i = 0; i < targetCount; i++) { const srcIdx = Math.floor(i * step); result[i*3] = posAttr.getX(srcIdx); result[i*3 + 1] = posAttr.getY(srcIdx); result[i*3 + 2] = posAttr.getZ(srcIdx); } return result; } // 原顶点数少于目标值:先拷贝全部原顶点,剩余部分在三角面随机采样 for (let i = 0; i < originalVertexCount; i++) { result[i*3] = posAttr.getX(i); result[i*3 + 1] = posAttr.getY(i); result[i*3 + 2] = posAttr.getZ(i); } const indexAttr = geometry.index; const triangleCount = indexAttr ? indexAttr.count / 3 : originalVertexCount / 3; let fillIdx = originalVertexCount; while (fillIdx < targetCount) { // 随机选一个三角面 const triIdx = Math.floor(Math.random() * triangleCount); let aIdx, bIdx, cIdx; if (indexAttr) { aIdx = indexAttr.getX(triIdx*3); bIdx = indexAttr.getX(triIdx*3 + 1); cIdx = indexAttr.getX(triIdx*3 + 2); } else { aIdx = triIdx*3; bIdx = triIdx*3 + 1; cIdx = triIdx*3 + 2; } // 读取三角面三个顶点 tempTri.a.set(posAttr.getX(aIdx), posAttr.getY(aIdx), posAttr.getZ(aIdx)); tempTri.b.set(posAttr.getX(bIdx), posAttr.getY(bIdx), posAttr.getZ(bIdx)); tempTri.c.set(posAttr.getX(cIdx), posAttr.getY(cIdx), posAttr.getZ(cIdx)); // 三角面内生成随机点 let u = Math.random(); let v = Math.random(); if (u + v > 1) { u = 1 - u; v = 1 - v; } tempVec.set( tempTri.a.x + u*(tempTri.b.x - tempTri.a.x) + v*(tempTri.c.x - tempTri.a.x), tempTri.a.y + u*(tempTri.b.y - tempTri.a.y) + v*(tempTri.c.y - tempTri.a.y), tempTri.a.z + u*(tempTri.b.z - tempTri.a.z) + v*(tempTri.c.z - tempTri.a.z) ); // 写入结果 result[fillIdx*3] = tempVec.x; result[fillIdx*3 + 1] = tempVec.y; result[fillIdx*3 + 2] = tempVec.z; fillIdx++; } return result; } // 逐个处理mesh,生成固定长度顶点块 const allVertexChunks = validMeshes.map(mesh => { const sampled = sampleGeometryVertices(mesh.geometry, perMeshParticleCount); // 应用世界矩阵转换到全局坐标 mesh.updateWorldMatrix(true, false); const pos = new Vector3(); for (let i = 0; i < perMeshParticleCount; i++) { pos.set(sampled[i*3], sampled[i*3+1], sampled[i*3+2]); pos.applyMatrix4(mesh.matrixWorld); sampled[i*3] = pos.x; sampled[i*3+1] = pos.y; sampled[i*3+2] = pos.z; } return sampled; }); // 拼接所有块,长度刚好匹配总粒子数,无越界问题 const finalPositionData = concatFloat32Arrays(allVertexChunks); return new Float32BufferAttribute(finalPositionData, 3);
可选调整项
- 如果不需要粒子严格分布在模型表面,去掉三角面采样逻辑,原顶点不足时直接随机抽取已有顶点重复填充即可,加载速度会快很多
- 对粒子分布均匀度要求高的场景,可以把均匀步长采样替换为泊松盘采样,不过计算开销会明显提升
- 总粒子数无法被有效Mesh数整除时,余下的少量粒子可以直接追加到任意一个几何体的采样结果中,视觉上几乎无影响
内容的提问来源于stack exchange,提问作者jahilldev
相关产品推荐
相关产品推荐

