BufferGeometry.boundingBox的应用:BoxHelper的实现

703 阅读3分钟

BufferGeometry.boundingBox的应用:BoxHelper的实现

通过boundingBox实现的BoxHelper

官方提供了一个BoxHelper,查看源码,它是通过boundingBox实现的。

BoxHelper通过LineSegment画出一个长方体外框,即需要通过BufferAttributes指定要画的顶点属性(这里主要是position以及index),通过创建LineSegment对象进行绘制,BoxHelper继承了LineSegemnt

// BoxHelper.js
// class BoxHelper extends LineSegment
constructor( object, color = 0xffff00 ) {
  const indices = new Uint16Array( [ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 ] );
  const positions = new Float32Array( 8 * 3 );

  const geometry = new BufferGeometry();
  geometry.setIndex( new BufferAttribute( indices, 1 ) );
  geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );

  super( geometry, new LineBasicMaterial( { color: color, toneMapped: false } ) );

  this.object = object;
  this.type = 'BoxHelper';

  this.matrixAutoUpdate = false;

  this.update();
}

在构造器中,并没有给出position的具体位置,这一部分的实现在update方法中。在update中,间接的使用了boundingBox来计算位置属性。

下面看一下update()方法:

// BoxHelper.js
update( object ) { 
  // 这里就是要确保调用update方法时,不会传入参数
  if ( object !== undefined ) {
    console.warn( 'THREE.BoxHelper: .update() has no longer arguments.' );
  }

  // 通过Box3.setFromObject()获取Box的坐标
  if ( this.object !== undefined ) {
    _box.setFromObject( this.object );
  }

  // 坐标获取成功
  if ( _box.isEmpty() ) return;

  const min = _box.min;
  const max = _box.max;

  // 设置position
  /*
    5____4
  1/___0/|
  | 6__|_7
  2/___3/
  0: max.x, max.y, max.z
  1: min.x, max.y, max.z
  2: min.x, min.y, max.z
  3: max.x, min.y, max.z
  4: max.x, max.y, min.z
  5: min.x, max.y, min.z
  6: min.x, min.y, min.z
  7: max.x, min.y, min.z
  */

  const position = this.geometry.attributes.position;
  const array = position.array;

  array[ 0 ] = max.x; array[ 1 ] = max.y; array[ 2 ] = max.z;
  array[ 3 ] = min.x; array[ 4 ] = max.y; array[ 5 ] = max.z;
  array[ 6 ] = min.x; array[ 7 ] = min.y; array[ 8 ] = max.z;
  array[ 9 ] = max.x; array[ 10 ] = min.y; array[ 11 ] = max.z;
  array[ 12 ] = max.x; array[ 13 ] = max.y; array[ 14 ] = min.z;
  array[ 15 ] = min.x; array[ 16 ] = max.y; array[ 17 ] = min.z;
  array[ 18 ] = min.x; array[ 19 ] = min.y; array[ 20 ] = min.z;
  array[ 21 ] = max.x; array[ 22 ] = min.y; array[ 23 ] = min.z;

  // 因为BufferAttribute更新了,所以要重新向GPU发送数据
  // 通过设置BufferAttribute.needsUpdate,重新向GPU发送数据
  position.needsUpdate = true;

  this.geometry.computeBoundingSphere();
}

update()中,最重要的就是通过获取Box3.setFromObject()获取Box的坐标以及设置BufferAttribute.needsUpdatetrue,从而向GPU发送更新后的数据。

接下来看一下Box3.setFromObject()的源码:

setFromObject( object, precise = false ) {
  this.makeEmpty();
  return this.expandByObject( object, precise );
}

makeEmpty()就是将Box3对象恢复成默认值。

makeEmpty() {

  this.min.x = this.min.y = this.min.z = + Infinity;
  this.max.x = this.max.y = this.max.z = - Infinity;

  return this;

}

接下来才是setFromObject()方法的重点,调用Box3.expandByObject()方法:

expandByObject( object, precise = false ) {

  // Computes the world-axis-aligned bounding box of an object (including its children),
  // accounting for both the object's, and children's, world transforms

  object.updateWorldMatrix( false, false );

  const geometry = object.geometry;

  if ( geometry !== undefined ) {

    if ( precise && geometry.attributes != undefined && geometry.attributes.position !== undefined ) {
      // 这里调用的时候传入的为false,所以就不看这部分代码了
      // 其实这里是通过更复杂的计算,让结果更精确
    } else {

      if ( geometry.boundingBox === null ) {
        // 计算对象自己的boundingBox
        geometry.computeBoundingBox();

      }
      // 对这个boundingBox变换到世界空间
      _box.copy( geometry.boundingBox );
      _box.applyMatrix4( object.matrixWorld );

      // 把结果“联合”
      // 在后续还要递归的将子对象的boundingBox整合进来
      // 其实就是取了对象自己和其后代对象的所有boundingBox的最大的max和最小的min
      this.union( _box );
    }

  }

  // 对象本身的boundingBox已经计算完毕,还要计算并整合其后代对象
  const children = object.children;

  for ( let i = 0, l = children.length; i < l; i ++ ) {
    this.expandByObject( children[ i ], precise );
  }

  return this;
}

这个方法就是将一个对象及其后代对象的所有boundingBox都整合为一个boundingBox,这样,就能“包裹”住所有的对象。

通过阅读代码也可以看到,最终返回的this是被转换到了世界空间中,所以,在使用BoxHelper时,BoxHelper的实例是添加到scene而不是对象自身的局部空间。放在局部空间中的BoxHelper,首先,它的位置不对;其次,也很难保证它的边框是和世界空间的坐标轴对齐。

不要再动画循环中更新BoxHelper

BoxHelper这个对象很不同,他每次更新都会向GPU重新发送数据。一般其他的对象都是向GPU发送一次数据,后续的变换都是向GPU发送一个变换矩阵,在OpenGL的顶点着色器中,完成坐标变换。而BoxHelper则是在使用Box3完成坐标变换,然后把变换后的顶点发送给GPU。

就像BufferGeometry中对translate的描述:

Translate the geometry. This is typically done as a one time operation, and not during a loop. Use Object3D.position for typical real-time mesh translation.

这样直接更新BufferAttribute(即,重新向GPU发送数据的方法),因该是“一次性”的操作,不因该把update放在动画循环中。BoxHelper应该仅在换其他对象时,调用setFromObject()方法,从而触发update()

如果想要实现一个想要跟随对象变换的线框(即,在动画循环要更新线框),应该是使用WireframeGeometry配合LineSegment