使用while loop 构建BVH,遍历BVH (1)

> 写在前面的一些话

其实我早就想写这篇博客了,尤其是前段时间需要完成一个跟BVH打交道的算法,当时在网上搜索相关的 BVH 构建,遍历代码,全都是用 recursion syntax 写的。

的确, recursion syntax 看起来很“简洁” 和“高效”。但是,我在写程序的时候,花最多的时间是用来除错的。写起来简洁高效的代码除错的时候一点都不简洁高效。相反如果用 while loop 来写的话,我除错是容易多了。

所以在接下来的博客里我会介绍怎么用 while loop 构建 BVH 和遍历BVH

> 下面正式开启写代码

我们先从构建BVH 开始。假设我们有一个用三角形顶点数组表示的 3D model,我们给这个 3D model 构建BVH。在我们的BVH中,每个三角形都有一个包围盒(aabb)将其包住,而 BVH 的每个叶子节点(leaf node)都指向一个aabb,且这个aabb里面只有一个三角形。

我们首先创建一个包围盒类 - Myaabb:

class Myaabb
{
public:
    int idx;  // triangle index
    vec3 nn;  // the min point of the aabb
    vec3 mm;  // the max point of the aabb
    
    Myaabb(){ idx = -1; } 

    void creat_box(vec3 vmax, vec3 vmin, int _idx)
    {
        mm = vmax;
        nn = vmin;
        idx = _idx;
    }

    // just for check the box information
    void print_box()
    {
        printf("max: %f %f %f\n", mm.x, mm.y, mm.z);
        printf("min: %f %f %f\n", nn.x, nn.y, nn.z);
        printf("tri idx: %d\n", idx);
    }

};

其中 idx 用于判断存在aabb里的三角形, nn 和 mm 分别是 aabb 最小的顶点和最大的顶点,并且是正好可以包住存在aabb 的三角形的。

函数 creat_box() 根据输入的 vmax, vmin 和 _idx 创建 aabb。

函数 print_box() 会输出aabb 的信息,主要是除错用的。

有了aabb,我们就有了构建BVH的基本元素,接下来我们创建 BVH 的node类,用于指向,如下:

class MyNode
{
public:
    MyNode *left, *right;
    Myaabb box;
    std::vector<int> idx;

    MyNode(){ memset(this, 0, sizeof(*this)); }
}

这个node 类包含了一个aabb类元素 - box,用于指明当前node 指向的 bounding box;还包含了两个 node 类型指针 - *left 和 * right,分别指向当前node 的左子树和右子树。 

如果node 为叶子节点,left 和 right 都为null, box 只包裹一个三角形。如果node 为 中间节点 或者根节点的话,left 和 right 分别指向该节点的左子节点和右子节点,box 则为包着左节点和右子节点的大包围盒。

有了aabb 和 node,我们就可以开始构建BVH了。构建过程主要包括两个步骤,首先把 3D model 的每一个三角形都创建一个包围盒,并生成一个包围盒数组,代码如下:

MyNode* build_sceneBVH( const std::vector<vec3> &vbuf, const std::vector<uint32_t> &ibuf)
{
  std::vector<Myaabb> src;  
  int i;
  int n_face = ibuf.size()/3; //the number of triangles 

  for(i=0; i<n_face; i++)
  {
    Myaabb box;
    int tidx = i;
    vec3 v0  = vbuf[ibuf[3*tidx+0]];
    vec3 v1  = vbuf[ibuf[3*tidx+1]];
    vec3 v2  = vbuf[ibuf[3*tidx+2]];

    vec3 mm = vmax(vmax(v0, v1), v2);
    vec3 nn = vmin(vmin(v0, v1), v2);

    box.creat_box(mm, nn, tidx);
    src.push_back(box);
  }
 
  return process_aabbs(src);
}

vbuf 数组存的是 3D model 三角形的顶点,ibuf 数组存的是三角形顶点对应的index。std::vector<Myaabb> src 是保存三角形包围盒的数组,我们接着就将它传进去 process_aabbs() 函数来生成BVH,代码如下:

MyNode* process_aabbs(vector<Myaabb> &src)
{
  int i, j;
  MyNode* node = new MySceneBVH; // root node of the generated BVH

  // if the 3D model only has one triangle
  if(src.size()==1) 
  {
    node->box = src[0];
    node->idx.push_back(src[0].idx);
    return node;
  }else
  {
    std::vector<Myaabb> current_src;
    MyNode *current_node = node;

    std::vector<Myaabb> mynote_srca[512];
    std::vector<Myaabb> mynote_srcb[512];
    MyNode *mynote_left[512];
    MyNode *mynote_right[512];


    int nsrca = 0, nsrcb=0;
    int nlnode = 0, nrnode=0;
    mynote_left[nlnode++] = node;
    mynote_srca[nsrca++] = src;
    
    // while loop generate BVH
    while( nsrca>0 || nsrcb>0 )
    {
      if(nsrca>0)
      {
        current_src = mynote_srca[nsrca-1];
        current_node = mynote_left[nlnode-1];
        nsrca--;
        nlnode--;
      }else if(nsrcb>0)
      {
        current_src = mynote_srcb[nsrcb-1];
        current_node = mynote_right[nrnode-1];
        nsrcb--;
        nrnode--;
      }


      vec3 mm = vec3(-FLT_MAX, -FLT_MAX, -FLT_MAX);
      vec3 nn = vec3( FLT_MAX,  FLT_MAX,  FLT_MAX);
      for(i=0;i<current_src.size(); i++)
      {
        mm = vmax(mm, current_src[i].mm);
        nn = vmin(nn, current_src[i].nn);
      }

      if(current_src.size()==1)
      {
        current_node->box = current_src[0];
        current_node->idx.push_back(current_src[0].idx);
        continue;
      }

      // divide to srca and srcb
      float b;
      int dim[3];
      vec3 a = (mm+nn)/2.f; //center
      vec3 r = (mm-nn)/2.f;
      {
        if(r.x > r.y && r.x > r.z)
        {
          dim[0] = 0;
          dim[1] = 1;
          dim[2] = 2;
          if( r.y < r.z )
            g_swap(dim[1],dim[2]);
        }else if( r.y > r.z )
        {
          dim[0] = 1;
          dim[1] = 2;
          dim[2] = 0;
          if( r.z < r.x )
            g_swap(dim[1],dim[2]);
        }else
        {
          dim[0] = 2;
          dim[1] = 0;
          dim[2] = 1;
          if( r.x < r.y )
            g_swap(dim[1],dim[2]);
        }
      }

      std::vector<Myaabb> srca, srcb;
      for( j=0; j<3; j++ )
      {
        srca.clear();
        srcb.clear();
        for(i=0; i<current_src.size(); i++)
        {
          vec3 center = (current_src[i].mm + current_src[i].nn)/2.f;
          if(  ((float*)&center)[dim[j]] < ((float*)&a)[dim[j]]  )
            srca.push_back(current_src[i]);
          else
            srcb.push_back(current_src[i]);
        }
        if( srca.size() && srcb.size() )
          break;
      }
      if(srca.size()==0 || srcb.size()==0)
      {
        srca.assign(current_src.begin(), current_src.begin()+current_src.size()/2);
        srcb.assign(current_src.begin()+current_src.size()/2, current_src.end());
      }

      current_node->box.creat_box(mm, nn, 0);

      if(srca.size()>0)
      {
        current_node->left = new MyNode;
        mynote_left[nlnode++] = current_node->left;
        mynote_srca[nsrca++] = srca;
      }
      if(srcb.size()>0)
      {
        current_node->right = new MyNode;
        mynote_srcb[nsrcb++] = srcb;
        mynote_right[nrnode++] = current_node->right;
      }
    };
    return node;
  }
}

   具体使用这个函数的代码例子如下:


    std::vector<vec3> vbuf = the 3D model vertices;
    std::vector<uint> ibuf = the indices of all vertices;

    MyNode *mysbvh;
      mysbvh = build_sceneBVH(vbuf, ibuf);

> 函数解释

process_aabbs() 这个函数传进来的是 3D model 的包围盒数组, 即src,传出去的是 BVH 的根节点, 即node

之前就说过,我们 BVH 的叶子节点指向只包含一个三角形的aabb。为了达到这个目的,我们需要将 src 分成左包围盒数组 srca1 和右包围盒数组 srcb1,然后 srca1 又会分为左包围盒数组 srca2 和右包围盒 srcb2, srcb1 会分为左包围盒数组 srca3 和右包围盒 srcb3, 直至分到左右各只有一个包围盒。 我们一次loop 只可以分一个包围盒数组,然后产生两个待下次分割的包围盒数组。那我们下次要分哪个包围盒呢?

我们为此准备了两个工作列表,左右各一个,将每次loop 分割的包围盒数组都分别放到各自的工作列表中,然后我们选择一直分左边的包围盒数组,即下次loop分的包围盒数组是左工作列表里最后一个包围盒数组,直至左工作列表里没有了,我们再分记录在右工作列表的最后一个包围盒数组,这里要注意的是,这个数组可能又会分成左包围盒数组和右包围盒数组,那么下次loop开始我们又会继续拿左工作列表里的数组进行分割,而不是取右工作列表的数组进行分割。当左工作列表和右工作列表都没有东西了,就证明工作完成了,while loop 结束了,BVH 生成了,收工! 

具体到函数执行呢,在第一次 loop 时,我们先把 3D model 所有包围盒生成一个最大包围盒,然后我们比较每一个小包围盒与当前最大包围盒的中心,将小包围盒记录到左节点的包围盒数组(srca)或者右结点包围盒数组(srcb), 在 loop 最后,我们让 node 指向最大包围盒,给node 生成左子节点和右子节点,并记录到对应的子节点数组中(mynote_leftmynote_right), srca  srcb 也会记录到对应的包围盒数组的数组(mynote_srcamynote_right, 也就是我们的左右工作列表)。在下一次loop开始,我们会从子节点数组中拿出一个 node (current_node),再从对应的包围盒数组的数组中拿出一个包围盒数组(current_src),重复上面的操作,while loop 结束后,我们就得到了想要的BVH。

> 参数解释

current_src 是我们每一次 loop 处理的包围盒数组,在 loop 最开始的时候它是 3D model 所有包围盒的数组;在之后的 loop 里,它被设定为之前 loop 划分的左包围盒数组的数组(mynote_srca,如果里面有value的话)里的最后一个数组;如果 mynote_srca size 为0, current_src 就会被设定为右包围盒数组的数组(mynote_srcb)里的最后一个数组。

current_node 是我们每一次 loop 处理的节点,在loop最开始的时候它被设置为root node;在之后的 loop 中,它会先从mynote_left 中查找是否有可以处理的node (即继续处理左子树的节点),不然的话就处理 右子树 mynote_right 中的node。

mynote_srca 和 mynote_srcb 分别是记录 current_node  左子树和右子树下需要处理的aabb 数组, 我们每次开始都先从处理 mynote_srca 下的 aabb 开启,在函数最开始的时候,mynote_srca 存的是所有包围盒的数组,即 src;在每次while loop 结束后,mynote_srca 和 mynote_srcb 会分别记录下此次分出来的左子树 和右子树的包围盒数组 (srca 和 srcb),供之后的while loop 处理。

nsrca 和 nsrcb 分别记录mynote_srca 和 mynote_srcb 里还有多少项要处理,也是我们终止while loop 的判断条件。 nlnode 和 nrnode 功能类似于 nsrca 和 nsrcb。

mm 和 nn 是current_node 所指向的 aabb 的最大和最小顶点。
dim 这个数组是我把单个包围盒分左右的一个参考依据,但具体为什么这么做我忘了,之后补上。。。

> 总结

写到这里,用 while loop 构建 BVH 的内容基本上就完结了。总结一下呢,我这个构建方法是自上而下构建的,效率可能也没有彻底优化,而且代码可能看起来觉得好长;但是用上 while loop,有错的时候我们可以很快速找到是哪里出了错, 在我看来,啰嗦但能让我快速除错的代码比让我看起来简洁却不好除错的代码更有用。

这一篇文章我写的好啰嗦,但有这些啰嗦的内容,下一篇我们介绍如何用 while loop 来遍历BVH 就会简单很多,下篇见!

> vmax, vmin 和 g_swap function

vec3 vmax(const vec3 &a, const vec3 &b)
{
  vec3 c;
    c.x = (a.x > b.x) ? a.x : b.x;
    c.y = (a.y > b.y) ? a.y : b.y;
    c.z = (a.z > b.z) ? a.z : b.z;
  return c;
}

vec3 vmin(const vec3 &a, const vec3 &b)
{
  vec3 c;
    c.x = (a.x < b.x) ? a.x : b.x;
    c.y = (a.y < b.y) ? a.y : b.y;
    c.z = (a.z < b.z) ? a.z : b.z;
  return c;
}

void g_swap( int &a, int &b )
{ 
  int c;  
    c = a;  
    a = b;  
    b = c; 
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在3DSMAX中打开BVH文件的方法如下: 1. 在3DSMAX中选择"Import" 2. 选择"Motion capture (*.bvh)" 3. 选择你要导入的BVH文件 4. 点击"Open" 5. 在导入面板中进行设置 6. 点击"OK"导入文件 注意: 如果你的3DSMAX版本没有BVH导入选项, 你可以下载并安装BVH Importer插件来支持导入BVH文件。 ### 回答2: 要使用3DSMAX来打开BVH文件,首先确保已经安装了3DSMAX软件。然后按照以下步骤进行操作: 1. 打开3DSMAX软件并创建一个新的项目。 2. 在导航栏中选择“文件”>“导入”>“运动捕捉数据”。 3. 在弹出的对话框中,选择要打开的BVH文件,并点击“打开”按钮。 4. 在“导入运动捕捉文件”对话框中,选择合适的选项来配置导入的BVH文件。例如,可以选择是否导入骨架或者动画数据。 5. 点击“确定”按钮,3DSMAX将根据选择的配置导入BVH文件。 6. 导入完成后,可以在3DSMAX中查看和编辑导入的运动数据。可以使用时间轴来预览动画,或者使用控制器来修改骨架的姿态。 需要注意的是,BVH文件是一种包含骨架和动画数据的文件格式。在导入过程中,要确保选择正确的导入选项以正确解析文件。此外,导入的BVH文件可能需要进行额外的调整和修改,以适应3DSMAX的渲染和动画需求。 总之,使用3DSMAX打开BVH文件是一个相对简单的过程,只需导入文件并进行适当的配置即可在3DSMAX中查看和编辑导入的运动数据。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值