Cocos2dx 2D界面实现一个景深小效果

Cocos2dx 2D界面实现一个景深小效果

  • 前言:
    • 矫情一下
  • 说正经的:
    • 1 方案琢磨
    • 2 主代码分析 TPerspectiveLayer
      • 关注点 1:TPerspectiveLayer::enableAction
      • 关注点 2:TPerspectiveLayer::visit
      • 关注点 3:TPerspectiveLayer::onGridBeginDraw
      • 关注点 4:TPerspectiveLayer::onGridEndDraw
    • 3 辅代码分析 NoCullSprite
    • 4 使用说明
  • 后续
    • 叨叨一下

前言:

矫情一下

公司做老虎机,总需要保持与时俱进,主题玩法和活动玩法都要跟上用户需求。
多数主题玩法的基本表现是这样式儿的:

Cocos2dx 2D界面实现一个景深小效果_第1张图片


制作人也会参考实体机的表现,或一些视频里的效果来设计玩法。
这儿就接到个需求,要按照一个实体机的表现来制作一个主题玩法,表现是这样式儿的:
Cocos2dx 2D界面实现一个景深小效果_第2张图片


仔细分析参考视频,主要需求点还是在这个有透视效果的滚动轮盘。
正向3X5的信号块矩阵表现是最终结算界面,而与结算盘面衔接,并逐渐向屏幕里面延伸的轴面表现是假数据。
但这已然不重要,重点还是这个向内延展的透视效果,是我们需要的。


公司现在的项目用的是coco2dx 3.16 C++ lua 版本,用cocos2dx做2d项目很方便,效率也很高。
所以就有问题了,视频中的表现,琢磨着无论怎样都需要一些简单的3D技术来支持:
1 要么把所有信号块改成具有3d属性的模型,来构架一个3d小场景,不断移动各个模型位置和角度来实现。
2 要么就把所有的2D信号块绘制到一个RT上,再将这个RT以纹理的方式赋予一个指定形状模型上。
3 最重要的是,需要一个透视投影来计算,才能达到效果。


当前项目已经稳定在线,还是不希望大动作的改动现有框架,尽量简便的在原有2D项目上来实现。
经过几天的捣鼓,基本实现了以下效果,表现是这样式儿的:

Cocos2dx 2D界面实现一个景深小效果_第3张图片


说正经的:

记录了这个小功能的考虑和开发过程

1 方案琢磨

为了这个小效果很方便的融入2D UI的各种层级中,还是放弃了架设3D场景的念头,采用RT和自建模型的方式操作。此过程中需要建立一个容量很大的RT,来承载足够数量的信号块,这里指定的RT宽度是屏幕宽度,高度是2048,RT最终绘制的效果如下:
Cocos2dx 2D界面实现一个景深小效果_第4张图片

看到此图,就能明白点儿了,我们就是要把这个RT当纹理,贴到一个模型上,RT上这些内容,早已超出屏幕绘制区域。这样才能保证透视效果的远处,有足够的内容来填充。


对于模型就建成参考图中的样式,是手写的顶点数据,并未借助3dmax或者maya等工具。
正面图:
Cocos2dx 2D界面实现一个景深小效果_第5张图片

侧面图(蓝色点为顶点位置)
Cocos2dx 2D界面实现一个景深小效果_第6张图片
两侧相对的顶点左右对称,间距一样,Y轴高的顶点Z轴比较大,达到透视效果。


2 主代码分析 TPerspectiveLayer

建立一个继承自Node的TPerspectiveLayer类,主要是重写visit函数,将我们需要渲染的内容都绘制在这个节点上,再将这个节点绘制到屏幕上,使用方法和基本Node一摸一样。

关注点 1:TPerspectiveLayer::enableAction

初始化RT和透视模型顶点数据

1 初始化需要的RT,长宽要自己定义好,屏幕宽 X 2048 :(2048高目前够用,能容纳20个信号块)

Texture2D::PixelFormat format = Texture2D::PixelFormat::RGBA8888;
this->_texture = new (std::nothrow) Texture2D();
this->_texture->initWithData(data, dataLen,  format, win.width, 2048, s);

2 定义一个 Grabber,主要通知gl为此texture2d指定缓冲区ID,绘制的时候直接调用就行

_grabber = new (std::nothrow) Grabber();
_grabber->grab(_texture);

其中grab函数主要实现以下内容

glBindFramebuffer(GL_FRAMEBUFFER, _FBO);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture->getName(), 0);

3 根据指定数据初始化顶点,绘制模型用:(注意,在此节点释放的时候,要清理这些数据)

_vertices	   = malloc(numOfPoints * sizeof(Vec3));
_texCoordinates= malloc(numOfPoints * sizeof(Vec2));
_indices       = (GLushort*)malloc(_gridSize.width * _gridSize.height * sizeof(GLushort) * 6);
重点:顶点位置需要根据滚动轮盘大小来定数值,因为屏幕大小对Viewport影响,在不同的机型上,顶点显示的透视效果会不一致,所以需要对常规分辨率的屏幕尺寸做顶点位置适配。
顶点UV也是十分重要的,也会根据视口不同有变化,需要手动调节下数值,达到适配效果。

关注点 2:TPerspectiveLayer::visit

我们要重点操作的函数 需要重写里面的内容

void TPerspectiveLayer::visit(Renderer *renderer, const Mat4 &parentTransform, uint32_t parentFlags)
{
Code 1 : //-- 各种渲染准备 具体参考各个继承自Node的visit函数 –
Code 2重点:渲染子节点和自身之前,执行一个 TPerspectiveLayer::onGridBeginDraw 函数,目的是将GL的绘制缓冲区设置成咱们上面创建好的RT,希望接下来的内容都绘制在这个RT上,就当个预渲染函数。

_gridBeginCommand.init(_globalZOrder);
_gridBeginCommand.func = CC_CALLBACK_0( TPerspectiveLayer::onGridBeginDraw, this);
renderer->addCommand(&_gridBeginCommand);

Code 3: //-- 渲染子节点和自身 具体参考各个继承自Node的visit函数 –
Code 4: 重点:已经将所有子节点和自身绘制到了RT上,执行一个 TPerspectiveLayer::onGridEndDraw 函数,目的是将这个RT作为纹理映射到模型上,并将这个模型绘制到屏幕。

_gridEndCommand.init(_globalZOrder);
_gridEndCommand.func = CC_CALLBACK_0( TPerspectiveLayer::onGridEndDraw, this);
renderer->addCommand( &_gridEndCommand );

Code 5: //-- 恢复投影,便于接下来的渲染 具体参考各个继承自Node的visit函数 –
}

关注点 3:TPerspectiveLayer::onGridBeginDraw

上面提到的预渲染步骤,我们也详细说下

void TPerspectiveLayer::onGridBeginDraw
{
Code 1: set2DProjection();
说明:设置一个正焦投影,在创建投影的时候,我们需要设置投影的大小,以用来照射的范围更大,能容纳更多的信号块

Director *director = Director::getInstance();
Size    size = director->getWinSizeInPixels();
director->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION);
Mat4 orthoMatrix;
//-- 注意此处正焦投影的宽高 是我们需要修改的 目前指定2048 --
Mat4::createOrthographicOffCenter(0, size.width, 0, _rtHeight, -1, 1, &orthoMatrix);
director->multiplyMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION, orthoMatrix);
director->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
GL::setProjectionMatrixDirty();
**重点**:其中参数 _rtHeight 指定给了==2048==,并不是屏幕的高度,为什么要指定高度,为什么是 2048 ,上面简述流程1和代码分析创建RT的时候已经说明

Code 2: 设置视口

Size    size = director->getWinSizeInPixels();
glViewport(0, 0, (GLsizei)(size.width), (GLsizei)(2048) );
**重点**:需要将视口也同样指定到和投影范围大小,视口高度也是==2048== ,这样才能容纳需要渲染的内容

Code 3: 切换渲染缓冲区
_grabber->beforeRender(_texture);
说明:开启指定的绘制缓冲区,此时将gl会将绘制的缓冲区替换成RT
beforeRender主要实现以下:

glBindFramebuffer(GL_FRAMEBUFFER, _FBO);
glClearColor(0, 0, 0, 0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

}

关注点 4:TPerspectiveLayer::onGridEndDraw

当所有节点都渲染到RT后,需要进行***全局很重要步骤***,就是绘制透视效果的模型,并贴上纹理

void TPerspectiveLayer::onGridEndDraw
{
Code 1: _grabber->afterRender(_texture);

**重点**:停止渲染到纹理, Node 所有子节点已经绘制到 RT 上 
接下来切换渲染缓冲区,将刚才的RT换回默认的屏幕缓冲区

afterRender函数主要执行以下恢复操作:

glBindFramebuffer(GL_FRAMEBUFFER, _oldFBO);
glClearColor(_oldClearColor[0], _oldClearColor[1], _oldClearColor[2], _oldClearColor[3]);

Code 2: set3DProjection()
说明:接下来我们需要以3d视角来绘制事先创建好的顶点,才能实现透视效果

Director *director = Director::getInstance();
Size  size = director->getWinSizeInPixels();
float zeye = director->getZEye();

Mat4 matrixPerspective, matrixLookup;
//-- 重新设置透视投影 主要是为了加大远截面可视距离 目前指定 5000 --
Mat4::createPerspective(60, (GLfloat)size.width/size.height, 10, 5000, &matrixPerspective);

Vec3 eye(size.width/2, size.height/2, zeye), center(size.width/2, size.height/2, 0.0f), up(0.0f, 1.0f, 0.0f);
Mat4::createLookAt(eye, center, up, &matrixLookup);
Mat4 proj3d = matrixPerspective * matrixLookup;

director->loadMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_PROJECTION, proj3d);
director->loadIdentityMatrix(MATRIX_STACK_TYPE::MATRIX_STACK_MODELVIEW);
GL::setProjectionMatrixDirty();
**重点**:一定要注意创建透视投影的远截面参数==5000==,要设置大一点,这样才能满足我们模型顶部顶点的最大Z值

Code 3: 绘制顶点数据 目前所有像素都绘制到默认的屏幕缓冲区上

//-- 恢复视口  将需要绘制顶点都放在屏幕内
director->setViewport();
const auto& vp = Camera::getDefaultViewport();
glViewport(vp._left, vp._bottom, vp._width, vp._height);
//-- 设置纹理,就是上面的RT --
GL::bindTexture2D(_texture->getName());
int n = _gridSize.width * _gridSize.height;
//-- 设置顶点格式 --
GL::enableVertexAttribs( GL::VERTEX_ATTRIB_FLAG_POSITION | GL::VERTEX_ATTRIB_FLAG_TEX_COORD );
_shaderProgram->use();
_shaderProgram->setUniformsForBuiltins();
//-- 设置顶点数据 --
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, 0, _vertices);
glVertexAttribPointer(GLProgram::VERTEX_ATTRIB_TEX_COORD, 2, GL_FLOAT, GL_FALSE, 0, _texCoordinates);
//-- 绘制 --
glDrawElements(GL_TRIANGLES, (GLsizei) n*6, GL_UNSIGNED_SHORT, _indices);


3 辅代码分析 NoCullSprite

建立一个继承自Sprite的NoCullSprite类,主要为重写draw函数,使其在出了视口(viewport)和窗口(winsize)外还能被渲染,不被剔除,使用方法和基本的Sprite一摸一样。

**重点**:Cocos的Sprite  draw函数是下面这样式儿的 在渲染的时候会检测是否在投影内 是否在屏幕内:
void Sprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    if (_texture == nullptr)
    {
        return;
    }
#if CC_USE_CULLING
    // Don't calculate the culling if the transform was not updated
    auto visitingCamera = Camera::getVisitingCamera();
    auto defaultCamera = Camera::getDefaultCamera();
    if (visitingCamera == nullptr) {
        _insideBounds = true;
    }
    else if (visitingCamera == defaultCamera) {
        _insideBounds = ((flags & FLAGS_TRANSFORM_DIRTY) || visitingCamera->isViewProjectionUpdated()) ? renderer->checkVisibility(transform, _contentSize) : _insideBounds;
    }
    else
    {
        // XXX: this always return true since
        _insideBounds = renderer->checkVisibility(transform, _contentSize);
    }
    if(_insideBounds)
#endif
    {
        _trianglesCommand.init(_globalZOrder,
                               _texture,
                               getGLProgramState(),
                               _blendFunc,
                               _polyInfo.triangles,
                               transform,
                               flags);
        renderer->addCommand(&_trianglesCommand);
#if CC_SPRITE_DEBUG_DRAW
        _debugDrawNode->clear();
        auto count = _polyInfo.triangles.indexCount/3;
        auto indices = _polyInfo.triangles.indices;
        auto verts = _polyInfo.triangles.verts;
        for(ssize_t i = 0; i < count; i++)
        {
            //draw 3 lines
            Vec3 from =verts[indices[i*3]].vertices;
            Vec3 to = verts[indices[i*3+1]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), Vec2(to.x,to.y), Color4F::WHITE);
            from =verts[indices[i*3+1]].vertices;
            to = verts[indices[i*3+2]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), Vec2(to.x,to.y), Color4F::WHITE);
            from =verts[indices[i*3+2]].vertices;
            to = verts[indices[i*3]].vertices;
            _debugDrawNode->drawLine(Vec2(from.x, from.y), Vec2(to.x,to.y), Color4F::WHITE);
        }
#endif //CC_SPRITE_DEBUG_DRAW
    }
}

但是我们需要的是所有的信号块都要被绘制,所以简单重写下draw,去掉这些监测,耿直性绘制:

void NoCullSprite::draw(Renderer *renderer, const Mat4 &transform, uint32_t flags)
{
    if (_texture == nullptr)
    {
        return;
    }
    //-- 相对于基类 取消掉各种剔除判断 --
    if( true )
    {
        _trianglesCommand.init(_globalZOrder,
                               _texture,
                               getGLProgramState(),
                               _blendFunc,
                               _polyInfo.triangles,
                               transform,
                               flags);
        renderer->addCommand(&_trianglesCommand);
    }
}

4 使用说明

1 在这个透视效果的轮盘上,所有需要创建的信号块或者特效,都采用新的NoCullSprite类来创建。
2 创建的信号块按照原来的游戏逻辑add到TPerspectiveLayer创建的Node上,会在重写的visit方法中,将这个Node的所有子节点都渲染到指定的RT上,渲染完成后,再将这个RT,以纹理模式贴在自建模型上。(此自建模型所有数据都存在这个Node类里,在Node节点释放的时候,要释放掉相关的顶点缓冲,索引缓冲和UV缓冲数据 )。
3 至此,这个TPerspectiveLayer自建节点,就可以像其他CCNode一样,任意穿插在各个UI层级中。

**希望这个思路能说明白**

后续

叨叨一下

本以为这个功能算是结束了,又来个需求,这个透视的旋转轮盘,要根据2D UI的大小和位置进行缩放和平移。

结果最担心的问题还是出现了:
1 由于根结点自身的缩放和平移,而RT的大小并没有改变,导致绘制在RT上的像素偏移和大量的丢失。
2 由于在绘制模型的时候,3D投影和视口都是指定的,并且是直接绘制到屏幕缓冲区上的,并不会动态的和根结点进行缩放和平移。

寻思后,还是搬出了第二个RT,以这个作为载体,来承载模型的绘制,然后对这个RT进行缩放和平移,进行UI匹配。

现在把所有的流程再简单梳理一遍:

	 1 初始化工作:
        创建RT1:  大小为屏幕宽度 X 2048
        创建透视模型顶点数据:2X10 个顶点,自定义其3D位置和UV
        创建RT2:  尺寸为屏幕大小 
        创建四边形顶点数据:4个顶点,位置为屏幕四个角
     
     2 绘制工作:
  		Step 1:
        设置缓冲区为RT1
        设置2D Projection,将投影大小设置为 屏幕宽度 X 2048
        设置Viewport,将视口大小设置为 屏幕宽度 X 2048
        调用根节点的Visit,绘制所有的子节点到RT1
        注:此时第一步完成,所有需要的信号块都已在RT1上。
	
	    Step 2:
        设置缓冲区为RT2
        设置Viewport,将视口大小设置为 屏幕宽度大小
        设置3D Perjection
        指定即将绘制的透视模型的纹理为RT1
        绘制透视模型顶点
        注:此时第二部完成,已经将这个具有透视效果的模型绘制到了一张屏幕大小的RT2上

		Step 3:
        恢复屏幕默认缓冲区,即将把这个RT2绘制到屏幕上
        恢复默认视口
        设置即将绘制的模型纹理为RT2
        绘制方片模型顶点
        
        注:此时,通过改变这个RT2顶点的位置,可以将这个透视效果的轮盘,不会变形和丢失渲染像素,画到屏幕上,过程有些凌乱,还算是能完美表现

Cocos2dx 2D界面实现一个景深小效果_第7张图片

注:完全是个人瞎捣鼓 还没想出来更简单的方案

你可能感兴趣的:(Cocos2dx小效果,游戏开发,c++,cocos2d,cocos2d-lua)