通过阅读本文,你将获得以下收获: 1。如何将Bitmap传到Native层处理 2。如何使用代码实现纹理映射 3。通过纹理映射实现一些有趣的效果上篇回顾 上一篇一看就懂的OpenGLES教程临摹画手的浪漫之纹理映射(理论篇)已经详细叙述了纹理的概念以及纹理映射到图元上的原理,都是纯理论,略显枯燥。 今天就将理论付诸实践,一起来看看具体代码如何实现纹理映射。 最后再利用纹理映射来实现一些有意思的效果,绝对不能错过 代码实战如何将图片传入Native层 上一篇一看就懂的OpenGLES教程临摹画手的浪漫之纹理映射(理论篇)主页有已经说过,纹理就是携带图片信息的容器,所以这里首先要获取到图片的信息(不然还纹理映射个毛线),在android的Java层,获取位图的方式可谓妇孺皆知:Bitmapbitmap((BitmapDrawable)getResources()。getDrawable(R。drawable。liyingai))。getBitmap(); 因为我们OpenGL代码是在Native层的,那么怎么将Bitmap传给Native层处理呢?其实直接传入即可,在C层用jobject接收(如果对于ndk还不太熟悉可以看下我之前写的入门文章:初探ndk的世界(一)),然后ndk已经提供了对应的jnigraphics库来处理Bitmap相关操作,它可以直接操作Bitmap的像素。 使用之前,先要在CmakeList中链接jnigraphics库:targetlinklibraries(Specifiesthetargetlibrary。nativelibGLESv3EGLandroidjnigraphics操作Bitmap的库LinksthetargetlibrarytotheloglibraryincludedintheNDK。{loglib}) Java层创建绘制纹理的Native方法:publicnativevoiddrawTexture(Bitmapbitmap,Objectsurface); 在Native层对应的方法如下:JavacomexampleopenglstudydemoYuvPlayerdrawTexture(JNIEnvenv,jobjectthiz,jobjectbitmap,jobjectsurface) 注意到在这里Bitmap对象已经是jobject类型。 首先用jnigraphics库的AndroidBitmapgetInfo方法获取Bitmap对象的相关信息:Givenajavabitmapobject,filloutthe{linkAndroidBitmapInfo}structforit。Ifthecallfails,theinfoparameterwillbeignored。intAndroidBitmapgetInfo(JNIEnvenv,jobjectjbitmap,AndroidBitmapInfoinfo); 第一个参数就是JNIEnv指针,第二个参数为Bimtap对象,第三个为结构体AndroidBitmapInfo的指针。 AndroidBitmapInfo为何物呢?其实,它就类似一个水桶,在函数执行完就将数据舀出来,也就是获取到的信息会存放在AndroidBitmapInfo的结构体中,对于图片来说,最常见的信息莫过于宽高、像素格式等:Bitmapinfo,seeAndroidBitmapgetInfo()。typedefstruct{Thebitmapwidthinpixels。uint32twidth;Thebitmapheightinpixels。uint32theight;Thenumberofbyteperrow。uint32tstride;Thebitmappixelformat。See{linkAndroidBitmapFormat}int32tformat;Bitfieldcontaininginformationaboutthebitmap。pTwobitsareusedtoencodealpha。Use{linkANDROIDBITMAPFLAGSALPHAMASK}and{linkANDROIDBITMAPFLAGSALPHASHIFT}toretrievethem。pOnebitisusedtoencodewhethertheBitmapusestheHARDWAREConfig。Use{linkANDROIDBITMAPFLAGSISHARDWARE}toknow。pTheseflagswereintroducedinAPIlevel30。uint32tflags;}AndroidBitmapInfo; 执行AndroidBitmapgetInfo方法的返回值会是以下几种情况,成功返回为0。AndroidBitmapfunctionsresultcode。enum{Operationwassuccessful。ANDROIDBITMAPRESULTSUCCESS0,Badparameter。ANDROIDBITMAPRESULTBADPARAMETER1,JNIexceptionoccured。ANDROIDBITMAPRESULTJNIEXCEPTION2,Allocationfailed。ANDROIDBITMAPRESULTALLOCATIONFAILED3,}; 一旦返回为0,那么恭喜你,已经成功拿到了Bitmap基本信息。 C音视频学习资料免费获取方法:关注音视频开发T哥,点击链接即可免费获取2023年最新C音视频开发进阶独家免费学习大礼包! 但是光拿到Bitmap的基本信息还是不够的,还记得上一篇一看就懂的OpenGLES教程临摹画手的浪漫之纹理映射(理论篇)提到纹理映射原理的时候说过: 遍历图形中所有的片段,依次通过片段所在的位置坐标定位到其对应在纹理中的纹素,再获取到对应的颜色。 我们知道一张2D图片,其实就是一个二维数组,按照一定的格式,每若干个数组元素其实就是代表一个纹素,所以要拿到对应的纹素,首先要拿到图片的像素二维数组。 所幸的事,jnigraphics库的AndroidBitmaplockPixels已经帮我们做好这件事了:Givenajavabitmapobject,attempttolockthepixeladdress。LockingwillensurethatthememoryforthepixelswillnotmoveuntiltheunlockPixelscall,andensurethat,ifthepixelshadbeenpreviouslypurged,theywillhavebeenrestored。Ifthiscallsucceeds,itmustbebalancedbyacalltoAndroidBitmapunlockPixels,afterwhichtimetheaddressofthepixelsshouldnolongerbeused。Ifthissucceeds,addrPtrwillbesettothepixeladdress。Ifthecallfails,addrPtrwillbeignored。intAndroidBitmaplockPixels(JNIEnvenv,jobjectjbitmap,voidaddrPtr); 前两个参数不言而喻,最后一个参数就是指向Bitmap像素二维数组的二级指针(如果对于二级指针不太理解,可以看下我之前的博文:漫谈C语言指针(三)),简单来说,该方法的作用就是通过一个二级指针指向传过来的Bitmap的像素数组。 注意这个方法的名字带有lock,即它会锁一些东西。锁什么呢?通过方法的注释可知,会锁住像素数据的内存,直到AndroidBitmapunlockPixels方法调用才解锁。 关于如何处理Bitmap纹素就先看到这,至于拿到的像素数据二级指针要怎么用等会再解答,我们再看看其他的纹理映射逻辑先。 添加纹理坐标 上一篇一看就懂的OpenGLES教程临摹画手的浪漫之纹理映射(理论篇)已经提及过纹理坐标的概念: 所以这里我们需要指定纹理坐标,这里指定坐标的意义是指定需要进行纹理映射的那一部分纹理的顶点的坐标点,比如还是下面这张图,就是指定了左边需要映射的三角形的三个顶点在整个纹理中的坐标: 为了简单,我们这里先映射整张图吧:floatvertices〔〕{图元顶点坐标纹理坐标0。5f,0。5f,0。0f,1。0f,1。0f,topright0。5f,0。5f,0。0f,1。0f,0。0f,bottomright0。5f,0。5f,0。0f,0。0f,0。0f,bottomleft0。5f,0。5f,0。0f,0。0f,1。0ftopleft}; 这里的纹理坐标就是指定了整张图片四个顶点。(当然,也可以指定只采样图片的一部分,后面会演示) 然后依然像一看就懂的OpenGLES教程缓冲对象优化程序(二)一样使用VBO,VAO,EBO优化程序:unsignedintindices〔〕{0,1,3,firsttriangle1,2,3secondtriangle};unsignedintVBO,VAO,EBO;glGenVertexArrays(1,VAO);glGenBuffers(1,VBO);glGenBuffers(1,EBO);glBindVertexArray(VAO);glBindBuffer(GLARRAYBUFFER,VBO);glBufferData(GLARRAYBUFFER,sizeof(vertices),vertices,GLSTATICDRAW);glBindBuffer(GLELEMENTARRAYBUFFER,EBO);glBufferData(GLELEMENTARRAYBUFFER,sizeof(indices),indices,GLSTATICDRAW);复制代码着色器逻辑 既然添加了纹理坐标了,根据经验和直觉,着色器是不是就要添加一个变量去接收纹理坐标了呢? 没错,这是毋庸置疑的 顶点着色器:version300eslayout(location0)invec4aPosition;新增的接收纹理坐标的变量layout(location1)invec2aTexCoord;纹理坐标输出给片段着色器使用outvec2TexCoord;voidmain(){直接把传入的坐标值作为传入渲染管线。glPosition是OpenGL内置的glPositionaPosition;纹理坐标传给片段着色器TexCoordaTexCoord;}; 这里要新增一个接收纹理坐标的变量aTexCoord,不过,因为采样这个任务还是交给了片段着色器来完成,毕竟着色还是片段着色器要干的活,所以最终还是提供给片段着色器使用,所以又用输出变量TexCoord送了出去。 片段着色器:version300esprecisionmediumpfloat;新增的接收纹理坐标的变量invec2TexCoord;outvec4FragColor;传入的纹理uniformsampler2DourTexture;voidmain(){texture方法执行具体的采样FragColortexture(ourTexture,TexCoord);}; 这里用同名的TexCoord去接收顶点着色器传过来的纹理坐标。 这里开始出现了一个陌生的新变量类型:sampler2D,看下官网的定义: AsamplerisasetofGLSLvariabletypes。Variablesofoneofthesamplertypesmustbeuniformsorasfunctionparameters。Eachsamplerinaprogramrepresentsasingletextureofaparticulartexturetype。Thetypeofthesamplercorrespondstothetypeofthetexturethatcanbeusedbythatsampler。 可见它就是代表一个纹理对象,这里sampler2D中的2D代表的就是2D纹理。 但是说它代表一个纹理对象其实是不准确的,更准确的是代表一个纹理单元,通过纹理单元去绑定一个纹理对象,从而间接绑定纹理对象。 它只能被uniform修饰或者作为方法参数,这里被uniform修饰也就代表了一帧图像内,这个纹理单元是不会变的,即对应的纹理的图片是不变的。 再看看main函数里面唯一的宠儿:FragColortexture(ourTexture,TexCoord); 它就是传说中重中之重的采样函数了,具体来说就是获取到传入的具体纹理坐标值TexCoord在ourTexture对应的纹理上的纹素的颜色(当然由于不同的过滤模式会导致具体采样颜色的细节不同)。 你可能会问,这里的TexCoord具体的坐标值是多少呢?如果这样问,那你可能8成没看过我之前讲过光栅化插值这个骚操作的博文:一看就懂的OpenGLES教程这或许是你遇过最难画的三角形(五),如果看过就知道,在这里,你传入顶点着色器的纹理坐标的那几个值,已经经过光栅化等的处理,把它通过几何关系转化为对应的一个坐标值了。 这么一想,是不是整个流程都非常通畅了呢? 纹理对象配置 所以这里我们先通过AndroidBitmapgetInfo方法获取Bitmap基本信息:存储Bitmap基本信息的结构体AndroidBitmapInfobmpInfo;if(AndroidBitmapgetInfo(env,bitmap,bmpInfo)0){LOGD(AndroidBitmapgetInfo()failed!);return;} 然后获取Bitmap像素数组的指针:voidbmpPixels;AndroidBitmaplockPixels(env,bitmap,bmpPixels); 此时(Bitmap像素数组的指针bmpPixels)枪在手跟我走 接下来就是配置纹理对象了。 配置什么呢?还记得上一篇博文讲的纹理环绕和纹理过滤么?不记得的话直接回去看看这篇博文先吧。 前面讲过纹理对象就是一个OpenGLObject,所以它的用法和其他的OpenGLObject是非常相似的,以下是纹理对象的结构图: Diagramofthecontentsofatextureobject 可以看出,纹理对象由纹理数据存储区采样参数纹理参数构成。 根据之前博文一看就懂的OpenGLES教程缓冲对象优化程序(一)写的,使用一个OpenGLObject的几部曲: 创建对象绑定对象处理相关操作逻辑解绑对象销毁对象 纹理对象也是如此。纹理idunsignedinttexture1;创建纹理glGenTextures(1,texture1);绑定纹理glBindTexture(GLTEXTURE2D,texture1); 绑定纹理,开始具体的采样参数配置(当然不配置也有默认配置,一般最好配置一下为好):纹理环绕配置横坐标环绕配置glTexParameteri(GLTEXTURE2D,GLTEXTUREWRAPS,GLREPEAT);settexturewrappingtoGLREPEAT(defaultwrappingmethod)纵坐标环绕配置glTexParameteri(GLTEXTURE2D,GLTEXTUREWRAPT,GLREPEAT);纹理过滤配置settexturefilteringparameters(配置纹理过滤)纹理分辨率大于图元分辨率,即纹理需要被缩小的过滤配置glTexParameteri(GLTEXTURE2D,GLTEXTUREMINFILTER,GLNEAREST);纹理分辨率小于图元分辨率,即纹理需要被放大的过滤配置glTexParameteri(GLTEXTURE2D,GLTEXTUREMAGFILTER,GLLINEAR); 纹理对象的配置是通过glTexParameteri函数实现的: voidglTexParameteri( GLenumtarget, GLenumpname, GLintparam); target是指定绑定的纹理目标,必须为GLTEXTURE1D,GLTEXTURE1DARRAY,GLTEXTURE2D,GLTEXTURE2DARRAY,GLTEXTURE2DMULTISAMPLE,GLTEXTURE2DMULTISAMPLEARRAY,GLTEXTURE3D,GLTEXTURECUBEMAP,GLTEXTURECUBEMAPARRAY,o,GLTEXTURERECTANGLE中的一种。我们映射的是普通的2D纹理,所以使用GLTEXTURE2D。 pname为需要配置具体配置种类。 param为具体的配置的值。 首先是纹理环绕配置,这里通过首先是纹理环绕配置,这里指定的配置种类为GLTEXTUREWRAPS和GLTEXTUREWRAPT分别表示在s和t轴方向的采样环绕配置。GLREPEAT表示超过范围重复出现。 s和t轴是什么?看下上篇文章这个熟悉的表示纹理坐标图估计你就懂了 然后是纹理过滤配置:纹理分辨率大于图元分辨率,即纹理需要被缩小的过滤配置glTexParameteri(GLTEXTURE2D,GLTEXTUREMINFILTER,GLNEAREST);纹理分辨率小于图元分辨率,即纹理需要被放大的过滤配置glTexParameteri(GLTEXTURE2D,GLTEXTUREMAGFILTER,GLLINEAR); GLTEXTUREMINFILTER和GLTEXTUREMAGFILTER分别代表纹理被缩小和放大的场景。上一篇文章一看就懂的OpenGLES教程临摹画手的浪漫之纹理映射(理论篇)已经提到过,当进行采样的时候,纹理的纹素和图元的片段往往不是一样多的(简单理解就是图元面积和纹理图片的面积不一样大),这也就导致了,当纹理映射的时候,我们要做类似将纹理的几个顶点拉伸或者收缩到和图元顶点贴合在一起的时候,纹理会被放大或者缩小,于是就需要在纹理被放大和缩小2种情况下分别进行采样过滤的配置。 接下来,也就是最重要的一步,那就是将前一步获取到的图片数据传给纹理对象:glTexImage2D(GLTEXTURE2D,0,GLRGBA,bmpInfo。width,bmpInfo。height,0,GLRGBA,GLUNSIGNEDBYTE,bmpPixels); 看起来有点眼熟的bmpPixels正是前一步获取到的图片像素数组的指针。 glTexImage2D方法的声明为: voidglTexImage2D(GLenumtarget,GLintlevel,GLintinternalformat,GLsizeiwidth,GLsizeiheight,GLintborder,GLenumformat,GLenumtype,constvoiddata); target:依然代表纹理目标。 level:这里指的是mipmap的层级,mipmap还没讲到,这里我们暂时只传0。 internalformat:表示纹理存储在GPU中的颜色格式。包括BaseInternalFormats、SizedInternalFormats、CompressedInternalFormats。 最常见的BaseInternalFormats有以下格式: BaseInternalFormat RGBA,DepthandStencilValues InternalComponents GLDEPTHCOMPONENT Depth D GLDEPTHSTENCIL Depth,Stencil D,S GLRED Red R GLRG Red,Green R,G GLRGB Red,Green,Blue R,G,B GLRGBA Red,Green,Blue,Alpha R,G,B,A width和height:分别表示纹理图片的宽度和高度,一般要求至少有1024个纹素。 border:这个据说是历史遗留的一个参数,现在固定传0就好。 format:表示传入的纹理像素数据的颜色格式(注意和internalformat的区别)。比如:GLRED、GLRG、GLRGB,GLBGR、GLRGBA,GLBGRA。 type:表示传入的纹理像素数据数组的元素的数据类型,比如GLUNSIGNEDBYTE,GLBYTE,GLUNSIGNEDSHORT,GLSHORT,GLUNSIGNEDINT,GLINT,GLHALFFLOAT,GLFLOAT等等。 data:这就是传入的纹理像素数据的指针了。 还是那句话,OpenGL为了强大的功能性,牺牲了使用的方便性,导致它就像一个憨憨,需要把传入的数据的细枝末节非常唠叨地告诉它,它才知道怎么去解析传入的数据。 这里我们按如下参数来传:glTexImage2D(GLTEXTURE2D,0,GLRGBA,bmpInfo。width,bmpInfo。height,0,GLRGBA,GLUNSIGNEDBYTE,bmpPixels); 首先映射的是2D纹理,所以target传GLTEXTURE2D。然后通过AndroidBitmaplockPixels方法得到的像素数据格式为RGBA,所以internalformat和format都传GLRGBA。接下来尺寸数据传从bmpInfo获取的宽高数据,这里像素数据的每个通道由8位组成,即范围为0255,所以对应的格式为GLUNSIGNEDBYTE。 然后又是熟悉的解析顶点属性数组数据,分别传入顶点和纹理坐标数据(如果还不清楚具体是怎么解析的,请看系列的前面几篇博文):顶点坐标glVertexAttribPointer(0,3,GLFLOAT,GLFALSE,5sizeof(float),(void)0);glEnableVertexAttribArray(0);纹理坐标glVertexAttribPointer(1,2,GLFLOAT,GLFALSE,5sizeof(float),(void)(3sizeof(float)));glEnableVertexAttribArray(1); 为了增强大家的学习效果,这次纹理映射的图片就依旧用经典的女神图。 运行看下效果: 额,图片怎么上下颠倒了 这是Android平台的OpenGLes一个扎根多年的历史大坑,不要问我出现的原因,我只知道,在Android平台的OpenGLes,纹理坐标的原点是在左上角点(即一般一般情况下(0。0,1。0)点),而不是常见的左下角点,导致我们直接使用传入的纹理坐标会发生上下沿y轴0。5的直线发生镜面翻转。 在Android平台的OpenGLes,真正的纹理坐标如下图红色文字所示: 所以,这里顶点着色器传给片段着色器的纹理坐标我们需要做一点调整:version300eslayout(location0)invec4aPosition;layout(location1)invec2aTexCoord;outvec2TexCoord;voidmain(){glPositionaPosition;纹理坐标要经过上下翻转再传给片段着色器TexCoordvec2(aTexCoord。x,1。0aTexCoord。y);;}; 再运行一下: 完美! 聪明的你可能又觉察到一丝不对劲了 片段着色器中表示纹理(纹理单元)的变量ourTexture我们都没传,咋就能够采样了呢? 原因很简单:OpenGL内部帮我们传了。 如果当前的渲染只需要一个纹理单元的情况下,OpenGL会默认我们使用的是第一个纹理单元,即GLTEXTURE0。所以片段着色器声明的sampler2D对象就会默认赋值为0,0则代表和GLTEXTURE0的纹理关联。 而在客户端程序中,我们也并没有制定创建的纹理是属于哪个纹理单元的,所以默认也为第一个纹理单元,即GLTEXTURE0,所以对该纹理对象的所有操作,都默认为针对即GLTEXTURE0对应的纹理单元,所以我们的数据其实是默认和片段着色器的ourTexture变量关联上的。实现多图层混叠 刚才实现的是单个纹理单元的渲染,接下来,我要做一件有趣的事情,就是将石原美里和李英爱的图片混合在一起: 惊不惊喜 首先新增一个纹理对象并配置参数和纹理数据:glGenTextures(1,texture2);glBindTexture(GLTEXTURE2D,texture2);setthetexturewrappingparametersglTexParameteri(GLTEXTURE2D,GLTEXTUREWRAPS,GLREPEAT);settexturewrappingtoGLREPEAT(defaultwrappingmethod)glTexParameteri(GLTEXTURE2D,GLTEXTUREWRAPT,GLREPEAT);settexturefilteringparametersglTexParameteri(GLTEXTURE2D,GLTEXTUREMINFILTER,GLNEAREST);glTexParameteri(GLTEXTURE2D,GLTEXTUREMAGFILTER,GLLINEAR);glTexImage2D(GLTEXTURE2D,0,GLRGBA,bmpInfo1。width,bmpInfo1。height,0,GLRGBA,GLUNSIGNEDBYTE,bmpPixels1);AndroidBitmapunlockPixels(env,bitmap1); 上面刚说的我想就不用在这里重复了吧。 这里要增加的步骤是,因为现在是需要2个纹理单元了,所以我们需要手动对纹理单元进行赋值:对着色器中的纹理单元变量进行赋值glUniform1i(glGetUniformLocation(program,ourTexture),0);glUniform1i(glGetUniformLocation(program,ourTexture1),1); 关于Uniform变量的设置在一看就懂的OpenGLES教程这或许是你遇过最难画的三角形(五)已经提及,这里就不赘述了。 分别对片段着色器中的ourTexture和ourTexture1变量赋值0和1,分别表示GLTEXTURE0和GLTEXTURE1纹理单元一一对应。 然后将纹理单元和纹理对象进行绑定:将纹理单元和纹理对象进行绑定激活纹理单元,下面的绑定就会和当前激活的纹理单元关联上glActiveTexture(GLTEXTURE0);glBindTexture(GLTEXTURE2D,texture1);glActiveTexture(GLTEXTURE1);glBindTexture(GLTEXTURE2D,texture2); 先使用glActiveTexture方法激活纹理单元,然后根据OpenGL的规则,接下来执行的glBindTexture对应的纹理对象就会和这个激活的纹理单元关联上。 这样子,便完成了纹理对象texture1和着色器中的变量ourTexture、纹理对象texture2和着色器中的变量ourTexture1的绑定。(不得不说这个绑定真绕) 片段着色器变成:version300esprecisionmediumpfloat;invec2TexCoord;outvec4FragColor;传入的纹理uniformsampler2DourTexture;新增纹理单元uniformsampler2DourTexture1;voidmain(){对2个纹理进行混合FragColormix(texture(ourTexture,TexCoord),texture(ourTexture1,TexCoord),0。5);}; mix为OpenGL内置的函数,表示对2个数进行按比例混合叠加,这里就是对当前片段从2纹理采样得到的颜色值进行按照0。5的比例混合。 运行看下: 是不是有点电影转场内味了?是不是妙不可言 总结 本文主要从代码实践角度详细(盲猜可能网上没有比这个更详细的嘻嘻)讲解为如何进行纹理映射,最后通过将2个纹理进行混合,实现了一个挺有意思的效果。当然不仅仅是混合,这里就可以充分发挥想象力,去干一些灰常有趣的事情,这就是下一篇文章的内容,即开始玩一些奇技淫巧了。 项目代码 openglesstudydemo(不断更新中)参考 纹理TextureSampler(GLSL)作者:半岛铁盒里的猫链接:https:juejin。cnpost7155040552353234951 来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 在开发的路上你不是一个人,欢迎加入C音视频开发交流群链接大家庭讨论交流!