Surface Normal Averaging
eryar@163.com
摘要Abstract:正確設置網(wǎng)格面上點的法向,對幾何體在光照等情況下顯示得更真實,這樣就可以減少頂點數(shù)量,提高渲染速度。本文通過將OpenCascade中的形狀離散成網(wǎng)格數(shù)據(jù)后在OpenSceneGraph中顯示,及使用OSG的快速法向osgUtil::SmoothingVisitor優(yōu)化與使用OpenCascade來計算正確的法向的結果的對比,說明面法向量的重要性。
關鍵字Key Words:OpenCascade, OpenSceneGraph, Normal Averaging, Triangulation Mesh
一、引言 Introduction
OpenGL中的頂點(Vertex)不是一個值,而由其空間坐標值、法向、顏色坐標、紋理坐標、霧坐標等所組成的一個集合。一個最基本的幾何體對象至少需要設置一個合法的頂點數(shù)組,并記錄頂點數(shù)據(jù);如有必要,還可以設置顏色數(shù)組、法線數(shù)組、紋理坐標數(shù)組等多種信息。
在很多應用中,網(wǎng)格上的各點都需要一個表面法向量,它的作用非常廣泛。例如可用來計算光照、背面剔除、模擬粒子系統(tǒng)在表面的“彈跳”效果、通過只需要正面而加速碰撞檢測等。
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格式的文件進行模型的數(shù)據(jù)交換。其中STL結構中只保存了三角網(wǎng)格的頂點坐標和三角面的法向量。為了將拓樸數(shù)據(jù)轉換成STL的網(wǎng)格數(shù)據(jù),先將拓樸形狀進行三角剖分,再將剖分的網(wǎng)格保存成STL即可。其中每個三角面的法向計算也是直接根據(jù)兩個向量的叉乘得來。
Figure 2.1 A normal vector as cross product of two vectors
實現(xiàn)文件是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
直接將網(wǎng)格頂點的法向設置成三角面的法向產(chǎn)生的效果不是很理想,通過改變頂點法向的方向可以讓曲面更滑,這種技術稱為法向平均(Normal Averaging)。利用法向平均技術可以產(chǎn)生一些有意思的視覺效果。如果有個面像下面圖所示:
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的模式,通過遍歷場景中的幾何體,生成頂點的法向量。對于上面同一個球的網(wǎng)格,使用osgUtil::SmoothingVisitor生成法向后在光照下的顯示效果如下圖所示:
Figure 3.5 Use osgUtil::SmoothingVisitor to generate normals for the sphere
四、計算正確的法向 Finding the Correct Normal for the Face
不管是STL中三角面的法向還是使用osgUtil::SmoothingVisitor來生成面的法向都是無奈之舉,因為都是在離散的三角網(wǎng)格上找出法向,不精確,在光照下渲染效果都不是很理想。但是OpenCascade作為幾何造型內核,提供了計算曲面法向的功能,因此有能力計算出頂點處的法向的精確值。
當計算網(wǎng)格曲面頂點的法向時,共享頂點處的法向最好設置為頂點各相連面的法向的平均值。對于參數(shù)化的曲面,是可以直接計算出每個頂點處的法向,就不需要再求法向平均值了,因為已經(jīng)有了曲面法向數(shù)學定義的值。所以在OpenCascade中計算出來曲面中某個頂點的法向就是數(shù)學定義上面的法向。計算方法如下:
對頂點處的參數(shù)u,v分別求一階導數(shù),得出頂點處在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
將這三種情況產(chǎn)生的渲染效果放在一起來比較,程序代碼如下所示:
/*
* 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
由上圖可知,相同的球面網(wǎng)格,當頂點的法向為三角面的法向時,在有光照的情況下,渲染效果最差。使用osgUtil::SmoothingVisitor法向生成算法生成的頂點法向與使用類BRepLProp_SLProps計算出的法向,在光照情況下顯示效果相當。
Figure 5.3 Pipe and equipments with correct vertex normals
六、結論 Conclusion
正確設置網(wǎng)格面頂點的法向可以在光照環(huán)境中看上去更光滑真實。利用法向平均算法或使用曲面的參數(shù)方程求解曲面頂點上法向,可以在滿足顯示效果基本相同的條件下減少網(wǎng)格頂點的數(shù)量,可以提高渲染速度。
七、參考資料 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三維渲染引擎編程指南,清華大學出版社