OpenGL ES 着色器(5)
简述
着色器是在GPU上运行的程序,它会对每一个点都执行一次程序,并且计算出每个像素需要渲染的颜色,我们主要关注着色器的怎么传递数据,在OpenGL ES中,着色器传递数据分几种场景,一种是Cpu传递数据给GPU,一种是顶点缓冲区的数据传递到着色器,还有一种是顶点着色器数据传递给片段着色器。
着色器主要有三种类型传递数据参数:
- attribute,用于顶点缓冲区数据传递到着色器中。
- uniform,统一变量,由CPU将数据传递到GPU。
- varying,用于顶点着色器数据向片段着色器传递。
着色器使用
我们以实现一个颜色渐变的圆形来介绍着色器的使用。
我们之前说过OpenGL渲染都是以三角形为基础的,那应该怎么渲染圆形呢,我的第一反应就是高数里微积分,通过无数个三角形拼接起来,不过这个方案有个问题,我们需要使用多少个三角形才合适,才能让圆形足够圆滑。
虽然GPU更善于执行计算,但是其实GPU也是可以处理逻辑的,所以其实只需要在着色器里做一个简单的逻辑就可以了。
所以我们的思路就是绘制一个正方形,在片段着色器里判断坐标,如果坐标在圆形范围内则为白色, 否则就配置透明的颜色,这样就可以渲染出一个圆形了。
在开始之前,我们需要了解用到的顶点着色器和片段着色器。
顶点着色器
顶点着色器主要用于获取顶点数据,一般是从顶点缓冲区中获取,里面有内置变量gl_Position作为顶点位置的输出,顶点着色器在读取顶点数据并且做预处理后,可以将坐标信息给gl_Position作为输出。
顶点着色器会对每一个顶点都执行一次。
片段着色器
片段着色器用于计算输出的颜色,有一个内置变量gl_FragColor,作为颜色的输出,片段着色器会对每一个像素都执行一次,然后返回该像素需要显示的颜色。
片段着色器可以获取从顶点着色器中获取数据,这里传递数据有两种模式,区别为是否插值,默认是平滑的,会插值的。(就是说几个顶点中的数据会根据坐标线性平滑的渐变,我们之前在顶点缓冲区章节的渐变色三角形demo就是这个原理)
我们想要实现圆形的判断逻辑,需要在片段着色器中处理逻辑,因为片段着色器会对每个像素执行一次。
顶点数据
我们顶点数据为四个顶点,每个顶点7个数据,前三个是坐标,后四个是颜色。
使用索引缓冲区,这里和之前绘制正方形的demo差不多,这里坐标覆盖了全屏幕,作用相当于画板,获取我们会通过一个统一变量来控制圆形半径。
private float[] vertexArray = new float[] {
-1.0f, -1.0f, 0.0f, 1f, 1f, 1f, 1f,
1.0f, -1.0f, 0.0f, 1f, 1f, 1f, 1f,
-1.0f, 1.0f, 0.0f, 1f, 1f, 1f, 1f,
1.0f, 1.0f, 0.0f, 1f, 1f, 1f, 1f
};
private short[] indexArray = new short[] {
0,1,2,
1,2,3
};
着色器代码
顶点着色器逻辑:
两个属性,vPosition是坐标,除了直接传给gl_Position外,还通过一个varying的变量传给片段着色器vertexPosition,我们说过这个参数传递默认是会插值的,所以片段着色器拿到的值相当于每个点的坐标。
属性inputColor是从顶点缓冲区中拿的数据,用作颜色,这里通过一个varying变量outputColor传给片段着色器。
private final String vertexShaderCode =
"attribute vec4 vPosition;" +
"attribute vec4 inputColor;" +
"varying vec4 outputColor;" +
"varying vec4 vertexPosition;" +
"void main() {" +
" gl_Position = vPosition;" +
" vertexPosition = vPosition;" +
" outputColor = inputColor;" +
"}";
片段着色器逻辑:
这里有两个传过来的参数,一个为vertexPosition,为像素点坐标位置,一个为outputColor,作为输出颜色。
还有一个统一变量radius作为圆形半径,我们以(0,0)为圆形,这里一个简单几何逻辑,x * x + y * y < radius * radius的部分就是圆形部分,属于圆形部分颜色输出outputColor,否则输出一个透明色。
private final String fragmentShaderCode =
"precision mediump float;" +
"varying vec4 vertexPosition;" +
"varying vec4 outputColor;" +
"uniform float radius;" +
"void main() {" +
" if (vertexPosition.x * vertexPosition.x + vertexPosition.y * vertexPosition.y > radius * radius) {" +
" gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);" +
" } else {" +
" gl_FragColor = outputColor;" +
" }" +
"}";
顶点缓冲区数据填充
这里的逻辑和之前正方形绘制基本一样,就不细说了。
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
// 清除颜色
GLES30.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
// 创建顶点缓冲区
int[] idBuffer = new int[2];
GLES30.glGenBuffers(2, idBuffer, 0);
vertexBufferId = idBuffer[0];
elementBufferId = idBuffer[1];
// 顶点缓冲区数据填充
FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(vertexArray.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
GLES30.glBufferData(
GLES30.GL_ARRAY_BUFFER,
vertexArray.length * 4,
vertexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
ShortBuffer indexBuffer = ByteBuffer.allocateDirect(indexArray.length * 4).order(ByteOrder.nativeOrder()).asShortBuffer();
indexBuffer.put(indexArray);
indexBuffer.position(0);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
GLES30.glBufferData(
GLES30.GL_ELEMENT_ARRAY_BUFFER,
indexArray.length * 4,
indexBuffer,
GLES30.GL_STATIC_DRAW
);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
// shader
shaderProgramId = initShaderProgram(vertexShaderCode, fragmentShaderCode);
}
顶点布局以及渲染
配置顶点缓冲区布局,前三个数作为坐标,后4个数作为颜色。
设置统一变量radius,作为圆形半径。
public void onDrawFrame(GL10 gl) {
// 清除屏幕
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
// 使能着色器程序
GLES30.glUseProgram(shaderProgramId);
// 顶点缓冲区每个顶点前3个数作为坐标
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int positionLocation = GLES30.glGetAttribLocation(shaderProgramId, "vPosition");
GLES30.glEnableVertexAttribArray(positionLocation);
GLES30.glVertexAttribPointer(positionLocation, 3, GLES30.GL_FLOAT, false, 7 * 4, 0);
// 顶点缓冲区每个顶点后4个数作为颜色
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vertexBufferId);
int inputColorLocation = GLES30.glGetAttribLocation(shaderProgramId, "inputColor");
GLES30.glEnableVertexAttribArray(inputColorLocation);
GLES30.glVertexAttribPointer(inputColorLocation, 4, GLES30.GL_FLOAT, false, 7 * 4, 3 * 4);
// 配置统一变量,圆形半径
int radiusLocation = GLES30.glGetUniformLocation(shaderProgramId, "radius");
GLES30.glUniform1f(radiusLocation, 0.5f);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, elementBufferId);
// 调用DrawCall绘制三角形
GLES30.glDrawElements(GLES30.GL_TRIANGLES, 6, GLES30.GL_UNSIGNED_SHORT, 0);
// 清除配置
GLES30.glDisableVertexAttribArray(positionLocation);
GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, 0);
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
GLES30.glUseProgram(0);
}
效果
小结
看完本节的demo,应该对着色器的使用有了一定的,着色器的语法和C语言有一些类似,不过它其实不是C语言,而是自定义的语言,然后会编辑成显卡驱动可以理解的字节码。
OpenGL ES章节更多的都是以一些简单的demo来介绍,这样会更加容易理解,后面我们会介绍纹理,也会需要在片段着色器中做处理。