计算三角形网格中的法线

时间:2011-07-11 20:47:43

标签: c++ opengl computational-geometry normals

我画了一个有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 这是我写的代码。我只使用数组而不是向量,并将所有法线存储在名为normals的结构中。但它仍然不起作用。我在* 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();
}
//***************************************************************************************************************************

6 个答案:

答案 0 :(得分:106)

  

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

像往常一样,答案是:“这取决于”。由于法线被定义为垂直于给定平面内所有向量的向量(在N维中),因此需要一个平面来计算法线。顶点位置只是一个点,因此是单数,所以你实际上需要一个面来计算法线。因此,天真地,可以假设法线每面,因为正常计算的第一步是通过评估面边缘的叉积来确定面法线。

假设您有一个点 A B C 的三角形,那么这些点的位置矢量↑A ↑B ↑C ,边缘有矢量↑B - ↑A ↑C - ↑A 所以面法线向量↑N f =(↑B - ↑A)×(↑C - ↑A)

请注意,如上所述,↑N f 的大小与面部区域成正比。

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

↑N v =Σp↑N f ;其中 p 是每张脸的加权。

可以假设参与面法线之间的权重相等。但是假设面部越大,对正常情况的贡献就越大,这就更有意义了。

现在回想一下你用矢量↑v 进行标准化,然后用它的接收长度缩放它:↑v i =↑v / |↑v | < / strong>即可。但正如已经说过的,脸部法线的长度已经取决于脸部的面积。因此,上面给出的加权因子 p 已经包含在向量本身中:它的长度,即幅度。因此,我们可以通过简单地总结所有面法线来获得顶点法线向量。

在照明计算中,法向量必须是单位长度,即归一化为可用。总结之后,我们将新发现的顶点法线标准化并使用它。

细心的读者可能已经注意到我特意说 smooth 曲面共享顶点。事实上,如果几何体中有一些折痕/硬边,则两侧的面不共享顶点。在OpenGL中,顶点是

的整个组合
  • 位置
  • 正常
  • (颜色)
  • N个纹理坐标
  • M其他属性

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


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

 glIndexPointer( GL_UNSIGNED_BYTE, 0, indices );

索引指针与顶点数组索引有 nothing !这是一个anachronsim,从图形仍然使用调色板而不是真正的颜色。通过给出RGB值来设置像素颜色,但是通过偏移到有限的颜色调色板中的单个数字来设置像素颜色。调色板颜色仍然可以在多种图形文件格式中找到,但是没有相应的硬件使用它们。

请从你的内存和你的代码中删除 glIndexPointer (和glIndex),它们不会按照你的想法去做。整个索引颜色模式是使用的晦涩,坦白说我不知道1998年之后仍然支持它的任何硬件。

答案 1 :(得分:22)

每个顶点。

使用交叉积来计算给定顶点周围三角形的面法线,将它们加在一起并进行标准化。

答案 2 :(得分:21)

为datenwolf竖起大拇指!我完全同意他的做法。为每个顶点添加相邻三角形的法线向量,然后进行标准化是要走的路。我只想稍微提一下答案,仔细看看矩形光滑网格的特殊但非常常见的情况,其中常数x / y step 。换句话说,矩形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----+--..
                                             |   /|\   |

更优雅,甚至更快计算。

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

答案 3 :(得分:2)

看起来很简单,计算三角形的法线只是问题的一部分。在三角形的情况下,多边形的两个边的叉积就足够了,除非三角形塌陷到自身上并退化。在这种情况下,没有一个有效的法线,因此您可以根据自己的喜好选择一个。

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

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

从同样的意义上说,由于绕组数的定义不明确,不连续的多边形汤和点云对准确重建提出了挑战。

一种经常用于解决此问题的潜在策略是从每个半三角测量的外部向外发射光线(即 ray-stabbing )。但是,如果多边形可以包含多个顶点,则不能认为三角剖分有效,因此光线可能会错过该特定子三角。如果有光线照射,则可以将与光线方向相反的法线(即满足 dot(ray,n)<.5 的法线)用作整个多边形的法线。显然,这相当昂贵,并且会随着每个多边形的顶点数量缩放。

非常感谢new work描述了一种替代方法,该方法不仅更快(适用于大型和复杂的网格物体),而且可概括 “绕线顺序” 概念,用于超出多边形网格的构造,例如点云和多边形汤,等值面和点集曲面,其中可能具有连通性甚至都没有定义!

如本文所述,该方法构造了一个分层分裂树表示,该表示逐步完善,并在每次拆分操作中均考虑了父级“偶极子”方向。这样,多边形法线将仅仅是该多边形的所有偶极(即点和法线对)上的积分(均值)。

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

答案 4 :(得分:0)

对于像我一样遇到此问题的人,您的答案可能是这样:

// 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]);

答案 5 :(得分:0)

最简单的方法是将(p1,p2,p3)个三角形(例如p1)中的一个转换为(0,0,0),这样就意味着(x2,y2,z2)->(x2-x1,y2-y1,z2-z1)(x3,y3,z3)->(x3-x1,y3-y1,z3-z1)。然后,对转换后的点执行点积,以获得平面斜率,或者叉积,以获得向外法线< / strong>。

请参阅:

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

简单直观地表示叉积和点积之间的差异。

将点之一移动到原点基本上等同于沿着p1p2p2p3生成矢量。