Surface Normal Averaging
Surface Normal Averaging
摘要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三维渲染引擎编程指南,清华大学出版社
PDF Version: Surface Normal Averaging
Surface Normal Averaging的更多相关文章
- Surface Normal Vector in OpenCascade
Surface Normal Vector in OpenCascade eryar@163.com 摘要Abstract:表面上某一点的法向量(Normal Vector)指的是在该点处与表面垂直的 ...
- unity, 让主角头顶朝向等于地面法线(character align to surface normal)
计算过程如下: 1,通过由主角中心raycast一条竖直射线获得主角所在处地面法线,用作主角的newUp. 注:一定要从主角中心raycast,而不要从player.transform.positio ...
- OpenCASCADE Face Normals
OpenCASCADE Face Normals eryar@163.com Abstract. 要显示一个逼真的三维模型,其顶点坐标.顶点法向.纹理坐标这三个信息必不可少.本文主要介绍如何在Open ...
- 39. Volume Rendering Techniques
Milan Ikits University of Utah Joe Kniss University of Utah Aaron Lefohn University of California, D ...
- DirectX 总结和DirectX 9.0 学习笔记
转自:http://www.cnblogs.com/graphics/archive/2009/11/25/1583682.html DirectX 总结 DDS DirectXDraw Surfac ...
- Game Engine Architecture 10
[Game Engine Architecture 10] 1.Full-Screen Antialiasing (FSAA) also known as super-sampled antialia ...
- ECCV 2014 Results (16 Jun, 2014) 结果已出
Accepted Papers Title Primary Subject Area ID 3D computer vision 93 UPnP: An optimal O(n) soluti ...
- cvpr2015papers
@http://www-cs-faculty.stanford.edu/people/karpathy/cvpr2015papers/ CVPR 2015 papers (in nicer forma ...
- Official Program for CVPR 2015
From: http://www.pamitc.org/cvpr15/program.php Official Program for CVPR 2015 Monday, June 8 8:30am ...
随机推荐
- Django 中 如何使用 settings.py 中的常量
在用django 框架开发 python web 程序的时候 , 在模板页面经常会用到 settings.py 中设置的常量,比如MEDIA_URL, 我尝试过在模板页面用类似如下的方式 程序代码 { ...
- ARCgis已知线裁剪已知面
经常遇到需要在ArcGIS中,根据已知线图层(要素)切分已知面图层(要素).经过研究,利用topology拓扑菜单中的construct features可以实现.具体如下 现有用线图层A.面图层B, ...
- C++函数返回局部指针变量
遇到过好几次关于函数返回指针变量问题,有时候是可以的,有时候是不可以的,然后就混乱了.今天研究了下,结果发现原来和内存分配有关. 用下面的例子分析下吧: char * test() { char a[ ...
- Async/Await FAQ
From time to time, I receive questions from developers which highlight either a need for more inform ...
- C# 装箱和拆箱的简单理解
一.装箱拆箱的意义 主要用途是可以向ArrayList中添加值类型的元素 二.理解 装箱的含义:理解为可以将子类对象隐式的转化为父类对象(保留自己特有的成员,和子类重写的成员) 装箱:例子为典型的装箱 ...
- sql查询重复记录、删除重复记录方法大全
查找所有重复标题的记录:SELECT *FROM t_info aWHERE ((SELECT COUNT(*)FROM t_infoWHERE Title = a.Title) > 1)ORD ...
- python 之readability与BeautifulSoup
以前要采集某个网页,一般做法是写程序源代码爬出来,然后用正则去匹配出来,这种针对指定的网页去爬效果还可以,但是如果是批量的网页这种实现就会变得不现实,在这时候就有readability出手的意义了,r ...
- mongodb(mongoose-redis-cache)
在传统的项目中,我们经常会用到缓存来优化数据库的读取,比如java中,我们利用spring的AOP能力,在读写数据库前增加对缓存的操作. 在node与mongodb的项目中也仍然会存在类似问题,本文参 ...
- 【情人节来一发】网站添加QQ客服功能
今年的元宵节遇到情人节,挺不自量力的,呵呵,开篇给各位讲个段子,早上一美女同学在空间发说说道:“开工大吉 起床啦,卖元宵,卖玫瑰,卖避孕套啦-有木有一起去发财的小伙伴?Let’s go…”,对于此种长 ...
- 用“MEAN”技术栈开发web应用(一)AngularJs前端架构
前言 不知何时突然冒出“MEAN技术栈”这个新词,听起来很牛逼的样子,其实就是我们已经熟悉了的近两年在前端比较流行的技术,mongodb.express.angularjs.nodejs,由于这几项技 ...