GLES初识OpenGL ES 3.x

一、OpenGL ES 3.x概述

OpenGL ES 是基于 OpenGL 三维图形 API 的子集,主要是针对手机以及 PDA(掌上电脑)等嵌入式设备设计的。 OpenGL ES 的 API 由 Khronos 组织定义并推广, Khronos 是一个图形软硬件行业协会,该协会致力于为这些 API 建立免费的开发标准。

OpenGL ES3.x简介
OpenGL 的应用领域较为广泛,适用于 UNIX、 Mac OS、 Linux 以及 Windows 等几乎所有的操作系统,可以开发游戏、工业建模以及嵌入式设备应用。

OpenGL ES 是专门针对嵌入式设备而设计的,其实际是 OpenGL 的剪裁版本,去除了OpenGL 中许多不是必须存在的特性,如 GL_QUADS(四边形)与 GL_POLYGONS(多边形)绘制模式以及 glBegin(开始) /glEnd(结束)操作等。

GLES初识OpenGL ES 3.x_第1张图片

GLES初识OpenGL ES 3.x_第2张图片

GLES初识OpenGL ES 3.x_第3张图片

GLES初识OpenGL ES 3.x_第4张图片

GLES初识OpenGL ES 3.x_第5张图片

GLES初识OpenGL ES 3.x_第6张图片

初识OpenGL ES 3.0应用程序

GLES初识OpenGL ES 3.x_第7张图片

ShaderUtil.java:

功能为将其着色器(Shader)脚本加载进显卡并编译。最先开发的是该类中从着色器sh脚本中加载着色器内容loadFromAssetsFile方法和检查每一步操作是否有错的checkGlError方法。

checkGlError 方法的作用是在向 GPU 着色程序中加入顶点着色器或者片元着色器时,检查每一步操作是否有错误。这是由于在开发着色器脚本文件中的代码时,没有一个开发环境实时地进行编译、查错,因此开发一个检查错误的方法尤为重要。

loadFromAssetsFile 方法的作用为从项目根目录的 assets 文件夹下加载着色器代码脚本。其通过输入流将脚本信息读入,然后交给 createProgram 方法创建着色器程序。

开发完加载着色器脚本内容的loadFromAssetsFile方法和检查错误的checkGlError方法后,下面开发的是加载着色器编码进入 GPU 并进行编译的 loadShader 方法与创建着色器程序的createProgram 方法。

加载指定着色器的 loadShader 方法中:第 2 行通过调用 glCreateShader 方法创建了一个着色器;如果着色器创建成功,则加载着色器的源代码,并编译着色器,同时检测编译的情况。若编译成功则返回着色器 id,反之则删除着色器并且打印错误信息。

通过调用 loadShader 方法,分别加载顶点着色器与片元着色器的源代码进入GPU,并分别进行编译的代码。

首先创建一个着色器程序,然后分别将相应的顶点与片元着色器添加到其中,最后将两个着色器链接为一个整体的着色器程序。

package com.bn.Sample3_1;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import android.annotation.SuppressLint;
import android.content.res.Resources;
import android.opengl.GLES30;
import android.util.Log;
//加载顶点Shader与片元Shader的工具类
public class ShaderUtil {
	// 加载制定shader的方法
	public static int loadShader(int shaderType, String source) { // shader的类型 GLES30.GL_VERTEX_SHADER(顶点)
        // GLES30.GL_FRAGMENT_SHADER(片元) ;shader的脚本字符串
		// 创建一个新shader
		int shader = GLES30.glCreateShader(shaderType);
		// 若创建成功则加载shader
		if (shader != 0) {
			// 加载shader的源代码
			GLES30.glShaderSource(shader, source);
			// 编译shader
			GLES30.glCompileShader(shader);
			// 存放编译成功shader数量的数组
			int[] compiled = new int[1];
			// 获取Shader的编译情况
			GLES30.glGetShaderiv(shader, GLES30.GL_COMPILE_STATUS, compiled, 0);
			if (compiled[0] == 0) {// 若编译失败则显示错误日志并删除此shader
				Log.e("ES20_ERROR", "Could not compile shader " + shaderType + ":");
				Log.e("ES20_ERROR", GLES30.glGetShaderInfoLog(shader));
				GLES30.glDeleteShader(shader);
				shader = 0;
			}
		}
		return shader;
	}

	// 创建shader程序的方法
	public static int createProgram(String vertexSource, String fragmentSource) {
		// 加载顶点着色器
		int vertexShader = loadShader(GLES30.GL_VERTEX_SHADER, vertexSource);
		if (vertexShader == 0) {
			return 0;
		}

		// 加载片元着色器
		int pixelShader = loadShader(GLES30.GL_FRAGMENT_SHADER, fragmentSource);
		if (pixelShader == 0) {
			return 0;
		}

		// 创建程序
		int program = GLES30.glCreateProgram();
		// 若程序创建成功则向程序中加入顶点着色器与片元着色器
		if (program != 0) {
			// 向程序中加入顶点着色器
			GLES30.glAttachShader(program, vertexShader);
			checkGlError("glAttachShader");
			// 向程序中加入片元着色器
			GLES30.glAttachShader(program, pixelShader);
			checkGlError("glAttachShader");
			// 链接程序
			GLES30.glLinkProgram(program);
			// 存放链接成功program数量的数组
			int[] linkStatus = new int[1];
			// 获取program的链接情况
			GLES30.glGetProgramiv(program, GLES30.GL_LINK_STATUS, linkStatus, 0);
			// 若链接失败则报错并删除程序
			if (linkStatus[0] != GLES30.GL_TRUE) {
				Log.e("ES20_ERROR", "Could not link program: ");
				Log.e("ES20_ERROR", GLES30.glGetProgramInfoLog(program));
				GLES30.glDeleteProgram(program);
				program = 0;
			}
		}
		return program;
	}

	// 检查每一步操作是否有错误的方法
	public static void checkGlError(String op) {
		int error;
		while ((error = GLES30.glGetError()) != GLES30.GL_NO_ERROR) {
			Log.e("ES20_ERROR", op + ": glError " + error);
			throw new RuntimeException(op + ": glError " + error);
		}
	}

	// 从sh脚本中加载shader内容的方法
	public static String loadFromAssetsFile(String fname, Resources r) {
		String result = null;
		try {
			InputStream in = r.getAssets().open(fname);
			int ch = 0;
			ByteArrayOutputStream baos = new ByteArrayOutputStream();
			while ((ch = in.read()) != -1) {
				baos.write(ch);
			}
			byte[] buff = baos.toByteArray();
			baos.close();
			in.close();
			result = new String(buff, "UTF-8");
			result = result.replaceAll("\\r\\n", "\n");
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}
}

Sample3_1Activity.java:

开发与本案例功能直接相关的类,首先需要开发的是本案例的主控制类Sample3_1Activity,该类继承自 Activity,在程序开始时执行。该类的主要工作是创建 MyTDView类的对象,然后调用 setContentView 方法跳转到相关界面。

继承 Activity 后重写的 onCreate 方法,在该方法中主要是创建 MyTDView 类的对象,然后设置 MyTDView 获得焦点以及可触控,最后调用 setContentView 方法跳转到相关界面。

继承Activity后重写的onResume 方法,在该方法中首先调用父类的onResume方法,然后调用 MyTDView 类对象的 onResume 方法。

继承 Activity 后重写的onPause 方法,该方法中首先调用父类的 onPause 方法,然后调用 MyTDView 类对象的 onPause 方法。

package com.bn.Sample3_1;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
public class Sample3_1Activity extends Activity {// 创建继承Activity的主控制类
	MyTDView mview;// 声明MyTDView类的引用

	@Override
	public void onCreate(Bundle savedInstanceState) {// 继承Activity后重写的方法
		super.onCreate(savedInstanceState);// 调用父类
		// 设置为竖屏模式
		setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
		mview = new MyTDView(this);// 创建MyTDView类的对象
		mview.requestFocus();// 获取焦点
		mview.setFocusableInTouchMode(true);// 设置为可触控
		setContentView(mview);
	}

	@Override
	public void onResume() {// 继承Activity后重写的onResume方法
		super.onResume();
		mview.onResume();// 通过MyTDView类的对象调用onResume方法
	}

	@Override
	public void onPause() {// 继承Activity后重写的onPause方法
		super.onPause();
		mview.onPause();// 通过MyTDView类的对象调用onPause方法
	}
}

Triangle.java:

用于创建三角形的类 Triangle。该类的主要功能为初始化顶点数据、初始化着色器、设置相应的平移矩阵及旋转矩阵。

声明相关矩阵的引用、自定义渲染管线着色器程序的 id、顶点位置和颜色属性的引用、顶点着色器以及片元着色器代码脚本字符串、顶点坐标和顶点着色数据缓冲、顶点的数量以及绕 x 轴旋转的角度。

本类的构造器,在该构造器中主要是调用 initVertexData 方法初始化顶点相关的数据,并且调用 intShader 方法初始化着色器。

初始化顶点数据的 initVertexData 方法。该方法中需要指定顶点的坐标数据以及顶点的颜色数据,将数据写入到对应的缓冲区中,并设置缓冲区的起始位置。

通过物体的 3D 变换矩阵、摄像机参数矩阵、投影矩阵计算产生最终总变换矩阵的方法。

initShader为初始化着色器的方法。在该方法中首先需要加载相应的着色器脚本,然后创建自定义的渲染管线着色器程序,并保留程序 id 于 mProgram 中。接着通过 GLES30 类调用相应的方法获取着色器程序中顶点坐标数据的引用、顶点颜色数据的引用以及总变换矩阵的引用。

drawSelf通过GLES30类调用glUseProgram方法给出着色器程序id指定使用的着色器程序。初始化变换矩阵,设置沿 z 轴正方向的平移值以及绕 x 轴旋转的角度值。通过调用 GLES30 类的 glVertexAttribPointer 方法,将顶点坐标数据以及顶点颜色数据传送进渲染管线,以备渲染时在顶点着色器中使用。通过调用 GLES30 类的 glEnableVertexAttribArray 方法启用顶点位置数据以及启用顶点颜色数据,并通过调用 GLES30 类的 glDrawArrays 方法绘制三角形。

package com.bn.Sample3_1;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import android.annotation.SuppressLint;
import android.opengl.GLES30;
import android.opengl.Matrix;
public class Triangle {
	public static float[] mProjMatrix = new float[16];// 4x4 投影矩阵
	public static float[] mVMatrix = new float[16];// 摄像机位置朝向的参数矩阵
	public static float[] mMVPMatrix;// 最后起作用的总变换矩阵

	int mProgram;// 自定义渲染管线程序id
	int muMVPMatrixHandle;// 总变换矩阵引用
	int maPositionHandle; // 顶点位置属性引用
	int maColorHandle; // 顶点颜色属性引用
	String mVertexShader;// 顶点着色器代码脚本
	String mFragmentShader;// 片元着色器代码脚本
	static float[] mMMatrix = new float[16];// 具体物体的移动旋转矩阵,包括旋转、平移、缩放

	FloatBuffer mVertexBuffer;// 顶点坐标数据缓冲
	FloatBuffer mColorBuffer;// 顶点着色数据缓冲
	int vCount = 0;
	float xAngle = 0;// 绕x轴旋转的角度

	public Triangle(MyTDView mv) {
		// 调用初始化顶点数据的initVertexData方法
		initVertexData();
		// 调用初始化着色器的intShader方法
		initShader(mv);
	}

	public void initVertexData() {// 初始化顶点数据的方法
		// 顶点坐标数据的初始化
		vCount = 3;
		final float UNIT_SIZE = 0.2f;
		float vertices[] = new float[] { 
				-4 * UNIT_SIZE, 0, 0,
				0, -4 * UNIT_SIZE, 0,
				4 * UNIT_SIZE, 0, 0};// 顶点坐标数组

		ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length * 4);
		vbb.order(ByteOrder.nativeOrder());// 设置字节顺序为本地操作系统顺序
		mVertexBuffer = vbb.asFloatBuffer();// 转换为浮点(Float)型缓冲
		mVertexBuffer.put(vertices);// 在缓冲区内写入数据
		mVertexBuffer.position(0);// 设置缓冲区起始位置

		float colors[] = new float[] { // 顶点颜色数组
				1, 1, 1, 0, // 白色
				0, 0, 1, 0, // 蓝
				0, 1, 0, 0// 绿
		};

		ByteBuffer cbb = ByteBuffer.allocateDirect(colors.length * 4);
		cbb.order(ByteOrder.nativeOrder());// 设置字节顺序为本地操作系统顺序
		mColorBuffer = cbb.asFloatBuffer();// 转换为浮点(Float)型缓冲
		mColorBuffer.put(colors);// 在缓冲区内写入数据
		mColorBuffer.position(0);// 设置缓冲区起始位置
	}

	// 初始化着色器的方法
	public void initShader(MyTDView mv) {
		// 加载顶点着色器的脚本内容
		mVertexShader = ShaderUtil.loadFromAssetsFile("vertex.sh", mv.getResources());
		// 加载片元着色器的脚本内容
		mFragmentShader = ShaderUtil.loadFromAssetsFile("frag.sh", mv.getResources());
		// 基于顶点着色器与片元着色器创建程序
		mProgram = ShaderUtil.createProgram(mVertexShader, mFragmentShader);
		// 获取程序中顶点位置属性引用
		maPositionHandle = GLES30.glGetAttribLocation(mProgram, "aPosition");
		// 获取程序中顶点颜色属性引用
		maColorHandle = GLES30.glGetAttribLocation(mProgram, "aColor");
		// 获取程序中总变换矩阵引用
		muMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix");
	}

	public void drawSelf() {
		// 指定使用某套shader程序
		GLES30.glUseProgram(mProgram);
		// 初始化变换矩阵
		Matrix.setRotateM(mMMatrix, 0, 0, 0, 1, 0);
		// 设置沿Z轴正向位移1
		Matrix.translateM(mMMatrix, 0, 0, 0, 1);
		// 设置绕x轴旋转
		Matrix.rotateM(mMMatrix, 0, xAngle, 1, 0, 0);
		// 将变换矩阵传入渲染管线
		GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, Triangle.getFianlMatrix(mMMatrix), 0);
		// 将顶点位置数据传送进渲染管线
		GLES30.glVertexAttribPointer(maPositionHandle, 3, GLES30.GL_FLOAT, false, 3 * 4, mVertexBuffer);
		// 将顶点颜色数据传送进渲染管线
		GLES30.glVertexAttribPointer(maColorHandle, 4, GLES30.GL_FLOAT, false, 4 * 4, mColorBuffer);
		GLES30.glEnableVertexAttribArray(maPositionHandle);// 启用顶点位置数据
		GLES30.glEnableVertexAttribArray(maColorHandle);// 启用顶点着色数据
		// 绘制三角形
		GLES30.glDrawArrays(GLES30.GL_TRIANGLES, 0, vCount);
	}

	public static float[] getFianlMatrix(float[] spec) {
		mMVPMatrix = new float[16];
		Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, spec, 0);
		Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);
		return mMVPMatrix;
	}
}

MyTDView.java:

用于显示 3D 场景的类 MyTDView。该类继承自 GLSurfaceView,并且在该类中通过内部类的形式创建了场景渲染器。

本类的成员变量,主要有每次旋转的角度、旋转线程类 RotateThread 对象的引用以及自定义渲染器 SceneRenderer 的引用。

本类的构造器,在该构造器中设置使用 OpenGL ES 3.0 版本,创建了SceneRenderer 类的对象,设置了渲染器,并且设置渲染模式为主动渲染。

实现 GLSurfaceView.Renderer 接口后重写的 onDrawFrame 方法,在该方法中首先需要清除深度缓存和颜色缓存,然后通过 Triangle 类的对象调用 drawSelf 方法绘制三角形。

实现 GLSurfaceView.Renderer 接口后重写的 onSurfaceChanged 方法,在该方法中设置了视口、透视投影相关参数以及摄像机的位置。

实现 GLSurfaceView.Renderer 接口后重写的 onSurfaceCreated 方法,在该方法中设置了屏幕的背景颜色、创建了 Triangle 类的对象、开启了深度检测、创建了 RotateThread类的对象并启动了该线程。

自定义的内部类线程,在该线程中通过 while 循环不断更改 Triangle 类中xAngle 的值,使得三角形以一定的角速度绕 x 轴转动。

package com.bn.Sample3_1;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.content.Context;
import android.opengl.GLES30;
import android.opengl.GLSurfaceView;
import android.opengl.Matrix;
public class MyTDView extends GLSurfaceView {
	final float ANGLE_SPAN = 0.375f;

	RotateThread rthread;
	SceneRenderer mRenderer;// 自定义渲染器的引用

	public MyTDView(Context context) {
		super(context);
		this.setEGLContextClientVersion(3);
		mRenderer = new SceneRenderer();
		this.setRenderer(mRenderer);
		this.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
	}

	private class SceneRenderer implements GLSurfaceView.Renderer {
		Triangle tle;

		@Override
		public void onDrawFrame(GL10 gl) {
			// 清除深度缓冲与颜色缓冲
			GLES30.glClear(GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT);
			// 绘制三角形对象
			tle.drawSelf();
		}

		@Override
		public void onSurfaceChanged(GL10 gl, int width, int height) {
			// 设置视窗大小及位置
			GLES30.glViewport(0, 0, width, height);
			// 计算GLSurfaceView的宽高比
			float ratio = (float) width / height;
			// 调用此方法计算产生透视投影矩阵
			Matrix.frustumM(Triangle.mProjMatrix, 0, -ratio, ratio, -1, 1, 1, 10);
			// 调用此方法产生摄像机9参数位置矩阵
			Matrix.setLookAtM(Triangle.mVMatrix, 0, 0, 0, 3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);
		}

		@Override
		public void onSurfaceCreated(GL10 gl, EGLConfig config) {
			// 设置屏幕背景色RGBA
			GLES30.glClearColor(0, 0, 0, 1.0f);
			// 创建三角形对对象
			tle = new Triangle(MyTDView.this);
			// 打开深度检测
			GLES30.glEnable(GLES30.GL_DEPTH_TEST);
			rthread = new RotateThread();
			rthread.start();
		}
	}

	public class RotateThread extends Thread {// 自定义的内部类线程
		public boolean flag = true;
		@Override
		public void run() {// 重写的run方法
			while (flag) {
				mRenderer.tle.xAngle = mRenderer.tle.xAngle + ANGLE_SPAN;
				try {
					Thread.sleep(20);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}
}

完成了 Java 代码的开发后,接着就可以用着色语言开发着色器了,着色器的代码可以存储在后缀名为“.sh”文件中,这些文件存放到项目的 assets 目录下。

frag.sh:

首先开发的是顶点着色器,其主要作用为执行顶点变换、纹理坐标变换等顶点的相关操作。

#version 300 es
uniform mat4 uMVPMatrix; //总变换矩阵
layout (location = 0) in vec3 aPosition;  //顶点位置
layout (location = 1) in vec4 aColor;    //顶点颜色
out  vec4 vColor;  //用于传递给片元着色器的变量
void main() {
   gl_Position = uMVPMatrix * vec4(aPosition, 1); //根据总变换矩阵计算此次绘制此顶点位置
   vColor = aColor;//将接收的颜色传递给片元着色器 
}

完成了顶点着色器代码的开发后,下面将开发的是片元着色器的代码。

frag.sh:

其主要作用为执行纹理的访问、颜色的汇总等操作。

#version 300 es
precision mediump float;
in  vec4 vColor; //接收从顶点着色器过来的参数
out vec4 fragColor;//输出到的片元颜色
void main() {                     
   fragColor = vColor;//给此片元颜色值
}

OpenGL ES 3.0 本身在各个平台上是通用的,直接相关于 OpenGL ES 3.0 的知识基于哪种平台进行学习关系并不大,学成之后可以在所有支持其的平台上使用。

书中的案例很多是使用 Java 语言开发的,性能没有影响吗?其实不必担心,原因如下:

❏本书的不少案例中在调用与 OpenGL ES 3.0 相关的 API 时虽然使用的是 Java 语言,但这部分 API 本质上并不是由 Java 实现的,Java 调用时也是通过 JNI 直接调用的底层 C 库,因此这部分代码用 Java 开发还是用 C 开发性能上基本没有什么差异。
❏由于本质上是直接调用的底层 C 库,因此 API 与 C 版本的几乎没有差异,这样本书案例中的很多代码直接就可以在 C 版本的应用中使用。就算少量不一样的部分,移植也很容易,因为相似度非常高。

OpenGL ES 3.1新特性简介

目前的 OpenGL ES 3.0 是基于 OpenGL 3.x 规范的子集,而 OpenGL ES 3.1 则是基于 OpenGL4.x 规范的子集,同时向下兼容 ES3.0/2.0 规范。 OpenGL ES 3.1 新特性主要包括如下几个部分。
❏计算着色器(Compute Shaders)。
其是新版的支柱性功能,来自 OpenGL 4.3。通过计算着色器,应用可使用 GPU 执行通用目的的计算任务,并与图形渲染紧密相连,将大大增强移动设备的计算能力。
❏独立的着色器对象。
应用可为 GPU 的顶点、片元着色器阶段独立编程,无需明确的连接步骤即可将顶点、片元着色器程序混合匹配在一起。
❏增强的纹理功能。
主要包括多重采样纹理、模版纹理、纹理聚集等。
❏着色语言的改进。
包含新的算法和位字段(bitfield)操作,还有现代方式的着色器编程等。
❏向下兼容。
能够兼容 OpenGL ES 2.0/3.0,编程人员可在已有基础上增加 3.1 特性。

二、着色器与渲染管线

OpenGL ES 2.0的渲染管线

渲染管线有时也称为渲染流水线,一般是由显示芯片(GPU)内部处理图形信号的并行处理单元组成。这些并行处理单元两两之间是相互独立的,在不同型号的硬件上独立处理单元的数量也有很大差异。一般越高端型号的硬件,其中独立处理单元的数量也就越多。

与普通应用程序通过 CPU 串行执行不同的是,渲染工作是通过渲染管线中多个相互独立的处理单元进行并行处理的,这种模式极大地提升了渲染效率。

从另一个角度看, OpenGL ES 中的渲染管线实质上指的是一系列绘制过程。这些过程输入的是待渲染 3D 物体的相关描述信息数据,经过渲染管线,输出的是一帧想要的图像。 OpenGL ES 2.0的渲染管线如下图所示:

GLES初识OpenGL ES 3.x_第8张图片
 

1.基本处理

该阶段设定3D空间中物体的顶点坐标、顶点对应的颜色、顶点的纹理坐标等属性,并且指定绘制方式,如点绘制、线段绘制或者三角形绘制等。

2.顶点缓冲对象

这部分功能在应用程序中是可选的,对于某些在整个场景中顶点的基本数据不变的情况。可以在初始化阶段将顶点数据经过基本处理后送入顶点缓冲对象,在绘制每一帧想要的图像时就省去了顶点数据IO的麻烦,直接从顶点缓冲对象中获取顶点数据即可。相比于每次绘制时单独将顶点数据送入GPU的方式,可以在一定程度上节省GPU的IO带宽,提高渲染效率。

3.顶点着色器

顶点着色器是一个可编程的处理单元,功能为执行顶点的变换、光照、材质的应用与计算等顶点的相关操作,其每顶点执行一次。其工作过程为首先将原始的顶点几何信息及其他属性传送到顶点着色器中,经过自己开发的顶点着色器处理后产生纹理坐标、颜色、点位置等后继流程需要的各项顶点属性信息,然后将其传递给图元装配阶段。
顶点着色器替代了原有固定管线的顶点变换、光照计算,开发人员可以根据自己的需求自行开发顶点变换、光照等功能,大大增加了程序的灵活性。但凡事有利皆有弊,增加灵活性的同时也增加了开发的难度。顶点着色器的工作原理如下图所示:

GLES初识OpenGL ES 3.x_第9张图片

❏ attribute 变量指的是 3D 物体中每个顶点各自不同的信息所属的变量,一般顶点的位置、颜色、法向量等每个顶点各自不同的信息都是以 attribute 变量的方式传入顶点着色器的。
❏ uniform 变量指的是对于同一组顶点组成的单个 3D 物体中所有顶点都相同的量,一般为场景中当前的光源位置、当前的摄像机位置、投影系列矩阵等。
❏ varying 变量(易变变量)是从顶点着色器计算产生并传递到片元着色器的数据变量。顶点着色器可以使用易变变量来传递需要插值到片元的颜色、法向量、纹理坐标等任意值。
❏ 内建输出变量 gl_Position、 gl_FrontFacing 和 gl_PointSize 等。 gl_Position 是经过变换矩阵变换、投影后顶点的最终位置,gl_FrontFacing 指的是片元所在面的朝向, gl_PointSize 指的是点的大小。

需要特别注意的是,易变变量在顶点着色器赋值后并不是直接将赋的值送入到后继的片元着色器中,而是在光栅化阶段由管线根据片元所属图元各个顶点对应的顶点着色器对此易变变量的赋值情况及片元与各顶点的位置关系插值产生,下图说明了这个问题。

GLES初识OpenGL ES 3.x_第10张图片 易变变量的工作原理

有一些数学知识的读者可能会想到一个问题,对每个片元进行一次插值计算将会非常耗费时间,严重影响性能。幸运的是, OpenGL ES 2.0 的设计者也考虑到了这个问题,这些插值操作都是由 GPU 中的专用硬件实现的,因此速度很快,不影响性能。

4.图元装配

这个阶段主要有两个任务,一个是图元组装,另一个是图元处理。所谓图元组装是指顶点数据根据设置的绘制方式被结合成完整的图元。例如,点绘制方式下每个图元仅需要一个单独的顶点,此方式下每个顶点为一个图元;线段绘制方式每个图元则需要两个顶点,此方式下每两个顶点构成一个图元;三角形绘制方式下需要 3 个顶点构成一个图元。
图元处理最重要的工作是剪裁,其任务是消除位于半空间(half-space)之外的部分几何图元,这个半空间是由一个剪裁平面所定义的。例如,点剪裁就是简单地接受或者拒绝顶点,线段或多边形剪裁可能需要增加额外的顶点,具体取决于直线或者多边形与剪裁平面之间的位置关系,如下图所示:

GLES初识OpenGL ES 3.x_第11张图片

上图给出了一个三角形图元(图中为点划线绘制)被 4 个剪裁平面剪裁的情况。 4 个剪裁平面分别为:上面、左侧面、右侧面、后面。

之所以要进行剪裁是因为随着观察位置、角度的不同,并不总能看到(这里可以简单地理解为显示到设备屏幕上)特定 3D 物体某个图元的全部。例如,当观察一个正四面体并离某个三角形面很近时,可能只能看到此面的一部分,这时在屏幕上显示的就不再是三角形了,而是经过裁剪后形成的多边形,如下图所示:

GLES初识OpenGL ES 3.x_第12张图片

剪裁时,若图元完全位于视景体以及自定义剪裁平面的内部,则将图元传递到后面的步骤进行处理;如果其完全位于视景体或者自定义剪裁平面的外部,则丢弃该图元;如果其有一部分位于内部,另一部分位于外部,则需要剪裁该图元。

5.光栅化

虽然虚拟 3D 世界中的几何信息是三维的,但由于目前用于显示的设备都是二维的。因此,在真正执行光栅化工作之前,首先需要将虚拟 3D 世界中的物体投影到视平面上。需要注意的是,由于观察位置的不同,同一个 3D 场景中的物体投影到视平面可能会产生不同的效果,如下所示:

GLES初识OpenGL ES 3.x_第13张图片
另外,由于虚拟 3D 世界当中物体的几何信息一般采用连续的数学量来表示,因此投影的平面结果也是用连续数学量表示的。但目前的显示设备屏幕都是离散化的(由一个一个的像素组成),因此还需要将投影的结果离散化。将其分解为一个一个离散化的小单元,这些小单元一般称为片元,具体效果如下图所示:

GLES初识OpenGL ES 3.x_第14张图片

其实每个片元都对应于帧缓冲中的一个像素,之所以不直接称为像素是因为 3D 空间中的物体是可以相互遮挡的。而一个 3D 场景最终显示到屏幕上虽然是一个整体,但每个 3D 物体的每个图元是独立处理的。这就可能出现这样的情况,系统先处理的是位于离观察点较远的图元,其光栅化成为了一组片元,暂时送入帧缓冲的对应位置。

但后面继续处理离观察点较近的图元时也光栅化出了一组片元,两组片元中有对应到帧缓冲中同一个位置的,这时距离近的片元将覆盖距离远的片元(如何覆盖的检测是在深度检测阶段完成)。因此,某片元就不一定能成为最终屏幕上的像素,称为像素就不准确了,可以将其理解为候选像素。

每个片元包含其对应的顶点坐标、顶点颜色、顶点纹理坐标以及顶点的深度等信息,这些信息是系统根据投影前此片元对应的 3D 空间中的位置及与此片元相关的图元的各顶点信息进行插值计算而生成的。

6.片元着色器

片元着色器是用于处理片元值及其相关数据的可编程单元,其可以执行纹理的采样、颜色的汇总、计算雾颜色等操作,每片元执行一次。片元着色器主要功能为通过重复执行(每片元一次),将 3D 物体中的图元光栅化后产生的每个片元的颜色等属性计算出来送入后继阶段,如剪裁测试、深度测试及模板测试等。
从渲染管线的图中可以看出,可编程片元着色器替代了老版本中固定管线纹理采样、颜色求和、雾以及 Alpha 测试等阶段。与顶点着色器类似,被其替代的功能系统将不再提供,需要完全由开发人员用着色器语言编程完成。这在提高了灵活性的同时也增加了开发的难度,尤其是对于初学者,其基本工作原理如下图所示:

GLES初识OpenGL ES 3.x_第15张图片
❏ Varying0~n 指的是从顶点着色器传递到片元着色器的易变变量,如前面所介绍,由系统在顶点着色器后的光栅化阶段自动插值产生。其个数是不一定的,取决于具体的需要。
❏ gl_FragColor 内建变量指的是片元的最终颜色。一般在片元着色器的最后都需要对gl_FragColor 进行赋值。

经过光栅化、顶点着色器与片元着色器的介绍,可以看出顶点着色器每顶点执行一次,而片元着色器每片元执行一次,片元着色器的执行次数明显大于顶点着色器的执行次数。因此在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。

7.剪裁测试

如果程序中启用了剪裁测试, OpenGL ES 会检查每个片元在帧缓冲中对应的位置,若对应位置在剪裁窗口中则将此片元送入下一阶段,否则丢弃此片元。

8.深度测试和模板测试

❏  深度测试是指将输入片元的深度值与帧缓冲区中存储的对应位置片元的深度值进行比较,若输入片元的深度值小则将输入片元送入下一阶段准备覆盖帧缓冲中的原片元或与帧缓冲中的原片元混合,否则丢弃输入片元。
❏  模板测试的主要功能为将绘制区域限定在一定的范围内,一般用在湖面倒影、镜像等场合。

9.颜色缓冲混合

若程序中开启了 Alpha 混合,则根据混合因子将上一阶段送来的片元与帧缓冲中对应位置的片元进行 Alpha 混合;否则送入的片元将覆盖帧缓冲中对应位置的片元。

10.抖动

抖动是一种简单的操作,其允许只使用少量的颜色模拟出更宽的颜色显示范围,从而使颜色视觉效果更加丰富。例如,可以使用白色以及黑色模拟出一种过渡的灰色。
但使用抖动也是有缺点的,那就是会损失一部分分辨率,因此,对于现在主流的原生颜色就很丰富的显示设备一般是不需要启用抖动的。
当下的一些系统中虽然在 API 方面支持开启抖动,但这仅是为了 API 的兼容,其可能根本不会去进行事实上的抖动操作。

11.帧缓冲

OpenGL ES 中的物体绘制并不是直接在屏幕上进行的,而是预先在帧缓冲区中进行绘制,每绘制完一帧再将绘制的结果交换到屏幕上。因此,在每次绘制新的一帧时都需要清除缓冲区中的相关数据,否则有可能产生不正确的绘制效果。
同时需要了解的是,为了应对不同方面的需要,帧缓冲是由一套组件组成的,主要包括颜色缓冲、深度缓冲以及模板缓冲,各组件的具体用途如下所列。

❏  颜色缓冲用于存储每个片元的颜色值,每个颜色值包括 RGBA(红、绿、蓝、透明度) 4个色彩通道,应用程序运行时在屏幕上看到的就是颜色缓冲中的内容。
❏  深度缓冲用来存储每个片元的深度值,所谓深度值是指以特定的内部格式表示的从片元处到观察点(摄像机)的距离。在启用深度测试的情况下,新片元想进入帧缓冲时需要将自己的深度值与帧缓冲中对应位置片元的深度值进行比较,若结果为小于才有可能进入缓冲,否则被丢弃。
❏  模板缓冲用来存储每个片元的模板值,供模板测试使用。模板测试是几种测试中最为灵活和复杂的一种。

OpenGL ES 3.0的渲染管线

OpenGL ES 2.0 采用的是可编程的渲染管线,编程人员可以插入一些特殊操作,方便高效地完成 OpenGL ES 1.x 固定渲染管线难以完成的任务。而 OpenGL ES 3.0 采用的也是可编程渲染管线,只是 OpenGL ES 3.0 增加了一些新特性。
通过增加的新特性,能够让移动平台的游戏画面更加逼真、细腻,提高设备 3D 渲染的性能,OpenGL ES 3.0 可编程渲染管线如下图所示:

GLES初识OpenGL ES 3.x_第16张图片

从图中可以看出,总体上 OpenGL ES 3.0 可编程渲染管线与 OpenGL ES 2.0 的相同,但其实顶点着色器与片元着色器内部还是有不少区别的,详细内容如下。

1.顶点着色器

此处的顶点着色器与 OpenGL ES 2.0 的顶点着色器相同,也是一个可编程的处理单元,其功能也是执行顶点的变换、光照、材质的应用与计算等顶点的相关操作,每顶点执行一次。其工作过程也是首先将原始的顶点几何信息及其他属性传送到顶点着色器中,经过自己开发的顶点着色器处理后产生纹理坐标、颜色、点位置等后继流程需要的各项顶点属性信息,然后将其传递给图元装配阶段。
通过可编程的顶点着色器,开发人员可以根据自己的需求自行开发顶点变换、光照等功能,下面给出顶点着色器的工作原理图,如下图所示:

GLES初识OpenGL ES 3.x_第17张图片    对比ES 2.0:GLES初识OpenGL ES 3.x_第18张图片

❏ 顶点着色器的输入主要为待处理顶点相应的 in 变量、 uniform(一致)变量、采样器以及临时变量,输出主要为经过顶点着色器后生成的 out 变量及一些内建输出变量。
❏ 顶点着色器中的 in 变量指的是 3D 物体中每个顶点各自不同的信息所属的变量,一般顶点的位置、颜色、法向量等每个顶点各自不同的信息都是以 in 变量的方式传入顶点着色器的。例如,前面“初识 OpenGL ES 3.0 应用程序”中顶点着色器里的 aPosition(顶点位置)和 aColor(顶点颜色)变量等。
❏ uniform 变量指的是对于同一组顶点组成的单个 3D 物体中所有顶点都相同的量,一般为场景中当前的光源位置、当前的摄像机位置、投影系列矩阵等。例如,前面“初识 OpenGL ES3.0 应用程序”案例中顶点着色器里的 uMVPMatrix(总变换矩阵)变量等。
❏ 顶点着色器中的 out 变量是从顶点着色器计算产生并用于传递到片元着色器的数据变量。顶点着色器可以使用 out 变量来传递需要插值或不需要插值到片元的颜色、法向量、纹理坐标等任意值。例如,“初始 OpenGL ES 3.0 应用程序”中由顶点着色器传入片元着色器中的vColor 变量。
❏ 内建输出变量 gl_Position、 gl_PointSize 以及内建输入变量 gl_VertexID、 gl_InstanceID 等。gl_Position 是经过变换矩阵变换、投影后的顶点的最终位置, gl_PointSize 指的是点的大小。gl_VertexID 用来记录顶点的整数索引。 gl_InstanceID 是指实例 ID,只在顶点着色器中使用,对于指定的每一组图元,该 ID 相应递增。

out变量在顶点着色器赋值后并不是直接将赋的值传递到后继的片元着色器对应的 in变量中,在此存在两种情况:
❏ 如果 out 限定符之前含有 smooth 限定符或者不含有任何限定符,则传递到后继片元着色器对应的 in 变量的值,是在光栅化阶段由管线根据片元所属图元各个顶点对应的顶点着色器对此out 变量的赋值情况及片元与各顶点的位置关系插值产生,下图所示说明了这个问题:

GLES初识OpenGL ES 3.x_第19张图片

❏ 如果 out 限定符之前含有 flat 限定符,则传递到后继片元着色器对应的 in 变量的值不是在光栅化阶段插值产生的,而是由图元的最后一个顶点对应的顶点着色器对此 out 变量所赋的值决定的,此种情况下图元中每个片元的值均相同。

2.片元着色器

与 OpenGL ES 2.0 相同, OpenGL ES 3.0 的片元着色器同样是用于处理片元值及其相关数据的可编程单元,其可以执行纹理的采样、颜色的汇总、计算雾颜色等操作,每片元执行一次。
与 OpenGL ES 2.0 不同的是,片元着色器内的varying 变量变成了 in 变量,并且内建输出变量gl_FragColor 不存在了。其基本原理图如下图所示:

GLES初识OpenGL ES 3.x_第20张图片    对比ES 2.0:GLES初识OpenGL ES 3.x_第21张图片

❏ in0~in(n)指的是从顶点着色器传递到片元着色器的变量,如前面所介绍,由系统在顶点着色器后的光栅化阶段自动产生,其个数是不一定的,取决于具体的需要。例如, “初识 OpenGL ES 3.0 应用程序”中片元着色器里的 vColor 变量。
❏ out 变量一般指的是由片元着色器写入计算完成的片元颜色值的变量,一般在片元着色器的最后,都需要对其进行赋值,最后将其送入渲染管线的后继阶段进行处理。例如, “初识 OpenGL ES 3.0 应用程序”中片元着色器里创建的 fragColor 变量。

原来 OpenGL ES 2.0 中片元着色器的内建输出变量 gl_FragColor 在 OpenGL ES3.0 中不存在了,如果需要输出颜色值,则需要自己声明 out(类型为 vec4)变量,用声明的变量替代 gl_FragColor。在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。

OpenGL ES中立体物体的构建

GLES初识OpenGL ES 3.x_第22张图片

从两幅照片中可以对比出,现实世界的某些建筑物远看是平滑的曲面,其实近看是由一个一个的小平面组成的。 3D 虚拟世界中也是如此,任何立体物体都是由多个小平面搭建而成的。这些小平面切分得越小,越细致,搭建出来的物体就越平滑。

当然 OpenGL ES 的虚拟世界与现实世界还是有区别的,现实世界中可以用任意形状的多边形来搭建建筑物,例如图中的国家大剧院就是用四边形搭建的,而 OpenGL ES 中仅允许采用三角形来搭建物体。其实这从构造能力上来说并没有区别,因为任何多边形都可以拆分为多个三角形,只需开发时稍微注意一下即可。

下图更加具体地说明了,在 OpenGL ES 中如何采用三角形来构建立体物体。

GLES初识OpenGL ES 3.x_第23张图片
从最右侧图中可以看出, OpenGL ES 中采用的是左手标架坐标系。一般来说,初始情况下 y轴平行于屏幕的竖边, x 轴平行于屏幕的横边, z 轴垂直于屏幕平面。

三、主流GPU性能参数比较

GLES初识OpenGL ES 3.x_第24张图片

你可能感兴趣的:(《OpenGL,ES,3.x,游戏开发(上下卷)》)