惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
C
Cyber Attacks, Cyber Crime and Cyber Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
A
Arctic Wolf
Security Latest
Security Latest
Simon Willison's Weblog
Simon Willison's Weblog
I
Intezer
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Troy Hunt's Blog
Latest news
Latest news
Help Net Security
Help Net Security
S
Security Affairs
Webroot Blog
Webroot Blog
The Hacker News
The Hacker News
AI
AI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Tor Project blog
Forbes - Security
Forbes - Security
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
Attack and Defense Labs
Attack and Defense Labs
P
Proofpoint News Feed
www.infosecurity-magazine.com
www.infosecurity-magazine.com
H
Help Net Security
L
Lohrmann on Cybersecurity
S
SegmentFault 最新的问题
Google Online Security Blog
Google Online Security Blog
MongoDB | Blog
MongoDB | Blog
Cyberwarzone
Cyberwarzone
The Last Watchdog
The Last Watchdog
S
Securelist
N
News and Events Feed by Topic
S
Secure Thoughts
F
Fortinet All Blogs
博客园_首页
C
Cybersecurity and Infrastructure Security Agency CISA
量子位
M
MIT News - Artificial intelligence
F
Full Disclosure
T
The Blog of Author Tim Ferriss
T
Tailwind CSS Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ
P
Privacy International News Feed
L
LangChain Blog
Know Your Adversary
Know Your Adversary
C
CERT Recently Published Vulnerability Notes

音视频开发进阶

音视频教程-第三节 音视频教程-第二节 真的,AI 可能就是新时代的信息差 充值 Cursor 之后,工作有了哪些变化?🤔 个人'蒸馏'大模型能做哪些有意思的事情 DeepSeek 大模型在 Mac 上的部署和运行 音视频教程-第一节 【WebRTC 专栏】-- Android 开发集成 WebRTC 库的几种方式 【WebRTC 专栏】-- 在 Mac M1 等系列芯片编译和开发 WebRTC-Android 库 Meta Llama3 大模型在 Mac 上的部署和运行 iOS VideoToolBox 解码 HEVC Open-GOP 视频的问题排查 Flutter 状态管理之 InheritedWidget 使用和分析 用 ChatGPT 回答技术问题怎么样 ? 音视频开发系统入门大致路线 UE 4.27 添加自定义 ShadingModel 用 UE4 虚幻引擎做个捏脸小功能~~ UE4 材质练习 之 凹凸贴图偏移的使用 UE4 材质练习系列基础 OpenGL上下文创建以及共享机制 007 | 播放器系列专栏-解析 MP4 文件读取信息 006 | 播放器系列专栏-在 Mac 上查看 MP4 格式信息 干货 | 快速抽取缩略图是怎么练成的? 关于直播、WebRTC、FFmpeg 的那些事 005 | 播放器系列专栏-在 Windows 上查看 MP4 格式信息 将音视频中的花屏、绿屏、黑屏问题一网打尽 关于音视频里面的解码帧率和渲染帧率 004 | 播放器系列专栏-认识MP4视频(下) 003 | 播放器系列专栏-认识MP4视频(上) 入门或者转行音视频,应该要怎么做? H264视频文件如何缩放分辨率 002 | 播放器系列专栏-FFmpeg依赖库的配置 001 | 播放器系列专栏-关于播放器项目的一个小实践 Seek策略以及在有B帧情况下的处理 目前流媒体开发工程师工作内容主要是什么? 一个音视频领域专业问答的小圈子 干货收藏 || Vulkan Game Engine 视频教程 音视频春节假期内卷指南(实操) Vulkan 在 FFmpeg 中的支持 Windows 下 FFmpeg 和 LibX264 的编译和配置 Metal 开发 | 使用 C++ 进行接口调用 音视频开发工作经验分享 || 视频版 FFmpeg 调用 MediaCodec 硬解码到 Surface 上 代码吸猫 | 用 OpenGL 图像渲染的养猫计划 百倍变速--解码到底能不能丢 非参考帧 ?FFmpeg 有话说!!! 老生常谈-FFmpeg 的编译问题轻松搞定 FFmpeg 调用 Android MediaCodec 进行硬解码(附源码) 【WebRTC 专栏】--创建相机预览 Unity Shader 光照基础之 Half Lambert 光照模型 Unity Shader 光照基础之Lambert光照模型 Unity Shader 光照基础内容 Unity Shader 显示一张图片纹理 UnityShader 的基本概念 Unity 物体的基本操作 C++ 模板系列小结07-尾置返回类型 C++ 模板系列小结06-可变参数模板特性 C++ 中的多线程的使用和线程池建设 C++ 模板系列小结05-模板类型作为模板参数 C++ 模板系列小结04-类模板中的成员模板 C++ 模板系列小结03-在模板中指定变量类型 C++ 模板系列小结02-非类型模板参数 C++ 模板系列小结01-函数模板和类模板 从零打造渲染引擎系列01-什么是渲染引擎 iOS开发 - 在 Swift 中去调用 C/C++ 代码 2021 技术新番 - 从零打造渲染引擎系列 iOS 音视频开发的一些基础准备工作 音视频交流群又来啦~~~ 【WebRTC 专栏】WebRTC & Android 开发学习环境搭建~ 【喜大普奔】域名终于备案通过啦 Shader 优化 | OpenGL 绘制网格效果 【音视频连载-011】第二季 FFmpeg 一层一层获取文件信息 KodeLife | Shader 实时编辑预览的强大工具使用实践 推荐几个堪称教科书级别的 Android 音视频入门项目 【音视频连载-010】第二季 FFmpeg 日志打印 【音视频连载-008】基础学习篇-SDL 播放 PCM 音频文件(下) 【音视频连载-007】基础学习篇-SDL 播放 PCM 音频文件(上) 【音视频连载-006】基础学习篇-SDL 播放 YUV 视频文件 【音视频连载-005】基础学习篇-SDL 加载 YUV 文件并显示 【音视频连载-004】基础学习篇-SDL 加载图片并显示 【音视频连载-003】基础学习篇-SDL 消息循环和事件响应 【音视频连载-002】基础学习篇-SDL 创建窗口并显示颜色 【音视频连载-001】基础学习篇- SDL 介绍以及工程配置 LearnOpenGL 源码在 MAC 上的编译与调试 2019 年终总结与回顾 Android NDK 开发的免费技术视频来啦~~ OpenGL 实现视频编辑中的转场效果 OpenGL 实践之贝塞尔曲线绘制 图像库 libjpeg-turbo 编译与实践 图像库 libpng 编译与实践 rust 开发编译 Android 动态库实践 Android NDK 开发 —— 从 Assets 文件夹加载图片并上传纹理 简单易用的图像解码库介绍 —— stb_image 博客图床迁移记 进击的 Vulkan 移动开发之 SwapChain 进击的 Vulkan 移动开发之 Command Buffer 进击的 Vulkan 移动开发之 Instance & Device & Queue 进击的 Vulkan 移动开发(一)之今生前世 Java 显式锁 Lock 与条件队列 C++ 标准容器库小结 一文读懂 YUV 的采样与格式 《OpenGL ES 3.x 游戏开发》碰撞检测之 AABB 包围盒
《OpenGL ES 3.x 游戏开发》顶点缓冲区 VBO 的使用
音视频开发进阶 · 2018-07-17 · via 音视频开发进阶

一个专注音视频领域的小圈子

在之前的绘制过程中,首先都需要将物体的顶点数据保存在内存中,然后 glDrawArraysglDrawElements 绘制前,将顶点数据送入到显存中,这样会存在 I/O 开销较大的问题,性能也不够好。 可以将顶点数据存放在顶点缓冲区中,就不需要在每次绘制前把顶点数据复制进显存,而是在初始化顶点缓冲区对象时一次性将顶点数据送入显存,每次绘制时直接使用显存中的数据,可以大大提高渲染性能。

顶点缓冲区

在 OpenGL ES 中支持两种类型的顶点缓冲区对象,分别是数组缓冲区对象(Array Buffer)和 元素数组缓冲区对象(Element Array Buffer)。

  • 数组缓冲区对象对应的类型为 GL_ARRAY_BUFFER,一般用于存放待绘制物体的顶点相关数据,如顶点坐标、纹理坐标、法向量等。
  • 元素数组缓冲区对象对应的类型为 GL_ELEMENT_ARRAY_BUFFER ,一般用于存放图元的组装索引数据,在使用索引法进行绘制时使用。

使用顶点缓冲区的好处就在于顶点数量非常多的时候,可以一次性将大量顶点数据送入到显存中。

关于顶点缓冲区,又有另一种说法,叫做 VBO,其实就是 Vertex Buffer Object的缩写啦。

缓冲区对象的常用操作方法

下面介绍一下关于顶点缓冲区的常用方法。

创建缓冲区对象 – glGenBuffers

glGenBuffers 方法用于创建缓冲对象,是在使用每个自定义缓冲前都需要调用的。

	// 方法一
    public static native void glGenBuffers(
        int n,
        java.nio.IntBuffer buffers
    );
    // 方法二
    public static native void glGenBuffers(
        int n,
        int[] buffers,
        int offset
    );
  1. 在方法一中,参数 n 为需要创建的缓冲区数量,参数 buffers 为用于存放创建的 n 个缓冲区编号的 IntBuffer 。
  2. 在方法二中,参数 n 为需要创建的缓冲区数量,参数 buffers 为用于存放创建的 n 个缓冲区编号的数组,参数 offset 为 buffers 数组所需的偏移量。

绑定缓冲区对象 – glBindBuffer

glBindBuffer 方法用于绑定当前缓冲区对象,第一次调用 glBindBuffer 方法绑定缓冲区对象时,缓冲区对象以默认状态分配;如果分配成功,则分配的对象绑定为当前缓冲区对象。

    public static native void glBindBuffer(
        int target,
        int buffer
    );

其中,参数 target 用于描述需绑定的缓冲区类型,参数 buffer 为需要绑定的缓冲区编号。

target 值如下:

target 值 缓冲区类型
GL_ARRAY_BUFFER 数组缓冲
GL_ELEMENT_ARRAY_BUFFER 元素数组缓冲
GL_COPY_READ_BUFFER 复制只读缓冲
GL_COPY_WRITE_BUFFER 复制可写缓冲
GL_PIXEL_PACK_BUFFER 像素打包缓冲
GL_PIXEL_UNPACK_BUFFER 像素解包缓冲
GL_TRANSFORM_FEEDBACK_BUFFER 变换反馈缓冲
GL_UNIFORM_BUFFER 一致变量缓冲

向缓冲区送入数据 – glBufferData 和 glBufferSubData

glBufferData 方法一般用于向指定缓冲中送入数据,也可以用于对指定缓冲进行相关存储空间初始化。

  public static native void glBufferData(
        int target,
        int size,
        java.nio.Buffer data,
        int usage
    );

其中,target 用于描述指定的缓冲区类型,如上表所示;参数 size 用于给出缓冲区的大小(单位为字节),参数 data 为需要送入缓冲的数据,若没有数据要送入缓冲区,其值可以为 null;参数 usage 用于指定缓冲区的用途,可取的值如下表:

缓冲区用途可选参数值 参数说明
GL_STATIC_DRAW 在绘制时,缓冲区对象数据可以被修改一次,使用多次
GL_STATIC_READ 在 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用多次,且该数据可以冲应用程序中查询
GL_STATIC_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用多次,该数据将直接作为绘制图元或者指定图像的信息来源
GL_DYNAMIC_DRAW 在绘制时,缓冲区对象数据可以被重复修改、使用多次
GL_DYNAMIC_READ 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被重复修改、使用多次,且该数据可以从应用程序中查询
GL_DYNAMIC_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被重复修改、使用多次,该数据将直接作为绘制图元或者指定图像的信息来源
GL_STREAM_DRAW 在绘制时,缓冲区对象数据可以被修改一次,使用少数几次
GL_STREAM_READ 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,且该数据可以从应用程序中查询
GL_STREAM_COPY 从 OpenGL ES 中读回的数据,缓冲区对象数据可以被修改一次,使用少数几次,且该数据将直接作为绘制图元或者指定图像的信息来源

glBufferSubData 方法一般用于向指定缓冲中送入部分数据进行初始化或者更新,方法如下:

    public static native void glBufferSubData(
        int target,
        int offset,
        int size,
        java.nio.Buffer data
    );

其实,参数 target 还是用于描述指定的缓冲区类型,参数 offset 用于给出缓冲区被修改的数据的起始内存偏移量,参数 size 用于给出缓冲区中数据被修改的字节数,参数 data 为需要送入缓冲的数据。

通过 glBufferSubData 方法,可以做到更改指定范围内的缓冲区数据。

删除缓冲区对象 – glDeleteBuffers

glDeteleBuffers 方法用于删除指定的缓冲区对象,其方法签名如下:

	// 方法一
    public static native void glDeleteBuffers(
        int n,
        java.nio.IntBuffer buffers
    );
    // 方法二
    public static native void glDeleteBuffers(
        int n,
        int[] buffers,
        int offset
    );

其中,第一个方法有两个参数,参数 n 为将要被删除的缓冲区对象数量,参数 buffers 为存储了 n 个要删除的缓冲区编号的 IntBuffer 。

第二个方法有三个参数,参数 n 为将要被删除的缓冲区对象数量,参数 buffers 为存储了 n 个要删除缓冲区编号的数组,参数 offset 为数组所需的偏移量。

缓冲区查询 – glGetBufferParameteriv

可以通过 glGetBufferParameteriv 方法查询指定缓冲区的信息,其方法签名如下:

    public static native void glGetBufferParameteriv(
        int target,
        int pname,
        int[] params,
        int offset
    );

其中,参数 target 用于描述指定的缓冲区类型,参数 pname 为要查询的信息项目,具体项目见下表,参数 params 用于存放查询结果,offset 代表查询偏移量。

缓冲区查询信息 说明
GL_BUFFER_SIZE 缓冲区以字节计的大小
GL_BUFFER_USAGE 缓冲区用途
GL_BUFFER_MAPPED 是否为映射缓冲区
GL_BUFFER_ACCESS_FLAGS 缓冲区访问标志
GL_BUFFER_MAP_LENGTH 缓冲区映射长度
GL_BUFFER_MAP_OFFSET 缓冲区映射偏移量

实践

前面说了那么理论,那么针对数组缓冲区 Array Buffer 和元素数组缓冲区 Element Array Buffer 分别进行实践,实现的效果都是绘制一个带纹理的矩形。

如下:

顶点缓冲区的使用

数组缓冲区使用

对于 Array Buffer 数组缓冲区,它可以存放顶点相关数据,比如顶点坐标、纹理坐标、法向量等,最后采用 glDrawArrays 方法来绘制,

首先,定义好顶点坐标:

        float vertices[] =
                {
                        -width / 2, height / 2, 0,
                        -width / 2, -height / 2, 0,
                        width / 2, height / 2, 0,

                        -width / 2, -height / 2, 0,
                        width / 2, -height / 2, 0,
                        width / 2, height / 2, 0
                };

然后定义好纹理坐标:

        float textures[] =
                {
                        0f, 0f, 0f, 1f, 1f, 0f,
                        0f, 1f, 1f, 1f, 1f, 0f,
                };

接下来就是创建顶点坐标和纹理坐标的缓存数据,通过 ByteBuffer 进行转换得到 FloatBuffer

完成准备工作接下来就是创建对应的顶点缓冲区了。

		// 指定缓存数量并生成
        int[] bufferIds = new int[3];
        GLES20.glGenBuffers(3, bufferIds, 0);

        // 创建顶点坐标缓冲,绑定并传递数据
        mVertexBufferId = bufferIds[0];
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferId);
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertices.length * 4, mVertexBuffer, GLES20.GL_STATIC_DRAW);
        // 解除绑定
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

        // 创建纹理坐标缓冲,绑定并传递数据
        mTextureBufferId = bufferIds[1];
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mTextureBufferId);
        GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, textures.length * 4, mTextureBuffer, GLES20.GL_STATIC_DRAW);
        // 解除绑定
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);

接下来就是绘制的过程:

		// 绑定到顶点坐标数据缓冲
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBufferId);
        // 启用顶点坐标数据数组
        GLES20.glEnableVertexAttribArray(maPositionHandle);
        // 指定顶点位置数据使用对应缓冲
        GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false, 3 * 4, 0);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
		
		// 绑定到纹理坐标数据缓冲
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mTextureBufferId);
        // 启用纹理坐标数据数组
        GLES20.glEnableVertexAttribArray(maTexCoorHandle);
        // 指定纹理位置数据使用对应缓冲
        GLES20.glVertexAttribPointer(maTexCoorHandle, 2, GLES20.GL_FLOAT, false, 2 * 4, 0);
        GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
		// 执行绘制
	    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vCount);

在绘制时会使用到 glVertexAttribPointer 函数,这里有个细节差别,它和之前使用时参数不一样了。

元素数组缓冲区使用

使用 元素数组缓冲区 Element Array Buffer 就得采用 glDrawElement 的方式进行绘制,先要更改顶点坐标数据和纹理坐标数据,然后定义顶点索引数组,然后为索引数组生成对应的 元素数组缓冲区。

		// 索引数组
        byte[] indices = {
                1, 2, 3,
                0, 1, 2,
        };

为索引数组生成对应的 元素数组缓冲区。

     // 索引缓冲
        mIndicesBufferId = bufferIds[2];
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, mIndicesBufferId);
        GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER, indices.length, mIndicesBuffer, GLES20.GL_STATIC_DRAW);
        GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);

另外在绘制时,也要绑定元素数组缓冲区后进行绘制:

 GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, mIndicesBufferId);
 GLES20.glDrawElements(GLES20.GL_TRIANGLES, vCount, GLES20.GL_UNSIGNED_BYTE, 0);

小结

对于博客的例子,一个纹理矩形只有四个顶点,体现不出顶点数组缓存的优势,多余大量点的情况下,这种优势还是挺明显的,尤其是存在要更改顶点数据的情况下,直接使用 glBufferSubData 方法去更改,而不是在重新传入新的顶点坐标数据。

关于具体的代码实现,可以参考我的 Github 项目:

https://github.com/glumes/AndroidOpenGLTutorial

参考

  1. 《OpenGL ES 3.x 游戏开发》

原创文章,转载请注明来源:    《OpenGL ES 3.x 游戏开发》顶点缓冲区 VBO 的使用