1.如何将Bitmap传到Native层处理
2.如何利用代码实现纹理映射
3.通过纹理映射实现一些有趣的效果
上篇回顾上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经详细阐述了纹理的观点以及纹理映射到图元上的事理,都是纯理论,略显呆板。
本日就将理论付诸实践,一起来看看详细代码如何实现纹理映射。
末了再利用纹理映射来实现一些故意思的效果,绝对不能错过~
代码实战如何将图片传入Native层
上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)主页有已经说过,纹理便是携带图片信息的容器,以是这里首先要获取到图片的信息(不然还纹理映射个毛线),在android的Java层,获取位图的办法可谓妇孺皆知:
Bitmap bitmap = ((BitmapDrawable) getResources().getDrawable(R.drawable.liyingai)).getBitmap();
由于我们OpenGL代码是在Native层的,那么怎么将Bitmap传给Native层处理呢?实在直接传入即可,在C++层用jobject吸收(如果对付ndk还不太熟习可以看下我之前写的入门文章:初探ndk的天下(一) ),然后ndk已经供应了对应的jnigraphics库来处理Bitmap干系操作,它可以直接操作Bitmap的像素。
利用之前,先要在CmakeList中链接jnigraphics库:
target_link_libraries( # Specifies the target library. native-lib GLESv3 EGL android jnigraphics # 操作Bitmap的库 # Links the target library to the log library # included in the NDK. ${log-lib} )
Java层创建绘制纹理的Native方法:
public native void drawtexture(Bitmap bitmap, Object surface);
在Native层对应的方法如下:
Java_com_example_openglstudydemo_YuvPlayer_drawTexture(JNIEnv env, jobject thiz, jobject bitmap, jobject surface)
把稳到在这里Bitmap工具已经是jobject类型。
首先用jnigraphics库的AndroidBitmap_getInfo方法获取Bitmap工具的干系信息:
/ Given a java bitmap object, fill out the {@link AndroidBitmapInfo} struct for it. If the call fails, the info parameter will be ignored. /int AndroidBitmap_getInfo(JNIEnv env, jobject jbitmap, AndroidBitmapInfo info);
第一个参数便是JNIEnv指针,第二个参数为Bimtap工具,第三个为构造体AndroidBitmapInfo的指针。
AndroidBitmapInfo为何物呢?实在,它就类似一个水桶,在函数实行完就将数据舀出来,也便是获取到的信息会存放在AndroidBitmapInfo的构造体中,对付图片来说,最常见的信息莫过于宽高、像素格式等:
/ Bitmap info, see AndroidBitmap_getInfo(). /typedef struct { / The bitmap width in pixels. / uint32_t width; / The bitmap height in pixels. / uint32_t height; / The number of byte per row. / uint32_t stride; / The bitmap pixel format. See {@link AndroidBitmapFormat} / int32_t format; / Bitfield containing information about the bitmap. <p>Two bits are used to encode alpha. Use {@link ANDROID_BITMAP_FLAGS_ALPHA_MASK} and {@link ANDROID_BITMAP_FLAGS_ALPHA_SHIFT} to retrieve them.</p> <p>One bit is used to encode whether the Bitmap uses the HARDWARE Config. Use {@link ANDROID_BITMAP_FLAGS_IS_HARDWARE} to know.</p> <p>These flags were introduced in API level 30.</p> / uint32_t flags;} AndroidBitmapInfo;
实行AndroidBitmap_getInfo方法的返回值会因此下几种情形,成功返回为0。
/ AndroidBitmap functions result code. /enum { / Operation was successful. / ANDROID_BITMAP_RESULT_SUCCESS = 0, / Bad parameter. / ANDROID_BITMAP_RESULT_BAD_PARAMETER = -1, / JNI exception occured. / ANDROID_BITMAP_RESULT_JNI_EXCEPTION = -2, / Allocation failed. / ANDROID_BITMAP_RESULT_ALLOCATION_FAILED = -3,};
一旦返回为0,那么恭喜你,已经成功拿到了Bitmap基本信息。
C++音视频学习资料免费获取方法:关注音视频开拓T哥,点击「链接」即可免费获取2023年最新C++音视频开拓进阶独家免费学习大礼包!
但是光拿到Bitmap的基本信息还是不足的,还记得上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)提到纹理映射事理的时候说过:
遍历图形中所有的片段,依次通过片段所在的位置坐标定位到其对应在纹理中的纹素,再获取到对应的颜色。
我们知道一张2D图片,实在便是一个二维数组,按照一定的格式,每多少个数组元素实在便是代表一个纹素,以是要拿到对应的纹素,首先要拿到图片的像素二维数组。
所幸的事,jnigraphics库的AndroidBitmap_lockPixels已经帮我们做好这件事了:
/ Given a java bitmap object, attempt to lock the pixel address. Locking will ensure that the memory for the pixels will not move until the unlockPixels call, and ensure that, if the pixels had been previously purged, they will have been restored. If this call succeeds, it must be balanced by a call to AndroidBitmap_unlockPixels, after which time the address of the pixels should no longer be used. If this succeeds, addrPtr will be set to the pixel address. If the call fails, addrPtr will be ignored. /int AndroidBitmap_lockPixels(JNIEnv env, jobject jbitmap, void addrPtr);
前两个参数不言而喻,末了一个参数便是指向Bitmap像素二维数组的二级指针(如果对付二级指针不太理解,可以看下我之前的博文: 漫谈C措辞指针(三) ),大略来说,该方法的浸染便是通过一个二级指针指向传过来的Bitmap的像素数组。
把稳这个方法的名字带有lock,即它会锁一些东西。锁什么呢?通过方法的注释可知,会锁住像素数据的内存,直到AndroidBitmap_unlockPixels方法调用才解锁。
关于如何处理Bitmap纹素就先看到这,至于拿到的像素数据二级指针要怎么用等会再解答,我们再看看其他的纹理映射逻辑先。
添加纹理坐标
上一篇一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经提及过纹理坐标的观点:
以是这里我们须要指定纹理坐标,这里指定坐标的意义是指定须要进行纹理映射的那一部分纹理的顶点的坐标点,比如还是下面这张图,便是指定了左边须要映射的三角形的三个顶点在全体纹理中的坐标:
为了大略,我们这里先映射整张图吧:
float vertices[] = { // 图元顶点坐标 // 纹理坐标 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // top right 0.5f, -0.5f,0.0f, 1.0f, 0.0f, // bottom right -0.5f, -0.5f,0.0f, 0.0f, 0.0f, // bottom left -0.5f, 0.5f, 0.0f, 0.0f, 1.0f // top left};
这里的纹理坐标便是指定了整张图片四个顶点。(当然,也可以指定只采样图片的一部分,后面会演示)
然后依然像一看就懂的OpenGL ES教程——缓冲工具优化程序(二) 一样利用VBO, VAO, EBO优化程序:
unsigned int indices[] = { 0, 1, 3, // first triangle 1, 2, 3 // second triangle};unsigned int VBO, VAO, EBO;glGenVertexArrays(1, &VAO);glGenBuffers(1, &VBO);glGenBuffers(1, &EBO);glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);复制代码
着色器逻辑
既然添加了纹理坐标了,根据履历和直觉,着色器是不是就要添加一个变量去吸收纹理坐标了呢?
没错,这是毋庸置疑的~
顶点着色器:
#version 300 es layout (location = 0) in vec4 aPosition; //新增的吸收纹理坐标的变量 layout (location = 1) in vec2 aTexCoord; //纹理坐标输出给片段着色器利用 out vec2 TexCoord; void main() { //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的 gl_Position = aPosition; //纹理坐标传给片段着色器 TexCoord = aTexCoord; };
这里要新增一个吸收纹理坐标的变量aTexCoord,不过,由于采样这个任务还是交给了片段着色器来完成,毕竟着色还是片段着色器要干的活,以是终极还是供应给片段着色器利用,以是又用输出变量TexCoord“送”了出去。
片段着色器:
#version 300 es precision mediump float; //新增的吸收纹理坐标的变量 in vec2 TexCoord; out vec4 FragColor; //传入的纹理 uniform sampler2D ourTexture; void main() { //texture方法实行详细的采样 FragColor = texture(ourTexture, TexCoord); };
这里用同名的TexCoord去吸收顶点着色器传过来的纹理坐标。
这里开始涌现了一个陌生的新变量类型:sampler2D,看下官网的定义:
A sampler is a set of GLSL variable types. Variables of one of the sampler types must be uniforms or as function parameters. Each sampler in a program represents a single texture of a particular texture type. The type of the sampler corresponds to the type of the texture that can be used by that sampler.
可见它便是代表一个纹理工具,这里sampler2D中的“2D”代表的便是2D纹理。
但是说它代表一个纹理工具实在是不准确的,更准确的是代表一个纹理单元,通过纹理单元去绑定一个纹理工具,从而间接绑定纹理工具。
它只能被uniform润色或者作为方法参数,这里被uniform润色也就代表了一帧图像内,这个纹理单元是不会变的,即对应的纹理的图片是不变的。
再看看main函数里面唯一的"宠儿":
FragColor = texture(ourTexture, TexCoord);
它便是传说中重中之重的采样函数了,详细来说便是获取到传入的详细纹理坐标值TexCoord在ourTexture对应的纹理上的纹素的颜色(当然由于不同的过滤模式会导致详细采样颜色的细节不同)。
你可能会问,这里的TexCoord详细的坐标值是多少呢?如果这样问,那你可能8成没看过我之前讲过光栅化插值这个“骚操作”的博文:一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五),如果看过就知道,在这里,你传入顶点着色器的纹理坐标的那几个值,已经经由光栅化等的处理,把它通过几何关系转化为对应的一个坐标值了。
这么一想,是不是全体流程都非常通畅了呢?
纹理工具配置
以是这里我们先通过AndroidBitmap_getInfo方法获取Bitmap基本信息:
//存储Bitmap基本信息的构造体AndroidBitmapInfo bmpInfo;if (AndroidBitmap_getInfo(env, bitmap, &bmpInfo) < 0) { LOGD("AndroidBitmap_getInfo() failed ! "); return;}
然后获取Bitmap像素数组的指针:
void bmpPixels;AndroidBitmap_lockPixels(env, bitmap, &bmpPixels);
此时(Bitmap像素数组的指针bmpPixels)枪在手跟我走~
接下来便是配置纹理工具了。
配置什么呢?还记得上一篇博文 讲的纹理环抱和纹理过滤么?不记得的话直接回去看看这篇博文先吧。
前面讲过纹理工具便是一个OpenGL Object,以是它的用法和其他的OpenGL Object是非常相似的,以下是纹理工具的构造图:
Diagram of the contents of a texture object
可以看出,纹理工具由纹理数据存储区+采样参数+纹理参数构成。
根据之前博文 一看就懂的OpenGL ES教程——缓冲工具优化程序(一) 写的,利用一个OpenGL Object的几部曲:
创建工具——绑定工具——处理干系操作逻辑——解绑工具——销毁工具
纹理工具也是如此。
//纹理idunsigned int texture1;//创建纹理glGenTextures(1, &texture1);//绑定纹理glBindTexture(GL_TEXTURE_2D, texture1);
绑定纹理,开始详细的采样参数配置(当然不配置也有默认配置,一样平常最好配置一下为好):
//纹理环抱配置//横坐标环抱配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)//纵坐标环抱配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //纹理过滤配置// set texture filtering parameters(配置纹理过滤)//纹理分辨率大于图元分辨率,即纹理须要被缩小的过滤配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//纹理分辨率小于图元分辨率,即纹理须要被放大的过滤配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
纹理工具的配置是通过glTexParameteri函数实现的:
void glTexParameteri(
GLenum target,
GLenum pname,
GLint param)`;
target是指定绑定的纹理目标,必须为GL_TEXTURE_1D, GL_TEXTURE_1D_ARRAY, GL_TEXTURE_2D, GL_TEXTURE_2D_ARRAY, GL_TEXTURE_2D_MULTISAMPLE, GL_TEXTURE_2D_MULTISAMPLE_ARRAY, GL_TEXTURE_3D, GL_TEXTURE_CUBE_MAP, GL_TEXTURE_CUBE_MAP_ARRAY, o,GL_TEXTURE_RECTANGLE中的一种。我们映射的是普通的2D纹理,以是利用 GL_TEXTURE_2D。
pname为须要配置详细配置种类。
param为详细的配置的值。
首先是纹理环抱配置,这里通过 首先是纹理环抱配置,这里指定的配置种类为GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T分别表示在s和t轴方向的采样环抱配置。GL_REPEAT表示超过范围重复涌现。
s和t轴是什么?看下上篇文章这个熟习的表示纹理坐标图估计你就懂了~
然后是纹理过滤配置:
//纹理分辨率大于图元分辨率,即纹理须要被缩小的过滤配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);//纹理分辨率小于图元分辨率,即纹理须要被放大的过滤配置glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
GL_TEXTURE_MIN_FILTER和GL_TEXTURE_MAG_FILTER分别代表纹理被缩小和放大的场景。上一篇文章一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)已经提到过,当进行采样的时候,纹理的纹素和图元的片段每每不是一样多的(大略理解便是图元面积和纹理图片的面积不一样大),这也就导致了,当纹理映射的时候,我们要做类似将纹理的几个顶点“拉伸”或者“紧缩”到和图元顶点贴合在一起的时候,纹理会被放大或者缩小,于是就须要在纹理被放大和缩小2种情形下分别进行采样过滤的配置。
接下来,也便是最主要的一步,那便是将前一步获取到的图片数据传给纹理工具:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo.width, bmpInfo.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels);
看起来有点眼熟的bmpPixels正是前一步获取到的图片像素数组的指针。
glTexImage2D方法的声明为:
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void data);
target:依然代表纹理目标。
level:这里指的是mipmap的层级,mipmap还没讲到,这里我们暂时只传0。
internalformat:表示纹理存储在GPU中的颜色格式。包括`Base Internal Formats、Sized Internal Formats、Compressed Internal Formats。
最常见的Base Internal Formats有以下格式:
Base Internal Format
RGBA, Depth and Stencil Values
Internal Components
GL_DEPTH_COMPONENT
Depth
D
GL_DEPTH_STENCIL
Depth, Stencil
D, S
GL_RED
Red
R
GL_RG
Red, Green
R, G
GL_RGB
Red, Green, Blue
R, G, B
GL_RGBA
Red, Green, Blue, Alpha
R, G, B, A
width和height:分别表示纹理图片的宽度和高度,一样平常哀求至少有1024个纹素。
border:这个听说是历史遗留的一个参数,现在固定传0就好。
format:表示传入的纹理像素数据的颜色格式(把稳和internalformat的差异)。比如:GL_RED、GL_RG、GL_RGB, GL_BGR、GL_RGBA, GL_BGRA。
type:表示传入的纹理像素数据数组的元素的数据类型,比如GL_UNSIGNED_BYTE, GL_BYTE, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_INT, GL_INT, GL_HALF_FLOAT, GL_FLOAT等等。
data:这便是传入的纹理像素数据的指针了。
还是那句话,OpenGL为了强大的功能性,捐躯了利用的方便性,导致它就像一个憨憨,须要把传入的数据的细枝末节非常唠叨地见告它,它才知道怎么去解析传入的数据。
这里我们按如下参数来传:
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo.width, bmpInfo.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels);
首先映射的是2D纹理,以是target传GL_TEXTURE_2D。然后通过AndroidBitmap_lockPixels方法得到的像素数据格式为RGBA,以是internalformat和format都传GL_RGBA。接下来尺寸数据传从bmpInfo获取的宽高数据,这里像素数据的每个通道由8位组成,即范围为0-255,以是对应的格式为GL_UNSIGNED_BYTE。
然后又是熟习的解析顶点属性数组数据,分别传入顶点和纹理坐标数据(如果还不清楚详细是怎么解析的,请看系列的前面几篇博文):
//顶点坐标glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 sizeof(float), (void ) 0);glEnableVertexAttribArray(0);//纹理坐标glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 sizeof(float), (void ) (3 sizeof(float)));glEnableVertexAttribArray(1);
为了增强大家的学习效果,这次纹理映射的图片就依旧用经典的女神图。
运行看下效果:
额,图片怎么高下颠倒了。。
这是Android平台的OpenGL es一个扎根多年的历史大坑,不要问我涌现的缘故原由,我只知道,在Android平台的OpenGL es,纹理坐标的原点是在左上角点(即一样平常一样平常情形下(0.0,1.0)点),而不是常见的左下角点,导致我们直策应用传入的纹理坐标会发生高下沿y轴=0.5的直线发生镜面翻转。
在Android平台的OpenGL es,真正的纹理坐标如下图赤色笔墨所示:
以是,这里顶点着色器传给片段着色器的纹理坐标我们须要做一点调度:
#version 300 es layout (location = 0) in vec4 aPosition; layout (location = 1) in vec2 aTexCoord; out vec2 TexCoord; void main() { gl_Position = aPosition; //纹理坐标要经由高下翻转再传给片段着色器 TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);; };
再运行一下:
完美!
聪明的你可能又觉察到一丝不对劲了……
片段着色器中表示纹理(纹理单元)的变量ourTexture我们都没传,咋就能够采样了呢?
缘故原由很大略:OpenGL内部帮我们传了。
如果当前的渲染只须要一个纹理单元的情形下,OpenGL会默认我们利用的是第一个纹理单元,即GL_TEXTURE0。以是片段着色器声明的sampler2D工具就会默认赋值为0,0则代表和GL_TEXTURE0的纹理关联。
而在客户端程序中,我们也并没有制订创建的纹理是属于哪个纹理单元的,以是默认也为第一个纹理单元,即GL_TEXTURE0,以是对该纹理工具的所有操作,都默认为针对即GL_TEXTURE0对应的纹理单元,以是我们的数据实在是默认和片段着色器的ourTexture变量关联上的。
实现多图层混叠刚才实现的是单个纹理单元的渲染,接下来,我要做一件有趣的事情,便是将石原美里和李英爱的图片稠浊在一起:
惊不惊喜~~
首先新增一个纹理工具并配置参数和纹理数据:
glGenTextures(1, &texture2);glBindTexture(GL_TEXTURE_2D, texture2);// set the texture wrapping parametersglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); // set texture wrapping to GL_REPEAT (default wrapping method)glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);// set texture filtering parametersglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, bmpInfo1.width, bmpInfo1.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, bmpPixels1);AndroidBitmap_unlockPixels(env, bitmap1);
上面刚说的我想就不用在这里重复了吧。
这里要增加的步骤是,由于现在是须要2个纹理单元了,以是我们须要手动对纹理单元进行赋值:
//对着色器中的纹理单元变量进行赋值glUniform1i(glGetUniformLocation(program, "ourTexture"), 0);glUniform1i(glGetUniformLocation(program, "ourTexture1"), 1);
关于Uniform变量的设置在一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)已经提及,这里就不赘述了。
分别对片段着色器中的ourTexture和ourTexture1变量赋值0和1,分别表示GL_TEXTURE0和GL_TEXTURE1纹理单元逐一对应。
然后将纹理单元和纹理工具进行绑定:
//将纹理单元和纹理工具进行绑定//激活纹理单元,下面的绑定就会和当前激活的纹理单元关联上glActiveTexture(GL_TEXTURE0);glBindTexture(GL_TEXTURE_2D, texture1);glActiveTexture(GL_TEXTURE1);glBindTexture(GL_TEXTURE_2D, texture2);
先利用glActiveTexture方法激活纹理单元,然后根据OpenGL的规则,接下来实行的glBindTexture对应的纹理工具就会和这个激活的纹理单元关联上。
这样子,便完成了纹理工具texture1和着色器中的变量ourTexture、纹理工具texture2和着色器中的变量ourTexture1的绑定。(不得不说这个绑定真绕= =)
片段着色器变成:
#version 300 es precision mediump float; in vec2 TexCoord; out vec4 FragColor; //传入的纹理 uniform sampler2D ourTexture; //新增纹理单元 uniform sampler2D ourTexture1; void main() { //对2个纹理进行稠浊 FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.5); };
mix为OpenGL内置的函数,表示对2个数进行按比例稠浊叠加,这里便是对当前片段从2纹理采样得到的颜色值进行按照0.5的比例稠浊。
运行看下:
是不是有点电影转场内味了?是不是妙不可言~
总结
本文紧张从代码实践角度详细(盲猜可能网上没有比这个更详细的嘻嘻)讲解为如何进行纹理映射,末了通过将2个纹理进行稠浊,实现了一个挺故意思的效果。当然不仅仅是稠浊,这里就可以充分发挥想象力,去干一些灰常有趣的事情,这便是下一篇文章的内容,即开始玩一些奇技淫巧了。
项目代码
opengl-es-study-demo (不断更新中)
参考纹理 Texture Sampler (GLSL)
作者:半岛铁盒里的猫 链接:https://juejin.cn/post/7155040552353234951
来源:稀土掘金 著作权归作者所有。商业转载请联系作者得到授权,非商业转载请注明出处。
在开拓的路上你不是一个人,欢迎加入C++音视频开拓互换群「链接」大家庭谈论互换!