大家好,下面和大学一起学习如何使用顶点着色器vertex shader
来做顶点变换,在我的github上有一个项目OpenGLES2.0SamplesForAndroid
,我会不断地编写学习样例,文章和代码同步更新,欢迎关注,链接:github.com/kenneycode/…
在之前的例子中,我们都使用到了顶点着色器vertex shader
,但只是简单地用了一下,把输入的顶点坐标又原样地输出了,没有做任何操作,这篇文章给大家介绍如何在vertex shader
做顶点变换。
我们先看了解一下OpenGL的渲染管线(pipeline):
drawXXX()
方法后执行的流程,我们传递的顶点首先会经过顶点着色器
vertex shader
的处理,一般会在里面做顶点变换相关的逻辑,然后进行图元装配,再经过几何着色器
geometry shader
,这个着色器相对来说使用得少一些,可暂时先忽略,然后接下来就是光栅化,所谓光栅化就是把我们要渲染的图像打碎成屏幕上的像素,因为最终要显示到屏幕上,就必须将图形对应到像素上,光栅化完成后,我们就有了要渲染的图形对应的像素,此时像素还没有颜色,需要我们填上颜色,这时就到达到了片段着色器
fragment shader
,在
fragment shader
中我们通常进行颜色的计算,确定对应的像素显示什么颜色,
fragment shader
将在下篇文章中介绍。
在整个渲染管线中,vertex shader
、geometry shader
和fragment shader
这三部分是可编程分部,可编写shader
代码实现相应的功能,我们目前重点关注vertex shader
和fragment shader
。
这里特别注意一点,我们的shader
代码并不是像普通程序那样,一次性输入所有顶点,然后再输出,例如对于vertex shader
,我们传递了3个顶点,并不是3个顶点一起执行一次vertex shader
,而是分别对这3个顶点执行一次,也就是执行了3次。对于fragment shader
也是类似的,并不是执行一次为所有的像素填充颜色,而是对每个像素都执行一次。这个特点有时让初学者感到困惑。
先来回顾一下我们简单的vertex shader
:
precision mediump float;
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
}
复制代码
第一行,我们声明了这个shader
使用的精度,mediump
表示使用中等精度,一些对精度要求很高的应用,可以声明高精度。
第二行,我们声明了一个attribute vec4 a_Position
变量,它表明这是一个attribute
类型的四维向量,什么attribute
类型?上文提到如果我们传递了3个顶点,就会对这3个顶点分别执行一次vertex shader
,在一次执行中,这个attribute
类型的变量所对应的就是这3个顶点中的某个顶点,与此相对的是uniform
变量,uniform
变量在所有vertex shader
的执行过程中都是同一个值,在本文中我们也会遇到。
这里为什么一个顶点是四维向量?我们不是只传了二维的x、y
坐标吗?在OpenGL中,顶点总是四维的,即x、y、z、w
,其中x、y、z
不传的话默认是0,w
不传的话默认是1,w
是用来做归一化(标准化)的,后续文章会有介绍。
回看我们之前《Android OpenGL ES 2.0 手把手教学(1)- Hello World!》例子,我们有这样一句:
// 指定a_Position所使用的顶点数据
// Specify the vertex data of a_Position
GLES20.glVertexAttribPointer(location, 2, GLES20.GL_FLOAT, false,0, buffer)
复制代码
其中第2个参数就是指定了一个顶点有多少个成份,因此在vertex shader
中,vec4 a_Position
接受的只有x、y
,z
和w
保持默认值。
我们继续往下看,vertex shader
中包含一个main()
方法作为入口,和很多编程语言类似。gl_Position
是vertex shader
的一个内置变量,表示vertex shader
的输出,在我们之前的例子,是直接将输入的顶点原样又输出了,本文将对顶点做变换,先看个简单的例子:
precision mediump float;
attribute vec4 a_Position;
void main() {
gl_Position = a_Position + vec4(0.3, 0.3, 0, 0);
}
复制代码
我们给顶点加上了一个偏移量,来看看效果:
可以看到,三角形发生的偏移,接下来,我们来完善一下,将平移、缩放和旋转一起写到vertex shader
中,为了计算上的方便,我们使用平移矩阵、缩放矩阵和旋转矩阵,这些矩阵的写法是数学上的知识,并不是OpenGL特有的,这里就不展开讲了,来看看加入变换矩阵后的vertex shader
:
precision mediump float;
attribute vec4 a_Position;
uniform vec2 u_Translate;
uniform float u_Scale;
uniform float u_Rotate;
void main() {
mat4 translateMatrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
u_Translate.x, u_Translate.y, 0.0, 1.0);
mat4 scaleMatrix = mat4(u_Scale, 0.0, 0.0, 0.0,
0.0, u_Scale, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
mat4 rotateMatrix = mat4(cos(u_Rotate), sin(u_Rotate), 0.0, 0.0,
-sin(u_Rotate), cos(u_Rotate), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
gl_Position = translateMatrix * rotateMatrix * scaleMatrix * a_Position;
}
复制代码
我们向vertex shader
中传递平移量u_Translate
、缩放量u_Scale
和旋转量u_Rotate
(单位是弧度),前面演示过了平移,下面我们利用这个vertex shader
来演示缩放,将u_Scale
设置为0.5,平移和旋转设置为0:
// 获取字段u_Offset在shader中的位置
// Get the location of translate in the shader
val uTranslateLocation = GLES20.glGetUniformLocation(programId, "u_Translate")
// 启动对应位置的参数
// Enable the parameter of the location
GLES20.glEnableVertexAttribArray(uTranslateLocation)
// 指定u_Offset所使用的顶点数据
// Specify the vertex data of translate
GLES20.glUniform2f(uTranslateLocation, 0f, 0f)
// 获取字段u_Offset在shader中的位置
// Get the location of u_Scale in the shader
val uScaleLocation = GLES20.glGetUniformLocation(programId, "u_Scale")
// 启动对应位置的参数
// Enable the parameter of the location
GLES20.glEnableVertexAttribArray(uScaleLocation)
// 指定u_Scale所使用的顶点数据
// Specify the vertex data of u_Scale
GLES20.glUniform1f(uScaleLocation, 0.5f)
// 获取字段u_Offset在shader中的位置
// Get the location of u_Rotate in the shader
val uRotateLocation = GLES20.glGetUniformLocation(programId, "u_Rotate")
// 启动对应位置的参数
// Enable the parameter of the location
GLES20.glEnableVertexAttribArray(uRotateLocation)
// 指定u_Rotate所使用的顶点数据
// Specify the vertex data of u_Rotate
GLES20.glUniform1f(uRotateLocation, Math.toRadians(0.0).toFloat())
复制代码
来看看效果:
我们再来看看让它旋转90度,平移设为0,缩放设为1:
奇怪,旋转是旋转了,但为什么感觉形状变了呢?在第一篇文章中有提到过,我们直接给gl_Position
传递坐标的话,这时就相当于是传了设备标准化坐标,也就是x
和y
的取值范围都是-1~1
,而宽比长要小,同样的一个值在x
轴上就显示小一些,比如(0, 0.5)
这个点,旋转90度后变成(-0.5, 0)
,x
轴上的0.5
比y
轴上的0.5
要短些,可以做些简单的换算,让它变得正常,我们传入GLSurfaceView
宽高比,来做换算:
precision mediump float;
attribute vec4 a_Position;
uniform vec2 u_Translate;
uniform float u_Scale;
uniform float u_Rotate;
uniform float u_Ratio;
void main() {
vec4 p = a_Position;
p.y = p.y / u_Ratio;
mat4 translateMatrix = mat4(1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
u_Translate.x, u_Translate.y, 0.0, 1.0);
mat4 scaleMatrix = mat4(u_Scale, 0.0, 0.0, 0.0,
0.0, u_Scale, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
mat4 rotateMatrix = mat4(cos(u_Rotate), sin(u_Rotate), 0.0, 0.0,
-sin(u_Rotate), cos(u_Rotate), 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0);
p = translateMatrix * rotateMatrix * scaleMatrix * p;
p.y = p.y * u_Ratio;
gl_Position = p;
}
复制代码
// 获取字段u_Ratio在shader中的位置
// Get the location of u_Rotate in the shader
val uRatioLocation = GLES20.glGetUniformLocation(programId, "u_Ratio")
// 启动对应位置的参数
// Enable the parameter of the location
GLES20.glEnableVertexAttribArray(uRatioLocation)
// 指定u_Ratio所使用的顶点数据
// Specify the vertex data of u_Ratio
GLES20.glUniform1f(uRatioLocation, glSurfaceViewWidth * 1.0f / glSurfaceViewHeight)
复制代码
现在再看看效果:
现在就正常了。
我们来做一下组合,将平移设为(0.3, 0.3)
,缩放设为0.5
,旋转设为45
度,看看效果:
代码在我github的OpenGLES2.0SamplesForAndroid
项目中,本文对应的是SampleVertexShader
,项目链接:github.com/kenneycode/…
感谢阅读!