首页 / 资讯中心 / 文章详情

OpenGL ES纹理加载实战:用GLSL着色器渲染Bitmap图片

OpenGL ES纹理加载实战:用GLSL着色器渲染Bitmap图片 ★ FEATURED ARTICLE
标题是“初探OpenGLES(三) - 使用GLSL加载图片”一看就知道是接着前面两篇来的。前两篇如果没记错应该已经讲完了EGL环境搭建、着色器编译、还有最简单的三角形绘制。这次轮到正儿八经的纹理Texture操作了——也就是把一张图片从内存加载到GPU再通过着色器画到屏幕上。这一步迈过去你才算是真正理解了OpenGL ES的渲染管线后面什么滤镜、视频帧、PBO回读、离屏渲染全都要踩在这套纹理机制上。我先说下这篇文章的目标切换成OpenGL ES的思考方式搞懂“CPU端加载图片”和“GPU端显示图片”之间那道坎然后手写GLSL顶点/片元着色器把一张Bitmap稳稳当当地渲染出来。文章会贴完整的代码思路和关键参数也把我踩过的坑一并交代清楚如果你正在学OpenGL ES刚画完三角形准备碰纹理这篇应该能帮你省不少时间。1. 为什么用OpenGL ES加载一张图片这么麻烦如果是用普通的方式显示一张图片其实非常简单。拿到Bitmap往ImageView里一放就结束了。但在OpenGL ES里你会发现事情完全不是这么回事。它不是“把图片显示到控件上”而是“把图片作为数据交给GPU再由GPU绘制到屏幕上”。理解这层思维转换后面所有的工作才顺理成章。1.1 从“显示一张图”到“绘制一张图”的思维转变OpenGL ES本质上是一个光栅化引擎。它不认识“图片”这个概念全身只有两个最基础的东西顶点Vertex和片元Fragment。顶点决定图形在屏幕上占多大、在哪个位置片元决定每个像素最终是什么颜色。图片要显示出来就必须先拆成“顶点颜色信息”的组合再交给管线处理。打个比方。普通显示图片相当于把一张已经打印好的海报直接贴在墙上OpenGL ES则相当于你在墙上手动贴马赛克瓷砖每一块瓷砖什么颜色都需要你事先指定。图片在OpenGL ES里的角色是一块“色彩数据的来源”而不是一个现成的显示对象。它被上传到GPU显存后由片元着色器按需采样再把采样结果写到像素上。所以你在Android里用OpenGL ES显示图片至少要干三件事把Bitmap数据上传到GPU显存编写着色器让GPU知道怎么取这些数据准备一组纹理坐标把图片和三角形顶点一一对应起来。一件都不能少。1.2 纹理CPU内存到GPU显存的中转站纹理这个概念很多人一开始容易混淆。它不是一个文件也不是一个Bitmap对象而是GPU显存里一块连续的图像数据缓冲区。你可以把纹理理解为“GPU世界里的Bitmap”。Bitmap存在Java堆内存里CPU可以随便读纹理存在显存里GPU可以高速访问。两者之间靠纹理上传操作完成数据搬运。上传纹理最核心的API是glTexImage2D它做的事情就是告诉GPU我这有一块宽width、高height、格式为RGBA8888的图像数据你把它复制到显存里去。需要注意的是这里有一个好理解但极其重要的区别图片数据的颜色排列、宽高步长必须和你告诉GPU的参数完全一致否则就会出现颜色错乱、图片拉伸等问题。纹理上传以后CPU手里的Bitmap就可以释放了GPU已经完全接管了这份数据。我最初的认知误区是以为创建了纹理再设置个Bitmap进去它就会自动和某个画面绑定。实际上纹理只是一个数据容器它本身和画面没有任何关系。真正决定“怎么画”的是着色器和顶点。纹理只是被动地被着色器采样。理解这一点之后OpenGL ES的渲染流程在你眼里就不再是一团雾了。2. 着色器与GLSL写给GPU的加工指令GLSLOpenGL Shading Language是OpenGL ES的着色器语言它运行在GPU上负责处理顶点和片元。加载图片这件事真正干活的其实是两个着色器顶点着色器负责处理顶点的位置片元着色器负责计算每个像素的颜色。图片能不能正确显示出来完全取决于这两个小程序写得对不对。2.1 顶点着色器与片元着色器各管哪一段顶点着色器的工作很简单接收你传入的顶点坐标经过矩阵变换后输出最终的裁剪坐标系位置。整个过程是逐顶点执行的。你的三角形有三个顶点那么顶点着色器至少要执行三次。对于加载图片来说我们需要一个由两个三角形组成的矩形四个顶点两次绘制调用完成。片元着色器的工作则是计算出每个片元的颜色。片元这个词听着抽象其实就是“候选像素”。GPU在对三角形进行光栅化时把三角形覆盖到的每一个像素点都生成一个片元然后逐一交给片元着色器处理。图片纹理采样就发生在这个阶段片元着色器根据当前片元对应纹理坐标的位置去纹理缓冲区里取那个位置的颜色值。用大白话说顶点着色器告诉GPU“图贴在哪里”片元着色器告诉GPU“每个位置该是什么颜色”。这两个阶段是流水线式衔接的先顶点后片元中间夹着一个光栅化过程由GPU自动完成。2.2 纹理坐标让图片“贴”上去的关键纹理坐标是OpenGL ES里最容易出错的概念没有之一。它的取值范围不是像素而是归一化的0.0到1.0。纹理坐标用两个分量s, t表示其中s对应横轴t对应纵轴。坐标(0,0)默认对应纹理的左下角(1,1)对应右上角。这里要特别注意的是OpenGL ES的纹理坐标系原点在左下角t轴向上为正。而Android里Bitmap的像素坐标原点在左上角y轴向下为正。说白了你上传的Bitmap数据第一行像素会被GPU当成纹理的“最上面一行”但采样时t0却对应纹理的“最下面一行”。如果不调整一下思路出来的图片通常是上下颠倒的。解决方案一般是上传前做一次Y轴翻转或者手动把纹理坐标调成“从左上角开始采样”二选一即可但两者不能同时做否则图片又会变成颠倒的。纹理坐标和顶点坐标是怎么对应的呢我们画矩形通常用一组三角形扇顶点顺序是左下、右下、左上、右上。纹理坐标就按同样的顺序给左下对应(0,0)右下对应(1,0)左上对应(0,1)右上对应(1,1)。只要保证这个对应关系一致图片就能完整地“贴”在矩形上。如果对应关系乱了图片就会发生旋转、镜像或者畸形拉伸。2.3 一个最小可用的着色器长什么样加载图片最常规的着色器写法顶点着色器代码如下attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position aPosition; vTexCoord aTexCoord; }片元着色器代码如下precision mediump float; uniform sampler2D uTexture; varying vec2 vTexCoord; void main() { gl_FragColor texture2D(uTexture, vTexCoord); }解释一下这两个着色器在干嘛。顶点着色器里aPosition是你传入的顶点坐标归一化坐标范围[-1,1]aTexCoord是对应的纹理坐标。varying关键字声明的变量会在顶点和片元之间做插值也就是说三角形内部的每一个片元都会拿到一个由顶点纹理坐标插值出来的纹理坐标。片元着色器里uniform sampler2D声明了一个二维纹理采样器它指代我们已经绑定到某个纹理单元上的纹理对象。texture2D函数就是核心采样函数传入纹理和插值后的坐标返回这个坐标处的颜色值。这套着色器就是OpenGL ES加载图片的“Hello World”几乎所有后续的图片相关操作滤镜、混合、色彩空间转换都是在这个基础上加加减减。先把这个跑通你就算摸到GPU图像处理的门槛了。3. 实操从Bitmap到屏幕的完整链路理论说再多不如上手跑一遍。这一节我按实际开发流程把从拿到Bitmap到最终显示出来的每一步都拆开讲里面会穿插我在实际操作中遇到过的坑。3.1 纹理的创建、绑定与参数设置第一步是创建纹理对象。OpenGL ES里创建一个纹理、生成一个ID的过程和创建一个文件描述符非常像。代码如下int[] textureIds new int[1]; GLES20.glGenTextures(1, textureIds, 0); int textureId textureIds[0];这行代码执行完GPU会分配一个纹理ID但这个ID对应的纹理对象默认是什么都没有的。接下来关键的一步是绑定GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);绑定操作的含义是把纹理ID挂在GL_TEXTURE_2D这个绑定点上后续所有针对GL_TEXTURE_2D的配置和上传命令都作用于这个纹理。如果不先绑定你后面写glTexParameteri、glTexImage2DGPU根本不知道你想操作哪个纹理。绑完之后设置纹理参数。这一步极其重要很多人图片显示不对问题就出在参数上。最常用的两个参数是S轴和T轴的缠绕方式以及缩小、放大过滤器GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);缠绕方式我建议从一开始就老老实实用GL_CLAMP_TO_EDGE这是“边缘拉伸”模式纹理坐标超出[0,1]范围时会使用边缘像素的颜色。另一种是GL_REPEAT会无限平铺纹理。对加载单张图片来说GL_CLAMP_TO_EDGE最稳妥因为采样坐标即使因为浮点误差超出一点也不会出现莫名其妙的边缘混色。过滤器设置上缩小和放大都推荐GL_LINEAR也就是双线性过滤。它会在采样点附近的多个纹素之间做插值让图像放大时更平滑。如果用GL_NEAREST放大后会出现明显的马赛克锯齿。如果你做的不是像素风游戏不要用GL_NEAREST。3.2 把Bitmap数据喂给纹理参数设置完毕就可以上传数据了。这里你要注意一个关键点纹理数据只认字节数组不认Bitmap对象。所以我们需要把Bitmap的像素数据读出来再传给glTexImage2D。int[] pixels new int[bitmap.getWidth() * bitmap.getHeight()]; bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); GLES20.glTexImage2D( GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, bitmap.getWidth(), bitmap.getHeight(), 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, IntBuffer.wrap(pixels) );逐参数拆解一下。第一个参数是纹理目标二维纹理就写GL_TEXTURE_2D第二个参数是mipmap层级现在没有使用多级纹理写0即可第三个参数是纹理内部格式也就是GPU显存里存储像素数据的格式这里用GL_RGBA占4个字节第四、第五个参数是宽和高必须和Bitmap一致第六个参数是边框宽度必须写0第七、第八个参数是数据格式和数据类型要和第三参数、实际数据匹配最后一个参数是把像素数据包装成的Buffer对象。这里有个细节bitmap.getPixels拿到的int数组每个int的低8位是透明度接下来是红色、绿色、蓝色。在Android默认情况下这个排列顺序其实已经是RGBA了可以直接交给OpenGL ES的GL_RGBA格式。但如果你从其他地方拿到的是ARGB排列的数据就需要小心处理字节顺序否则会出现颜色篡色最常见的就是红色和蓝色互换。所以如果你发现自己加载图片后红蓝颜色对调了不用急着怀疑着色器先检查数据排布格式。上传完数据之后就可以把Bitmap释放或回收了纹理已经和它没有关系了。不过这里注意bitmap.getPixels拿到的int数组通过IntBuffer.wrap包装后数据是Java层的大端序而GPU底层通常按字节读取。对于RGBA_8888每通道8位的布局来说如果直接按字节序列来看顺序是A、B、G、R这会导致红蓝通道互换。所以实操里我习惯用ByteBuffer手动填充或者直接用Bitmap再配GLUtils的方式会更稳妥一点。下面这段是我平时推荐的做法ByteBuffer buffer ByteBuffer.allocateDirect(bitmap.getWidth() * bitmap.getHeight() * 4); buffer.order(ByteOrder.nativeOrder()); bitmap.copyPixelsToBuffer(buffer); buffer.position(0); GLES20.glTexImage2D( GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, bitmap.getWidth(), bitmap.getHeight(), 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer );copyPixelsToBuffer会把Bitmap内部像素按RGBA通道顺序直接拷进Buffer字节顺序正确和GL_RGBA完美匹配。如果你之前用getPixels出现了红蓝互换换成这个方案就是最快的解决办法。3.3 绘制调用绑定着色器、激活纹理、画两个三角形纹理数据已经就位着色器也已经就位现在就差最后一步把绘制命令发出去。这个阶段的顺序是有讲究的乱一步就画不出东西。完整流程如下GLES20.glUseProgram(programId); // 绑定顶点坐标和纹理坐标数据 GLES20.glEnableVertexAttribArray(aPositionLoc); GLES20.glVertexAttribPointer(aPositionLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); GLES20.glEnableVertexAttribArray(aTexCoordLoc); GLES20.glVertexAttribPointer(aTexCoordLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer); // 激活纹理单元并绑定纹理 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(uTextureLoc, 0); // 绘制两个三角形组成的矩形 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);glActiveTexture指定当前激活的纹理单元纹理单元可以理解为GPU上的纹理槽位。GL_TEXTURE0是第一个槽位我们把它激活然后把纹理绑定到这个槽位上。接下来用glUniform1i把着色器里那个uniform sampler2D uTexture的值设置为0意思是告诉GPU这个采样器用的是0号纹理单元。如果你的设备支持多个纹理单元可以同时绑定多张纹理在片元着色器里做混合这就是后话了。glDrawArrays是最终的绘制命令。用GL_TRIANGLE_STRIP模式传4个顶点它会自动生成两个三角形共用一个边。这样比分别画两个三角形少一次顶点提交而且不会出现中间的接缝。顶点坐标和纹理坐标的Buffer按数组顺序理解就是顶点坐标归一化(-1,-1) (1,-1) (-1,1) (1,1) 纹理坐标 (0,0) (1,0) (0,1) (1,1)这里要特别留意纹理翻转的坑我前面提过OpenGL ES纹理坐标原点在左下角而Bitmap像素原点在左上角。上面这组纹理坐标是把纹理左下角对应到顶点左下角也就是说图片在屏幕上会是倒着的。要解决这个问题最简单的方法就是把纹理坐标改成(0,1) (1,1) (0,0) (1,0)让左上角对应顶点左下角。实际操作中我用的是这个方案因为它不用额外创建翻转Bitmap省内存也省CPU。3.4 换一种写法用工具类封装纹理加载如果你把上面的代码直接写进Renderer里很快就会发现代码又臭又长而且每个图片渲染都要复制一遍。我建议把纹理加载封装成工具方法。核心逻辑就一个给一个资源ID加载Bitmap、上传纹理、返回纹理ID。封装时需要注意一点纹理创建和上传必须在GL线程执行而Bitmap加载可以在任何线程。所以工具类方法内部我是先加载Bitmap再在Renderer的onDrawFrame里调用纹理上传。如果方法封装得不好很容易把GL调用放到非GL线程然后你会遇到一个非常诡异的现象代码不报错但屏幕就是黑屏。这是因为GL命令在非当前上下文的线程执行被静默丢弃了。这里我把自己的封装套路分享一下public static int loadTexture(Bitmap bitmap) { int[] textureIds new int[1]; GLES20.glGenTextures(1, textureIds, 0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); ByteBuffer buffer ByteBuffer.allocateDirect(bitmap.getWidth() * bitmap.getHeight() * 4); buffer.order(ByteOrder.nativeOrder()); bitmap.copyPixelsToBuffer(buffer); buffer.position(0); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, bitmap.getWidth(), bitmap.getHeight(), 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0); return textureIds[0]; }注意最后一步上传完纹理后立刻glBindTexture为0也就是解绑。这一步很多人会漏。解绑的好处是防止后面在不经意间对一个纹理做参数设置却作用到了前一个纹理上。这是一种防御式写法养成习惯后续做离屏渲染多重纹理时会省掉很多排查时间。4. 踩坑实录为什么图是黑的、是歪的、是模糊的纹理加载这套流程代码量不多但坑是真的不少。我把自己在学习和实际项目里遇到的典型问题整理成了几类每个问题都附上排查思路。4.1 纹理翻转坐标原点之争这个问题几乎每个新手都会碰到。你兴冲冲跑起来屏幕上的图片是上下颠倒的或者左右镜像的。前面已经说过根因是OpenGL ES的纹理坐标原点和Bitmap像素原点不一致。但有意思的是在实际开发中我发现不同Android机型的默认行为还不太一样有些设备上即使你纹理坐标写对了出来的图依然是反的。这通常是因为某些系统在解码图片时已经做了特殊处理。我的建议是图片是否翻转不要在脑子里推断直接跑一下看结果。如果倒了就把纹理坐标的t轴整体取反即把0和1互换。这比在CPU侧翻转Bitmap要快得多不产生额外的内存拷贝。另外如果你在做相机预览或视频帧渲染纹理翻转的处理方式和静态图片还不完全一样因为相机输出的数据方向是由硬件决定的那个场景我一般会配合矩阵来做翻转和旋转而不是直接改纹理坐标。4.2 参数设置拉伸与重复的学问当你的绘图区域宽高比和图片宽高比不一致时如果不做任何处理图片会被拉伸变形。这是因为你看似是在画一个矩形但矩形的宽高比和图片不一致纹理被整体拉伸铺满整个矩形。要解决这个问题有两种常见思路。一种做法是调整视口和顶点坐标让矩形保持和图片一致的宽高比。另一种做法是在片元着色器里对采样坐标做裁剪计算超出图片范围的部分返回固定颜色。两种方法各有适用场景静态图片我推荐用顶点坐标控制比例顶点坐标计算简单不增加着色器负担相机帧或视频帧我推荐在着色器里做裁剪因为顶点坐标每帧改起来比较麻烦而着色器是每帧执行一次的只要更新一个比例uniform即可。还有一个细节是如果你把纹理坐标保持默认的[0,1]范围同时设置了GL_REPEAT但顶点坐标正好对应矩形的边缘通常不会触发重复采样。真正触发重复采样的是纹理坐标超出[0,1]的情况。如果你把坐标代成1.5这样的值图片就会循环平铺。故意利用这个特性可以做重复纹理背景但加载人像图片时千万不要用GL_REPEAT否则一条很窄的边缘缝隙都可能采样出意想不到的颜色。4.3 常见问题速查表我把实际操作中遇到的典型问题整理成了一张速查表排查的时候可以对着查现象优先排查方向常见原因屏幕黑屏无报错GL线程上下文在非GL线程调用了GL命令图片红蓝颜色互换纹理上传数据格式Bitmap像素读取时通道排列不对改copyPixelsToBuffer图片上下颠倒纹理坐标t轴坐标未做翻转把0/1互换图片模糊、马赛克纹理过滤参数用GL_NEAREST了改GL_LINEAR图片被拉伸变形宽高比顶点坐标矩形宽高比没匹配图片或没有做坐标裁剪图片边缘有杂色条纹纹理缠绕参数用GL_REPEAT了改GL_CLAMP_TO_EDGE纹理没生效但三角形能看到uniform采样器glUniform1i没传给采样器或纹理单元编号不对某些机型颜色偏差内部格式与数据格式不匹配纹理内部格式用了GL_RGB但数据是RGBA的改成GL_RGBA这里面最坑的其实是第一个黑屏无报错。GL命令在线程和上下文不匹配时不会抛异常而是静默失效。所以排查黑屏问题时第一反应应该是确认GL调用所在线程而不是去检查Shader代码。OpenGL ES调试时最大的困难也在这很多错误不给你日志反馈你必须自己形成一套排查方法论。5. 性能与内存不要忽略的两个细节图片能显示出来只是万里长征第一步。实际项目里纹理加载和内存管理还有两件容易被忽略的事处理不好会在你项目上线后给你颜色看。5.1 RGBA_8888 与内存占用一张512x512的图在RGBA_8888格式下占用的显存是512x512x4字节也就是1MB。你可能觉得没多少。但如果你的应用里有几十张小图加上各种分辨率的资源显存占用就会迅速膨胀。而且纹理上传时CPU侧还要分配一块同样大小的ByteBuffer。如果加载大图比如一个1080x1920的屏幕背景算一下是8MB左右。内存翻倍消耗Bitmap还在Java堆里占一份垃圾回收压力一下就上来了。我建议在加载纹理前先检查图片尺寸。如果原图超过设备最大纹理尺寸可以通过GLES20.GL_MAX_TEXTURE_SIZE查询就必须做缩放。不要试图把一个超大Bitmap直接丢给glTexImage2D即使它能传进去采样时也会因为超出纹理坐标范围而产生各种非法行为。在实际项目中我有一个工具方法先读取Bitmap的宽高如果超过2048或者设备限制就先用Bitmap.createScaledBitmap缩放后再上传。还有一个点是GLES20.GL_RGBA和GLES20.GL_RGB的区别。如果你加载的图片本身没有透明通道上传时可以用GL_RGB格式每像素省1字节内存直接省25%。但代价是着色器里采样出来的颜色没有alpha信息混合时就无法做透明效果。所以该省的时候省该花的时候花不要一刀切。5.2 纹理的回收与生命周期纹理对象是GPU资源它不像Java对象那样会自动被垃圾回收。如果你的纹理创建了很多但从来不删除显存就会一直涨直到把GPU显存耗光最终导致程序崩溃。删除纹理的API是GLES20.glDeleteTextures(1, textureIds, 0);这个调用同样必须在GL线程执行。最稳妥的做法是在onSurfaceCreated里创建纹理和着色器在onSurfaceChanged里处理尺寸变化在销毁时统一释放。如果你反复创建新纹理但不释放旧纹理就会出现“切换页面几次就黑屏闪退”的现象这就是典型的纹理泄漏。此外bytebuffer的内存释放也很关键。即使是ByteBuffer.allocateDirect分配的内存它也不受Java堆大小限制而是占用Native堆。如果你频繁加载图片每次都不释放旧的BufferNative堆很快就会被撑爆。我的习惯是纹理上传完成后立即把ByteBuffer置为null同时调用Bitmap.recycle()如果是Android 8.0之前的系统这个方法的意义比较大8.0之后像素数据已经存在Native堆recycle的意义小一些但模型上保持这个习惯没有坏处。性能这块还有一个实操心得如果你需要频繁更新纹理内容比如相机帧或视频帧不要每次都调用glTexImage2D重新分配纹理存储那样会产生大量的内部碎片和上下文切换开销。正确做法是用一次glTexImage2D分配好尺寸之后每次更新数据用glTexSubImage2D后者只是替换数据不重新分配内存。这个优化在连续帧处理场景下效果非常明显一帧大约可以省下几毫秒的CPU时间对帧率的影响肉眼可见。6. 从三角形到图片OpenGL ES算是真正入门了画完三角形你其实已经掌握了OpenGL ES的整个骨架创建着色器、准备顶点数据、发起绘制调用。但只有在纹理的阶段你才真正开始和GPU的“内容生产”打交道。图片的加载意味着你的程序开始有能力处理“颜色信息”了而不只是几条线段和色块。我在实际项目中回头看这一段学习路径最大的感触是纹理这套机制在OpenGL ES里是绕不开的核心。你后面做滤镜本质上就是改片元着色器里面的采样公式你做相机实时美颜多了一个OES纹理采样器你做离屏渲染就是为了把纹理再作为输入传给下一个Pass。如果纹理这块地基打不牢后面每一步都会走得很痛苦。最后分享一个小技巧。调试纹理问题时我会故意在片元着色器里写一行gl_FragColor vec4(vTexCoord.x, vTexCoord.y, 0.0, 1.0);。这样做可以直观地看到纹理坐标的分布如果图片不是倒的那么左上角应该是绿色v1右下角应该是红色u1。如果发现颜色分布和预期不一样说明你的顶点坐标或者纹理坐标配错了。这个办法比看像素颜色直观得多排查方向瞬间清晰。纹理加载这一关过了之后你接下来就可以去试试各种酷炫的滤镜效果了那才刚开始但路已经铺好了。
阅读完成 · 觉得有帮助?
咨询建站