javascript - 三.js/jsCAD : how to smooth a mesh normals for an outline effect?

标签 javascript three.js mesh normals topology

我正在尝试使用以下方法在某些 3D 网格上创建轮廓效果:

  • 绘制原始的初始着色网格
  • 平滑网格法线
  • 按照其法线将顶点向外推
  • 仅使用纯轮廓颜色绘制背面

除了平滑部分外,我一切正常。需要平滑网格才能创建漂亮的连续外部轮廓(否则 Angular 处会出现不规则现象)。 I've allready asked how to smooth a mesh normals 。平滑过程如下:

  let geometry = new THREE.BoxGeometry();
  geometry.deleteAttribute('normal');
  geometry.deleteAttribute('uv');
  geometry = THREE.BufferGeometryUtils.mergeVertices(geometry);
  geometry.computeVertexNormals();

尽管这对于简单且定义明确的几何图形效果很好,但它会在更复杂的模型上创建工件。我相信这是由于丢弃了原始法线造成的。 computeVertexNormals()然后从里到外解释一些面孔。

知道如何解决这个问题吗?

平滑法线之前

before smoothing normals

平滑法线后

after smoothing normals


旁注: 1/我无法修改原始网格拓扑,因为它们是使用 jscad 在运行时生成的。

2/我尝试编写一个算法来解决这个问题,但没有取得很大成功。我的推理如下:

我们假设 computeVertexNormals()根据组成三 Angular 形的顶点的顺序将三 Angular 形解释为面朝上或面朝下。我将找到法线反转的顶点的三 Angular 形,并交换三 Angular 形的两个顶点以翻转它。如果 computeVertexNormals() 之后法线的点积,则顶点法线反转。和之前 computeVertexNormals()为负数。

这是代码:

const fixNormals: (bufferGeometry: THREE.BufferGeometry) => THREE.BufferGeometry
  = (bufferGeometry) => {
    // this function is only made for non indexed buffer geometry
    if (bufferGeometry.index)
      return bufferGeometry;
    const positionAttribute = bufferGeometry.getAttribute('position');
    if (positionAttribute == undefined)
      return bufferGeometry;
    let oldNormalAttribute = bufferGeometry.getAttribute('normal').clone();
    if (oldNormalAttribute === undefined)
      return bufferGeometry;

    bufferGeometry.deleteAttribute('normal');
    bufferGeometry.deleteAttribute('uv');
    bufferGeometry.computeVertexNormals();

    let normalAttribute = bufferGeometry.getAttribute('normal');
    if (normalAttribute === undefined) {
      console.error("bufferGeometry.computeVertexNormals() resulted in empty normals")
      return bufferGeometry;
    }

    const pA = new THREE.Vector3(),
      pB = new THREE.Vector3(),
      pC = new THREE.Vector3();
    const onA = new THREE.Vector3(),
      onB = new THREE.Vector3(),
      onC = new THREE.Vector3();
    const nA = new THREE.Vector3(),
      nB = new THREE.Vector3(),
      nC = new THREE.Vector3();

    for (let i = 0, il = positionAttribute.count; i < il; i += 3) {
      pA.fromBufferAttribute(positionAttribute, i + 0);
      pB.fromBufferAttribute(positionAttribute, i + 1);
      pC.fromBufferAttribute(positionAttribute, i + 2);

      onA.fromBufferAttribute(oldNormalAttribute, i + 0);
      onB.fromBufferAttribute(oldNormalAttribute, i + 1);
      onC.fromBufferAttribute(oldNormalAttribute, i + 2);

      nA.fromBufferAttribute(normalAttribute, i + 0);
      nB.fromBufferAttribute(normalAttribute, i + 1);
      nC.fromBufferAttribute(normalAttribute, i + 2);

      // new normals for this triangle are  inverted, 
      // need to switch 2 vertices order to keep right face up
      if (onA.dot(nA) < 0 && onB.dot(nB) < 0 && onC.dot(nC) < 0) {
        positionAttribute.setXYZ(i + 0, pB.x, pB.y, pB.z)
        positionAttribute.setXYZ(i + 1, pA.x, pA.y, pA.z)
      }
    }

    bufferGeometry.deleteAttribute('normal');
    bufferGeometry.deleteAttribute('uv');
    bufferGeometry.computeVertexNormals();

    return bufferGeometry;
  }

最佳答案

THREE 通过使用面的缠绕顺序来执行 computeVertexNormals 以确定法线。如果使用左旋定义面,则法线将指向错误的方向。这也会影响光照,除非您不使用着色 Material 。

也就是说,您还可以看到计算的法线在面之间不同的情况。为了避免这种情况,您需要跟踪给定顶点位置的所有法线(不仅仅是单个顶点,因为该位置可以在缓冲区中的其他位置复制)。

第 1 步:创建顶点贴图

这里的想法是,您想要创建一个实际顶点位置到您定义的稍后将使用的某些属性的映射。拥有索引和组可能会影响此处理,但最终您希望创建一个 Map并将其key分配为顶点值的序列化版本。例如,顶点值 x = 1.23, y = 1.45, z = 1.67 可以成为键 1.23|1.45|1.67。这很重要,因为您需要能够引用顶点位置,并且关闭两个新的 Vector3 实际上会映射到两个不同的键。 无论如何...

您需要保存的数据是每个顶点的法线,以及最终的平滑法线。因此,您将推送到每个键的值是一个对象:

let vertMap = new Map()

// while looping through your vertices...
if( vertMap.has( serializedVertex ) ){
  vertMap.get( serializedVert ).array.push( vector3Normal );
}
else{
  vertMap.set( serializedVertex, {
    normals: [ vector3Normal ],
    final: new Vector3()
  } );
}

第 2 步:平滑法线

现在您已经拥有每个顶点位置的所有法线,迭代 vertMap,对 normals 数组求平均值,并将最终值分配给 final 每个条目的属性。

vertMap.forEach( ( entry, key, map ) => {

  let unifiedNormal = entry.normals.reduce( ( acc, normal ) => {
    acc.add( normal );
    return acc;
  }, new Vector3() );

  unifiedNormal.x /= entry.normals.length;
  unifiedNormal.y /= entry.normals.length;
  unifiedNormal.z /= entry.normals.length;
  unifiedNormal.normalize();

  entry.final.copy( unifiedNormal );

} );

第 3 步:重新分配法线

现在循环遍历您的顶点(position 属性)。由于顶点和法线是成对的,因此您可以使用当前顶点的索引来访问其关联法线的索引。通过再次序列化当前顶点并使用该值引用 vertMap 来引用当前顶点。将 final 属性的值分配给关联的法线。

let pos = geometry.attributes.position.array;
let nor = geometry.attributes.normal.array;

for( let i = 0, l = pos.length; i < l; i += 3 ){
  //serializedVertex = however you chose to do it during vertex mapping
  let newNor = vertMap.get( serializedVertex ).final;
  nor[ i ] = newNor.x;
  nor[ i + 1 ] = newNor.y;
  nor[ i + 2 ] = newNor.z;
}
geometry.attributes.normal.needsUpdate = true;

警告

可能有更优雅或 API 驱动的方法来执行我上面描述的操作。我提供的代码并不意味着高效也不完整,只是让您了解如何解决问题。

关于javascript - 三.js/jsCAD : how to smooth a mesh normals for an outline effect?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68299142/

相关文章:

3d - 流行3D软件中使用的网格数据结构

javascript - 如何升级到React Native 0.57.0版本?

opengl - CAD中C2曲面是什么,C2是什么意思?

javascript - 如何将对象中的所有属性值转换为字符串类型?

javascript - 三.js多重效果图

javascript - 寻找一种在 Three.js 中球体上的两点之间画线的方法

javascript - 如何使用HTML5 canvas和three.js实现4点透视变换?

computational-geometry - OpenCASCADE 增量网格中的线性/角度偏转是什么?

javascript - 查找 iFrame 内容

javascript - 如何检测浏览器(不是窗口)关闭事件