大家好,下面和大学一起学习纹理,在我的github上有一个项目OpenGLES2.0SamplesForAndroid
,我会不断地编写学习样例,文章和代码同步更新,欢迎关注,链接:github.com/kenneycode/…
在前面的例子中,我们渲染的都是一些比较简单的颜色,如果我们要渲染一张图片,该怎么做呢?这就需要用到纹理,我们需要创建一个纹理并把图片加载到纹理中,然后在fragment shader
中对纹理进行采样,从而将纹理渲染出来。
我们先通过glGenTextures
创建一个纹理,然后设置一些参数,这里得到的纹理只是一个id
:
// 创建图片纹理
// Create texture
val textures = IntArray(1)
GLES20.glGenTextures(textures.size, textures, 0)
val imageTexture = textures[0]
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, imageTexture)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
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)
复制代码
创建好纹理之后,它还是空的,我们要给这个纹理填充内容,我们先将一张图片解码成bitmap
,并将像素数据copy到一个ByteBuffer
中,因为将bitmp
加载到纹理中的方法接受的是一个ByteBuffer
:
val bitmap = Util.decodeBitmapFromAssets("image_0.jpg")
val b = ByteBuffer.allocate(bitmap.width * bitmap.height * 4)
bitmap.copyPixelsToBuffer(b)
b.position(0)
复制代码
下面通过glTexImage2D
方法将上面得到的ByteBuffer
加载到纹理中:
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D,
0,
GLES20.GL_RGBA,
bitmap.width,
bitmap.height,
0,
GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE,
b)
复制代码
这时纹理中才真正有了内容,接下来需要将纹理传递给fragment shader
进行采样,从而渲染出来,我们先来看看如何在fragment shader
中使用纹理:
// vertex shader
precision mediump float;
attribute vec4 a_position;
attribute vec2 a_textureCoordinate;
varying vec2 v_textureCoordinate;
void main() {
v_textureCoordinate = a_textureCoordinate;
gl_Position = a_position;
}
// fragment shader
precision mediump float;
varying vec2 v_textureCoordinate;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_textureCoordinate);
}
复制代码
关键点是uniform sampler2D u_texture;
这句,它声明了一个2D的采样器用于采样纹理。
varying vec2 v_textureCoordinate;
则是从vertex shader
中传递过来的一个经过插值的纹理坐标值,关于varying
变量,在之前的一个篇文章《Android OpenGL ES 2.0 手把手教学(4)- 片段着色器 fragment shader》中有涉及到。
gl_FragColor = texture2D(u_texture, v_textureCoordinate);
就是从纹理中采样出v_textureCoordinate
坐标所对应的颜色作为fragment shader
的输出,我们可以看到,fragment shader
的输出实际上就是一个颜色,在之前的文章中,是我们自己去控制这个颜色,而当这个颜色如果是来自纹理的采样,那最终渲染出来就是纹理的样子。
现在,纹理和shader
都准备好了,如果把它们联系越来呢?首先需要像之前一样先获取shader
中纹理变量的location
:
val uTextureLocation = GLES20.glGetAttribLocation(programId, "u_texture")
复制代码
然后给这个location
指定对应哪个纹理单元,这里我们使用0号纹理单元:
GLES20.glUniform1i(uTextureLocation, 0)
复制代码
看到这里,可能有些朋友有点懵,纹理单元又是个什么东西?是这样的,纹理单元可以想像成是一种类似寄存器的东西,在OpenGL
使用纹理前,我们先要把纹理放到某个纹理单元上,之后的操作OpenGL
就会去我们指定的纹理单元上去取对应的纹理。
我们刚才让location
对应0号纹理单元,但是我们好像没并没有哪里说我们把纹理放在了0号纹理单元,这是怎么回事呢?因为默认情况下,OpenGL
是使用0号纹理单元的,我们因为没有更改过使用的纹理单元,因此默认就是0号了,我们如果想使用其它纹理单元,可以通过glActiveTexture
来指定,例如:
GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
复制代码
就会指定使用1号纹理单元,如果我们的例子改为使用1号纹理单元,那么uTextureLocation
就要相应地改为让它对应1号纹理单元:
GLES20.glUniform1i(uTextureLocation, 1)
复制代码
下面我看来看看顶点坐标和纹理坐标:
private val vertexData = floatArrayOf(-1f, -1f, -1f, 1f, 1f, 1f, -1f, -1f, 1f, 1f, 1f, -1f)
private val textureCoordinateData = floatArrayOf(0f, 1f, 0f, 0f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 1f)
复制代码
如何给shader
传递顶点坐标,方法大家现在应该比较熟悉了,其实纹理坐标的传递也几乎是一样的,只是值不一样而已。
之前提到过,顶点坐标的作用是告诉OpenGL
要渲染到什么区域,顶点坐标的坐标系是每个轴的范围都是-1~1
,其实也可以超出-1
和1
,只不过超出就不在渲染的范围内了,就看不见了,并不是就算错误,顶点坐标系的原点在中间。
纹理坐标的坐标原点在左下角,每个轴的范围是0~1
,同样的也可以超出0
和1
,超出之后的表现会根据设置的纹理参数有所不同。
在这个例子中,我们使用GL_TRIANGLES
的绘制模式进行渲染,关于渲染模式,可以参考我的上一篇文章《Android OpenGL ES 2.0 手把手教学(5)- 绘制模式》,在这种绘制模式下,每三个点构成一个独立三形,因此纹理坐标构成的两个三角形会对应渲染到顶点坐标指定的两个三角形中,我们来看一下效果:
细心的朋友可能会发现,我们的顶点坐标和纹理坐标上下是颠倒的,比如顶点坐标(-1,-1)
对应纹理坐标(0,1)
,也就是渲染区域的左下角对应纹理的左上角,这样一样,渲染出来的图像不是应该倒过来吗?但我们看到的效果却是正确的。
这是因为我们的纹理来自于bitmap
,而bitmap
的坐标原点是左上角,也就是它和OpenGL
中的纹理坐标系是上下颠倒的,所以我们把纹理坐标再颠倒一次,就是正的了。
我们刚才创建纹理时给纹理设置了几个参数,我们来看一下:
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
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)
复制代码
其中GL_TEXTURE_MIN_FILTER
和GL_TEXTURE_MAG_FILTER
是纹理过滤参数,指定当我们渲染出来的纹理比原来的纹理小或者大时要如何处理,这里我们使用了GL_LINEAR
的方式,这是一种线性插值的方式,得到的结果会更平滑,除此之外,还有其它很多选项,还有另一个比较常用的是GL_NEAREST
,它会选择和它最近的像素,得到的结果锯齿感比GL_LINEAR
要大,我们将顶点坐标扩大5倍,即变成-5~5
,来得到放大的效果,可以来看看放大时的这两种效果:
GL_TEXTURE_WRAP_S
和GL_TEXTURE_WRAP_T
则是指定纹理坐标超出了纹理范围之后,该如何填充,比较常用的有GL_CLAMP_TO_EDGE
和GL_REPEAT
,它们的效果分别是填充边缘像素和重复这个纹理,我们将纹理坐标改为0~3
,看看效果:
以上就是关于纹理的一些基础知识,代码在我github的OpenGLES2.0SamplesForAndroid
项目中,本文对应的是SampleTexture
,项目链接:github.com/kenneycode/…
感谢阅读!