这边文章代码基于 Android OpenGL学习入门:曲棍球1

上一篇文章最后出来的效果,形状是对了,但颜色感觉有点死板,这节我们调整下颜色,让图形平滑着色,看起来更真实。

OpenGL允许我们平滑地混合一条直线或一个三角形的表面上每个顶点的颜色值。现在计划使用这种平滑着色,使得桌子中心表现得更加明亮,而其他边缘显得比较暗淡,这就好像一盏灯挂在桌子中间的上方一样。

这节主要调整的有 Renderer 和 着色器,其他几个类不动。
先看着色器。

顶点着色器 basic.vs

1
2
3
4
5
6
7
8
9
10
attribute vec4 a_Position;  
attribute vec4 a_Color;

varying vec4 v_Color;

void main(){
v_Color = a_Color;
gl_Position = a_Position;
gl_PointSize = 20.0;
}

在顶点着色器中加入了新的属性a_Color,也加入了一个叫做v_Color的新的varying。

varying是一个特殊的变量类型,它把给它的那些值进行混合,并把这些混合后的值发送给片段着色器。
如果顶点0的a_Color是红色,且顶点1的a_Color是绿色,然后,通过把a_Color赋值給v_Color,来告诉OpenGL我们需要每个片段都接收一个混合的颜色。接近顶点0的片段,混合后的颜色显得更红,而接近顶点1的片段,颜色就会越绿。

片元着色器 basic.fs

上边我们加入了 varying,所以片元着色器也需要加入 varying。同时去掉上一篇文章中的 u_Color。修改结果如下:

1
2
3
4
5
6
7
precision mediump float;  

varying vec4 v_Color;

void main(){
gl_FragColor = v_Color;
}

这样修改之后,如果那个片元属于一个三角形,那OpenGL就会用构成那个三角形的三个顶点计算其混合的颜色。

Renderer渲染器

渲染器的改动有点多,我们先看代码,修改地方加了 修改 两个字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class HockeyRenderer(context: Context) : GLSurfaceView.Renderer {  
companion object {
private const val TAG = "HockeyRenderer"

// Float占4个字节
private const val BYTES_PER_FLOAT = 4

// 一个顶点有2个分量(x, y)
private const val POSITION_COMPONENT_COUNT = 2

// RGB颜色有3个分量(R,G,B) <修改:新增>
private const val COLOR_COMPONENT_COUNT = 3
// 跨距:数据数组里包含了 顶点坐标和颜色RGB,要获取数据就会 跳过 指定数量的数据 <修改:新增>
private const val STRIDE =
(POSITION_COMPONENT_COUNT + COLOR_COMPONENT_COUNT) * BYTES_PER_FLOAT

// 着色器参数名称
private const val A_COLOR = "a_Color" // <修改:调整>
private const val A_POSITION = "a_Position"
}

private var vertexShaderSource: String
private var fragmentShaderSource: String

// 着色器参数值
private var aColorLocation = 0
private var aPositionLocal = 0

private var vertexData: FloatBuffer

// 桌子、中线、摇杆坐标定义 <修改:调整>
private val tableVerticesWithTriangles = floatArrayOf(
// 桌子 X, Y, R, G, B
0f, 0f, 1f, 1f, 1f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, -0.5f, 0.7f, 0.7f, 0.7f,
0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, 0.5f, 0.7f, 0.7f, 0.7f,
-0.5f, -0.5f, 0.7f, 0.7f, 0.7f,

// 中间分界线
0f, 0.5f, 1f, 0f, 0f,
0f, -0.5f, 1f, 0f, 0f,
// 两个摇杆的质点位置
-0.25f, 0f, 0f, 0f, 1f,
0.25f, 0f, 0f, 1f, 0f
)

/**
* 当Surface被创建的时候,GLSurfaceView会调用这个方法;
* 这发生在应用程序第一次运行的时候,并且当设备长时间休眠,系统回收资源后重新被唤醒,这个方法也可能会被调用。
* 这意味着,本方法可能会被调用多次。
*/
override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
// 设置清空屏幕用的颜色;前三个参数分别对应红,绿和蓝,最后的参数对应透明度。
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

val programId = ShaderHelper.buildProgram(vertexShaderSource, fragmentShaderSource)
GLES20.glUseProgram(programId)

//
aPositionLocal = GLES20.glGetAttribLocation(programId, A_POSITION)
aColorLocation = GLES20.glGetAttribLocation(programId, A_COLOR)

vertexData.position(0)
// 调用GLES20.glVertexAttribPointer告诉OpenGL,它可以在缓冲区vertexData中找到a_Position对应的数据
GLES20.glVertexAttribPointer(
aPositionLocal,
POSITION_COMPONENT_COUNT,
GLES20.GL_FLOAT,
false,
STRIDE, // <修改:调整>
vertexData
)
GLES20.glEnableVertexAttribArray(aPositionLocal)

// <修改:新增>
vertexData.position(POSITION_COMPONENT_COUNT)//跳过的数据量,也就是顶点坐标分量
GLES20.glVertexAttribPointer(
aColorLocation,
COLOR_COMPONENT_COUNT,
GLES20.GL_FLOAT,
false,
STRIDE,
vertexData
)
GLES20.glEnableVertexAttribArray(aColorLocation)
}

/**
* 在Surface被创建以后,每次Surface尺寸变化时,在横竖屏切换的时候,这个方法都会被调用到。
*/
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
// 设置视口的尺寸,这个视口尺寸怎么理解呢,就是锁定你操作的渲染区域是哪部分,
// 整个屏幕那就是 (0.0)点开始,宽度为width,长度为height咯。
// 如果你只想渲染左半个屏幕,那就(0.0),宽度width/2,长度为height/2。
// 这样设置viewport大小后,你之后的GL画图操作,都只作用这部分区域,右半屏幕是不会有任何反应的。
GLES20.glViewport(0, 0, width, height)
}

/**
* 当绘制一帧时,这个方法会被调用。
* 在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;
* 因为在这方法返回之后,渲染缓冲区会被交换,并显示在屏幕上,
* 如果什么都没画,可能会看到糟糕的闪烁效果。
*
* @param gl OpenGL.ES 1.0的API遗留下来的,直接忽略即可
*/
override fun onDrawFrame(gl: GL10?) {
// 清空屏幕,这会擦除屏幕上的所有颜色,并用之前glClearColor调用定义的颜色填充整个屏幕
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)

// <修改:下面的代码都做了调整>
//========= 绘制桌子 =====================
// 第一个参数告诉OpenGL我们要画三角扇
// 第二个参数0,告诉OpenGL从顶点数组(tableVerticesWithTriangles )的开头处开始读顶点
// 第三个参数6,告诉OpenGL读入六个顶点。因为每个三角形有三个顶点,所以调用最终会画出4个三角形,也就是一个矩形桌子
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 6)

//========= 绘制中线 =====================
// 这次画的是线 GLES20.GL_LINES,从顶点数据的第6组开始,每组也是2个元素
GLES20.glDrawArrays(GLES20.GL_LINES, 6, 2)

//========= 绘制摇杆 =====================
// 这次画的是点 GLES20.GL_POINTS,从顶点数据的第8组开始,每组1个元素
GLES20.glDrawArrays(GLES20.GL_POINTS, 8, 1)
// 这次画的是点 GLES20.GL_POINTS,从顶点数据的第9组开始,每组1个元素
GLES20.glDrawArrays(GLES20.GL_POINTS, 9, 1)
}


init {
vertexShaderSource = Utils.loadStringFromAssets(context, "hockey/basic.vs")
fragmentShaderSource = Utils.loadStringFromAssets(context, "hockey/basic.fs")

vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.size * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
vertexData.put(tableVerticesWithTriangles)
}
}

调整内容主要包含以下几点:

  • 去掉和 u_Color 相关的常量、变量。
  • 增加和 a_Color 相关的常量、变量。
  • 增加在 vertexData 缓冲区中获取顶点颜色的逻辑,调用glVertexAttribPointer把颜色数据和着色器的a_Color关联起来。这里使用了一个特殊的常量 STRIDE,中文就叫 跨距 吧。因为现在数据数组内包含了 顶点坐标 和 顶点颜色 两种数据,所以在处理这两种数据时候,需要通过特殊规则获取,也就是获取数据是要 跳读 的,跨距 就是每次跳读跳过的数据量。
  • 更新 onDrawFrame 函数,将顶点颜色绘制逻辑都去掉。因为我们已经将顶点数据和颜色关联起来,所以现在之需要调用 glDrawArrays 即可,OpenGL会自动从vertexData数据里读入颜色属性。

最终效果


这个最终效果是不是比上一篇的看起来好多了?