【问题标题】:Calculating normals in a triangle mesh计算三角形网格中的法线
【发布时间】:2011-10-03 03:17:14
【问题描述】:

我已经绘制了一个具有 10000 个顶点(100x100)的三角形网格,它将是一个草地。我为此使用了 gldrawelements()。我看了一整天,仍然无法理解如何为此计算法线。每个顶点都有自己的法线还是每个三角形都有自己的法线?有人可以指出我如何编辑我的代码以合并法线的正确方向吗?

struct vertices {
    GLfloat x;
    GLfloat y;
    GLfloat z;
}vertices[10000];

GLuint indices[60000];

/*
99..9999
98..9998
........
01..9901
00..9900
*/

void CreateEnvironment() {
    int count=0;
    for (float x=0;x<10.0;x+=.1) {
        for (float z=0;z<10.0;z+=.1) {
            vertices[count].x=x;
            vertices[count].y=0;
            vertices[count].z=z;
            count++;
        }
    }
    count=0;
    for (GLuint a=0;a<99;a++){
        for (GLuint b=0;b<99;b++){
            GLuint v1=(a*100)+b;indices[count]=v1;count++;
            GLuint v2=(a*100)+b+1;indices[count]=v2;count++;
            GLuint v3=(a*100)+b+100;indices[count]=v3;count++;
        }
    }
    count=30000;
    for (GLuint a=0;a<99;a++){
        for (GLuint b=0;b<99;b++){
            indices[count]=(a*100)+b+100;count++;//9998
            indices[count]=(a*100)+b+1;count++;//9899
            indices[count]=(a*100)+b+101;count++;//9999
        }
    }
}

void ShowEnvironment(){
    //ground
    glPushMatrix();
    GLfloat GroundAmbient[]={0.0,0.5,0.0,1.0};
    glMaterialfv(GL_FRONT,GL_AMBIENT,GroundAmbient);
    glEnableClientState(GL_VERTEX_ARRAY);
    glIndexPointer( GL_UNSIGNED_BYTE, 0, indices );
    glVertexPointer(3,GL_FLOAT,0,vertices);
    glDrawElements(GL_TRIANGLES,60000,GL_UNSIGNED_INT,indices);
    glDisableClientState(GL_VERTEX_ARRAY);
    glPopMatrix();
}

编辑 1 这是我写的代码。我只是使用数组而不是向量,并将所有法线存储在称为法线的结构中。但是它仍然不起作用。我在 *indices 处得到一个未处理的异常。

struct Normals {
    GLfloat x;
    GLfloat y;
    GLfloat z;
}normals[20000];
Normals* normal = normals;
//***************************************ENVIRONMENT*************************************************************************
struct vertices {
    GLfloat x;
    GLfloat y;
    GLfloat z;
}vertices[10000];

GLuint indices[59403];

/*
99..9999
98..9998
........
01..9901
00..9900
*/

void CreateEnvironment() {
    int count=0;
    for (float x=0;x<10.0;x+=.1) {
        for (float z=0;z<10.0;z+=.1) {
            vertices[count].x=x;
            vertices[count].y=rand()%2-2;;
            vertices[count].z=z;
            count++;
        }
    }
    //calculate normals 
    GLfloat vector1[3];//XYZ
    GLfloat vector2[3];//XYZ
    count=0;
    for (int x=0;x<9900;x+=100){
        for (int z=0;z<99;z++){
            vector1[0]= vertices[x+z].x-vertices[x+z+1].x;//vector1x
            vector1[1]= vertices[x+z].y-vertices[x+z+1].y;//vector1y
            vector1[2]= vertices[x+z].z-vertices[x+z+1].z;//vector1z
            vector2[0]= vertices[x+z+1].x-vertices[x+z+100].x;//vector2x
            vector2[1]= vertices[x+z+1].y-vertices[x+z+100].y;//vector2y
            vector2[2]= vertices[x+z+1].z-vertices[x+z+100].z;//vector2z
            normals[count].x= vector1[1] * vector2[2]-vector1[2]*vector2[1];
            normals[count].y= vector1[2] * vector2[0] - vector1[0] * vector2[2];
            normals[count].z= vector1[0] * vector2[1] - vector1[1] * vector2[0];count++;
        }
    }
    count=10000;
    for (int x=100;x<10000;x+=100){
        for (int z=0;z<99;z++){
            vector1[0]= vertices[x+z].x-vertices[x+z+1].x;//vector1x -- JUST ARRAYS
            vector1[1]= vertices[x+z].y-vertices[x+z+1].y;//vector1y
            vector1[2]= vertices[x+z].z-vertices[x+z+1].z;//vector1z
            vector2[0]= vertices[x+z+1].x-vertices[x+z-100].x;//vector2x
            vector2[1]= vertices[x+z+1].y-vertices[x+z-100].y;//vector2y
            vector2[2]= vertices[x+z+1].z-vertices[x+z-100].z;//vector2z
            normals[count].x= vector1[1] * vector2[2]-vector1[2]*vector2[1];
            normals[count].y= vector1[2] * vector2[0] - vector1[0] * vector2[2];
            normals[count].z= vector1[0] * vector2[1] - vector1[1] * vector2[0];count++;
        }
    }

    count=0;
    for (GLuint a=0;a<99;a++){
        for (GLuint b=0;b<99;b++){
            GLuint v1=(a*100)+b;indices[count]=v1;count++;
            GLuint v2=(a*100)+b+1;indices[count]=v2;count++;
            GLuint v3=(a*100)+b+100;indices[count]=v3;count++;
        }
    }
    count=30000;
    for (GLuint a=0;a<99;a++){
        for (GLuint b=0;b<99;b++){
            indices[count]=(a*100)+b+100;count++;//9998
            indices[count]=(a*100)+b+1;count++;//9899
            indices[count]=(a*100)+b+101;count++;//9999
        }
    }
}

void ShowEnvironment(){
    //ground
    glPushMatrix();
    GLfloat GroundAmbient[]={0.0,0.5,0.0,1.0};
    GLfloat GroundDiffuse[]={1.0,0.0,0.0,1.0};
    glMaterialfv(GL_FRONT,GL_AMBIENT,GroundAmbient);
    glMaterialfv(GL_FRONT,GL_DIFFUSE,GroundDiffuse);
    glEnableClientState(GL_VERTEX_ARRAY);
    glEnableClientState(GL_NORMAL_ARRAY);
    glNormalPointer( GL_FLOAT, 0, normal);
    glVertexPointer(3,GL_FLOAT,0,vertices);
    glDrawElements(GL_TRIANGLES,60000,GL_UNSIGNED_INT,indices);
    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_NORMAL_ARRAY);
    glPopMatrix();
}
//***************************************************************************************************************************

【问题讨论】:

  • 顶点不能有法线(除非它们是周围面的面法线的平均值)。我从来没有使用过openGL,所以我不知道是否有办法简单地获得它们。我想是这样,但如果你想自己做,你总是可以从顶点计算边向量,并通过取两条边的叉积来计算法线(然后归一化为幅度 1)。
  • 我刚刚编辑了它。我对代码做了一些更改,但它没有编译。
  • glDrawElements(GL_TRIANGLES,60000,GL_UNSIGNED_INT,indices); 将在 59403 元素 indices 数组的末尾运行。
  • 对草地使用 10000 个顶点似乎相当大,除非它是一个具有许多有趣地形特征的非常大的区域。您可能会考虑使用带有纹理映射和法线映射的粗网格。只是一个想法。

标签: c++ opengl computational-geometry normals


【解决方案1】:

简单的方法是将三角形(p1,p2,p3) 之一(比如p1)转换为(0,0,0),这样就意味着(x2,y2,z2)-&gt;(x2-x1,y2-y1,z2-z1)(x3,y3,z3)-&gt;(x3-x1,y3-y1,z3-z1)。然后对变换后的点进行点积以获得平面斜率,或叉积获得外向法线。

见:

https://en.wikipedia.org/wiki/Cross_product#/media/File:Cross_product_vector.svg

用于简单直观地表示叉积和点积之间的区别。

其中一个点移动到原点,基本上相当于沿p1p2p2p3生成向量。

【讨论】:

    【解决方案2】:

    对于像我这样遇到这个问题的人,你的答案可能是这样的:

    // Compute Vertex Normals
    std::vector<sf::Glsl::Vec3> verticesNormal;
    verticesNormal.resize(verticesCount);
    
    for (i = 0; i < indices.size(); i += 3)
    {
        // Get the face normal
        auto vector1 = verticesPos[indices[(size_t)i + 1]] - verticesPos[indices[i]];
        auto vector2 = verticesPos[indices[(size_t)i + 2]] - verticesPos[indices[i]];
        auto faceNormal = sf::VectorCross(vector1, vector2);
        sf::Normalize(faceNormal);
    
        // Add the face normal to the 3 vertices normal touching this face
        verticesNormal[indices[i]] += faceNormal;
        verticesNormal[indices[(size_t)i + 1]] += faceNormal;
        verticesNormal[indices[(size_t)i + 2]] += faceNormal;
    }
    
    // Normalize vertices normal
    for (i = 0; i < verticesNormal.size(); i++)
        sf::Normalize(verticesNormal[i]);
    

    【讨论】:

      【解决方案3】:

      虽然看起来很简单,但计算三角形的法线只是问题的一部分。在三角形情况下,多边形两条边的叉积就足够了,除非三角形折叠到自身上并退化;在这种情况下,没有一个有效的正常,所以你可以根据自己的喜好选择一个。

      那么为什么归一化叉积只是问题的一部分?该多边形中顶点的缠绕顺序定义了法线的方向,即如果一对顶点交换到位,法线将指向相反的方向。所以事实上,如果网格本身在这方面包含不一致,这可能是有问题的,即它的一部分假设一个顺序,而其他部分假设不同的顺序。一个著名的例子是原始的Stanford Bunny 模型,其中表面的某些部分将指向内部,而其他部分则指向外部。这样做的原因是因为模型是使用扫描仪构建的,并且没有注意生成具有规则缠绕图案的三角形。 (显然,兔子的干净版本也存在)

      如果多边形可以有多个顶点,则缠绕问题会更加突出,因为在这种情况下,您将平均该多边形的半三角剖分的部分法线。考虑部分法线指向相反方向的情况,在取平均值时导致长度为 0 的法线向量!

      在同样的意义上,断开的多边形汤和点云由于不明确的绕组数而对精确重建提出了挑战。

      一种经常用于解决此问题的潜在策略是从外部向每个半三角剖分的中心发射随机光线(即光线刺伤)。但是如果多边形可以包含多个顶点,则不能假设三角剖分是有效的,因此光线可能会错过那个特定的子三角形。如果一条射线命中,则与射线方向相反的法线,即满足dot(ray, n) ,可以用作整个多边形的法线。显然,这是相当昂贵的,并且会随着每个多边形的顶点数量而变化。

      谢天谢地,有一个很棒的 new work 描述了一种替代方法,它不仅更快(对于大型和复杂的网格)而且概括 '缠绕顺序'概念用于多边形网格之外的构造,例如点云和多边形汤、等值面和点集表面,其中连接性可能不被定义!

      如论文中所述,该方法构造了一个逐步细化的分层分裂树表示,在每个分裂操作中都考虑了父“偶极子”方向。然后,多边形法线将只是多边形所有偶极子(即点+法线对)的积分(平均值)。

      对于处理来自激光雷达扫描仪或其他来源的不干净网格/pcl 数据的人来说,这可以定义。成为改变游戏规则的人。

      【讨论】:

        【解决方案4】:

        为 datenwolf 点赞!我完全同意他的做法。为每个顶点添加相邻三角形的法向量然后归一化是要走的路。我只是想稍微推动一下答案,并仔细研究具有 constant x 的 rectangularsmooth 网格的特殊但非常常见的情况/y 步骤。换句话说,一个矩形 x/y 网格,每个点的高度可变。

        这样的网格是通过在 x 和 y 上循环并为 z 设置一个值来创建的,并且可以表示诸如山的表面之类的东西。所以网格的每个点都用一个向量表示

        P = (x, y, f(x,y)) 
        

        其中 f(x,y) 是一个函数,给出网格上每个点的 z。

        通常我们使用 TriangleStrip 或 TriangleFan 来绘制这样的网格,但任何技术都应该为生成的三角形提供类似的地形。

             |/   |/   |/   |/
        ...--+----U----UR---+--...
            /|   /| 2 /|   /|           Y
           / |  / |  / |  / |           ^
             | /  | /  | /  | /         |
             |/ 1 |/ 3 |/   |/          |
        ...--L----P----R----+--...      +-----> X
            /| 6 /| 4 /|   /|          
           / |  / |  / |  / |         
             | /5 | /  | /  | /      
             |/   |/   |/   |/
        ...--DL---D----+----+--...
            /|   /|   /|   /|
        

        对于 triangleStrip,每个顶点 P=(x0, y0, z0) 有 6 个相邻的顶点表示

        up       = (x0     , y0 + ay, Zup)
        upright  = (x0 + ax, y0 + ay, Zupright) 
        right    = (x0 + ax, y0     , Zright) 
        down     = (x0     , y0 - ay, Zdown)
        downleft = (x0 - ax, y0 - ay, Zdownleft) 
        left     = (x0 - ax, y0     , Zleft)
        

        其中 ax/ay 分别是 x/y 轴上的恒定网格步长。在方格上 ax = ay。

        ax = width / (nColumns - 1)
        ay = height / (nRows - 1)
        

        因此每个顶点有 6 个相邻的三角形,每个三角形都有自己的法向量(表示为 N1 到 N6)。这些可以使用定义三角形边的两个向量的叉积来计算,并注意我们做叉积的顺序。如果法线向量指向你的 Z 方向:

        N1 = up x left =
           = (Yup*Zleft - Yleft*Zup, Xleft*Zup - Xup*ZLeft, Xleft*Yup - Yleft*Xup) 
        
           =( (y0 + ay)*Zleft - y0*Zup, 
              (x0 - ax)*Zup   - x0*Zleft, 
              x0*y0 - (y0 + ay)*(x0 - ax) ) 
        
        N2 = upright  x up
        N3 = right    x upright
        N4 = down     x right
        N5 = downleft x down
        N6 = left     x downleft
        

        每个点 P 的最终法向量是 N1 到 N6 的总和。我们在求和后归一化。很容易创建一个循环,计算每个法线向量的值,将它们相加然后归一化。然而,正如 Shickadance 先生所指出的,这可能需要相当长的时间,尤其是对于大型网格和/或嵌入式设备。

        如果我们仔细观察并手动执行计算,我们会发现大多数项相互抵消,从而为结果向量 N 留下一个非常优雅且易于计算的最终解。这里是为了加快计算速度,避免计算 N1 到 N6 的坐标,对每个点做 6 个叉积和 6 个加法。代数帮助我们直接找到解决方案,使用更少的内存和 CPU 时间。

        我不会显示计算的细节,因为它很长但很直接,并且会跳转到网格上任何点的法线向量的最终表达式。为了清楚起见,只分解了 N1,其他向量看起来相似。求和后我们得到 N 尚未归一化:

        N = N1 + N2 + ... + N6
        
          = .... (long but easy algebra) ...
        
          = ( (2*(Zleft - Zright) - Zupright + Zdownleft + Zup - Zdown) / ax,
              (2*(Zdown - Zup)    + Zupright + Zdownleft - Zup - Zleft) / ay,
               6 )
        

        给你!只需将此向量归一化,您就可以得到网格上任意点的法线向量,前提是您知道其周围点的 Z 值以及网格的水平/垂直步长。

        请注意,这是周围三角形法线向量的加权平均值。权重是三角形的面积,已经包含在叉积中。

        您甚至可以通过仅考虑四个周围点(上、下、左和右)的 Z 值来进一步简化它。在这种情况下,您会得到:

                                                     |   \|/   |
        N = N1 + N2 + N3 + N4                    ..--+----U----+--..
          = ( (Zleft - Zright) / ax,                 |   /|\   |
              (Zdown -  Zup  ) / ay,                 |  / | \  |
               2 )                                 \ | / 1|2 \ | /
                                                    \|/   |   \|/
                                                 ..--L----P----R--...
                                                    /|\   |   /|\
                                                   / | \ 4|3 / | \
                                                     |  \ | /  |
                                                     |   \|/   |
                                                 ..--+----D----+--..
                                                     |   /|\   |
        

        计算更优雅,计算速度更快。

        希望这将使一些网格更快。 干杯

        【讨论】:

        • 我不明白为什么,例如,第一个三角形的法线向量等于“N1 = 上 x 左”。不应该通过叉积三角形的两条边来执行计算,这些边可以是 E1 = (UP) 和 E2 = (LP),那么法线应该等于“N1 = (UP) x (LP)”。
        【解决方案5】:

        每个顶点都有自己的法线还是每个三角形都有自己的法线?

        答案通常是:“视情况而定”。由于法线被定义为垂直于给定平面(N 维)内所有矢量的矢量,因此您需要一个平面来计算法线。顶点位置只是一个点,因此是奇异的,因此您实际上需要一个面来计算法线。因此,天真地,可以假设法线是每个面,因为法线计算的第一步是通过评估面边缘的叉积来确定面法线。

        假设你有一个三角形,有点ABC,那么这些点有位置向量↑A, ↑B, ↑C 和边缘有向量 ↑B - ↑A↑C - ↑A 所以人脸法向量为 ↑Nf = (↑B - ↑A) × (↑C - ↑A)

        请注意,如上所述,↑Nf 的大小与面部面积成正比。

        在光滑表面中,顶点在面之间共享(或者您可以说这些面共享一个顶点)。在这种情况下,顶点的法线不是它所属的面的面法线之一,而是它们的线性组合:

        ↑Nv = ∑ p ↑Nf ;其中 p 是每个人脸的权重。

        可以假设参与的面部法线之间的权重相等。但更有意义的是,假设一张脸越大,它对法线的贡献就越大。

        现在回想一下,您通过向量 ↑v 对其进行标准化,方法是使用它的倒数长度对其进行缩放:↑vi = ↑v/|↑v|。但正如已经说过的,面部法线的长度已经取决于面部的面积。所以上面给出的权重因子 p 已经包含在向量本身中:它的长度,也就是幅度。所以我们可以通过简单的将所有的面法线相加得到顶点法线向量。

        在照明计算中,法线向量必须是单位长度,即归一化才能使用。所以总结之后,我们对新找到的顶点法线进行归一化并使用。

        细心的读者可能已经注意到我特别说过平滑表面共享顶点。事实上,如果你的几何图形中有一些折痕/硬边,那么两边的面就不会共享顶点。在OpenGL中,一个顶点是整个组合

        • 位置
        • 正常
        • (颜色)
        • N 个纹理坐标
        • M 更多属性

        你改变其中一个,你得到一个完全不同的顶点。现在一些 3D 建模者仅将顶点视为一个点的位置,并存储每个面的其余属性(Blender 就是这样的建模者)。这节省了一些内存(或相当大的内存,取决于属性的数量)。但是 OpenGL 需要整个东西,所以如果使用这样一个混合范式文件,您必须首先将其分解为 OpenGL 兼容数据。看看 Blender 的一个导出脚本,比如 PLY 导出器,看看它是如何完成的。


        现在来介绍一些其他的东西。在你的代码中你有这个:

         glIndexPointer( GL_UNSIGNED_BYTE, 0, indices );
        

        索引指针与顶点数组索引无关!这是一个时代错误,当时图形仍然使用调色板而不是真彩色。像素颜色不是通过给出它的 RGB 值来设置的,而是通过一个数字偏移到有限的调色板中来设置的。调色板颜色仍然可以在多种图形文件格式中找到,但没有像样的硬件使用它们了。

        请从你的记忆和你的代码中删除 glIndexPointer(和 glIndex),他们不会做你认为他们会做的事情 整个索引颜色模式很难使用,坦率地说我没有知道 1998 年之后制造的任何硬件仍然支持它。

        【讨论】:

          【解决方案6】:

          每个顶点。

          使用叉积计算围绕给定顶点的三角形的面法线,将它们相加并归一化。

          【讨论】:

          • 非常正确。 :-) 我不会添加我自己的答案,而是添加关于 this 答案的评论,其中我已经详细描述了计算法线。
          • 有更有效的方法吗?它适用于简单模型,但是一旦我有大量顶点,计算所有每个顶点法线需要很长时间。
          • 然后是所有边缘情况,例如面积为零的三角形或倾斜法线权重的细三角形 - 按三角形面积对法线加权(对未归一化的叉积求和,仅在结束)是可能的,但并不总是能给出完美的结果。
          猜你喜欢
          • 2014-11-07
          • 2011-05-18
          • 1970-01-01
          • 2017-09-23
          • 2013-12-21
          • 2015-02-12
          • 2020-01-25
          • 1970-01-01
          • 2017-06-30
          相关资源
          最近更新 更多