« Shadow Volume 阴影锥技术之探ⅡShadow Volume Demo2 »

Shadow Volume 阴影锥技术之探Ⅲ

 本文是ZwqXin关于Shadow volume阴影锥技术实践记录的第三辑。固定管道之 Z-PASS 算法的最后一篇。本文着重接着第Ⅱ篇谈谈针对复杂模型的阴影生成的实现。继续接下来可能会从Z-FAIL算法或者shader实现方面继续探讨本系列。——ZwqXin.com  前两篇为:

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/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结构,用于存储“具阴影模型”的面信息:索引,邻面,平面方程参数结构和面的朝光性。

  1. struct sFace
  2. {
  3.   struct EqrPlane
  4.     {
  5.         GLfloat a,b,c,d;
  6.     }eqr;
  7.     int vertIndex[3]; 
  8.     int neigh_face[3];
  9.     bool facelight;
  10. };
  11.  
  12. struct s3DObject
  13. {
  14.    int numofFaces;
  15.    sFace *psFaces;
  16. };

ImportShadowModel()是新的初始化函数(客户端调用):

  1. void CShadow3DS::ImportShadowModel(GLuint Model_id, char *strFileName)
  2. {
  3.    ImportModel(Model_id, strFileName); //步骤1,载入模型
  4.  
  5.    s3DObject newsobject= {0};
  6.  
  7. //步骤2,这里是初始化shadow版本模型:遍历原模型各个对象,把该对象中有用的信息:面数,面索引等存入新结构s3DObject ,s3Face 中,并初始化各面的邻面索引为-1(无),非朝光,其余填充0
  8.    for(vector<t3DObject>::iterator p_r = GetModel().pObject.begin(); p_r!=GetModel().pObject.end(); p_r++)
  9.    {
  10.        newsobject.numofFaces= p_r->numOfFaces;
  11.        newsobject.psFaces = new sFace [newsobject.numofFaces];
  12.        memset(newsobject.psFaces, 0, sizeof(sFace) * newsobject.numofFaces);
  13.  
  14.        for(int j = 0 ; j< newsobject.numofFaces; j++)    
  15.            for(int i = 0 ; i<3; i++)
  16.            {
  17.                newsobject.psFaces[j].vertIndex[i] = p_r->pFaces[j].vertIndex[i];
  18.                newsobject.psFaces[j].neigh_face[i] = NO_NEIGH_FACE; 
  19.            }
  20. //把遍历中每次得到的新结构压入vector<s3DObject >psObject中,它能满足shadow版本模型的各种数据存储需求
  21.        psObject.push_back(newsobject);    
  22.    }
  23.  
  24.     SetConnectivity();//步骤3,找邻面
  25.  
  26. //步骤4,对每个面,进行Face to Plane的转换
  27.    for(int iter_obj = 0; iter_obj < GetModel().numOfObjects; iter_obj++)
  28.      for(int iter_face = 0 ; iter_face < GetModel().pObject[iter_obj].numOfFaces; iter_face++)   
  29.              FaceToPlane(iter_obj, iter_face);
  30.  
  31. }

RenderShadowModel()是新的渲染函数(客户端调用):

  1. void CShadow3DS::RenderShadowModel()
  2. {   
  3.     ToModelCoord();//步骤1:转换光源到模型空间
  4.     glLoadIdentity();//把当前矩阵作为单位矩阵(接下来的变换以此为基准)
  5.    RenderModel();//步骤2:渲染原Model 
  6.     RenderShadow();//步骤345,判断并渲染阴影
  7.  
  8. }
  9. //先看看步骤345:
  10. void CShadow3DS::RenderShadow()
  11. {
  12. ....//转换光源到模型空间必要的反变换,后述
  13.  ShadowCast()
  14.  {
  15.     FaceLightJudging();//步骤3.找出每帧中哪些面具有朝光性
  16.    .........//步骤4:接下来是蒙板值非0之判断,代码跟第一篇中三角形面片的ShadowCast()代码是一样的,功能也一样,只不过这里仅处理朝光的面,并且画ShadowVolume的方法(ShadowVolume() )不同
  17.    ShadowVolume() ;//具体的画法思路见"第二篇-第2个问题"
  18.   ..........
  19. //注意阴影锥各侧面(silhouette edge)分正面朝观察者与背面朝贯彻者观察者渲染两次.逆时针为正的保证由3DS模型自己提供(通过顶点索引)
  20.   ShadowVolume() ; 
  21.   ........
  22.   }
  23. }

再提醒一下:"逆时针为正"这个假设非常灰常重要(见"第二篇-第1,2个问题",两处)。

那么,还有一个也非常灰常重要的问题:把光源位置从视图空间转换入模型空间。这里并不是说改动实际光源位置(实际的光源还是由glLightPosition在视图空间确定),而是改动“它在模型空间”的位置,而且要保证这种转换不影响实际视图空间的任何其他东西!OPENGL中把模型空间转换成视图空间,需要在模型空间原矩阵的基础上,左乘 模视矩阵(ModelViewMatrix) ;因此,把现空间从视图空间转回模型空间,需要用 模视矩阵(ModelViewMatrix)的逆矩阵 左乘现存空间的矩阵——这样抵消后就能把一个物件转回模型空间,并可在模型空间操控物件了。重点是:模视矩阵(ModelViewMatrix)的逆矩阵。

怎么求逆呢?这里我得相当佩服NEHE,因为他在我参考的NEHE 27#中,并没有真正“求逆”,而是用了一个简单的技巧,花费极小的计算开销,就完成了光源的转换!最初我根本没有懂得他那串“calculate to local coordinate system”在干嘛,还特意在网上找了个可算逆矩阵(用的高斯消主元,虽然计算方法课程上学过,要我自己写出来还是为难- -)的矩阵类, 准备....但是我还是被NEHE那几句简短的代码折服了,最后宁愿花一整夜去领会去尝试,他的这种“ doing all the actions in reverse order”的方法。当中想明白其用意的契机是突然间脑海中闪起以前看的一些有关3D矩阵的片段,恍然大白!好了,具体的我想专门写篇文章来记录。这里只在代码中简短说说:

  1. void CShadow3DS::ToModelCoord()
  2. {
  3.     CVector4D antiRotLight,antiTransLight;
  4.     CMatrix16 current_mvmatrix;
  5.  
  6. //分别保存MV矩阵的旋转部分(包括缩放)和移动部分
  7.     antiRotLight = LightPos;
  8.     antiTransLight.set(0,0,0,1);
  9.  
  10. //把当前矩阵作为单位矩阵(接下来的变换以此为基准),glLoadIdentity()其实也在压栈
  11.     glLoadIdentity(); 
  12.  
  13. //根据CLoad3DS中的模型变换,作其反变换
  14.     glRotatef(-GetRot().y,0,1,0);   
  15.     glRotatef(-GetRot().z,0,0,1);
  16.     glRotatef(-GetRot().x,1,0,0);
  17.     glRotatef(-1, 1, 0, 0);
  18.     glScalef(1/GetSize(),1/GetSize(),1/GetSize());
  19.  
  20. //存储旋转部分为:MV矩阵乘以光源实际坐标
  21.     glGetFloatv(GL_MODELVIEW_MATRIX,current_mvmatrix.mt);
  22.     antiRotLight = current_mvmatrix * antiRotLight;
  23. //继续
  24.     glTranslatef(-GetPos().x, -GetPos().y, -GetPos().z);
  25.  
  26. //存储平移部分为:MV矩阵乘以(0,0,0,1)   
  27.     glGetFloatv(GL_MODELVIEW_MATRIX,current_mvmatrix.mt);    
  28.     antiTransLight = current_mvmatrix * antiTransLight;  
  29.  
  30. //结合这两部分到mvLightPos:在模型空间和视图空间之间玩无间道的家伙  
  31.     mvLightPos.x = antiRotLight.x + antiTransLight.x;
  32.     mvLightPos.y = antiRotLight.y + antiTransLight.y;
  33.     mvLightPos.z = antiRotLight.z + antiTransLight.z;   
  34.     mvLightPos.w = 1.0; 
  35. }
  36. //.......接下来在RenderShadow()中,不压栈地作正变换,使两者变换抵消,实际中因而只剩下RenderModel()中受glPushMatrix()保护的正变换,一切跟没做这些的时候一致,做这些处理只是为了得到上面那个mvLightPos
  37.  
  38. //mvLightPos,应用于判断面朝光性判断和画ShadowVolume,它在模型空间中可以不断变化,但在视图空间(基于实际视觉)中永远与实际光源坐标重合,即相对于光源不动.(注意,模型在这个过程中与之相反.)

大致要说的就是这些.。程序中其他部分的小动作相信很容易看明白,就不多费舌头了。截图:
Shadow Volume Demo2
Shadow Volume Demo2
更详细截图,包括成功之前的,看这里:
Shadow Volume Demo2 -ZwqXin.com

下一篇见:Shadow Volume 阴影锥技术之探Ⅳ 
放上本DEMO:ShadowVolumeDemo2byZwqxin.rar
按键:
    →   ←   ↑   ↓  PageUp  移动光源
   鼠标左键下按不放并移动鼠标:旋转视角;鼠标滚轮移动
   鼠标右键:自动旋转开关
   空格:转变查看模式:正常模式/辅助可视阴影锥/光源射线模式

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
      原文地址:http://www.zwqxin.com/archives/opengl/shadow-volume-3.html

  • quote 1.xx
  • 我说你下次别那么啰嗦行不行~
    我只是要来看一个小问题,结果,被迫看了一堆的东西。
    还有你那叫代码,改写小说得了~
    zwqxin 于 2009-11-13 13:21:51 回复
    ........汗
  • 2009-11-10 14:26:33 回复该留言
  • quote 2.求教

  • 为什么要进行坐标系的转换呢????始终没有理解啊
    zwqxin 于 2011-2-24 11:02:18 回复
    这是因为我们生成阴影锥体的时候用的是模型的本地坐标来进行计算;但是实际上你一般会经过坐标系变换,把物体“移动”到一个位置,譬如右移3单位,那么你的阴影锥计算就会出错。所以一般两个选择:把物体的所有顶点x坐标都+3,再计算阴影锥;把光源位置x坐标-3(转换到模型坐标系),计算阴影锥。哪一种计算量少点很明显。
  • 2011-2-23 23:32:05 回复该留言
  • quote 3.流浪地球
  • 你好,请问下面这段代码中:

    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,1000.0f);
    camera.Look();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    相机变换不是应该作用在模型视图矩阵吗?您这样写的原理是什么?求教
    zwqxin 于 2011-3-30 21:55:51 回复
    你的理解是对嘀~~这样写在逻辑上是不妥嘀~~ :)调一下中间两句的顺序吧~
    流浪地球 于 2011-3-30 22:24:22 回复
    可是调整以后,整个模型视图都变化了啊,为什么原来可以正确显示?调整以后反而不行了呢?还是不明白啊~! 下面这段代码中:
    glMatrixMode (GL_PROJECTION);
    glLoadIdentity ();
    gluPerspective (45.0f, (GLfloat)(width)/(GLfloat)(height),0.001f,100.0f);

    camera.Look();
    glMatrixMode (GL_MODELVIEW);
    glLoadIdentity ();

    相机变换不是应该作用在模型视图矩阵,上面这样是作用在了投影矩阵了吗?
    这是什么原理呢?求指教~,谢谢

    zwqxin 于 2011-3-30 22:43:02 回复
    因为矩阵是一路左乘下来的,所以在正常渲染时看8出问题,但是当需要去获取的时候就会获取错误的值咯~~至于调整后你也该看看获取的时候是不是也该做些调整。恩,这是俺的疏忽,感谢指出。

    由 zwqxin 于 2011-3-30 22:45:30 最后编辑
    流浪地球 于 2011-3-30 22:58:04 回复
    另外,关于此算法中坐标系的变换,能再解释详细点吗?
    我理解是glLightPosition是光源的局部坐标,同样经过MODELVIEW变换到视觉坐标。
    和物体顶点是一样变换的。那么这个变换到底是从哪里到哪里呢?为什么直接使用glLightPos不行呢?实在参透不了啊~!能再解释详细点吗?谢谢了~
    zwqxin 于 2011-3-30 23:43:36 回复
    glLightPosition貌似一般我们都定义在世界坐标系吧,甚至相机坐标系吧?这里就是要把它转换到每一个模型的局部坐标系中。(我窃认为至于“光源的局部坐标”就是(0,0,0)嘛。)
  • 2011-3-30 20:44:00 回复该留言
  • quote 4.cryscan
  • 但是我怎么也想不出让那三个物体分别运动(现在是一起运动),又不会导致计算出错的方法,求解!
  • 2014-5-1 17:09:15 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.