什么是PBO?OpenGL ES 之 PBO 像素缓冲对象

关于Opengl的系列已经有较长的一段时间没有更新了,然而这个系列还远没有到完毕地步,后续至少还有关于Opengl矩阵变换、YUV与RGB互转、Opengl水印贴图、Opengl转场动画等主题文章。

断更的主要原因如果给自己找个借口的话可以说是工作比价忙,如果说的比较现实一点就是自己懒且没啥动力,毕竟写技术博客文章是一件时间成本投入很大,而收益产出极小的一件事情…

进入正题…

了解过Opengl的童鞋们都知道,在Opengl中存在这个各种O,例如VAO、VBO、FBO等,而出现各种各样的O一般都是因为考虑到性能的原因。

今天我们要介绍的主角PBO,它和之前我们介绍VBO很像,了解完PBO之后童鞋们可以对比一下PBO与VBO的差异点。

下面从两个方面介绍PBO,什么是PBO以及如何使用PBO。

本文首发于微信公总号号:思想觉悟

更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

什么是PBO

PBO(Pixel Buffer Object像素缓冲对象)。在了解什么是PBO之前,我们先来了解一下为什么会出现PBO这么一个东西?

所谓存在即合理,用发展的眼光看问题,PBO的出现肯定是为了替代某种东西,或者是为了解决某个问题。

在使用Opengl的时候经常需要在GPU和CPU之间传递数据,例如在使用Opengl将YUV数据转换成RGB数据时就需要先将YUV数据上传到GPU,一般使用函数glTexImage2D,处理完毕后再将RGB结果数据读取到CPU, 这时使用函数glReadPixels即可将数据取回。但是这两个函数都是比较缓慢的,特别是在数据量比较大的时候。PBO就是为了解决这个访问慢的问题而产生的。使用PBO交换数据图

PBO可以让我们通过一个内存指针,直接访问显存(GPU)的数据,我们将这块内存指针称作缓冲区。我们可以通过函数glMapBuffer得到它的内存指针,然后就对这块缓冲区的数据可以为所欲为了。

例如使用函数glReadPixels原本是要传一个内存指针进去的,但是有了缓冲区,它就可以把数据直接复制到缓冲区中去,而不是复制到内存条中去,这样就大大提高了数据的传递效率。

PBO的主要优点是通过直接内存访问的方式与显存进行快速像素数据传输,而无需占用CPU周期。PBO与DMA

可能看到这张图你没什么感觉,但是对比看看下面这张CPU与GPU直接传递数据的图你就会有所发现了:cpu与gpu常规数据交换

注意:PBO是OpenGL ES 3.0开始提供的一种方式,主要应用于从内存快速复制纹理到显存,或从显存复制像素数据到内存。

PBO的使用方式

既然PBO这么有效率,那么我们在什么情况下可能会用到PBO呢?有个常见的例子,例如我们在安卓上开发Camera应用录制视频时,如果需要用到x264进行软编码的话可能就会用到PBO, 首先我们将相机纹理图像送到Surface渲染显示,然后将Surface数据使用PBO的方式读取处理送到X264编码器中进行编码,当然在安卓上你也可以使用ImageReader…

下面我们来介绍下PBO的使用方式。

PBO的创建和初始化类似于VBO,但是在使用的时候需要用到GL_PIXEL_UNPACK_BUFFER GL_PIXEL_PACK_BUFFER 这两个令牌,其中GL_PIXEL_UNPACK_BUFFER绑定表示该PBO用于将像素数据从程序(CPU)传送到OpenGL中;绑定为GL_PIXEL_PACK_BUFFER表示该PBO用于从OpenGL中读回像素数据。

  1. Pbo创建

先上代码,跟着注释看

void PBOOpengl::initPbo() {

    int imgByteSize = imageWidth * imageHeight * 4; // RGBA

    glGenBuffers(1, &uploadPboId);
    // 绑定pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 设置pbo内存大小
    // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
    //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
    //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
    glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);

    glGenBuffers(1, &downloadPboId);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    LOGD("uploadPboId:%d---downloadPboId:%d---imgByteSize:%d", uploadPboId, downloadPboId,
         imgByteSize);
}

上面的代码创建了两个PBO,其中uploadPboId用于纹理上传,downloadPboId用于纹理下载。创建好PBO之后然后使用两个PBO专用的令牌进行绑定,之后就调用glBufferData给PBO分配缓冲区,当然,你也可以在使用的时候先进行绑定,然后重新调用glBufferData分配新的缓冲区。

  1. Pbo上传纹理

所谓上传纹理是值将纹理数据从CPU传递到OpenGL,使用Pbo上传纹理时需要先使用令牌GL_PIXEL_UNPACK_BUFFER绑定对应的PBO,然后才行使用PBO的缓冲区:

// 单个PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel");
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPbo();

    glGenTextures(1, &imageTextureId);

    // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
    // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);
    
    // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    // pixels参数传递空,后面会通过pbo更新纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

    // 生成mip贴图
    glGenerateMipmap(GL_TEXTURE_2D);

    int dataSize = width * height * 4;
    // 使用Pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
}

注释已经很详细了,就不多解析了,还是看不懂的私聊交流呗…

  1. Pbo下载纹理

所谓上传纹理是值将纹理数据从OpenGL中读取回CPU,与上传纹理一样,下载纹理也是需要先使用令牌绑定PBO才能使用,下载纹理使用的令牌是GL_PIXEL_PACK_BUFFER。 下面的代码作用是将Opengl的渲染结果使用PBO读取出来:

// 单PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];
    // 方法一 正常的glReadPixels读取 最简单
//    glReadPixels(0, 0, eglHelper->viewWidth, eglHelper->viewHeight,
//                 GL_RGBA, GL_UNSIGNED_BYTE, *data);

    // 方法二 pbo读取
    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    // 重新分配一下空间 如果必要,这里就懒得判断了
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

其实无论有没有使用PBO都是使用的函数glReadPixels进行读取,但是他们的参数却是不一样的,你发现细节了吗?如果是在PBO模式下使用glReadPixels,则会自动将结果发送到PBO的缓冲区, 并不会像单独使用glReadPixels那样需要阻塞进行等待返回,因此效率就高了…

双PBO

PBO的另一个优势是它还具备异步DMA(Direct Memory Access)传输,也正因为这个特性,使得在使用单个PBO的情况下,在一些机型上可能性能提升并不明显,所以通常需要两个PBO配合使用。

图片
双PBO上传纹理数据
图片
双PBO获取纹理数据

通过上面这两张图我们可以看到利用交替使用双PBO的方式可以将PBO的快速特性更进一步。

下面的代码展示了如何通过双PBO上传纹理和下载渲染结果:

使用双PBO上传纹理数据

// 双PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel  uploadPboIndex:%d",uploadPboIndex);
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPboArray();

    if(imageTextureId == 0){

        glGenTextures(1, &imageTextureId);

        // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
        // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
        glActiveTexture(GL_TEXTURE2);
        glUniform1i(textureSampler, 2);

        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, imageTextureId);
        // 为当前绑定的纹理对象设置环绕、过滤方式
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // pixels参数传递空,后面会通过pbo更新纹理数据
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

        // 生成mip贴图
        glGenerateMipmap(GL_TEXTURE_2D);
    }

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    int dataSize = width * height * 4;
    // 使用Pbo
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + uploadPboIndex % NUM_PBO ));
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
    // 索引自加
    uploadPboIndex++;
}

使用双PBO下载渲染结果:


// 双PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];

    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + downloadPboIndex % NUM_PBO));
    // 重新分配一下空间
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    // Pbo索引自加
    downloadPboIndex++;
}

下面我们使用双PBO上传纹理和双PBO下载实现一个纹理动态切换和下载渲染结果的小demo,完整代码如下:

PBOOpengl.h

#ifndef NDK_OPENGLES_LEARN_PBOOPENGL_H
#define NDK_OPENGLES_LEARN_PBOOPENGL_H

#include "BaseOpengl.h"

static const int NUM_PBO = 2;

class PBOOpengl: public BaseOpengl{

public:
    PBOOpengl();
    virtual ~PBOOpengl();
    // override要么就都写,要么就都不写,不要一个虚函数写override,而另外一个虚函数不写override,不然可能编译不过
    virtual void onDraw() override;
    virtual void setPixel(void *data, int width, int height, int length) override;
    virtual void readPixel(uint8_t **data,int *width,int *height) override;
private:
    void initPbo();
    void initPboArray();
    GLint positionHandle{-1};
    GLint textureHandle{-1};
    GLuint vbo{0};
    GLuint vao{0};
    GLuint ebo{0};
    // 本身图像纹理id
    GLuint imageTextureId{0};
    GLint textureSampler{-1};
    int imageWidth{0};
    int imageHeight{0};
    // 上传纹理的pbo
    GLuint uploadPboId{0};
    // cpu下载纹理的pbo
    GLuint downloadPboId{0};

    // 上传纹理的pbo
    GLuint *uploadPboIds{nullptr};
    // cpu下载纹理的pbo
    GLuint *downloadPboIds{nullptr};

    // Pbo 的索引,用于双PBO时
    int uploadPboIndex{0};
    int downloadPboIndex{0};

  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
};


#endif //NDK_OPENGLES_LEARN_PBOOPENGL_H

PBOOpengl.cpp:

#include "PBOOpengl.h"

#include "../utils/Log.h"

// 顶点着色器
static const char *ver = "#version 300 esn"
                         "in vec4 aPosition;n"
                         "in vec2 aTexCoord;n"
                         "out vec2 TexCoord;n"
                         "void main() {n"
                         "  TexCoord = aTexCoord;n"
                         "  gl_Position = aPosition;n"
                         "}";

// 片元着色器
static const char *fragment = "#version 300 esn"
                              "precision mediump float;n"
                              "out vec4 FragColor;n"
                              "in vec2 TexCoord;n"
                              "uniform sampler2D ourTexture;n"
                              "void main()n"
                              "{n"
                              "    FragColor = texture(ourTexture, TexCoord);n"
                              "}";

const static GLfloat VERTICES_AND_TEXTURE[] = {
        0.5f, -0.5f, // 右下
        // 纹理坐标
        1.0f, 1.0f,
        0.5f, 0.5f, // 右上
        // 纹理坐标
        1.0f, 0.0f,
        -0.5f, -0.5f, // 左下
        // 纹理坐标
        0.0f, 1.0f,
        -0.5f, 0.5f, // 左上
        // 纹理坐标
        0.0f, 0.0f
};

// 真正的纹理坐标在图片的左下角
const static GLfloat FBO_VERTICES_AND_TEXTURE[] = {
        1.0f, -1.0f, // 右下
        // 纹理坐标
        1.0f, 0.0f,
        1.0f, 1.0f, // 右上
        // 纹理坐标
        1.0f, 1.0f,
        -1.0f, -1.0f, // 左下
        // 纹理坐标
        0.0f, 0.0f,
        -1.0f, 1.0f, // 左上
        // 纹理坐标
        0.0f, 1.0f
};

// 使用byte类型比使用short或者int类型节约内存
const static uint8_t indices[] = {
        // 注意索引从0开始!
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形
        0, 1, 2, // 第一个三角形
        1, 2, 3  // 第二个三角形
};

PBOOpengl::PBOOpengl():uploadPboIds(nullptr),downloadPboIds(nullptr) {
    initGlProgram(ver, fragment);
    positionHandle = glGetAttribLocation(program, "aPosition");
    textureHandle = glGetAttribLocation(program, "aTexCoord");
    textureSampler = glGetUniformLocation(program, "ourTexture");
    LOGD("program:%d", program);
    LOGD("positionHandle:%d", positionHandle);
    LOGD("textureHandle:%d", textureHandle);
    LOGD("textureSample:%d", textureSampler);
    // VAO
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

    // vbo
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(VERTICES_AND_TEXTURE), VERTICES_AND_TEXTURE,
                 GL_STATIC_DRAW);

    // stride 步长 每个顶点坐标之间相隔4个数据点,数据类型是float
    glVertexAttribPointer(positionHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) 0);
    // 启用顶点数据
    glEnableVertexAttribArray(positionHandle);
    // stride 步长 每个颜色坐标之间相隔4个数据点,数据类型是float,颜色坐标索引从2开始
    glVertexAttribPointer(textureHandle, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
                          (void *) (2 * sizeof(float)));
    // 启用纹理坐标数组
    glEnableVertexAttribArray(textureHandle);

    // EBO
    glGenBuffers(1, &ebo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 这个顺序不能乱啊,先解除vao,再解除其他的,不然在绘制的时候可能会不起作用,需要重新glBindBuffer才生效
    // vao解除
    glBindVertexArray(0);
    // 解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // 解除绑定
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

    LOGD("program:%d", program);
    LOGD("positionHandle:%d", positionHandle);
    LOGD("colorHandle:%d", textureHandle);
}

void PBOOpengl::initPbo() {

    int imgByteSize = imageWidth * imageHeight * 4; // RGBA

    glGenBuffers(1, &uploadPboId);
    // 绑定pbo
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
    // 设置pbo内存大小
    // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
    //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
    //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
    glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
    glGenBuffers(1, &downloadPboId);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
    glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
    // 解除绑定
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    LOGD("uploadPboId:%d---downloadPboId:%d---imgByteSize:%d", uploadPboId, downloadPboId,
         imgByteSize);
}

void PBOOpengl::initPboArray() {
    int imgByteSize = imageWidth * imageHeight * 4; // RGBA
    if(nullptr == uploadPboIds){
        uploadPboIds = new GLuint[NUM_PBO];
        glGenBuffers(NUM_PBO, uploadPboIds);
        for (int i = 0; i < NUM_PBO; ++i) {
            // 绑定pbo
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + i));
            // 设置pbo内存大小
            // 这一步十分重要,第2个参数指定了这个缓冲区的大小,单位是字节,一定要注意
            //  然后第3个参数是初始化用的数据,如果你传个内存指针进去,这个函数就会把你的
            //  数据复制到缓冲区里,我们这里一开始并不需要什么数据,所以传个nullptr就行了
            glBufferData(GL_PIXEL_UNPACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
            glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
        }
    }

    if(nullptr == downloadPboIds){
        downloadPboIds = new GLuint[NUM_PBO];
        glGenBuffers(NUM_PBO, downloadPboIds);
        for (int i = 0; i < NUM_PBO; ++i) {
            // 绑定pbo
            glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + i));
            glBufferData(GL_PIXEL_PACK_BUFFER, imgByteSize, nullptr, GL_STREAM_DRAW);
            glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        }
    }

}

PBOOpengl::~PBOOpengl() {
    glDeleteBuffers(1, &ebo);
    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
    if(nullptr != uploadPboIds){
        glDeleteBuffers(NUM_PBO,uploadPboIds);
        delete [] uploadPboIds;
        uploadPboIds = nullptr;
    }
  // 本文首发于微信公总号号:思想觉悟
    // 更多关于音视频、FFmpeg、Opengl、C++的原创文章请关注微信公众号:思想觉悟
    if(nullptr != downloadPboIds){
        glDeleteBuffers(NUM_PBO,downloadPboIds);
        delete [] downloadPboIds;
        downloadPboIds = nullptr;
    }
}

void PBOOpengl::onDraw() {

    // 绘制屏幕宽高
    glViewport(0, 0, eglHelper->viewWidth, eglHelper->viewHeight);

    // 绘制到屏幕
    // 清屏
    glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glUseProgram(program);

    // 激活纹理
    glActiveTexture(GL_TEXTURE2);
    glUniform1i(textureSampler, 2);

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    // VBO与VAO配合绘制
    // 使用vao
    glBindVertexArray(vao);
    // 使用EBO
// 使用byte类型节省内存
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, (void *) 0);
    glUseProgram(0);
    // vao解除绑定
    glBindVertexArray(0);

    // 禁用顶点
    glDisableVertexAttribArray(positionHandle);
    if (nullptr != eglHelper) {
        eglHelper->swapBuffers();
    }

    glBindTexture(GL_TEXTURE_2D, 0);
}
//
//// 单个PBO测试
//void PBOOpengl::setPixel(void *data, int width, int height, int length) {
//    LOGD("texture setPixel");
//    imageWidth = width;
//    imageHeight = height;
//    // Pbo初始化
//    initPbo();
//
//    glGenTextures(1, &imageTextureId);
//
//    // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
//    // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
////    glActiveTexture(GL_TEXTURE0);
////    glUniform1i(textureSampler, 0);
//
//// 例如,一样的
//    glActiveTexture(GL_TEXTURE2);
//    glUniform1i(textureSampler, 2);
//
//    // 绑定纹理
//    glBindTexture(GL_TEXTURE_2D, imageTextureId);
//    // 为当前绑定的纹理对象设置环绕、过滤方式
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
//    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//    // pixels参数传递空,后面会通过pbo更新纹理数据
//    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
//
//    // 生成mip贴图
//    glGenerateMipmap(GL_TEXTURE_2D);
//
//    int dataSize = width * height * 4;
//    // 使用Pbo
//    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, uploadPboId);
//    // 将纹理数据拷贝进入缓冲区
//    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
//                                                   dataSize,
//                                                   GL_MAP_WRITE_BIT);
//    if (bufPtr) {
//        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
//        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
//    }
//    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
//    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
//    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
//    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
//    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
//    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
//    // Pbo解除
//    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
//    // 解绑定
//    glBindTexture(GL_TEXTURE_2D, 0);
//}



// 双PBO测试
void PBOOpengl::setPixel(void *data, int width, int height, int length) {
    LOGD("texture setPixel  uploadPboIndex:%d",uploadPboIndex);
    imageWidth = width;
    imageHeight = height;
    // Pbo初始化
    initPboArray();

    if(imageTextureId == 0){

        glGenTextures(1, &imageTextureId);

        // 激活纹理,注意以下这个两句是搭配的,glActiveTexture激活的是那个纹理,就设置的sampler2D是那个
        // 默认是0,如果不是0的话,需要在onDraw的时候重新激活一下?
//    glActiveTexture(GL_TEXTURE0);
//    glUniform1i(textureSampler, 0);

// 例如,一样的
        glActiveTexture(GL_TEXTURE2);
        glUniform1i(textureSampler, 2);

        // 绑定纹理
        glBindTexture(GL_TEXTURE_2D, imageTextureId);
        // 为当前绑定的纹理对象设置环绕、过滤方式
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        // pixels参数传递空,后面会通过pbo更新纹理数据
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

        // 生成mip贴图
        glGenerateMipmap(GL_TEXTURE_2D);
    }

    // 绑定纹理
    glBindTexture(GL_TEXTURE_2D, imageTextureId);

    int dataSize = width * height * 4;
    // 使用Pbo
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, *(uploadPboIds + uploadPboIndex % NUM_PBO ));
    // 将纹理数据拷贝进入缓冲区
    GLubyte *bufPtr = (GLubyte *) glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0,
                                                   dataSize,
                                                   GL_MAP_WRITE_BIT);
    if (bufPtr) {
        memcpy(bufPtr, data, static_cast<size_t>(dataSize));
        glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER);
    }
    // 将pbo缓冲区中的数据拷贝到纹理,调用 glTexSubImage2D 后立即返回,不影响 CPU 时钟周期
    // 这个函数会判断 GL_PIXEL_UNPACK_BUFFER 这个地方有没有绑定一个缓冲区
    //   如果有,就从这个缓冲区读取数据,而不是data参数指定的那个内存
    // 这样glTexSubImage2D就会从我们的缓冲区中读取数据了
    // 这里为什么要用glTexSubImage2D呢,因为如果用glTexImage2D,glTexImage2D会销毁纹理内存重新申请,glTexSubImage2D就仅仅只是更新纹理中的数据,这就提高了速度,并且优化了显存的利用率
    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    // Pbo解除
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    // 解绑定
    glBindTexture(GL_TEXTURE_2D, 0);
    // 索引自加
    uploadPboIndex++;
}

//// 单PBO读取测试
//void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {
//
//    *width = eglHelper->viewWidth;
//    *height = eglHelper->viewHeight;
//    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
//    *data = new uint8_t[dataSize];
//    // 方法一 正常的glReadPixels读取 最简单
////    glReadPixels(0, 0, eglHelper->viewWidth, eglHelper->viewHeight,
////                 GL_RGBA, GL_UNSIGNED_BYTE, *data);
//
//    // 方法二 pbo读取
//    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
//    glBindBuffer(GL_PIXEL_PACK_BUFFER, downloadPboId);
//    // 重新分配一下空间
//    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
//    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
//    // 前4个参数就是要读取的屏幕区域,不多解释
//    //  格式是RGB,类型是BYTE,每个像素1字节
//    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
//    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
//    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里
//
//    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
//    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
//    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
//    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
//                                                              GL_MAP_READ_BIT));
//    if (nullptr != mapPtr)
//    {
//        LOGD("readPixel 数据拷贝");
//        // 拷贝数据
//        memcpy(*data,mapPtr,dataSize);
//        // 解除Map
//        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
//    } else{
//        LOGD("readPixel glMapBufferRange null");
//    }
//    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
//    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
//}

// 双PBO读取测试
void PBOOpengl::readPixel(uint8_t **data,int *width,int *height) {

    *width = eglHelper->viewWidth;
    *height = eglHelper->viewHeight;
    int dataSize = eglHelper->viewWidth * eglHelper->viewHeight * 4;
    *data = new uint8_t[dataSize];

    // 首先我们要把缓冲区绑定到 GL_PIXEL_PACK_BUFFER 这个地方
    // 指针运算你要会一点呀
    glBindBuffer(GL_PIXEL_PACK_BUFFER, *(downloadPboIds + downloadPboIndex % NUM_PBO));
    // 重新分配一下空间
    glBufferData(GL_PIXEL_PACK_BUFFER, dataSize, nullptr, GL_STREAM_DRAW);
    // 这个函数会判断 GL_PIXEL_PACK_BUFFER 这个地方有没有绑定一个缓冲区,如果有,那就把数据写入到这个缓冲区里
    // 前4个参数就是要读取的屏幕区域,不多解释
    //  格式是RGB,类型是BYTE,每个像素1字节
    // 如果GL_PIXEL_PACK_BUFFER有绑定缓冲区,最后一个参数就作为偏移值来使用,传nullptr就行
    glReadPixels(0, 0, *width, *height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    // 好了我们已经成功把屏幕的像素数据复制到了缓冲区里

    // 这时候,你可以用 glMapBuffer 得到缓冲区的内存指针,来读取里面的像素数据,保存到图片文件
    // 注意glMapBuffer的第1个参数不一定要是GL_PIXEL_PACK_BUFFER,你可以把缓冲区绑定到比如上面init函数的GL_ARRAY_BUFFER
    //  然后这里也传GL_ARRAY_BUFFER,由于懒得再绑定一次,就接着用上面绑定的GL_PIXEL_PACK_BUFFER吧
    GLubyte *mapPtr = static_cast<GLubyte *>(glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, dataSize,
                                                              GL_MAP_READ_BIT));
    if (nullptr != mapPtr)
    {
        LOGD("readPixel 数据拷贝");
        // 拷贝数据
        memcpy(*data,mapPtr,dataSize);
        // 解除Map
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
    } else{
        LOGD("readPixel glMapBufferRange null");
    }
    // 完事了把GL_PIXEL_PACK_BUFFER这个地方的缓冲区解绑掉,以免别的函数误操作
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    // Pbo索引自加
    downloadPboIndex++;
}

运行结果:

图片

通过点击按钮可以实现使用双PBO的方式下载纹理数据。

关于PBO的坑

以下是笔者在网上看到某博主总结的关于PBO的坑,由于设备有限,笔者也没有进行过严格的对比测试,这里引用原文仅供记录参考:

然而PBO还有一个非常坑的地方,经测试表明,在部分硬件上glMapBufferRange映射出来的Buffer拷贝极为耗时,可以高达30+ms,这对于音视频处理显然是不能接受的。通常,映射出来的是一个DirectByteBuffer,也是一个堆外内存(C内存),这部分内存本身只能通过Buffer.get(byte[])拷贝来拿到数据,但正常情况下只需要2-3ms。出现这种问题估计是硬件上留下的坑。 所以,在Android上使用PBO是有比较多的兼容性问题的,包括上面说的。正确使用PBO的方式是,首先判断是否支持PBO,如果支持,则还是先使用glReadPixels进行读取测试,记录平均耗时,然后再使用PBO进行读取测试,记录平均耗时,最后对比两个方式的耗时,选择最快的一个。这样动态处理是比较复杂的,然而在这种情况下你不得不这样做。那么有没有一种既简单又高效的方式呢?

参考

http://www.songho.ca/opengl/gl_pbo.html

— END —

进技术交流群,扫码添加我的微信:Byte-Flow 

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/5198.html

(0)
字节流动的头像字节流动认证作者
0 0

相关推荐

  • OpenglEs之三角形绘制 技术文章

    OpenglEs之三角形绘制

    在前面我们已经在NDK层搭建好了EGL环境,也介绍了一些着色器相关的理论知识,那么这次我们就使用已经搭配的EGL绘制一个三角形吧。

    思想觉悟的头像 思想觉悟
    2022年11月23日
  • 一看就懂的 OpenGL 基础概念丨音视频基础 实时音视频

    一看就懂的 OpenGL 基础概念丨音视频基础

    这篇文章是音视频基础专栏系列关于渲染的第一篇文章,我们来聊一聊 OpenGL,希望能做到让没接触过 OpenGL 的同学能比较容易的建立起一个初步的印象。 这篇文章的内容包括: 常…

    RTC观主的头像 RTC观主
    2022年9月28日
  • Opengl ES之着色器 技术文章

    Opengl ES之着色器

    在前面我们介绍了 OpenglEs之EGL环境搭建 ,在后面的例子中,我们将无可避免地需要使用到着色器。而着色器才是Opengl的灵魂所在,有了着色器才有了Opengl天马行空的世界。

    思想觉悟的头像 思想觉悟
    2022年11月23日
  • OpenglEs之EGL环境搭建 技术文章

    OpenglEs之EGL环境搭建

    今天我们的主题依然是音视频开发的范畴,做过音视频开发的都知道Opengl也是音视频开发中的一项重要技能,特别是涉及到视频录制、特效处理、画质渲染细分功能。因此后续笔者打算再出一系列的Opengl ES的学习笔记,
    希望能与大家共同温故知新。

    思想觉悟的头像 思想觉悟
    2022年11月23日
  • 一看就懂的 OpenGL 基础概念(4):各种 O 之 FBO丨音视频基础 实时音视频

    一看就懂的 OpenGL 基础概念(4):各种 O 之 FBO丨音视频基础

    在前面的文章里,我们介绍了 OpenGL 在图形渲染应用中的角色,OpenGL 的渲染架构、状态机、渲染管线,以及 OpenGL 要在设备上实现渲染的桥梁 EDL,OpenGL 开…

    RTC观主的头像 RTC观主
    2022年11月22日
  • 微软通过 Direct3D 12 支持 OpenGL 4.6 行业资讯

    微软通过 Direct3D 12 支持 OpenGL 4.6

    太快了太快了,微软将其 Mesa Direct3D 12 代码从 OpenGL 4.3 升级到 OpenGL 4.4,不久后又升级到 OpenGL 4.5。现在,微软成功地在 Di…

    追风者的头像 追风者
    2023年11月20日

发表回复

登录后才能评论

热门文章

  • 什么是弱网?浅谈弱网评测
    26.3K

    什么是弱网?浅谈弱网评测

  • 显示器中的HDR10、HDR400、HDR600有什么区别?

    显示器中的HDR10、HDR400、HDR600有什么区别?

    18.7K
  • 使用 FFmpeg 轻松调整视频的大小/缩放/更改分辨率

    使用 FFmpeg 轻松调整视频的大小/缩放/更改分辨率

    14.5K
  • WebRTC 教程3:WebRTC如何在浏览器中启用/禁用

    WebRTC 教程3:WebRTC如何在浏览器中启用/禁用

    13.8K
  • 如何在 Chrome、Firefox、Safari 和 Opera 中禁用 WebRTC

    如何在 Chrome、Firefox、Safari 和 Opera 中禁用 WebRTC

    13.5K
  • 音频编解码器比较 – MP3、AAC、AC3、WAV、WMA 和 Opus

    音频编解码器比较 – MP3、AAC、AC3、WAV、WMA 和 Opus

    13.0K
  • HDR中的PQ和HLG曲线有何不同?

    HDR中的PQ和HLG曲线有何不同?

    11.8K
  • chrome 如何开启HEVC硬件解码

    chrome 如何开启HEVC硬件解码

    11.7K
  • AVC 和 HEVC 的区别(AVC和HEVC哪个好)

    AVC 和 HEVC 的区别(AVC和HEVC哪个好)

    10.7K
  • AV1编码及AV1的码流结构

    AV1编码及AV1的码流结构

    9.9K

玻璃钢生产厂家湖南酒店艺术玻璃钢雕塑工厂来凤玻璃钢雕塑设计价格仙林美陈商场广州市玻璃钢雕塑商场美陈布景黄南商场美陈信阳玻璃钢彩绘雕塑设计广西玻璃钢花盆厂家广州现代玻璃钢卡通雕塑玻璃钢卡通雕塑公仔定做上海拉丝玻璃钢雕塑询问报价室外校园玻璃钢景观雕塑定做青海景区玻璃钢雕塑哪家好沈阳园林玻璃钢雕塑制作浙江常见商场美陈采购山东玻璃钢小品雕塑安装玻璃钢红色主题雕塑杭州主题商场美陈商场美陈灯饰画深圳玻璃钢卡通雕塑品玻璃钢铜鼓雕塑图片大全四川常用商场美陈有哪些商洛玻璃钢门头雕塑湖滨玻璃钢雕塑价格深圳玻璃钢松鼠雕塑商场冬季美陈吊饰铜陵定制玻璃钢雕塑武都玻璃钢雕塑价格成都抽象玻璃钢雕塑价位阜阳环保玻璃钢雕塑定制香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化