Cocos2d-x 镜子特效

Cocos2d-x 镜子特效

声明:本文使用的是cocos2d-x-3.17的代码

实现镜子特效需要用到以下知识

镜子特效的原理很简单,就是镜子为轴对称,将镜子正面的物体在背面重新绘制一次。绘制物体时需要解决两个问题,首先需要确定物体的位置,其次是要保证物体绘制时不能超出镜子的范围。

确定反射位置

首先需要将镜子放在XOY(Z=0)平面上,镜子的形状可以不用管,但是必须位于XOY平面。如下,是一个正方形数据:

//这是一个正方行,位置:3float, 法线:3float,纹理:2float
const float PanelVertices[] = {
    -120,  120,  0.f,   0.000000,  0.000000,  1.000000, 0.f, 1.f,
     120, -120,  0.f,   0.000000,  0.000000,  1.000000, 1.f, 0.f,
     120,  120,  0.f,   0.000000,  0.000000,  1.000000, 1.f, 1.f,
    -120, -120,  0.f,   0.000000,  0.000000,  1.000000, 0.f, 0.f
};

const unsigned int PanelIndices[] = {
    0,   1,   2,   0,   3,   1
};

 

然后通过以下几个步骤可以确定世界坐标系下点C1在镜子中的反射位置。

  1. 将C1转换到镜子坐标系下
  2. 然后将C1的Z轴乘以-1,进行轴对称
  3. 此时将C1是反射后镜子的本地坐标,再将C1转换到世界坐标系下

设MirrorToWorld为镜子到世界坐标系的转换矩阵, VPMatrix为视图投影矩阵,确定世界坐标系下点C1绘制位置代码如下:

  VPMatrix * MirrorToWorld* Mat4::createScale(1.0, 1.0, -1.0) * MirrorToWorld.inverse() * C1

首先乘以MirrorToWorld.inverse()相当于以上第1步将C1转换到镜子坐标系下。点反射后的位置与点原本的绘制位置:VPMatrix * C1相比多了MirrorToWorld* Mat4::createScale(1.0, 1.0, -1.0) * MirrorToWorld.inverse(),这个矩阵相当于一个镜面反射矩阵。注意这个矩阵是乘在视图投影矩阵与模型矩阵之间的,因为C1是一个世界坐标系下矩阵,相当于已经乘了模型矩阵。

裁剪反射画面

裁剪是为了限制反射物体绘制在镜子中。裁剪画面可以使用模板缓存,Cocos2d中的ClippingNode使用的就是模板缓存进行裁剪。如果要在Cocos2d中使用模板缓存,要将模板命令与要裁剪的命令一同放在一组GroupCommand中,避免影响主渲染管线。当绘制反射物体时要重新绘制一遍之前的物体,这相当于要重新遍历一次场景然后生成渲染命令。如果使用模板缓存绘制镜子特效,则相当于Render在执行绘制镜子命令时,遍历一次场景生成绘制命令并绘制,Cocos2d本身不支持这种渲染顺序。如果要改动Cocos2d来支持这种渲染顺序,改动量也比较大,所以使用模板缓存裁剪不适合。

 

使用纹理缓存进行裁剪

Cocos支持帧缓存,创建一个FrameBuffer类然后绑定到一个摄像机中就可以实现将物体渲染到帧缓存,如果帧缓存的颜色附件为RenderTarget类则会渲染到纹理中。Cocos2d中渲染场景时会使用每个摄像机对场景遍历一次,如果增加一个摄像机,则会增加一次场景遍历。所以我们可以增加一个摄像机来绘制反射后的物体,然后利用FrameBuffer将反射后的物体绘制到一张纹理中。注意这里的纹理相当于增加了一个“屏幕”,绘制反射物体时,绘制在纹理中的位置和实际屏幕中的位置一样,纹理的大小也和“屏幕(窗口)”的大小一致。当绘制镜子时,需要利用图元在“屏幕”的位置映射纹理,如下代码:

uniform sampler2D u_ReflectTex;

 

//屏幕绘制尺寸

ivec2 texSzie = textureSize(u_ReflectTex, 0);

//使用屏幕坐标映射纹理

vec2 texCoord = vec2(gl_FragCoord.x/texSzie.x, gl_FragCoord.y/texSzie.y);

vec4 cReflect = texture2D(u_ReflectTex, texCoord);

 

绘制物体

结合前面的内容,Cocos2d所以绘制镜子的步骤大致如下:

  1. 创建一个FrameBuffer类,并设置一个RenderTarget类的颜色附件,因为绘制时要使用深度缓存,所以还需要设置一个RenderTargetDepthStencil附件。
  2. 增加一个镜面摄像机,将FrameBuffer类与摄像机关联,让镜面摄像机内容绘制到FrameBuffer中
  3. 先让镜面摄像机比场景摄像机先绘制,生成一个镜面反射纹理。
  4. 使用场景摄像机绘制镜子,绘制时使用之前生成的纹理进行映射。

由于使用了FrameBuffer单独绘制镜面纹理,镜面反射的内容全部绘制在镜面纹理中,这使得镜面反射的绘制会实际场景完全隔离,镜面反射不会对场景实际绘制有任何影响。

 

绘制步骤中没有说明如何对反射物体位置进行转换,根据前面镜面反射位置,我们知道反射物体绘制时要在视图投影矩阵与模型矩阵之间乘以一个反射矩阵,假设这矩阵为ReflectMatrix,则ReflectMatrix = MirrorToWorld* Mat4::createScale(1.0, 1.0, -1.0) * MirrorToWorld.inverse()。现在需要解决如何将反射矩阵ReflectMatrix用到实际的绘制中。

摄像机会提供绘制时的视图投影矩阵,模型矩阵则由物体自己决定,为了统一控制,可将这个ReflectMatrix矩阵增加到镜面摄像机中,当获取镜面摄像机的视图投影矩阵乘以ReflectMatrix。最简单的方法时重写Camera::getViewProjectionMatrix函数,在函数返回前乘以一个ReflectMatrix矩阵,如下代码:
 


const cocos2d::Mat4& MirrorCamera::getViewProjectionMatrix() const
{
	Mat4 sceneMat = _sceneCamera->getViewProjectionMatrix(); //场景摄像机的视图投影矩阵

	Mat4 toWorld = this->getNodeToWorldTransform();//因为镜面摄像机在镜子的原点所以,所获取的矩阵也是镜子坐标系到世界坐标系的转换矩阵
	Mat4 reflect;
	Mat4::createScale(Vec3(1.0, 1.0, -1.0), &reflect); //反射,因为镜子在XOY平面(Z=0)平面,反射也就相当于将 Z轴坐标取负值

	//这里要分三个步骤:1.将物体从世界坐标系转换到镜子坐标系
	//2.将物体的Z轴坐标乘以-1,
	//3.将物体从镜子坐标系转换到世界坐标系
	reflect = toWorld* reflect * toWorld.getInversed();

	//_viewProjection为视图投影矩阵,实际计算物体屏幕坐标为 _viewProjection * Model = sceneMat * reflect * Model, reflect为镜面变化
	_viewProjection = sceneMat * reflect;

	return _viewProjection;
}

因为Camera::getViewProjectionMatrix()函数不是虚函数,重写的函数无法用Camera的引用或指针调用,所以需要修改Cocos2d引擎,将这个函数设为虚函数。

/**get view projection matrix*/
virtual const Mat4getViewProjectionMatrix() const;

以下是一个完整的例子,代码下载地址,对应的程序菜单:“3:Mirror Reflect”:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值