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);// shadershaderProgramId = 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来介绍,这样会更加容易理解,后面我们会介绍纹理,也会需要在片段着色器中做处理。