Cocos2d-x 实时动态阴影

Cocos2d-x 实时动态阴影

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

本文特效需要用到以下知识

在Cocos2d-x-3.17的CPP Tests测试例子中有一个“假”阴影的例子,这个例子通过计算精灵在平面的位置,然后绘制一个圆形的黑色图片做为精灵的影子,如下图:

这种实现方式阴影是固定在平面上,阴影的形状也是固定的,与真实的阴影相差很大。这篇文章将使用另外一种方式实现逼近真实的阴影,阴影形状是和物体相符,且是动态的。

 

实现原理

在绘制物体生成阴影时,首先需要确定物体的哪些部分在阴影中,确定完成后把阴影部分的亮度调低就可以生成阴影。对于现实世界中阴影的产生是由于物体遮挡光源而引起的,要确定物体哪些部分在阴影中,就需要知道物体哪些部分有被其他物体遮挡光源。在绘制时物体会生成一个由多个像素组成的图像,我们只需要知道哪些像素是在阴影中,就可以在片元着色器中降低输出颜色的亮度,产生阴影。

现在我们的目标是绘制物体时确定哪些片元在阴影中。对于一个物体的光源是否有被遮挡,只需要知道物体与光源直线上离光源最近物体的距离,然后把物体与光源的距离与这个距离比较,如果距离更大则在阴影中,如果等于则说明自己就是离光源最近的物体,也就不在阴影中。因为绘制后物体就变成了像素,所以我们需要知道绘制图像每个像素里光源最近的距离,这可以利用深度缓存,先把摄像机放在光源位置绘制,绘制时开启深度缓存,绘制完成后深度缓存便是每个像素离光源最近的“距离”。这个并不是正真的距离,只是一个深度值,是基于距离进行计算的,当然你也可以计算一个真正的距离值存储到图像缓存,只是深度缓存由系统自动计算使用更加方便效率。

现在还需要当前片元的距离,来与最短距离比较,说明一下这里的当前片元是指摄像机在正常视角下绘制时的片元。是否记得在计算光照时,可以使用点着色器传递的世界坐标系坐标在片元着色器中计算光照强度,而实际上片元着色器使用的片元是根据摄像机坐标系下的坐标生成的,这种方式计算的光照依然是正确的。所以可以在正常视角绘制时点着色器中计算点转换到光源视角下的坐标,在片元着色器中这个坐标的Z轴分量就相应的深度缓存值(当然还需要一点小小的转换),这里的X,Y是光照视角下的像素的X,Y值,可以用这个坐标取出相应的深度缓存值与“Z轴值”比较,计算是否在阴影中。

详细的原理例子可以参考:OpenGL 编程指南(第八版)学习笔记——7 光照与阴影

实际OpenGL生成阴影时,步骤如下:

  1. 把摄像机移动到光源的位置,绘制一次,绘制的时候生成一张光源视角下的深度纹理,也就得到该视角下片元的深度缓存。
  2. 在观察视角下绘制图像,绘制时在点着色器中仍然需要将世界坐标系下的坐标转换到光源视角下,并传给片元着色器。
  3. 在片元着色器中读取第一步中的深度缓存值,并将这个值与点着色器传递的光源坐标系下坐标的深度进行比较,如果这个值更小,说明当前片元在视图视角是被物体遮挡的,也就是处于阴影中。
  4. 对处于阴影中的片元我们只计算环境光颜色,其他都不计算,这样阴影部分会更暗,也就产生了阴影。

 

以下部分大家可以对应着代码看:ShadowScene.hShadowScene.cpp着色器代码

生成深度纹理

生成阴影时除了在正常视角下绘制,还需要额外再光源视角下绘制,在Cocos2d中为了实现这个额外绘制,需要多创建一个光源视角的摄像机,这个摄像机需要渲染到帧缓存,帧缓存只需要一个绑定一个深度缓存附件就行,因为我们只需要深度缓存不需要实际的图像。和将图像渲染到纹理一样,需要创建一个深度纹理,然后将纹理作为帧缓存的深度缓存附件进行渲染。Cocos2d本身不支持深度纹理,所以需要修改引擎,并重写experimental::RenderTargetBase类,我重写的子类为RenderBufferDepthTexture,在子类的init函数中通过以下代码创业深度缓存纹理

glGenTextures(1, &_tex);
glBindTexture(GL_TEXTURE_2D_tex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32sizesize, 0, GL_DEPTH_COMPONENTGL_FLOATNULL);

并在onApplyFBO函数中使用以下代表绑定深度纹理到帧缓存

glFramebufferTexture(targetGL_DEPTH_ATTACHMENT_tex, 0); 
glDrawBuffer(GL_NONE);//屏蔽颜色绘制,不需要绘制颜色

因为我们只需要深度纹理,不需要颜色纹理,所以绘制的时候可以通过代码glDrawBuffer(GL_NONE)屏蔽帧缓存颜色的绘制。

说明:onApplyFBO是我对Cocos2d引擎进行修改,我增加的接口函数,用于帧缓存渲染到特殊纹理,原本的帧缓存只可以渲染到Texture 2d,不可渲染到其他纹理,

精灵的绘制

现在我们有两个3D摄像机,一个是正常视角,另一个是光源视角。对于一个有阴影的精灵,其会有两个着色器程序,一个用于光源视角绘制,一个用于正常视角绘制。当使用不同的摄像机绘制时需要选择相应的着色器。在Sprite3D中使用的都是Material绘制,每个Material可以有多个Technique,每个Technique相当于一种特效,所以我们可以创建一个Material并添加两种Technique,一种用于光源视角,一种用于正常视角。为了达到自动选择Technique的目的,可以重写Sprite3D::draw函数,在该函数中选择对应的Technique。

 

在光源视角只是要深度缓存不需要颜色,所以片元着色器只要有一个空main函数就行。在正常视角时点着色器需要比Cocos本身的点着色器多一件事,就是将世界坐标系坐标转换到光源视角下的坐标,片元着色器也需要多一件事情,就是多一个比较片元光源视角下深度值,判断是否在阴影中,如果在阴影中就只计算环境光,不计算反射光。

在正常视角下的点着色器中,如果直接用光源视角的视图投影与世界坐标系转换,转换后的值接去深度纹理取值比较是不行的。首先对应绘制时OpenGL 使用的时NDC坐标,也就是X,Y值在[-1, 1]中,而实际纹理坐标X,Y的取值范围为[0, 1],所以我们需要转换以下坐标的X,Y,将其范围缩放到[0, 1]之间。对于深度缓绘制时Z范围也是[-1, 1],而实际存储时OpenGL会自动将其压缩到[0, 1]之间,所以Z值也需要进行缩放。对于XYZ都需要缩放,所以可以把点乘以下的矩阵进行缩放:

Mat4 tsScalBias;
Mat4::createTranslation(0.5f, 0.5f, 0.5f, &t);
Mat4::createScale(0.5, 0.5, 0.5, &s);
ScalBias = t * s;

 

深度值比较

深度值比较需要用到阴影采样器sampler2DShadow,这种采样器Cocos2d不支持需要修改一下引擎只需要增加一行代码。实现比较步首先需要开启纹理比较并设置比较函数

         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);

         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LESS);

然后使用函数textureProj(DepthTex, v_shadowCoord)判断是否在阴影中,如果在阴影处会返回0,否则返回1。v_shadowCoord是一个vec4的类型,textureProj首先会进行齐次坐标的转换,用xyz除以w。然后用xy从深度纹理DepthTex取出深度值也z进行比较。

防止斑驳现象

斑驳现象是因为判断阴影时深度值比较接近或超出精度范围比较错误导致的,为了防止这种现象,可以先对深度值进行一定的偏移。有两种方法,可以使用在代码中启用多边形偏移,使用glPolygonOffset设置偏移值,也可以在片元着色器中直接增加深度值,因为Cocos2d使用多边形偏移比较麻烦要修改引擎,所以我使用的是着色器方法,代码如下:

void main(void)

{

    gl_FragDepth = gl_FragCoord.z + 0.001;

    gl_FragColor = vec4(0.0, 0.0, 0.0, 0.5);

}

 

点光源阴影和平行光阴影

对于点光源的阴影光源坐标系下摄像机投影需要使用透视投影,而对于平行光的阴影需要使用正交投影,两者代码如下:

    auto shadowCamera = ShadowCamera::create(cameraCameraFlag::USER2);
#define USE_POINT_LIGHT_SHADOW
#ifdef USE_POINT_LIGHT_SHADOW 
    
Vec3 LightPos(500, 800, 200);
    
shadowCamera->initPerspective(120, 1, 50, 5000);//点光源使用透视投影
#else 
    
Vec3 LightPos(1000, 1000, 000);
    
Mat4 projection;
    
Mat4::createOrthographicOffCenter(-1000, 1000, -1000, 1000, 50, 5000, &projection);
    
shadowCamera->setPorjectionMatrix(projection);//平行光要使用正交投影

#endif // USE_POINT_LIGHT_SHADOW
    shadowCamera->setPosition3D(LightPos);
    
shadowCamera->lookAt(Vec3(0.0f, 0.0f, 0.0f), Vec3(0.0f, 1.0f, 0.f));

 

两种阴影效果如下

 

以下是一个完整的例子,代码下载地址,对应的程序菜单:“4:Real Dynamic Shadow”:

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值