计算三角形网格中的法线

Calculating normals in a triangle mesh

本文关键字:三角形 网格 计算      更新时间:2023-10-16

我已经画了一个三角形网格,有10000个顶点(100x100),它将是一个草地。我使用了玻璃装饰()。我看了一整天,仍然不能理解如何计算法线。是每个顶点都有自己的法线还是每个三角形都有自己的法线?有人能指出我在正确的方向上如何编辑我的代码纳入法线?

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();
}
//***************************************************************************************************************************

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

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

假设你有一个三角形,有 a BC,那么这些点有位置向量↑a ↑B↑C>↑C并且边有向量↑B -↑a ↑C -↑a 所以面法向量为↑Nf =(↑B -↑a) ×(↑C -↑a)

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

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

↑N v <子> =∑f p↑N <子>;其中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年以后建造的任何硬件仍然支持它。

为datenwolf点赞!我完全同意他的做法。为每个顶点添加相邻三角形的法向量,然后规范化是要走的路。我只是想稍微推一下答案,并仔细研究一下特殊但非常常见的情况,即具有常数x/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还没有归一化:

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----+--..
                                             |   /|   |

更加优雅,计算起来也更快。

希望这将使一些网格更快。欢呼声

种每个顶点都具备的。

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

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

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

虽然看起来很简单,但计算三角形的法线只是问题的一部分。在三角形的情况下,多边形两条边的叉乘是充分的,除非三角形坍缩并简并;在这种情况下,没有一个有效的正常,所以你可以选择一个你喜欢的。

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

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

在同样的意义上,由于圈数定义不清,不连接的多边形汤和点云对精确重建提出了挑战。

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

值得庆幸的是,有一项伟大的新工作描述了一种替代方法,该方法不仅更快(对于大型和复杂的网格),而且还将的"缠绕顺序"概念推广到多边形网格以外的结构,例如点云和多边形汤,等面和点集面,连通性甚至可能没有定义!如本文所述,该方法构建了一个逐步细化的分层分裂树表示,在每次分裂操作中都考虑到父"偶极子"方向。一个多边形法线将简单地是对多边形的所有偶极(即点+法线对)的积分(平均值)。

对于那些正在处理来自激光雷达扫描仪或其他来源的不清洁网格/pcl数据的人来说,这可能会改变游戏规则。

简单的方法是将其中一个三角形(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)。然后对变换后的点进行点积得到平面斜率,或者叉积得到外法线

:

https://en.wikipedia.org/wiki/Cross_product/媒体/文件:Cross_product_vector.svg

为叉乘和点乘之间的区别的简单直观表示。

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