50

我已经绘制了一个具有 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();
}
//***************************************************************************************************************************
4

6 回答 6

143

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

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

假设您有一个带有点ABC的三角形,那么这些点具有位置向量↑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| . 但正如已经说过的,面部法线的长度已经取决于面部的面积。所以上面给出的权重因子p已经包含在向量本身中:它的长度,也就是幅度。所以我们可以通过简单地总结所有的面法线来得到顶点法线向量。

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

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

  • 位置
  • 普通的
  • (颜色)
  • N个纹理坐标
  • M 更多属性

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


现在来介绍一些其他的东西。在您的代码中,您有以下内容:

 glIndexPointer( GL_UNSIGNED_BYTE, 0, indices );

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

请从您的内存和代码中删除glIndexPointer(和 glIndex),它们不会像您认为的那样做整个索引颜色模式很难使用,坦率地说,我不知道 1998 年之后构建的任何硬件仍然支持它。

于 2011-07-12T08:13:09.373 回答
33

为 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 = 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----+--..
                                             |   /|\   |

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

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

于 2014-02-09T14:20:34.937 回答
27

每个顶点。

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

于 2011-07-11T20:58:26.663 回答
2

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

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

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

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

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

值得庆幸的是,有一项伟大的新工作描述了一种替代方法,该方法不仅更快(对于大型和复杂的网格),而且还概括多边形网格之外的构造的“缠绕顺序”概念,例如点云和多边形汤、等值面,和点集表面,甚至可能没有定义连通性!

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

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

于 2018-08-29T22:01:03.413 回答
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]);
于 2019-09-05T19:40:22.727 回答
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)。然后对变换后的点进行点积以获得平面坡度,或进行叉积以获得向外法线

看:

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

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

将其中一个点移动到原点基本上相当于沿p1p2和生成向量p2p3

于 2020-09-03T15:45:48.440 回答