本文是ZwqXin关于Shadow volume阴影锥技术实践记录的第三辑。固定管道之 Z-PASS 算法的最后一篇。本文着重接着第Ⅱ篇谈谈针对复杂模型的阴影生成的实现。继续接下来可能会从Z-FAIL算法或者shader实现方面继续探讨本系列。——ZwqXin.com 前两篇为:
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/shadow-volume-3.html
我们把上一篇最后的步骤再总结一次:
初始化步骤:ImportShadowModel()
- 1.用CLoad3DS载入模型(ImportModel)
- 2.把CLoad3DS中的Model结构,转化为CShadow3DS的ShadowModel结构
- 3.找邻面(边)(SetConnectivity)
- 4.Face To Plane转换
渲染实时步骤:RenderShadowModel()
- 1.转换光源到模型空间(所必要的逆矩阵操作)
- 2.渲染原Model
- 3.判断面的朝光性
- 4.蒙板判断,画Shadow Volume
- 5.渲染阴影
在CLoad3DS类里面,存储模型数据的是一个名为t3DModel的结构,我的CShadow3DS类要获得并扩充这个结构。其中通过CLoad3DS类的API:GetModel()直接取得这个模型。在一个读取3DS文件的类CLoad3DS浅析Ⅰ中说过,模型由一系列对象组成,每个对象由一系列三角面片组成 。而我需要扩充的是面信息,因此新建了一个结构sFace,并在与t3DObject对应的s3DObject结构(存储对象信息)中,把sFace类型的指针放进去。s3DObject完全是为了与CLoad3DS中的模型对象对应的。真正的主角是sFace,它相当于重写了CLoad3DS的tFace结构,用于存储“具阴影模型”的面信息:索引,邻面,平面方程参数结构和面的朝光性。
- struct sFace
- {
- struct EqrPlane
- {
- GLfloat a,b,c,d;
- }eqr;
- int vertIndex[3];
- int neigh_face[3];
- bool facelight;
- };
- struct s3DObject
- {
- int numofFaces;
- sFace *psFaces;
- };
ImportShadowModel()是新的初始化函数(客户端调用):
- void CShadow3DS::ImportShadowModel(GLuint Model_id, char *strFileName)
- {
- ImportModel(Model_id, strFileName); //步骤1,载入模型
- s3DObject newsobject= {0};
- //步骤2,这里是初始化shadow版本模型:遍历原模型各个对象,把该对象中有用的信息:面数,面索引等存入新结构s3DObject ,s3Face 中,并初始化各面的邻面索引为-1(无),非朝光,其余填充0
- for(vector<t3DObject>::iterator p_r = GetModel().pObject.begin(); p_r!=GetModel().pObject.end(); p_r++)
- {
- newsobject.numofFaces= p_r->numOfFaces;
- newsobject.psFaces = new sFace [newsobject.numofFaces];
- memset(newsobject.psFaces, 0, sizeof(sFace) * newsobject.numofFaces);
- for(int j = 0 ; j< newsobject.numofFaces; j++)
- for(int i = 0 ; i<3; i++)
- {
- newsobject.psFaces[j].vertIndex[i] = p_r->pFaces[j].vertIndex[i];
- newsobject.psFaces[j].neigh_face[i] = NO_NEIGH_FACE;
- }
- //把遍历中每次得到的新结构压入vector<s3DObject >psObject中,它能满足shadow版本模型的各种数据存储需求
- psObject.push_back(newsobject);
- }
- SetConnectivity();//步骤3,找邻面
- //步骤4,对每个面,进行Face to Plane的转换
- for(int iter_obj = 0; iter_obj < GetModel().numOfObjects; iter_obj++)
- for(int iter_face = 0 ; iter_face < GetModel().pObject[iter_obj].numOfFaces; iter_face++)
- FaceToPlane(iter_obj, iter_face);
- }
RenderShadowModel()是新的渲染函数(客户端调用):
- void CShadow3DS::RenderShadowModel()
- {
- ToModelCoord();//步骤1:转换光源到模型空间
- glLoadIdentity();//把当前矩阵作为单位矩阵(接下来的变换以此为基准)
- RenderModel();//步骤2:渲染原Model
- RenderShadow();//步骤345,判断并渲染阴影
- }
- //先看看步骤345:
- void CShadow3DS::RenderShadow()
- {
- ....//转换光源到模型空间必要的反变换,后述
- ShadowCast()
- {
- FaceLightJudging();//步骤3.找出每帧中哪些面具有朝光性
- .........//步骤4:接下来是蒙板值非0之判断,代码跟第一篇中三角形面片的ShadowCast()代码是一样的,功能也一样,只不过这里仅处理朝光的面,并且画ShadowVolume的方法(ShadowVolume() )不同
- ShadowVolume() ;//具体的画法思路见"第二篇-第2个问题"
- ..........
- //注意阴影锥各侧面(silhouette edge)分正面朝观察者与背面朝贯彻者观察者渲染两次.逆时针为正的保证由3DS模型自己提供(通过顶点索引)
- ShadowVolume() ;
- ........
- }
- }
再提醒一下:"逆时针为正"这个假设非常灰常重要(见"第二篇-第1,2个问题",两处)。
那么,还有一个也非常灰常重要的问题:把光源位置从视图空间转换入模型空间。这里并不是说改动实际光源位置(实际的光源还是由glLightPosition在视图空间确定),而是改动“它在模型空间”的位置,而且要保证这种转换不影响实际视图空间的任何其他东西!OPENGL中把模型空间转换成视图空间,需要在模型空间原矩阵的基础上,左乘 模视矩阵(ModelViewMatrix) ;因此,把现空间从视图空间转回模型空间,需要用 模视矩阵(ModelViewMatrix)的逆矩阵 左乘现存空间的矩阵——这样抵消后就能把一个物件转回模型空间,并可在模型空间操控物件了。重点是:模视矩阵(ModelViewMatrix)的逆矩阵。
怎么求逆呢?这里我得相当佩服NEHE,因为他在我参考的NEHE 27#中,并没有真正“求逆”,而是用了一个简单的技巧,花费极小的计算开销,就完成了光源的转换!最初我根本没有懂得他那串“calculate to local coordinate system”在干嘛,还特意在网上找了个可算逆矩阵(用的高斯消主元,虽然计算方法课程上学过,要我自己写出来还是为难- -)的矩阵类, 准备....但是我还是被NEHE那几句简短的代码折服了,最后宁愿花一整夜去领会去尝试,他的这种“ doing all the actions in reverse order”的方法。当中想明白其用意的契机是突然间脑海中闪起以前看的一些有关3D矩阵的片段,恍然大白!好了,具体的我想专门写篇文章来记录。这里只在代码中简短说说:
- void CShadow3DS::ToModelCoord()
- {
- CVector4D antiRotLight,antiTransLight;
- CMatrix16 current_mvmatrix;
- //分别保存MV矩阵的旋转部分(包括缩放)和移动部分
- antiRotLight = LightPos;
- antiTransLight.set(0,0,0,1);
- //把当前矩阵作为单位矩阵(接下来的变换以此为基准),glLoadIdentity()其实也在压栈
- glLoadIdentity();
- //根据CLoad3DS中的模型变换,作其反变换
- glRotatef(-GetRot().y,0,1,0);
- glRotatef(-GetRot().z,0,0,1);
- glRotatef(-GetRot().x,1,0,0);
- glRotatef(-1, 1, 0, 0);
- glScalef(1/GetSize(),1/GetSize(),1/GetSize());
- //存储旋转部分为:MV矩阵乘以光源实际坐标
- glGetFloatv(GL_MODELVIEW_MATRIX,current_mvmatrix.mt);
- antiRotLight = current_mvmatrix * antiRotLight;
- //继续
- glTranslatef(-GetPos().x, -GetPos().y, -GetPos().z);
- //存储平移部分为:MV矩阵乘以(0,0,0,1)
- glGetFloatv(GL_MODELVIEW_MATRIX,current_mvmatrix.mt);
- antiTransLight = current_mvmatrix * antiTransLight;
- //结合这两部分到mvLightPos:在模型空间和视图空间之间玩无间道的家伙
- mvLightPos.x = antiRotLight.x + antiTransLight.x;
- mvLightPos.y = antiRotLight.y + antiTransLight.y;
- mvLightPos.z = antiRotLight.z + antiTransLight.z;
- mvLightPos.w = 1.0;
- }
- //.......接下来在RenderShadow()中,不压栈地作正变换,使两者变换抵消,实际中因而只剩下RenderModel()中受glPushMatrix()保护的正变换,一切跟没做这些的时候一致,做这些处理只是为了得到上面那个mvLightPos
- //mvLightPos,应用于判断面朝光性判断和画ShadowVolume,它在模型空间中可以不断变化,但在视图空间(基于实际视觉)中永远与实际光源坐标重合,即相对于光源不动.(注意,模型在这个过程中与之相反.)
大致要说的就是这些.。程序中其他部分的小动作相信很容易看明白,就不多费舌头了。截图:
更详细截图,包括成功之前的,看这里:
Shadow Volume Demo2 -ZwqXin.com
下一篇见:Shadow Volume 阴影锥技术之探Ⅳ
放上本DEMO:ShadowVolumeDemo2byZwqxin.rar
按键:
→ ← ↑ ↓ PageUp 移动光源
鼠标左键下按不放并移动鼠标:旋转视角;鼠标滚轮移动
鼠标右键:自动旋转开关
空格:转变查看模式:正常模式/辅助可视阴影锥/光源射线模式
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/shadow-volume-3.html