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[];
aFile.Write ((Standard_Address)sval,);
WriteInteger (aFile, theMesh->NbTriangles()); int dum=;
StlMesh_MeshExplorer aMexp (theMesh); // create progress sentry for domains
Standard_Integer aNbDomains = theMesh->NbDomains();
Message_ProgressSentry aDPS (theProgInd, "Mesh domains", , aNbDomains, );
for (Standard_Integer nbd = ; nbd <= aNbDomains && aDPS.More(); nbd++, aDPS.Next())
{
// create progress sentry for triangles in domain
Message_ProgressSentry aTPS (theProgInd, "Triangles", ,
theMesh->NbTriangles (nbd), IND_THRESHOLD);
Standard_Integer aTriangleInd = ;
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 (., ., .);
} 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, ); // update progress only per 1k triangles
if (++aTriangleInd % IND_THRESHOLD == )
{
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), , 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), , 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 = ; t <= theTriangulation->NbTriangles(); ++t)
{
const Poly_Triangle& theTriangle = theTriangulation->Triangles().Value(t);
gp_Pnt theVertex1 = theTriangulation->Nodes().Value(theTriangle());
gp_Pnt theVertex2 = theTriangulation->Nodes().Value(theTriangle());
gp_Pnt theVertex3 = theTriangulation->Nodes().Value(theTriangle()); gp_Pnt2d theUV1 = theTriangulation->UVNodes().Value(theTriangle());
gp_Pnt2d theUV2 = theTriangulation->UVNodes().Value(theTriangle());
gp_Pnt2d theUV3 = theTriangulation->UVNodes().Value(theTriangle()); 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, , 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三维渲染引擎编程指南,清华大学出版社

Surface Normal Averaging的更多相关文章

  1. Surface Normal Vector in OpenCascade

    Surface Normal Vector in OpenCascade eryar@163.com 摘要Abstract:表面上某一点的法向量(Normal Vector)指的是在该点处与表面垂直的 ...

  2. unity, 让主角头顶朝向等于地面法线(character align to surface normal)

    计算过程如下: 1,通过由主角中心raycast一条竖直射线获得主角所在处地面法线,用作主角的newUp. 注:一定要从主角中心raycast,而不要从player.transform.positio ...

  3. OpenCASCADE Face Normals

    OpenCASCADE Face Normals eryar@163.com Abstract. 要显示一个逼真的三维模型,其顶点坐标.顶点法向.纹理坐标这三个信息必不可少.本文主要介绍如何在Open ...

  4. 39. Volume Rendering Techniques

    Milan Ikits University of Utah Joe Kniss University of Utah Aaron Lefohn University of California, D ...

  5. DirectX 总结和DirectX 9.0 学习笔记

    转自:http://www.cnblogs.com/graphics/archive/2009/11/25/1583682.html DirectX 总结 DDS DirectXDraw Surfac ...

  6. Game Engine Architecture 10

    [Game Engine Architecture 10] 1.Full-Screen Antialiasing (FSAA) also known as super-sampled antialia ...

  7. ECCV 2014 Results (16 Jun, 2014) 结果已出

    Accepted Papers     Title Primary Subject Area ID 3D computer vision 93 UPnP: An optimal O(n) soluti ...

  8. cvpr2015papers

    @http://www-cs-faculty.stanford.edu/people/karpathy/cvpr2015papers/ CVPR 2015 papers (in nicer forma ...

  9. Official Program for CVPR 2015

    From:  http://www.pamitc.org/cvpr15/program.php Official Program for CVPR 2015 Monday, June 8 8:30am ...

随机推荐

  1. 初探Lambda表达式

    简单例子 Expression<Func<; 了解Net方法,没有比IL来得更加容易.反编译IL代码如下(截取部分显示) [] <<int32, bool>> ex ...

  2. IDEA 创建Maven Web项目(图文版)

    前言:IDEA作为一款广泛使用的开发工具,无论是后台人员,还是前段工作者,都能在它上面发现它的魅力. IDEA提供了诸多项目模板,今天就以创建Maven Web项目作为示例,和大家一起分享: 第一步: ...

  3. 连接MySQL数据库(android、php、MySQL)

    管理MySQL数据库最简单和最便利的方式是PHP脚本.运行PHP脚本使用HTTP协议和android系统连接.我们以JSON格式编码数据,因为Android和PHP都有现成的处理JSON函数. 下面示 ...

  4. caffe 在window下编译(windows7, cuda8.0,matlab接口编译)

    1. 环境:Windows7,Cuda8.0,显卡GTX1080,Matlab2016a,VS2013 (ps:老板说服务器要装windows系统,没办法,又要折腾一番,在VS下编译好像在cuda8. ...

  5. C# - JSON详解

    最近在做微信开发时用到了一些json的问题,就是把微信返回回来的一些json数据做一些处理,但是之前json掌握的不好,浪费了好多时间在查找一些json有关的转换问题,我所知道的方法只有把json序列 ...

  6. 让Entity Framework启动不再效验__MigrationHistory表

    Entity Framework中DbContext首次加载OnModelCreating会检查__MigrationHistory表,作为使用Code Frist编程模式,而实际先有数据库时,这种检 ...

  7. 利用IIS应用请求转发ARR实现IIS和tomcat整合共用80端口

    现在网上流传的实现iis和tomcat共享80端口的方法是基于isapi_redirect插件实现的, 我的实现方法不同, 原理相似,具有更好的优点. 先说下基于isapi_redirect缺点,ja ...

  8. Blend 2015 教程 (一) 基础

    微软公司在Visual Studio 2015产品套件中作出了许多革命性的变更,包括.NET开源,.NET服务器端部分跨平台,推出向个人和小团队免费的社区版,移动应用开发部分跨平台支持,商店应用支持C ...

  9. Fedora23Server配置

    系统准备 启动网卡: sudo service network start 更新系统: sudo dnf update 远程管理: https://IP:9090/ Dnf使用: http://www ...

  10. android知识杂记(一)

    记录项目中用的零碎知识点,用以备忘. android:screenOrientation:portrait 限制横屏 activity启动状态 singleTop 只执行一次,通常用在欢迎页面 sin ...