Surface Normal Averaging

Surface Normal Averaging

eryar@163.com

摘要Abstract:正确设置网格面上点的法向,对几何体在光照等情况下显示得更真实,这样就可以减少顶点数量,提高渲染速度。本文通过将OpenCascade中的形状离散成网格数据后在OpenSceneGraph中显示,及使用OSG的快速法向osgUtil::SmoothingVisitor优化与使用OpenCascade来计算正确的法向的结果的对比,说明面法向量的重要性。

关键字Key Words:OpenCascade, OpenSceneGraph, Normal Averaging, Triangulation Mesh

一、引言 Introduction

OpenGL中的顶点(Vertex)不是一个值,而由其空间坐标值、法向、颜色坐标、纹理坐标、雾坐标等所组成的一个集合。一个最基本的几何体对象至少需要设置一个合法的顶点数组,并记录顶点数据;如有必要,还可以设置颜色数组、法线数组、纹理坐标数组等多种信息。

在很多应用中,网格上的各点都需要一个表面法向量,它的作用非常广泛。例如可用来计算光照、背面剔除、模拟粒子系统在表面的“弹跳”效果、通过只需要正面而加速碰撞检测等。

Figure 1.1 Lighting on a surface

Figure 1.2 Light is reflected off objects at specific angles

如上图所示,物体在光照情况下的反射光等的计算是与法向N有关的。

二、OpenCascade中面的法向计算 Finding Normal for OpenCascade Face

在OpenCascade中可以将拓朴形状转换成STL格式的文件进行模型的数据交换。其中STL结构中只保存了三角网格的顶点坐标和三角面的法向量。为了将拓朴数据转换成STL的网格数据,先将拓朴形状进行三角剖分,再将剖分的网格保存成STL即可。其中每个三角面的法向计算也是直接根据两个向量的叉乘得来。

Figure 2.1 A normal vector as cross product of two vectors

实现文件是RWStl.cxx,其中计算法向的程序代码如下所示:

 

//=====================================================================//function : WriteBinary
//purpose  : write a binary STL file in Little Endian format
//=====================================================================
Standard_Boolean RWStl::WriteBinary (const Handle(StlMesh_Mesh)& theMesh,
                                     const OSD_Path& thePath,
                                     const Handle(Message_ProgressIndicator)& theProgInd)
{
  OSD_File aFile (thePath);
  aFile.Build (OSD_WriteOnly, OSD_Protection());

  Standard_Real x1, y1, z1;
  Standard_Real x2, y2, z2;
  Standard_Real x3, y3, z3;

  // writing 80 bytes of the trash?
  char sval[80];
  aFile.Write ((Standard_Address)sval,80);
  WriteInteger (aFile, theMesh->NbTriangles());

  int dum=0;
  StlMesh_MeshExplorer aMexp (theMesh);

  // create progress sentry for domains
  Standard_Integer aNbDomains = theMesh->NbDomains();
  Message_ProgressSentry aDPS (theProgInd, "Mesh domains", 0, aNbDomains, 1);
  for (Standard_Integer nbd = 1; nbd <= aNbDomains && aDPS.More(); nbd++, aDPS.Next())
  {
    // create progress sentry for triangles in domain
    Message_ProgressSentry aTPS (theProgInd, "Triangles", 0,
        theMesh->NbTriangles (nbd), IND_THRESHOLD);
    Standard_Integer aTriangleInd = 0;
    for (aMexp.InitTriangle (nbd); aMexp.MoreTriangle(); aMexp.NextTriangle())
    {
      aMexp.TriangleVertices (x1,y1,z1,x2,y2,z2,x3,y3,z3);
      //pgo      aMexp.TriangleOrientation (x,y,z);
      gp_XYZ Vect12 ((x2-x1), (y2-y1), (z2-z1));
      gp_XYZ Vect13 ((x3-x1), (y3-y1), (z3-z1));
      gp_XYZ Vnorm = Vect12 ^ Vect13;
      Standard_Real Vmodul = Vnorm.Modulus ();
      if (Vmodul > gp::Resolution())
      {
        Vnorm.Divide(Vmodul);
      }
      else
      {
        // si Vnorm est quasi-nul, on le charge a 0 explicitement
        Vnorm.SetCoord (0., 0., 0.);
      }

      WriteDouble2Float (aFile, Vnorm.X());
      WriteDouble2Float (aFile, Vnorm.Y());
      WriteDouble2Float (aFile, Vnorm.Z());

      WriteDouble2Float (aFile, x1);
      WriteDouble2Float (aFile, y1);
      WriteDouble2Float (aFile, z1);

      WriteDouble2Float (aFile, x2);
      WriteDouble2Float (aFile, y2);
      WriteDouble2Float (aFile, z2);

      WriteDouble2Float (aFile, x3);
      WriteDouble2Float (aFile, y3);
      WriteDouble2Float (aFile, z3);

      aFile.Write (&dum, 2);

      // update progress only per 1k triangles
      if (++aTriangleInd % IND_THRESHOLD == 0)
      {
        if (!aTPS.More())
          break;
        aTPS.Next();
      }
    }
  }
  aFile.Close();
  Standard_Boolean isInterrupted = !aDPS.More();
  return !isInterrupted;
}

这种方式渲染的图形效果如下图所示:

Figure 2.2 A typical sphere made up of triangles

上面的球面是由三角形组成,由OpenCascade的三角剖分算法生成。如果将每个三角面的法向作为每个顶点的法向,则渲染效果如下图所示:

Figure 2.3 Specific the triangle face normal as the vertex normal of the trangle

如上图所示,在光照效果下每个三角面界限分明,感觉不是很光滑,面之间的过渡很生硬。

三、OpenSceneGraph中面的法向计算 Finding Normal for OpenSceneGraph Mesh

直接将网格顶点的法向设置成三角面的法向产生的效果不是很理想,通过改变顶点法向的方向可以让曲面更滑,这种技术称为法向平均(Normal Averaging)。利用法向平均技术可以产生一些有意思的视觉效果。如果有个面像下面图所示:

Figure 3.1 Jagged surface with the usual surface normals

当我们考虑两个相连面的顶点处的法向为两相连面的法向的平均值时,那么这两个相连表面的连接处在OpenGL中渲染时看上去就不那么棱角分明了,如下图所示:

Figure 3.2 Averaging the normals will make sharp corners appear softer

对于球面或更一般的自由曲面,法向平均的算法也是适用的。如下图所示:

Figure 3.3 An approximation with normals perpendicular to each face

Figure 3.4 Each normal is perpendicular to the surface itself

球面的法向计算还是相当简单的。但是对于一般的曲面就不是那么容易了。这种情况下需要计算多边形面片相连处的顶点的法向,将相连接处的顶点的法向设置为各相邻面的平均法向后,视觉效果还是很棒的,光滑。

The actual normal you assign to that vertex is the average of these normals. The visual effect is a nice, smooth, regular surface, even though it is actually composed of numerous small, flat segments.

在OpenSceneGraph中生成顶点法向量的类是osgUtil::SmoothingVisitor,它使用了Visitor的模式,通过遍历场景中的几何体,生成顶点的法向量。对于上面同一个球的网格,使用osgUtil::SmoothingVisitor生成法向后在光照下的显示效果如下图所示:

Figure 3.5 Use osgUtil::SmoothingVisitor to generate normals for the sphere

四、计算正确的法向 Finding the Correct Normal for the Face

不管是STL中三角面的法向还是使用osgUtil::SmoothingVisitor来生成面的法向都是无奈之举,因为都是在离散的三角网格上找出法向,不精确,在光照下渲染效果都不是很理想。但是OpenCascade作为几何造型内核,提供了计算曲面法向的功能,因此有能力计算出顶点处的法向的精确值。

当计算网格曲面顶点的法向时,共享顶点处的法向最好设置为顶点各相连面的法向的平均值。对于参数化的曲面,是可以直接计算出每个顶点处的法向,就不需要再求法向平均值了,因为已经有了曲面法向数学定义的值。所以在OpenCascade中计算出来曲面中某个顶点的法向就是数学定义上面的法向。计算方法如下:

对顶点处的参数u,v分别求一阶导数,得出顶点处在u,v方向的切向量,如下图所示:

Figure 4.1 Derivatives with respect to u and v 

Figure 4.1 Tangents on a surface

将u和v方向的切向量叉乘就得到了该顶点处的法向,计算方法如下所示:

叉乘后顶点处的法向如下图所示:

Figure 4.2 Normal on a surface

OpenCascade中计算曲面表面属性的类是BRepLProp_SLProps,计算法向部分程序如下所示:

 

Standard_Boolean LProp_SLProps::IsNormalDefined()
{

  if (normalStatus == LProp_Undefined) {
    return Standard_False;
  }
  else if (normalStatus >= LProp_Defined) {
    return Standard_True;
  }
 
  // first try the standard computation of the normal.
  CSLib_DerivativeStatus Status;
  CSLib::Normal(d1U, d1V, linTol, Status, normal);
  if (Status  == CSLib_Done ) {
    normalStatus = LProp_Computed;
    return Standard_True;
  }
 
  normalStatus = LProp_Undefined;
  return Standard_False;
}

此类的使用方法如下所示:

 

const TopoDS_Face& theFace = TopoDS::Face(faceExp.Current());
BRepLProp_SLProps theProp(BRepAdaptor_Surface(theFace), 1, Precision::Confusion());

theProp.SetParameters(u, v);

if (theProp.IsNormalDefined())
{
    gp_Vec theNormal = theProp.Normal();
}

计算法向后渲染效果如下图所示:

Figure 4.3 Sphere vertex normals computed by BRepLProp_SLProps

由图可知,OpenCascade计算的面的法向在渲染时效果很好。

五、程序示例 Putting It All Together

将这三种情况产生的渲染效果放在一起来比较,程序代码如下所示:

 

/*
*    Copyright (c) 2014 eryar All Rights Reserved.
*
*           File : Main.cpp
*         Author : eryar@163.com
*           Date : 2014-02-25 17:00
*        Version : 1.0v
*
*    Description : Learn the Normal Averaging from OpenGL SuperBible.
*
*      Key Words : OpenCascade, OpenSceneGraph, Normal Averaging
*                 
*/

// OpenCascade library.
#define WNT
#include <Poly_Triangulation.hxx>
#include <TColgp_Array1OfPnt2d.hxx>

#include <TopoDS.hxx>
#include <TopoDS_Face.hxx>
#include <TopoDS_Shape.hxx>
#include <TopExp_Explorer.hxx>

#include <BRep_Tool.hxx>
#include <BRepAdaptor_Surface.hxx>
#include <BRepLProp_SLProps.hxx>

#include <BRepMesh.hxx>

#include <BRepPrimAPI_MakeBox.hxx>
#include <BRepPrimAPI_MakeCone.hxx>
#include <BRepPrimAPI_MakeSphere.hxx>

#pragma comment(lib, "TKernel.lib")
#pragma comment(lib, "TKMath.lib")
#pragma comment(lib, "TKG3d.lib")
#pragma comment(lib, "TKBRep.lib")
#pragma comment(lib, "TKMesh.lib")
#pragma comment(lib, "TKPrim.lib")
#pragma comment(lib, "TKTopAlgo.lib")

// OpenSceneGraph library.
#include <osg/MatrixTransform>
#include <osg/Material>

#include <osgGA/StateSetManipulator>

#include <osgViewer/Viewer>
#include <osgViewer/ViewerEventHandlers>

#include <osgUtil/SmoothingVisitor>

#pragma comment(lib, "osgd.lib")
#pragma comment(lib, "osgDBd.lib")
#pragma comment(lib, "osgGAd.lib")
#pragma comment(lib, "osgUtild.lib")
#pragma comment(lib, "osgViewerd.lib")
#pragma comment(lib, "osgManipulatord.lib")

/**
* @breif Build the mesh for the OpenCascade TopoDS_Shape.
* @param [in] TopoDS_Shape theShape OpenCascade TopoDS_Shape.
* @param [in] Standard_Boolean bSetNormal If set to true, will set the vertex normal correctly
*             else will set vertex normal by its triangle face normal.
*/
osg::Geode* BuildMesh(const TopoDS_Shape& theShape, Standard_Boolean bSetNormal = Standard_False)
{
    Standard_Real theDeflection = 0.1;
    BRepMesh::Mesh(theShape, theDeflection);

    osg::ref_ptr<osg::Geode> theGeode = new osg::Geode();

    for (TopExp_Explorer faceExp(theShape, TopAbs_FACE); faceExp.More(); faceExp.Next())
    {
        TopLoc_Location theLocation;
        const TopoDS_Face& theFace = TopoDS::Face(faceExp.Current());
        const Handle_Poly_Triangulation& theTriangulation = BRep_Tool::Triangulation(theFace, theLocation);
        BRepLProp_SLProps theProp(BRepAdaptor_Surface(theFace), 1, Precision::Confusion());

        if (theTriangulation.IsNull())
        {
            continue;
        }

        osg::ref_ptr<osg::Geometry> theMesh = new osg::Geometry();
        osg::ref_ptr<osg::Vec3Array> theVertices = new osg::Vec3Array();
        osg::ref_ptr<osg::Vec3Array> theNormals = new osg::Vec3Array();

        for (Standard_Integer t = 1; t <= theTriangulation->NbTriangles(); ++t)
        {
            const Poly_Triangle& theTriangle = theTriangulation->Triangles().Value(t);
            gp_Pnt theVertex1 = theTriangulation->Nodes().Value(theTriangle(1));
            gp_Pnt theVertex2 = theTriangulation->Nodes().Value(theTriangle(2));
            gp_Pnt theVertex3 = theTriangulation->Nodes().Value(theTriangle(3));

            gp_Pnt2d theUV1 = theTriangulation->UVNodes().Value(theTriangle(1));
            gp_Pnt2d theUV2 = theTriangulation->UVNodes().Value(theTriangle(2));
            gp_Pnt2d theUV3 = theTriangulation->UVNodes().Value(theTriangle(3));

            theVertex1.Transform(theLocation.Transformation());
            theVertex2.Transform(theLocation.Transformation());
            theVertex3.Transform(theLocation.Transformation());

            // find the normal for the triangle mesh.
            gp_Vec V12(theVertex1, theVertex2);
            gp_Vec V13(theVertex1, theVertex3);
            gp_Vec theNormal = V12 ^ V13;
            gp_Vec theNormal1 = theNormal;
            gp_Vec theNormal2 = theNormal;
            gp_Vec theNormal3 = theNormal;

            if (theNormal.Magnitude() > Precision::Confusion())
            {
                theNormal.Normalize();
                theNormal1.Normalize();
                theNormal2.Normalize();
                theNormal3.Normalize();
            }

            theProp.SetParameters(theUV1.X(), theUV1.Y());
            if (theProp.IsNormalDefined())
            {
                theNormal1 = theProp.Normal();
            }

            theProp.SetParameters(theUV2.X(), theUV2.Y());
            if (theProp.IsNormalDefined())
            {
                theNormal2 = theProp.Normal();
            }

            theProp.SetParameters(theUV3.X(), theUV3.Y());
            if (theProp.IsNormalDefined())
            {
                theNormal3 = theProp.Normal();
            }

            if (theFace.Orientation() == TopAbs_REVERSED)
            {
                theNormal.Reverse();
                theNormal1.Reverse();
                theNormal2.Reverse();
                theNormal3.Reverse();
            }

            theVertices->push_back(osg::Vec3(theVertex1.X(), theVertex1.Y(), theVertex1.Z()));
            theVertices->push_back(osg::Vec3(theVertex2.X(), theVertex2.Y(), theVertex2.Z()));
            theVertices->push_back(osg::Vec3(theVertex3.X(), theVertex3.Y(), theVertex3.Z()));

            if (bSetNormal)
            {
                theNormals->push_back(osg::Vec3(theNormal1.X(), theNormal1.Y(), theNormal1.Z()));
                theNormals->push_back(osg::Vec3(theNormal2.X(), theNormal2.Y(), theNormal2.Z()));
                theNormals->push_back(osg::Vec3(theNormal3.X(), theNormal3.Y(), theNormal3.Z()));
            }
            else
            {
                theNormals->push_back(osg::Vec3(theNormal.X(), theNormal.Y(), theNormal.Z()));
                theNormals->push_back(osg::Vec3(theNormal.X(), theNormal.Y(), theNormal.Z()));
                theNormals->push_back(osg::Vec3(theNormal.X(), theNormal.Y(), theNormal.Z()));
            }
        }

        theMesh->setVertexArray(theVertices);
        theMesh->setNormalArray(theNormals);
        theMesh->setNormalBinding(osg::Geometry::BIND_PER_VERTEX);
        theMesh->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, theVertices->size()));

        theGeode->addDrawable(theMesh);
    }

    // Set material for the mesh.
    osg::ref_ptr<osg::StateSet> theStateSet = theGeode->getOrCreateStateSet();
    osg::ref_ptr<osg::Material> theMaterial = new osg::Material();

    theMaterial->setDiffuse(osg::Material::FRONT, osg::Vec4(1.0f, 0.0f, 0.0f, 1.0f));
    theMaterial->setSpecular(osg::Material::FRONT, osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
    theMaterial->setShininess(osg::Material::FRONT, 100.0f);

    theStateSet->setAttribute(theMaterial);

    return theGeode.release();
}

osg::Node* BuildScene(void)
{
    osg::ref_ptr<osg::Group> theRoot = new osg::Group();

    // 1. Build a sphere without setting vertex normal correctly.
    TopoDS_Shape theSphere = BRepPrimAPI_MakeSphere(1.6);
    osg::ref_ptr<osg::Node> theSphereNode = BuildMesh(theSphere);
    theRoot->addChild(theSphereNode);

    // 2. Build a sphere without setting vertex normal correctly, but will
    // use osgUtil::SmoothingVisitor to find the average normals.
    osg::ref_ptr<osg::MatrixTransform> theSmoothSphere = new osg::MatrixTransform();
    osg::ref_ptr<osg::Geode> theSphereGeode = BuildMesh(theSphere);
    theSmoothSphere->setMatrix(osg::Matrix::translate(5.0, 0.0, 0.0));

    // Use SmoothingVisitor to find the vertex average normals.
    osgUtil::SmoothingVisitor sv;
    sv.apply(*theSphereGeode);

    theSmoothSphere->addChild(theSphereGeode);
    theRoot->addChild(theSmoothSphere);

    // 3. Build a sphere with setting vertex normal correctly.
    osg::ref_ptr<osg::MatrixTransform> theBetterSphere = new osg::MatrixTransform();
    osg::ref_ptr<osg::Geode> theSphereGeode1 = BuildMesh(theSphere, Standard_True);
    theBetterSphere->setMatrix(osg::Matrix::translate(10.0, 0.0, 0.0));

    theBetterSphere->addChild(theSphereGeode1);
    theRoot->addChild(theBetterSphere);

    return theRoot.release();
}

int main(int argc, char* argv[])
{
    osgViewer::Viewer viewer;   

    viewer.setSceneData(BuildScene());

    viewer.addEventHandler(new osgViewer::StatsHandler());
    viewer.addEventHandler(new osgViewer::WindowSizeHandler());
    viewer.addEventHandler(new osgGA::StateSetManipulator(viewer.getCamera()->getOrCreateStateSet()));

    return viewer.run();
}

生成效果图如下所示:

Figure 5.1 Same sphere triangulation mesh

Figure 5.2 Same sphere mesh with different vertex normals

由上图可知,相同的球面网格,当顶点的法向为三角面的法向时,在有光照的情况下,渲染效果最差。使用osgUtil::SmoothingVisitor法向生成算法生成的顶点法向与使用类BRepLProp_SLProps计算出的法向,在光照情况下显示效果相当。

Figure 5.3 Pipe and equipments with correct vertex normals

六、结论 Conclusion

正确设置网格面顶点的法向可以在光照环境中看上去更光滑真实。利用法向平均算法或使用曲面的参数方程求解曲面顶点上法向,可以在满足显示效果基本相同的条件下减少网格顶点的数量,可以提高渲染速度。

七、参考资料 References

1. Waite group Press, OpenGL Super Bible(1st), Macmillan Computer Publishing, 1996

2. Richard S. Wright Jr., Benjamin Lipchak, OpenGL SuperBible(3rd), Sams Publishing, 2004

3. vsocc.cpp in netgen

4. Kelly Dempski, Focus on Curves and Surfaces, Premier Press, 2003

5. 王锐,钱学雷,OpenSceneGraph三维渲染引擎设计与实践,清华大学出版社

6. 肖鹏,刘更代,徐明亮,OpenSceneGraph三维渲染引擎编程指南,清华大学出版社

 

PDF Version: Surface Normal Averaging

时间: 2024-09-19 08:51:31

Surface Normal Averaging的相关文章

Surface Normal Vector in OpenCascade

Surface Normal Vector in OpenCascade eryar@163.com 摘要Abstract:表面上某一点的法向量(Normal Vector)指的是在该点处与表面垂直的方向.对于平面,其上各点的法向是一样的,统一为这个平面的法向.对于曲面,各点具有不同的法向量.几何对象的法向量定义了它在空间中的方向,法向量是在进行光照处理时的重要参数.所以在显示造型算法离散曲面后的网格时,设置正确的法向量对场景的光照.光线追踪效果有直接影响.本文结合OpenCascade中代码,

OpenCASCADE Outline

OpenCASCADE Outline eryar@163.com      有网友反映blog中关于OpenCASCADE的文章比较杂乱,不太好找,最好能提供一个大纲,这样方便查找.于是决定将这些学习时写的文章整理下,方便对OpenCASCADE的学习理解.其实在http://www.cnblogs.com/opencascade中,已经将文章按目录重新发表了一遍.可以按OpenCASCADE的模块的顺序来学习,也可以挑选自己感兴趣的部分来学习.      由于本人水平所限,文中的错误不妥之处

3D编程:第九章 Normal Mapping and Displacement Mapping

第九章 Normal Mapping and Displacement Mapping 本章主要讲述两种图形学技术,支持在不增加objects的poly primitive的情况下,在场景中增加更多的细节.第一种是normal mapping,通过创建一些"fake" geometry(虚设的多边形图元)模拟光照作用.第二种是displacement mapping,根据纹理数据moving vertices actually(与"fake"相对应,这里指真实的移动

OpenGL ES From the Ground Up, Part 4: Let There Be Light!

FRIDAY, MAY 1, 2009 OpenGL ES From the Ground Up, Part 4: Let There Be Light! Continuing on with OpenGL ES for the iPhone, let's talk about light. So far, we haven't done anything with light. Fortunately, OpenGL still lets us see what's going on if w

3D编程:Diffuse Lighting(漫反射光)

Diffuse Lighting(漫反射光) 不同的材质表面反射光的方式也不同.在镜面上光的反射角度与入身角度相等.当在一只猫的眼睛里看到一束怪异的光芒,这就是光的反射性:这是由于猫的眼睛反射光的方向与光源的照射方向平行,但是方向相反.漫反射表面对光的反射在各个方向上都一样. 近似计算一个漫反射光,最简单并且最常用的模型是Lambert's cosine law(朗伯余弦定律).根据Lambert's cosine law,照射到材质表面的光照亮度,与光源方向向量和面法线的夹角的余弦成正比.光源

Computing Tangent Space Basis Vectors for an Arbitrary Mesh

为一个任意网格模型计算其切线空间的基本向量(即切线空间的T B N三个向量) Modern bump mapping (also known as normal mapping) requires that tangent plane basis vectors be calculated for each vertex in a mesh. This article presents the theory behind the computation of per-vertex tangent

Reflect &amp; Refract (以水渲染为例)

我不是Shader帝,虽然知道Shader怎么写,但一直没仔细研究过.最近蛋疼至极,研究了下RenderMonkey,于是抽着几个看着比较有趣的效果做了一下.   先前的模型贴花http://www.cppblog.com/Leaf/archive/2011/01/07/138093.html 和CUBE MAP http://www.cppblog.com/Leaf/archive/2011/01/07/138106.html 就是此次蛋疼期的产物之一.   还是先围观,上图再说   本次要蛋

1.9. 坐标转换 Coordinate Transforms

1.9. 坐标转换 Coordinate Transforms The purpose of the OpenGL graphics processing pipeline is to convert threedimensional descriptions of objects into a two-dimensional image that can be displayed. In many ways, this process is similar to using a camera

Topology and Geometry in OpenCascade-Face

Topology and Geometry in OpenCascade-Face eryar@163.com 摘要Abstract:本文简要介绍了几何造型中的边界表示法(BRep),并结合程序说明OpenCascade中的边界表示的具体实现,即拓朴与几何的联系.对具有几何信息的拓朴结构顶点(vertex).边(edge).面(face)进行了详细说明.本文仅对面(Face)进行说明. 关键字Key Words:OpenCascade.BRep.Topology.Geometry.Face 一.