从OpenGL渲染的角度排查 creator native 局部换肤的问题

简介: 从OpenGL渲染的角度排查 creator native 局部换肤的问题

问题表现

我制作了一个最简单的龙骨动画,里面有骨骼嵌套,在父骨骼的时间轴上切换子骨骼的animation,正常的表现如下:

image.png

换肤后直接不动了

image.png

定位过程

因为代码是从js移植过来的,所有的经验都来自js引擎,其实native的c++引擎和js引擎比较像,但编码结构上也有差异,隐约感觉是这部分的问题。

问题比较难定位,所以我才做了这样的一个最小的demo,实际发现,排查起来也是无从下手,看来只能从最终提交顶点来反推问题了。

draw

下手的地方当然还是从OpenGL的draw函数下手。

c++
复制代码
void DeviceGraphics::draw(size_t base, GLsizei count)
{
    commitVertexBuffer();// 顶点数据是在这里进行提交的
    if (nextIndexBuffer)
    {
        // 这里调用draw,但是数据是绑定在buffer里面的,如果对OpenGL不熟悉,需要补补基础
        GL_CHECK(glDrawElements(ENUM_CLASS_TO_GLENUM(_nextState->primitiveType),
           count,
           ENUM_CLASS_TO_GLENUM(nextIndexBuffer->getFormat()),
           (GLvoid *)(base * nextIndexBuffer->getBytesPerIndex())));
    }
}

分配顶点缓冲区的数据

c++
复制代码
void DeviceGraphics::commitVertexBuffer()
{
    if (attrsDirty)
    {
        for (int i = 0; i < _caps.maxVertexAttributes; ++i)
            _newAttributes[i] = 0;
        for (int i = 0; i < _nextState->maxStream + 1; ++i)
        {
            auto vb = _nextState->getVertexBuffer(i);
            if (!vb)
                continue;
            // 仅仅做了 glBindBuffer的操作
            // vb->getHandle()仅仅是buffer的id
            GL_CHECK(ccBindBuffer(GL_ARRAY_BUFFER, vb->getHandle()));
            auto vboffset = _nextState->getVertexBufferOffset(i);
            const auto& attributes = _nextState->getProgram()->getAttributes();
            auto usedAttriLen = attributes.size();
            for (int j = 0; j < usedAttriLen; ++j)
            {
                const auto& attr = attributes[j];
                const auto* el = vb->getFormat().getElement(attr.hashName);
                if (!el || !el->isValid())
                {
                    RENDERER_LOGW("Can not find vertex attribute: %s", attr.name.c_str());
                    continue;
                }
                if (0 == _enabledAtrributes[attr.location])
                {
                    // 启用shader里面的attribute
                    GL_CHECK(ccEnableVertexAttribArray(attr.location));
                    _enabledAtrributes[attr.location] = 1;
                }
                _newAttributes[attr.location] = 1;
                // glVertexAttribPointer (GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
                GL_CHECK(ccVertexAttribPointer(attr.location,
                                      el->num,
                                      ENUM_CLASS_TO_GLENUM(el->type),
                                      el->normalize,
                                      el->stride,
                                      (GLvoid*)(el->offset + vboffset * el->stride)));
            }
        }
    }
}

查到这里,其实就要追查glGenBuffers的逻辑,但是这里很明显是没有的,那glGenBuffers的操作就一定是在其他地方,这里仅仅是直接拿来使用。

这里需要补充一个基础知识,在creator的架构里面,是存在一个非常大的buffer,所有的顶点数据都是存放在这个大Buffer里面,这样有一个好处,就是不用频繁的开辟内存。

glBufferData

这个函数是我们下一步追踪的重点函数,因为顶点数据就是通过这个函数和buffer建立联系的(OpenGL的基础),这部分的逻辑也是放在jsb触发的

image.png

我们全局搜glBufferDta,使用的地方很少,很容易我们就能看到是VertexBuffer有在使用,其实真正的顶点数据也正是在这个函数里面绑定的。

c++
复制代码
// 在MiddlewareMarco.h中定义的有最大容量相关的宏
// index buffer init capacity
#define INIT_INDEX_BUFFER_SIZE 1024000
// max vertex buffer size
#define MAX_VERTEX_BUFFER_SIZE 65535
// fill debug data max capacity
#define MAX_DEBUG_BUFFER_SIZE 409600
// type array pool min size
#define MIN_TYPE_ARRAY_SIZE 1024
bool VertexBuffer::init(DeviceGraphics* device, VertexFormat* format, Usage usage, const void* data, size_t dataByteLength, uint32_t numVertices)
{
    _bytes = _format->_bytes * numVertices;// 20*65535=1310700,会预申请一块非常大的内存
    // 我们看到了申请创建buffer的逻辑
    glGenBuffers(1, &_glID);
}
void VertexBuffer::update(uint32_t offset, const void* data, size_t dataByteLength)
{
    // 给vbo绑定数据,在init中申请了buffer
    glBufferData(GL_ARRAY_BUFFER, _bytes, (const GLvoid*)data, glUsage);
    // 这样绑定后,只需要持有这个buffer的handler,就能操作对应的内存数据
    // 这个data没有缓存,因为他是来自meshBuffer里面,所以排查方向转到了MeshBuffer
}
  • MeshBufer.h
c++
复制代码
  IOBuffer _vb;
void MeshBuffer::uploadVB()
{
    auto length = _vb.length();
    if (length == 0) return;
    auto glVB = _glVBArr[_bufferPos];
    // 数据来自_vb,buffer的数据类型是uint8_t,可能是要和js的ArrayBuffer同步
    glVB->update(0, _vb.getBuffer(), length); 
}
  • IOBuffer.h
C++
复制代码
class IOBuffer
{
public:
    uint8_t* _buffer = nullptr;
    IOBuffer (std::size_t defaultSize)
    {
        _bufferSize = defaultSize;
        _buffer = new uint8_t[_bufferSize]; // 在构造函数中有赋值
    }
    inline uint8_t* getBuffer () const
    {
        return _buffer; // 绑定数据的真正源头
    }
}

确定顶点数据的存放位置

一路反推glBufferData的data参数,发现就是来自上边的_buffer,再去看_buffer的赋值地方,就发现

image.png

顶点数据的来源如下:

c++
复制代码
auto mgr = middleware::MiddlewareManager::getInstance();
middleware::MeshBuffer* mb = mgr->getMeshBuffer(VF_XYUVC);
auto glvb = mb->getGLVB();
auto vb = mb->getVB().getBuffer();
auto ib = mb->getIB().getBuffer();

再看ccVertexAttribPointer确定顶点数据布局

  • a_position 8字节 x,y, offset为0
  • a_uv0 8字节,uv, offset为8
  • a_color 4字节,rgba, offset为16

所以数据布局格式为:

a_position a_uv0 a_color
8 8 4

一组数据一共20个字节

再次观察提交的顶点数量,发现内存顶点数据一个细节

正常的时候一直是6个,也就是2个三角形,发现换肤后,顶点变成了12个,

在上一步我们知道了顶点数据的内存地址,我观察了下该内存地址的数据

image.png

发现多了4个顶点,虽然我不知道这组数据是否和这个bug有关系,因为处理换肤顶点我有这样的逻辑:

c++
复制代码
memcpy(worldVerts, triangles.verts, triangles.vertCount * sizeof(middleware::V2F_T2F_C4B));

感觉可能是worldVerts可能发生变化,可能需要从大的bufferdata里面找到对应的数据,然后直接修改。

后续排查了下,发现其实和这个是没有关系的,worldVerts并不是那个大的renderBuffer,这样copy并不会带来额外的数据错乱,worldVerts仅仅也是一个中转的数据地址,最终会合并到MeshBuffer里面,所以问题不在这里。

这里虽然会多copy一些数据过来,但是用不用还是受indices的控制。

为什么会多提交一个Sprite

问题还是出在提交的顶点数量,为什么多了?

顶点的数量来自,这个是和InputAssembler有关联的

c++
复制代码
uint32_t InputAssembler::getPrimitiveCount() const
{
    if (-1 != _count)
        return _count;
    if (_indexBuffer)
        return _indexBuffer->getCount();
    assert(_vertexBuffer);
    return _vertexBuffer->getCount();
}

后来发现是inputAssembler的indices的长度发生了异常

c++
复制代码
void CCArmatureDisplay::dbRender()
{
    _curISegLen = 0;// 这个数值发生了变化
    traverseArmature(_armature);
    if (_preISegWritePos != -1)
    {
        // 这里面会更新inputAssembler的indices长度
        _assembler->updateIARange(_materialLen - 1, _preISegWritePos, _curISegLen);
    }
}

而在traverseArmature里面,的确是有处理_curISegLen

c++
复制代码
void CCArmatureDisplay::traverseArmature(Armature* armature, float parentOpacity)
{
    for (std::size_t i = 0, len = slots.size(); i < len; i++)
    {
        texture = slot->getTexture();// 问题就出在我hack了getTexture的实现
        if (!texture) continue;// 如果没有纹理就直接返回了,而我会一直返回我hack的纹理,导致indices被累加了
        // Record this turn index segmentation count,it will store in material buffer in the end.
        _curISegLen += triangles.indexCount;
    }
}

知道了原因,修复方法也变的很简单了,是否有纹理,应该以DragonBones的数据为基准,当有纹理了,再返回hack的纹理,修复后换肤就正常了

结尾

对Engine又更加熟悉了



目录
相关文章
|
存储 传感器 编解码
Android OpenGL 渲染图像读取哪家强
glReadPixels 是 OpenGL ES 的 API ,OpenGL ES 2.0 和 3.0 均支持。 使用非常方便,下面一行代码即可搞定,但是效率也是最低的。
1136 0
Android OpenGL 渲染图像读取哪家强
|
数据安全/隐私保护 开发者
OpenGL ES 多目标渲染(MRT)
Opengl ES连载系列
221 0
|
并行计算 C++
Opengl ES之YUV数据渲染
Opengl ES连载系列
131 0
|
缓存 算法 Java
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
1005 0
Android硬件加速(二)-RenderThread与OpenGL GPU渲染
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
OpenGL学习笔记(二):OpenGL语法、渲染管线以及具体实现过程详解
|
存储 缓存 Serverless
六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合
OpenGL 渲染技巧:深度测试、多边形偏移、 混合
260 0
六、OpenGL 渲染技巧:深度测试、多边形偏移、 混合
|
算法 开发者
五、OpenGL 渲染技巧:正背面剔除
OpenGL 渲染技巧:正背面剔除
355 0
五、OpenGL 渲染技巧:正背面剔除
|
API iOS开发 异构计算
三、OpenGL 渲染架构分析
OpenGL 渲染架构分析
317 0
三、OpenGL 渲染架构分析
|
异构计算 索引
OpenGL ES 内建变量以及多重纹理渲染计算
我们用GLSL来编写着色器代码时, 要了解他们的一些内建变量参数的含义. 本文就是对内建变量做一些介绍.
220 0
|
存储 缓存 算法
OpenGL图像渲染以及渲染问题解决方案
在绘制3D场景的时候,我们需要决定哪些部分是对观察者可见的,或者哪些部分是对观察者不可见的,对于不可见的部分,应该及早丢弃。例如在一个不透明的墙壁后,就不应该有渲染,这种情况叫做隐藏面消除(Hidden surface elimination).
544 0
OpenGL图像渲染以及渲染问题解决方案