公司做老虎机,总需要保持与时俱进,主题玩法和活动玩法都要跟上用户需求。
多数主题玩法的基本表现是这样式儿的:
制作人也会参考实体机的表现,或一些视频里的效果来设计玩法。
这儿就接到个需求,要按照一个实体机的表现来制作一个主题玩法,表现是这样式儿的:
仔细分析参考视频,主要需求点还是在这个有透视效果的滚动轮盘。
正向3X5的信号块矩阵表现是最终结算界面,而与结算盘面衔接,并逐渐向屏幕里面延伸的轴面表现是假数据。
但这已然不重要,重点还是这个向内延展的透视效果,是我们需要的。
公司现在的项目用的是coco2dx 3.16 C++ lua 版本,用cocos2dx做2d项目很方便,效率也很高。
所以就有问题了,视频中的表现,琢磨着无论怎样都需要一些简单的3D技术来支持:
1 要么把所有信号块改成具有3d属性的模型,来构架一个3d小场景,不断移动各个模型位置和角度来实现。
2 要么就把所有的2D信号块绘制到一个RT上,再将这个RT以纹理的方式赋予一个指定形状模型上。
3 最重要的是,需要一个透视投影来计算,才能达到效果。
当前项目已经稳定在线,还是不希望大动作的改动现有框架,尽量简便的在原有2D项目上来实现。
经过几天的捣鼓,基本实现了以下效果,表现是这样式儿的:
记录了这个小功能的考虑和开发过程
为了这个小效果很方便的融入2D UI的各种层级中,还是放弃了架设3D场景的念头,采用RT和自建模型的方式操作。此过程中需要建立一个容量很大的RT,来承载足够数量的信号块,这里指定的RT宽度是屏幕宽度,高度是2048,RT最终绘制的效果如下:
看到此图,就能明白点儿了,我们就是要把这个RT当纹理,贴到一个模型上,RT上这些内容,早已超出屏幕绘制区域。这样才能保证透视效果的远处,有足够的内容来填充。
对于模型就建成参考图中的样式,是手写的顶点数据,并未借助3dmax或者maya等工具。
正面图:
侧面图(蓝色点为顶点位置)
两侧相对的顶点左右对称,间距一样,Y轴高的顶点Z轴比较大,达到透视效果。
建立一个继承自Node的TPerspectiveLayer类,主要是重写visit函数,将我们需要渲染的内容都绘制在这个节点上,再将这个节点绘制到屏幕上,使用方法和基本Node一摸一样。
初始化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也是十分重要的,也会根据视口不同有变化,需要手动调节下数值,达到适配效果。
我们要重点操作的函数 需要重写里面的内容
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函数 –
}
上面提到的预渲染步骤,我们也详细说下
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);
}
当所有节点都渲染到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);
建立一个继承自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);
}
}
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顶点的位置,可以将这个透视效果的轮盘,不会变形和丢失渲染像素,画到屏幕上,过程有些凌乱,还算是能完美表现
注:完全是个人瞎捣鼓 还没想出来更简单的方案