阅读本文前如果是初次接触opengl可以先阅读前文:
openGL 3D图形和openGL简介和 android studio上第一个opengl es程序
在OpenGL中只能绘制点,直线,三角形。每个物体都是通过顶点聚合形成的点,直线,三角形组成基本图形,并且通过着色器着色绘制而成。所以,在OpenGL中顶点和着色器是非常重要的概念。
顶点:
顶点就是指几何物体拐角的点,最重要的属性就是位置,来标识顶点在空间的定位,通常用float[] 来存放顶点的坐标。
上图为一个正方形的基本顶点坐标,可以看出是由简单三角形构成,在OpenGL中(0,0)坐标位于左下角,采用逆时针笛卡尔坐标系, float[] tableVerticesWithTriangles = {
// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f
};
总是以逆时针方向排列顶点,叫卷曲顺序。
Android上Java程序和OpenGL通信方式
我们知道Android上的java程序是运行在虚拟机上的,而OpenGL程序是操作硬件的程序直接运行在硬件上,这就涉及Java程序和OpenGL通信问题,在Android中有两种方式实现该通信。
1.通过Java接口jni方式,这种方式Android软件包已经实现,例如当我们调用android.opengl.GLES20包里面方法时,实际使用Android通过jni方式来调用本地opengl库函数。
2.把内存从Java堆复制拷贝到本地堆。Java有一个特殊的类集合用来分配本地内存块,并且把Java数据拷贝到本地内存,本地内存可以被本地环境存取而不受java虚拟机垃圾回收管理,如下图。
代码层面可以参考本文末尾给的demo里面AirHockeyRenderer 代码如下:
/***
* Excerpted from "OpenGL ES for Android",
* published by The Pragmatic Bookshelf.
* Copyrights apply to this code. It may not be used to create training material,
* courses, books, articles, and the like. Contact us if you are in doubt.
* We make no guarantees that this code is fit for any purpose.
* Visit http://www.pragmaticprogrammer.com/titles/kbogla for more book information.
***/
package opengl.timothy.net.openglesproject_lesson2;
import static android.opengl.GLES20.GL_COLOR_BUFFER_BIT;
import static android.opengl.GLES20.GL_FLOAT;
import static android.opengl.GLES20.GL_LINES;
import static android.opengl.GLES20.GL_POINTS;
import static android.opengl.GLES20.GL_TRIANGLES;
import static android.opengl.GLES20.glClear;
import static android.opengl.GLES20.glClearColor;
import static android.opengl.GLES20.glDrawArrays;
import static android.opengl.GLES20.glEnableVertexAttribArray;
import static android.opengl.GLES20.glGetAttribLocation;
import static android.opengl.GLES20.glGetUniformLocation;
import static android.opengl.GLES20.glUniform4f;
import static android.opengl.GLES20.glUseProgram;
import static android.opengl.GLES20.glVertexAttribPointer;
import static android.opengl.GLES20.glViewport;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLSurfaceView.Renderer;
import opengl.timothy.net.openglesproject_lesson2.util.LoggerConfig;
import opengl.timothy.net.openglesproject_lesson2.util.ShaderHelper;
import opengl.timothy.net.openglesproject_lesson2.util.TextResourceReader;
public class AirHockeyRenderer implements Renderer {
private static final String U_COLOR = "u_Color";
private static final String A_POSITION = "a_Position";
private static final int POSITION_COMPONENT_COUNT = 2;
private static final int BYTES_PER_FLOAT = 4;
private final FloatBuffer vertexData;
private final Context context;
private int program;
private int uColorLocation;
private int aPositionLocation;
public AirHockeyRenderer() {
// This constructor shouldn't be called -- only kept for showing
// evolution of the code in the chapter.
context = null;
vertexData = null;
}
public AirHockeyRenderer(Context context) {
this.context = context;
/*
float[] tableVertices = {
0f, 0f,
0f, 14f,
9f, 14f,
9f, 0f
};
*/
/*
float[] tableVerticesWithTriangles = {
// Triangle 1
0f, 0f,
9f, 14f,
0f, 14f,
// Triangle 2
0f, 0f,
9f, 0f,
9f, 14f
// Next block for formatting purposes
9f, 14f,
, // Comma here for formatting purposes
// Line 1
0f, 7f,
9f, 7f,
// Mallets
4.5f, 2f,
4.5f, 12f
};
*/
float[] tableVerticesWithTriangles = {
// Triangle 1
-0.5f, -0.5f,
0.5f, 0.5f,
-0.5f, 0.5f,
// Triangle 2
-0.5f, -0.5f,
0.5f, -0.5f,
0.5f, 0.5f,
// Line 1
-0.5f, 0f,
0.5f, 0f,
// Mallets
0f, -0.25f,
0f, 0.25f
};
vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
}
@Override
public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {
/*
// Set the background clear color to red. The first component is red,
// the second is green, the third is blue, and the last component is
// alpha, which we don't use in this lesson.
glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
*/
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
String vertexShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_vertex_shader);
String fragmentShaderSource = TextResourceReader
.readTextFileFromResource(context, R.raw.simple_fragment_shader);
int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
if (LoggerConfig.ON) {
ShaderHelper.validateProgram(program);
}
glUseProgram(program);
uColorLocation = glGetUniformLocation(program, U_COLOR);
aPositionLocation = glGetAttribLocation(program, A_POSITION);
// Bind our data, specified by the variable vertexData, to the vertex
// attribute at location A_POSITION_LOCATION.
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
false, 0, vertexData);
glEnableVertexAttribArray(aPositionLocation);
}
/**
* onSurfaceChanged is called whenever the surface has changed. This is
* called at least once when the surface is initialized. Keep in mind that
* Android normally restarts an Activity on rotation, and in that case, the
* renderer will be destroyed and a new one created.
*
* @param width
* The new width, in pixels.
* @param height
* The new height, in pixels.
*/
@Override
public void onSurfaceChanged(GL10 glUnused, int width, int height) {
// Set the OpenGL viewport to fill the entire surface.
glViewport(0, 0, width, height);
}
/**
* OnDrawFrame is called whenever a new frame needs to be drawn. Normally,
* this is done at the refresh rate of the screen.
*/
@Override
public void onDrawFrame(GL10 glUnused) {
// Clear the rendering surface.
glClear(GL_COLOR_BUFFER_BIT);
// Draw the table.
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
glDrawArrays(GL_TRIANGLES, 0, 6);
// Draw the center dividing line.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_LINES, 6, 2);
// Draw the first mallet blue.
glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
glDrawArrays(GL_POINTS, 8, 1);
// Draw the second mallet red.
glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glDrawArrays(GL_POINTS, 9, 1);
}
}
代码中BYTES_PER_FLOAT = 4表示的是一个float占4个字节,FloatBuffer用来在本地底层内存存储数据。AirHockeyRenderer的构造函数中有如下代码
vertexData = ByteBuffer
.allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
vertexData.put(tableVerticesWithTriangles);
用ByteBuffer .allocateDirect分配了一块本地内存,这块内存不会被垃圾回收器管理,里面参数是需要申请内存字节大小,tableVerticesWithTriangles.length * BYTES_PER_FLOAT为本文需要申请大小。order(ByteOrder.nativeOrder())是告诉字节缓冲区(ByteBuffer)按照本地字节序(nativeOrder)组织他的内容。asFloatBuffer()是指我们不愿意用字节来操作内存还是float。 vertexData.put(tableVerticesWithTriangles)方法就把数据从dalvik/art虚拟机复制到本地内存。
字节序:
是描述一种硬件架构是如何来组织位(bit)和字节的.这个和具体硬件架构有关,有的硬件是按照大头序排列(即最重要的位在前面),有的按小头序(最重要的位在后面)。比如十进制10000转换为二进制数为10011100010000,在按大头序排列的硬件上是00100111 00010000,在按小头序排列的硬件上是 00010000 00100111 。
着色器:
在数据从Java层拷贝到底层内存后,最终是要在硬件GPU上渲染绘制。而着色器正是来告诉GPU这些数据如何组织渲染绘制。有两种着色器,需要在使用之前先定义:
1.顶点着色器(vetex shader),生产每个顶点的最终位置,针对每个顶点都会执行一次,一旦最终位置确定了,OpenGL就会把这些可见的顶点组装成点,直线,三角形。
2.片段着色器(fragment shader),为组成点,直线,三角形的每个片段生成最终的颜色,针对每个片段他都会执行一次.一个片段是一个小的,颜色单一的长方形区域,类似于计算器屏幕上的一个像素。一旦最后一块颜色生成,opengl会把他们写到一块称为帧缓冲区(frame buffer)的内存,然后Android会把这块帧缓冲器显示在屏幕上。
为什么要用着色器?
在着色器出现之前,OpenGL只能用一个固定的方式集合控制很少而有限的事情,比如场景里有多少雾,加多少光线,这些固定api好用不好扩展。故在opengles 2.0加入了使用着色器加入可编程api,而把旧的api完成删除,所以用户必须使用着色器。我们使用顶点着色器控制点,直线,三角形的空间位置,用片段着色器来控制绘制内容。
创建一个顶点着色器:
如本文的例子中在res目录下创建了顶点着色器和片段着色器。
其中顶点着色器simple_vertex_shader.glsl里代码如下:
attribute vec4 a_Position;
void main()
{
gl_Position = a_Position;
gl_PointSize = 10.0;
}
这些代码是按照着色器语言(GLSL)编写的,具体可参考相关资料。对于我们定义的每一个顶点顶点着色器都会调用一次,当他被调用时会在a_Position的属性里接收顶点位置信息,而这个属性是vec4 类型。一个vec4 包含4个分量,在位置上下文中指x,y,z,w坐标,默认情况下x,y,z值为0,w为1。attribute 就是把位置坐标放进着色器的手段。main方法是着色器入口函数,他会把前面定义的位置复制到输出变量gl_Position 中,必须顶点着色器一定要给gl_Position 赋值的,这样OpenGL才会把gl_Position 的值当做当前顶点最终的位置,组装成点,直线,三角形。
创建一个片段着色器(fragment shader):
首先需要了解“光栅化技术”,就是OpenGL会把点,直线,三角形分解成大量的小片段,类似像素,每个片段上有红,绿,蓝,透明度的颜色分量,每一个小片段根据不同的红,绿,蓝,透明度组合最终产生不同的颜色。这些小片段映射到屏幕上就是一个个像素,最终构成一幅美丽的图案。而片段着色器的作用就是告诉GPU每一个片段(像素)显示颜色的最终值。基于基本图元的每个片段,片段着色器都会被调用一次,如一个三角形被映射成1000个片段,则片段着色器会被调用1000次。
片段着色器simple_fragment_shader.glsl里代码如下:
precision mediump float;
uniform vec4 u_Color;
void main()
{
gl_FragColor = u_Color;
}
precision mediump float; 是用来描述float精度的,就像Java里面双精度,单精度一样。OpenGL里面精度有三种是lowp,mediump ,highp分别是低,中,高精度。只有在某些硬件上才指定为highp。其实在顶点着色器中也有精度,只是没有指定使用默认的highp,因为顶点着色器描述的是位置坐标所以默认使用的精度为最高级别,而片段着色器处于性能和质量权衡同时考虑到不同厂家兼容性使用mediump。
uniform和顶点着色器的attribute 不同。顶点着色器的每个顶点都需要设置用attribute 来描述坐标位置。而片段着色器的uniform设置一次,如果后面不赋新值在新的片段里面依旧是之前的值,这是一种状态机。u_Color也是4个分量,分别是红,绿,蓝,透明度。main里,把我们在uniform里面定义的颜色值赋值到OpenGL的变量gl_FragColor作为当前片段最终颜色值。下文中我们将继续深入opengl es着色器原理和具体过程,参考:
本文代码下载:
https://github.com/pangrui201/OpenGlesProject/tree/master/OpenGlesProject_lesson2