小星星的天空

O(∩_∩)O 小月亮的fans ^_^

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  16 随笔 :: 0 文章 :: 61 评论 :: 0 Trackbacks

2009年10月20日 #

多态(Polymorphism)是面向对象的核心概念,本文以C++为例,讨论多态的具体实现。C++中多态可以分为基于继承和虚函数的动态多态以及基于模板的静态多态,如果没有特别指明,本文中出现的多态都是指前者,也就是基于继承和虚函数的动态多态。至于什么是多态,在面向对象中如何使用多态,使用多态的好处等等问题,如果大家感兴趣的话,可以找本面向对象的书来看看。
    为了方便说明,下面举一个简单的使用多态的例子(From [1] ):

class Shape
{
protected:
  int m_x;    // X coordinate
  int m_y;  // Y coordinate
public:
  // Pure virtual function for drawing
  virtual void Draw() = 0;  

  // A regular virtual function
  virtual void MoveTo(int newX, int newY);

 // Regular method, not overridable.
  void Erase();

  // Constructor for Shape
  Shape(int x, int y); 

 // Virtual destructor for Shape
  virtual ~Shape();
};
// Circle class declaration
class Circle : public Shape
{
private:
   int m_radius;    // Radius of the circle
public:
   // Override to draw a circle
   virtual void Draw();    

   // Constructor for Circle
   Circle(int x, int y, int radius);

  // Destructor for Circle
   virtual ~Circle();
};
// Shape constructor implementation
Shape::Shape(int x, int y)
{
   m_x = x;
   m_y = y;
}
// Shape destructor implementation
Shape::~Shape()
{
//...
}
 // Circle constructor implementation
Circle::Circle(int x, int y, int radius) : Shape (x, y)
{
   m_radius = radius;
}

// Circle destructor implementation
Circle::~Circle()
{
//...
}

// Circle override of the pure virtual Draw method.
void Circle::Draw()
{
   glib_draw_circle(m_x, m_y, m_radius);
}

main()
{
  // Define a circle with a center at (50,100) and a radius of 25
  Shape *pShape = new Circle(50, 100, 25);

  // Define a circle with a center at (5,5) and a radius of 2
  Circle aCircle(5,5, 2);

  // Various operations on a Circle via a Shape pointer
  //Polymorphism
  pShape->Draw();
  pShape->MoveTo(100, 100);

  pShape->Erase();
  delete pShape;

 // Invoking the Draw method directly
  aCircle.Draw();
}   

     例子中使用到多态的代码以黑体标出了,它们一个很明显的特征就是通过一个基类的指针(或者引用)来调用不同子类的方法。
     那么,现在的问题是,这个功能是怎样实现的呢?我们可以先来大概猜测一下:对于一般的方法调用,到了汇编代码这一层次的时候,一般都是使用 Call funcaddr 这样的指令进行调用,其中funcaddr是要调用函数的地址。按理来说,当我使用指针pShape来调用Draw的时候,编译器应该将Shape::Draw的地址赋给funcaddr,然后Call 指令就可以直接调用Shape::Draw了,这就跟用pShape来调用Shape::Erase一样。但是,运行结果却告诉我们,编译器赋给funcaddr的值却是Circle::Drawde的值。这就说明,编译器在对待Draw方法和Erase方法时使用了双重标准。那么究竟是谁有这么大的法力,使编译器这个铁面无私的判官都要另眼相看呢?virtual!!
    
Clever!!正是virtual这个关键字一手导演了这一出“乾坤大挪移”的好戏。说道这里,我们先要明确两个概念:静态绑定和动态绑定。
    1、静态绑定(static bingding),也叫早期绑定,简单来说就是编译器在编译期间就明确知道所要调用的方法,并将该方法的地址赋给了Call指令的funcaddr。因此,运行期间直接使用Call指令就可调用到相应的方法。
    2、动态绑定(dynamic binding),也叫晚期绑定,与静态绑定不同,在编译期间,编译器并不能明确知道究竟要调用的是哪一个方法,而这,要知道运行期间使用的具体是哪个对象才能决定。
    好了,有了这两个概念以后,我们就可以说,virtual的作用就是告诉编译器:我要进行动态绑定!编译器当然会尊重你的意见,而且为了完成你这个要求,编译器还要做很多的事情:编译器自动在声明了virtual方法的类中插入一个指针vptr和一个数据结构VTable(vptr用以指向VTable;VTable是一个指针数组,里面存放着函数的地址),并保证二者遵守下面的规则:
    1、VTable中只能存放声明为virtual的方法,其它方法不能存放在里面。在上面的例子中,Shape的VTable中就只有Draw,MoveTo和~Shape。方法Erase的地址并不能存放在VTable中。此外,如果方法是纯虚函数,如 Draw,那么同样要在VTable中保留相应的位置,但是由于纯虚函数没有函数体,因此该位置中并不存放Draw的地址,而是可以选择存放一个出错处理的函数的地址,当该位置被意外调用时,可以用出错函数进行相应的处理。
    2、派生类的VTalbe中记录的从基类中继承下来的虚函数地址的索引号必须跟该虚函数在基类VTable中的索引号保持一致。如在上例中,如果在Shape的VTalbe中,Draw为 1 号, MoveTo 2 号,~Shape为 3 号,那么,不管这些方法在Circle中是按照什么顺序定义的,Circle的VTable中都必须保证Draw为 1 号,MoveTo为 2号。至于 3号,这里是~Circle。为什么不是~Shape啊?嘿嘿,忘啦,析构函数不会继承的。
    3、vptr是由编译器自动插入生成的,因此编译器必须负责为其进行初始化。初始化的时间选在对象创建时,而地点就在构造函数中。因此,编译器必须保证每个类至少有一个构造函数,若没有,自动为其生成一个默认构造函数。
     4、vptr通常放在对象的起始处,也就是Addr(obj) == Addr(obj.vptr)。
    你看,天下果然没有免费的午餐,为了实现动态绑定,编译器要为我们默默干了这么多的脏话累活。如果你想体验一下编译器的辛劳,那么可以尝试用C语言模拟一下上面的行为,【1】中就有这么一个例子。好了,现在万事具备,只欠东风了。编译,连接,载入,GO!当程序执行到 pShape->Draw()的时候,上面的设施也开始起作用了。。
    前面已经提到,晚期绑定时之所以不能确定调用哪个函数,是因为具体的对象不确定。好了,当运行到pShape->Draw()时,对象出来了,它由pShape指针标出。我们找到这个对象后,就可以找到它里面的vptr(在对象的起始处),有了vptr后,我们就找到了VTable,调用的函数就在眼前了。。等等,VTable中方法那么多,我究竟使用哪个呢?不用着急,编译器早已为我们做好了记录:编译器在创建VTable时,已经为每个virtual函数安排好了座次,并且把这个索引号记录了下来。因此,当编译器解析到pShape->Draw()的时候,它已经悄悄的将函数的名字用索引号来代替了。这时候,我们通过这个索引号就可以在VTable中得到一个函数地址,Call it!
    在这里,我们就体会到为什么会有第二条规定了,通常,我们都是用基类的指针来引用派生类的对象,但是不管具体对象是哪个派生类的,我们都可以使用相同的索引号来取得对应的函数实现。
     现实中有一个例子其实跟这个蛮像的:报警电话有110,119,120(VTable中不同的方法)。不同地方的人拨打不同的号码所产生的结果都是不一样的。譬如,在三环外的一个人(具体对象)跟一环内的一个人(另外一个具体对象)打119,最后调用的消防队肯定是不一样的,这就是多态了。这是怎么实现的呢,每个人都知道一个报警中心(VTable,里面有三个方法 110,119,120)。如果三环外的一个人需要火警抢险(一个具体对象)时,它就拨打119,但是他肯定不知道最后是哪一个消防队会出现的。这得有报警中心来决定,报警中心通过这个具体对象(例子中就是具体位置了)以及他说拨打的电话号码(可以理解成索引号),报警中心可以确定应该调度哪一个消防队进行抢险(不同的动作)。
     这样,通过vptr和VTable的帮助,我们就实现了C++的动态绑定。当然,这仅仅是单继承时的情况,多重继承的处理要相对复杂一点,下面简要说一下最简单的多重继承的情况,至于虚继承的情况,有兴趣的朋友可以看看 Lippman的《Inside the C++ Object Model》,这里暂时就不展开了。(主要是自己还没搞清楚,况且现在多重继承都不怎么使用了,虚继承应用的机会就更少了)
     首先,我要先说一下多重继承下对象的内存布局,也就是说该对象是如何存放本身的数据的。

class Cute
{
public:
 int i;
 virtual void cute(){ cout<<"Cute cute"<<endl; }
};
class Pet
{
public:
   int j;
   virtual void say(){ cout<<"Pet say"<<endl;  }
};
class Dog : public Cute,public Pet
{
public:
 int z;
 void cute(){ cout<<"Dog cute"<<endl; }
 void say(){ cout<<"Dog say"<<endl;  }
};

    在上面这个例子中,一个Dog对象在内存中的布局如下所示:                    

Dog

Vptr1

Cute::i

Vptr2

Pet::j

Dog::z


     也就是说,在Dog对象中,会存在两个vptr,每一个跟所继承的父类相对应。如果我们要想实现多态,就必须在对象中准确地找到相应的vptr,以调用不同的方法。但是,如果根据单继承时的逻辑,也就是vptr放在指针指向位置的起始处,那么,要在多重继承情况下实现,我们必须保证在将一个派生类的指针隐式或者显式地转换成一个父类的指针时,得到的结果指向相应派生类数据在Dog对象中的起始位置。幸好,这工作编译器已经帮我们完成了。上面的例子中,如果Dog向上转换成Pet的话,编译器会自动计算Pet数据在Dog对象中的偏移量,该偏移量加上Dog对象的起始位置,就是Pet数据的实际地址了。

int main()
{
 Dog* d = new Dog();
 cout<<"Dog object addr : "<<d<<endl;
 Cute* c = d;
 cout<<"Cute type addr : "<<c<<endl;
 Pet* p = d;
 cout<<"Pet type addr : "<<p<<endl;
 delete d;
}
output:
Dog object addr : 0x3d24b0
Cute type addr : 0x3d24b0
Pet type addr : 0x3d24b8   // 正好指向Dog对象的vptr2处,也就是Pet的数据

      好了,既然编译器帮我们自动完成了不同父类的地址转换,我们调用虚函数的过程也就跟单继承统一起来了:通过具体对象,找到vptr(通常指针的起始位置,因此Cute找到的是vptr1,而Pet找到的是vptr2),通过vptr,我们找到VTable,然后根据编译时得到的VTable索引号,我们取得相应的函数地址,接着就可以马上调用了。

      在这里,顺便也提一下两个特殊的方法在多态中的特别之处吧:第一个是构造函数,在构造函数中调用虚函数是不会有多态行为的,例子如下:

class Pet
{
public:
   Pet(){ sayHello(); }
   void say(){ sayHello(); }

   virtual void sayHello()
   {
     cout<<"Pet sayHello"<<endl;
   }
  
};
class Dog : public Pet
{
public:
   Dog(){};
   void sayHello()
   {
     cout<<"Dog sayHello"<<endl;
   }
};
int main()
{
 Pet* p = new Dog();
 p->sayHello();
 delete p;
}
output:
Pet sayHello //直接调用的是Pet的sayHello()
Dog sayHello //多态

     第二个就是析构函数,使用多态的时候,我们经常使用基类的指针来引用派生类的对象,如果是动态创建的,对象使用完后,我们使用delete来释放对象。但是,如果我们不注意的话,会有意想不到的情况发生。

class Pet
{
public:
   ~Pet(){ cout<<"Pet destructor"<<endl;  }
  //virtual ~Pet(){ cout<<"Pet virtual destructor"<<endl;  }
};
class Dog : public Pet
{
public:
   ~Dog(){ cout<<"Dog destructor"<<endl;};
   //virtual ~Dog(){ cout<<"Dog virtual destructor"<<endl;  }
};
int main()
{
 Pet* p = new Dog();
 delete p;
}
output:
Pet destructor  //糟了,Dog的析构函数没有调用,memory leak!

如果我们将析构函数改成virtual以后,结果如下
Dog virtual destructor
Pet virtual destructor   // That's OK!

    所以,如果一个类设计用来被继承的话,那么它的析构函数应该被声明为virtual的。

posted @ 2009-10-20 21:36 Little Star 阅读(177) | 评论 (0)编辑 收藏

通常在C的编程中,我们经常使用memset函数将一块连续的内存区域清零或设置为其它指定的值,最近在移植一段java代码到C++的时候,不当使用memset函数花费了我几个小时的调试时间。对于虚函数的底层机制很多资料都有较详细阐述,但对我个人而言,这次的调试让我感触颇深。

先来看一段代码,在继承的类Advance之中,有很多属性字段,我希望将其清成0或NULL,于是在构造函数中我通过memset将当前类的所有属性置0。

class Base{

public:

virtual void kickoff() = 0;

};
class Advance:public Base{

public:

Advance(){

memset(this, 0, sizeof(Advance));

}

void kickoff(){

count++;

//... do something else;

}

private:

int attr1, attr2;

char* label;

int count;

//... other attributes, they should be initiated to 0 or NULL at beginning.

};

int _tmain(int argc, _TCHAR* argv[])

{
Base* ptr = new Advance();
ptr->kickoff();
return 0;
}

这样看似能正常运行,但运行程序时,你会发现类似于下面的错误:

TestVirtual.exe 中的 0x00415390 处未处理的异常: 0xC0000005: 读取位置 0x00000000 时发生访问冲突

同时断点停留在ptr->kickoff()处,从错误提示我们可以得知无法调用kickoff方法,这个方法的指针没有被正确初始化,但为什么呢?

指出问题之前,先看看这段文献上的关于虚函数机制的说明:

函数赖以生存的底层机制:vptr + vtable。虚函数的运行时实现采用了VPTR/VTBL的形式,这项技术的基础:
①编译器在后台为每个包含虚函数的类产生一个静态函数指针数组(虚函数表),在这个类或者它的基类中定义的每一个虚函数都有一个相应的函数指针。
②每个包含虚函数的类的每一个实例包含一个不可见的数据成员vptr(虚函数指针),这个指针被构造函数自动初始化,指向类的vtbl(虚函数表)
③当客户调用虚函数的时候,编译器产生代码反指向到vptr,索引到vtbl中,然后在指定的位置上找到函数指针,并发出调用。

这里的问题,就出在

memset(this, 0, sizeof(Advance));

上面,虚函数指针应该在进入构造函数赋值体之前自动初始化的,而memset却又将已经初始化好的指针清0了,这就是为什么会产生上面的访问零址的错误。将上面的memset语句去除程序就可以正常运行了。

所以,从上面的问题中,我们可以看出在构造函数体内调用memset将整个对象清0是很有风险的,当没有虚函数的时候上面程序可以正常运行(可以试着将Base类的纯虚函数声明改成非虚函数再运行程序)。初始化类的属性对象时,比较稳妥的办法还是手动逐个进行初使化
posted @ 2009-10-20 21:11 Little Star 阅读(1920) | 评论 (7)编辑 收藏

2009年10月13日 #




       shadow map 以前早就研究过,不过一次不小心把以前做的东西都弄丢了,今天重新做了一下,
加到了系统里,给大家看下效果:)



shadow map算法原理很简单,先简单介绍下算法给新人:
1.以光源所在位置为观察点渲染场景(可以只渲染需要产生阴影的物体)将渲染后的深度值保存深度图(一张事先准备好的纹理)。
在此步需要注意的是 此次渲染用到的模型观视投影矩阵(以后简称mvp)需要保存一下,下一步要用到。
2.正常渲染场景,将顶点坐标乘以步骤一时候的mvp,将坐标变换到以光源为观察点的坐标系里,比较z值和从深度图中读出来的
值得大小判断遮挡,有遮挡的话将输出颜色减弱或者换成别的随笔你了。其中有个地方需要注意,如何从深度图纹理中读数据,
这个我是这么解决的:float2 suv = ((spos.xy/spos.z))//其中spos是变换到光源坐标系下的顶点数据,得到的suv经过处理后可以
当做深度图的纹理坐标值,读取方法为float4 shadow = tex2D(t11,(suv+1.0)*0.5),其中用到一个【-1,1】到【0,1】的变换。
剩下的就是比较了  :
            float sz =  1 - spos.z/(gDepthSize);//将深度值变换到【0,1】区间,gDepthSize是获得深度纹理时渲染场景的最深值  
            //增加阴影
            if((sz < shadow.x))//sz是就是
            {
                color = color*(1 - shadow);
                color.w = 1.0;
                //color = float4(sz,sz,sz,1);
            }
//----------------------------------------------------------------------------------------------------------------------------
关于shadow map 算法的缺点,跟大家讨论一下:
永远的困扰shadow map的失真问题,当光源照射场景稍大的时候失真现象就会很严重,有些改进算法,但都觉得治标不治本。
如果说我整个场景有很多到处跑的人,那他们的阴影效果要怎样做呢???

感觉shadow map用在生成当前角色的阴影挺好,如果是大范围的不大适合。很多静态的物体可以先把阴影事先计算好,用的时候
直接读取,没有必要每帧都重新计算。

//----------------------------------------------------------------------------------------------------------------------------
shadow map 最大的好处是可以处理透明纹理的阴影,以为我的场景的树是用透明纹理画上去的,如果得到的阴影是个矩
形那就很怪了,幸好shadow map 没有这个问题!!!



posted @ 2009-10-13 23:34 Little Star 阅读(3045) | 评论 (6)编辑 收藏



         今天考虑程序的优化问题,突然想到说现在vetex shader已经可以访问纹理资源了,可以把访问高度图的操作转移
到vetex shader中去做计算,GPU访问纹理的速度要比CPU访问内存快多了吧(我是这么认为的)。不过遇见一个问题。
用tex2D访问出来的值怪怪的,不是我要的高度纹理值,后来发现如果fragment shader里面访问了别的纹理会影响到vetex里
面的,上网google了一下,好多人都说得用 tex2DLod才行,试了下一点好转的迹象都没有,又有人说纹理得是float的,参照
着改了下,还是不行。(很多时候在网上查到的都不好用,当然也有好用的,有点废话,哈哈)
        后来突然发现我的sampler都没有跟寄存器绑定,绑定了以后就好了,很奇怪,难道说只调用cgGLSetTextureParameter
并不能实现纹理的绑定?   现在我把所有sampler都跟一个寄存器绑定后就一切正常了。

就像这样:
//--------------------------------------------------------------------------------
sampler2D t10 :TEXUNIT0;//地面高度图,    其他的sampler也需要绑定到寄存器才会好用

OutPut xS_v(float4 ipos:POSITION,
            float2 tex:TEXCOORD0,
            float3 normal:NORMAL)
{
    OutPut OUT;                            //out是关键字
    ipos.y =tex2D(t10,tex).z*256;
//---------------------------------------------------------------------------------


cg语言运行出来的结果经常怪怪的,有一次我故意写错了一句话,结果编译也能通过,只是显示出来的完全不
是我想要的,有没有高人指点一下,多谢!!


不过把访问高度图改到用GPU访问纹理后,效果还是很明显的,速度提升了差不多一倍。




posted @ 2009-10-13 09:02 Little Star 阅读(1844) | 评论 (4)编辑 收藏

2009年10月12日 #


最近的工作:

1.实现了刀客的动作控制。(md2文件,GPU实现帧动画)。
2.试验了水面效果(有反射纹理映射,水面法线贴图计算光照和扰动)。
3.试验了billboard效果。
4.修改程序中的一些bug。

有两个个问题:
1.OpenGL如何提高效率,欢迎指教!
2.flt文件如何简化,有好用的工具么,我的都是从3ds导过来的,面数太多了,不大适用。

发几张孬图,贻笑大方!



感觉水面反射加了树的倒影反而变得怪怪的!!

再来几张前几天截得图


这是我的小花园,大树的纹理烂透了。




小河流水 哗啦啦……





去掉地形的效果,感觉还蛮漂亮的,呵呵

大家有什么看法尽量留言,您的关注就是对我最大的帮助!!

posted @ 2009-10-12 00:48 Little Star 阅读(3237) | 评论 (9)编辑 收藏

2009年9月25日 #

normal map



Parallax Mapping



对应的网格图



趁着人少发两张等着挨砖头的效果图。

看了一天的资料,证实了原来的想法是对的。
normal map  是 bump map 的改进,主要是GPU可编程为其创造了条件。
parallax map 是 normal map 的改进版本,相当于是对normal map 的修正,没什么心意。

Displacement Mapping貌似挺牛,明天仔细研究下 :-)
posted @ 2009-09-25 03:33 Little Star 阅读(1721) | 评论 (2)编辑 收藏

2009年9月24日 #



先来张调整了参数的roam网格图片,把面片数约束在1w,这下roam的效果就明显了吧!
发现地势如果不是特别平坦的话,很难找到一个满意的参数,既能减少面片数,又能取得很好的显示效果。
如果有大片大片的平地,那就简单了。



这是使用cg语言实现的 texture blend 使用四张纹理加一个细节纹理混合而成,
增加了像素级的 Phone 光照模型。光照的颜色有点怪怪的。

posted @ 2009-09-24 02:09 Little Star 阅读(1173) | 评论 (3)编辑 收藏

2009年9月21日 #



面片数跟帧数永远的让人很矛盾。这样以后突变就很不明显了,肉眼几乎发现不了,不过感觉有退化成普通的lod的趋势。
当然,远处的网格还是会有跳动现象,不过那已经很远了,一般不会有人注意到发生了什么。
可是这样大大增加了网格数量,应用下视景体剪除效果会比这个好很多吧!

posted @ 2009-09-21 01:00 Little Star 阅读(1300) | 评论 (2)编辑 收藏

2009年9月20日 #



这个是我实现出来的 roam terrain 地形
问题是,当我在地形上漫游时,网格必然会发生变化。不同层次的网格之间跳跃的很厉害,让人觉得地形很不真实,
有没有人有好的办法能解决?

posted @ 2009-09-20 00:29 Little Star 阅读(303) | 评论 (0)编辑 收藏

2009年9月17日 #

1.测试一下 flt文件读取和现实效果。
2.测试下引擎框架,寻找bug。


这张是刚开始时没加载上外部文件时的



这张是爆炸效果图


这张是着火效果



被导弹追击

最后来张夜色美景




诚盼牛人指点不足!
posted @ 2009-09-17 00:35 Little Star 阅读(1612) | 评论 (7)编辑 收藏

仅列出标题  下一页