使用 OpenGL 4.x 为移动游戏 开发图形特效 曹家音内容技术开发工程师英伟达 (NVIDIA)
摘要 OpenGL 和 Tegra K1 简介 OpenGL4 的初始化 OpenGL4 带来的新特性 OpenGL4 高级优化技巧 Tegra K1 Demo 总结
OpenGL 的现状 OpenGL 最新版本为 4.4 过去几年中,OpenGL 产生了很大的变化 发布了 5 个以上的版本 发布了 70 个以上的 ARB extension OpenGL 新版本带来了很多新特性 增加了开发者的开发效率 提高了程序运行的效率 提供了一些新的功能
OpenGL 与 Direct 3D OpenGL 2 DX9 Shader OpenGL 3 DX10 Geometry Shader OpenGL 4 DX11 Tessellation & Compupte Shader 图形特性与 OS 相对独立, 可以在 Android 上运行 DX11 的新特性!
GPU: Kepler 架构 192 个 CUDA core 统一的 Shader 架构 与 Geforece 平台一致 CPU: 4+1 核 ARM Cortex-A15 2.3GHz Tegra K1
完全支持 OpenGL 4.4 与 GeForce 显卡共用一套驱动程序, 可以渲染 PC 平台的游戏画面 相对于现在常用的 OpenGL ES 2.0 有着非常大的优势 PC 平台的高级图形特效 更好的 API 性能 移植 PC 平台更加容易
Tegra K1 Demo
OpenGL 4.x 初始化 把 OpenGL API 与 EGL 进行绑定 eglbindapi(egl_opengl_api) 检查是否有 OpenGL 支持 : eglgetconfigattrib(display, configs[i], EGL_RENDERABLE_TYPE, &renderableflags); if ((renderableflags & EGL_OPENGL_BIT) == 0) continue; // skip this config; no GL support
OpenGL 4.x 初始化 在创建 EGL context 的时候, 加入如下属性 : EGLint contextattrs[] = { EGL_CONTEXT_MAJOR_VERSION_KHR,4, EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR,EGL _CONTEXT_OPENGL_COMPATIBILITY_PROFILE_BIT _KHR, EGL_NONE }; eglcreatecontext(display, config, NULL, contextattrs);
调用 OpenGL4.x 的函数 应用程序必须首先得到函数指针 : 函数指针定义 :typedef void (EGLAPIENTRYP PFNGLPATCHPARAMETERFVPROC)(GLenum pname, const GLfloat *values); 得到函数指针 :glpatchparameterfv = (PFNGLPATCHPARAMETERFVPROC)eglGetProcAdd ress("glpatchparameterfv"); 然后通过函数指针进行调用
调用 OpenGL 4.x 的函数 ( 续 ) 通过上述方式调用 OpenGL 4.x 函数则需要大量的函数指针, 会增加开发成本 解决方案 :REGAL 跨平台的开源代码库 (https://github.com/p3/regal) 包含了 OpenGL 的所有函数 ( 不需要额外获取指针 ) 增加了一些高级的 OpenGL 特性 保留了一些旧的特性 ( 例如, 固定管线 ) 增加了很多方便 Debug 的特性, 从而加速开发
如何使用 REGAL 设置一些基本的参数 LOCAL_STATIC_LIBRARIES :=regal_static Include $(BUILD_SHARED_LIBRARY) $(call import-module, regal_static) 在工程里面引用 REGAL 提供的头文件 #include <GL/Regal.h>
如何使用 REGAL( 续 ) REGAL 初始化 RegalMakeCurrent(eglGetCurrentContext()) 直接调用 OpenGL 4.x 的函数 glpatchparameteri( )
Direct State Access(DSA) OpenGL 是一个很大的状态机, 有很多函数用来切换 OpenGL 的状态 glactivetexture, glbindtexture 等 频繁的状态切换和管理使得程序代码显得冗余, 随着应用程序变得越来越复杂, 状态的维护就相对比较困难 : 当程序员想恢复某一个状态的时候, 需要大量的状态切换函数
Direct State Access(DSA) EXT_direct_state_access 该扩展为 OpenGL 增加了很多新的函数, 可以用来直接更改某些对象的状态 很多 OpenGL 的新的特性也会有 DSA 版本的函数调用
DSA 应用实例 例如, 更改一个 Texture 的采样过滤 : 没有 DSA glactivetexture(gl_texture0) glbindtexture( GL_TEXTURE_2D, id ) gltexparameteri( GL_TEXTURE_2D, GL_TEX_MIN_FILTER, GL_LINEAR ) 使用 DSA gltextureparameteriext( id, GL_TEXTURE_2D, GL_TEX_MIN_FILTER, GL_LINEAR )
DSA 支持的对象 DSA 支持很多 OpenGL 的对象 Texture Object Vertex Array Object Frame Buffer Object Program Object Buffer Object 等等
OpenGL 中的 Debug OpenGL 的错误信息一般是通过 glgeterror 来返回的 程序员往往不能直接知道问题的所在, 所以需要在很多地方调用 glgeterror, 增加很多开销 很多错误描述并不清晰, 错误不分级别 需要用宏或者 if 语句把 glgeterror 包起来, 从而可以在 Release 版本把这些错误检查去掉
新的 Debug 方式 ARB_debug_output 注册一个回调函数, 当 OpenGL 的调用出错误的时候, 会自动进入该函数 由驱动程序调用该回调函数, 而不是开发者 没有必要用宏或者 if 语句 可以随时动态开关 Debug 功能 错误信息分一定的级别 错误信息不是简单的枚举, 而是更具有描述性的 String
使用新的 Debug 功能 // Callback defination void APIENTRY DebugFunc( GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, GLvoid* userparam){ } // Register the callback gldebugmessagecallback( DebugFunc, NULL); // Enable debug messages and ensure they are not async glenable( GL_DEBUG_OUTPUT); glenable( GL_DEBUG_OUTPUT_SYNCHRONOUS); 在强制线程同步后, 可以通过 Call Stack 直接看到 OpenGL 出错的地方
插入自己的 Debug 错误信息 程序员可以在代码中加入自己的错误信息, 甚至可以加入 PerfMarker // Add a marker to the debug notations glpushdebuggroup( GL_DEBUG_SOURCE_APPLICATION, SCENE_RENDER_ID, 11, Render Scene ); // Perform application rendering Render_Scene(); // Closes the marker glpopdebuggroup();
Nsight 中的 PerfMarker 如上代码中, 可以在 Nsight 中看到其相应的 PerfMarker
Debug 功能的限制 回调函数中是有一些限制的 : 不能够调用其他 OpenGL 和 Windows 相关的函数 可能是在其他线程中异步调用的 回调函数同样是有一些开销的 在 Release 版本中, 去掉这些错误检查 回调函数返回的信息 不同的供应商提供的错误信息描述是不一致的 不要在应用程序中去解析这些字符串
传统的 Shader 工作方式 传统的 OpenGL 的 Shader 工作方式如下 : 创建 Shader 对象 编译 Shader 对象 创建 Program 对象 绑定相应阶段的 Shader 对象 链接所绑定的 Shader 对象 使用前调用 gluseprogram
该方式存在的一些问题 Vertex Shader 和 Fragment Shader 经常不是一一对应的 : Pre-Z 中, 多个 Vertex Shader 会对应一个简单的 Fragment shader 后处理算法中,Vertex Shader 基本是一样的, 不过 Fragment shader 是不一致的 最糟糕的情况 : N 个 vertex shader+m 个 fragment shader = MxN 个 Program 当 Stage 数量更多的时候 ( 例如 Tessellation,Geometry Shader), 情况会变得更加糟糕
Separate Shader Objects ARB_separate_shader_objects 一个 Program 可以只绑定一个类型的 Shader 增加了一个新的类型 :Program Pipeline 允许不同类型的 Shader 进行动态的链接 可以动态绑定不同阶段的 Shader, 而不需要在初始化的时候设置好
Separate Shader Objects Context 传统模型 Context SSO Program Program VS FS Program Program Pipeline VS FS
Separate Shader Objects 代码 // Create shaders GLuint fprog = glcreateshaderprogramv( GL_FRAGMENT_SHADER, 1, &fstext); GLuint vprog = glcreateshaderprogramv( GL_VERTEX_SHADER, 1, &vstext); // Bind pipeline glgenprogrampipelines( 1, &pipe); glbindprogrampipelines( pipe); // Bind shaders gluseprogramstages( pipe, GL_FRAGMENT_SHADER_BIT, fprog); gluseprogramstages( pipe, GL_VERTEX_SHADER_BIT, vprog);
一些必要的更改 由于 Shader 的链接是运行时动态进行的, 所以需要 Shader 代码中显式的声明相关的输入输出, 即使是 GLSL 内置的参数 // Rdeclare gl_position out gl_pervertex { vec4 gl_position; };
Shader 中进行文件引用 在 Shader 中进行文件的引用, 可以在不同的 Shader 脚本中共享公共的内容 在 OpenGL 中, 没有文件系统的概念 在 Shader 引用前, 必须把相应的需要引用的内容进行注册 : glnamedstringarb(gl_shader_include_arb, strlen(filename), filename, strlen(shader_content), shader_content );
显式的位置绑定 Shader 中的一些内容可以通过特殊的关键字显示的绑定到指定的位置 // specify the bind point for a buffer of uniform data layout( binding=1) uniform ConstBuffer { }; //specify the bind point for a Sampler layout( binding=2) uniform sampler2d texture; // specify the buffer used to store normals for deferred shading layout( location=3) out vec4 normaldata;
优势 显式位置绑定的优势与限制 节省一定的代码工作 固定某一个常用资源的绑定位置, 例如 ViewMatrix 等, 从而每帧只需要更新一次, 不需要每个 Draw call 都更新 限制 程序员需要保证 Shader 脚本与应用程序中的位置定义是一致的
Texture 的改进 Texture Object Texture Data Sampler State (Filter, Wrap ) View State (Format, Dimensions )
Texture Storage 传统的 Texture 创建的一些问题 : 每次只能创建一个 mipmap 级别 可能会导致一些错误, 比如读取没有创建的 mipmap 级别 Draw Call 前会有安全性检查 Texture Storage 带来的一些优势 : 简化 Texture 的创建方式 由于创建的 Texture 是不可写的, 所以可以节省一些 Draw call 的开销 ( 注意, 这里不可写的内容仅仅是 Texture 的一些参数, 例如尺寸, 格式等, 数据仍然是可以更改的 )
Texture Storage 的用法 // Classic OpenGL texture creation glbindtexture( GL_TEXTURE_2D, id); for (i = 0; i<9, i++) glteximage2d( GL_TEXTURE_2D, i,gl_rgba8, 256>>i,256>>i, 0, GL_RGBA, GL_FLOAT, NULL); // DSA-style version with Texture Storage gltexturestorage2dext( id, GL_TEXTURE2D, 9, GL_RGBA8,256, 256);
Texture 中的 Sampler Object Texture 会默认使用内置的 Sampler 进行采样 当 Texture 绑定的通道中有其他 Sampler 绑定时,Texture 将采用其绑定的 Sampler, 而不是内置的 其他的 API 的做法不同
曲面细分 (Tessellation) Tessellation 用来动态的增加低模的多边形, 从而丰富物体的几何信息
曲面细分 (Tessellation) Vertex Shader 与 Geometry Shader 中三个新的阶段 Tessellation Control Shader( 针对每个 Patch 中的点 ) Tessellator( 不可编程 ) Tessellation Evaluation Shader( 针对每个输出的图元中的顶点 ) tessellation VS Tessellation Control Tessellator Tessellation Evaluation GS
与 DX11 Tessellation 的区别 Tessellation Control Shader = Hull Shader Tessellation Evaluation Shader = Domain Shader OpenGL 的 Control Shader 中, 需要直接写如 Tess Factor, 而 Hull Shader 需要有额外的一个 Constant Function OpenGL 可以不提供 Control Shader, 而直接通过应用程序指定 Tess Factor 分割方式等内容都是在 Evaluation Shader 中定义的, 而 DX11 是在 Hull Shader 中定义的
Compute Shader 一个完全与渲染管线独立的阶段 该阶段可以用来计算任何通用的数据 粒子系统 (SPH 流体模拟 ) 后处理 (Blur) 物理碰撞 海水 非图形相关的计算 等等
Compute Shader 计算模型 每个 Dispatch call 中包含了多个 Thread Group, 每个 Thread Group 又由多个 Thread 组成 Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group Thread Group thread thread thread thread thread thread thread thread thread thread thread thread
更多的 Draw Call 驱动程序的开销 更多的图形处理工作 (GPU) 更多的驱动程序的开销 减少驱动程序的开销 减少 Draw Call 的数量 合并 Draw Call 的批次 Draw Call Breaker(Texture,Uniform)
Uniform 更新 如果需要合并 Draw Call 批次的话, 需要有大量的 Uniform 数据, 可以考虑存储在如下资源中 Shader Storage Buffer Object (SSBO) Uniform Block Texture Buffer
Uniform 资源的索引 两种解决方案 : 使用内置的 gl_drawidarb( 有些硬件不支持 ) 使用 baseinstance 参数进行模拟 ( 在不使用实例化功能的前提下可以用, 速度相对于前者有一定优势 )
glmapbuffer 优化 频繁的 MapBuffer 会产生很严重的驱动的开销 OpenGL 4 提供了 Map Persistent 功能, 可以在程序初始化的时候 map buffer, 然后在程序结束的之后 Unmap 程序员需要维护数据合理性, 防止写入正在读取的数据
glmapbuffer 优化 ( 续 ) mapflag = GL_MAP_WRITE_BIT GL_MAP_PERSISTENT_BIT GL_MAP_COHERENT_BIT; createflag = mapflag GL_MAP_DYNAMIC_STORAGE_BIT; mdesthead = 0; mbuffsize = 3 * maxverts * kvertexsizebytes; glbindbuffer(gl_array_buffer, VertexBuffer); glbufferstorage(gl_array_buffer, mbuffsize, null, createflags); mvertexdataptr = glmapbufferrange(gl_array_buffer, 0, mbuffsize, mapflags);
Bindless 资源 OpenGL4 提供了三种新的 Bindless 资源 Bindless Vertex Data Bindless Uniform Bindless Texture Bindless 资源可以直接获得其 GPU 地址, 然后提供给 Shader 脚本 以 Bindless Texture 为例, 其他两种资源的使用方式类似
传统的 Texture 工作方式 传统 Texture 的工作方式 创建 Texture 把 Texture 绑定到指定的通道 在 Shader 脚本中定义 sampler 调用 Draw call
传统的 Texture 工作方式 ( 续 ) 传统的 Texture 绑定方式 : Foreach( draw in draws ) { foreach( texture in draw->textures ) glbindtexture( GL_TEXTURE_2D, tex[id] ); gldrawelements( ); } 这种绑定的一些限制 同一个阶段只能够绑定有限数量的 Texture Draw call 之间需要频繁切换绑定的贴图 反复调用 glbindtexture 会有一些驱动开销
Bindless Texture 移除了 Texture 的绑定过程 // Create textures as normal, get handles from textures GLuint64 handle = glgettexturehandlearb(tex); // Make resident glmaketexturehandleresidentarb(handle); // Communicate handle to shader... Somehow // draw calls foreach(draw) { gldrawelements(...); }
Bindless Texture( 续 ) Shader 代码 uniform Samplers { sampler2d tex[500]; // Limited only by storage }; out vec4 ocolor; void main(void) { ocolor= texture(tex[123],...) + texture(tex[456],...); }
Bindless Texture 优势与限制 优势 可以为 Shader 同时提供更多数量的 Texture Draw Call 之间不需要频繁更新 Texture 的绑定 可以合并更多 DrawCall, 节省驱动程序的开销 限制 并不是所有硬件都是支持的 可以考虑使用 Texture Array
利用 DrawIndirect 进行优化 常规的物体渲染循环如下 : foreach( object ) DrawElementBaseVertex(GL_TRIANGLES, object->indexnum, GL_UNSIGNED_SHORT, object->indexoffset, object->basevertex );
利用 DrawIndirect 进行优化 ( 续 ) 使用 DrawIndirect foreach( object ) { updatecommand( &command ); gldrawelementindirect(, &command ); }
利用 DrawIndirect 进行优化 ( 续 ) 使用 MultiDrawIndirect 把 Draw Call 合并 foreach( object ) updatecommand( &commands[i++] ); glmultidrawelementindirect(, &command, commandnum );
Costant Time Gaussian Blur Blur 的半径与开销无关 (45fps, 1080p)
理论基础 Guassian 过滤可以通过多次 Box Filter 模拟
通过 Scan 求出 Box Filter 计算第四个像素的 BoxFilter,Blur 半径为 2 2 3 1 3 4 6 2 3 7 2 9 scan 2 5 6 9 13 19 21 24 31 33 41 2 5 6 9 13 19 21 24 31 33 41 Average = ( 19 2 ) / ( 2 * 2 + 1 ) = 3.4
具体实现步骤 1. 应用 Computer Shader 计算 Scan 结果 水平方向 Scan( 每一行分配一个 Thread Group), 计算水平方向的 box filter 竖直方向 Scan( 每一列分配一个 Thread Group), 计算竖直方向的 box filter 2. 重复第一步, 直到遍历次数足够 (2-3 次 )
总结 OpenGL 4 的一些新的特性与功能 OpenGL 4 的优化技巧 Tegra K1 Demo Q&A?