You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:36:23