在完成了OpenGL ES和ARCore基本的实现后,下一步肯定就是图像绘制了。在Google为ARCore提供的Demo中,主要进行了相机预览、检测点云、检测平面和Android小机器人的绘制。本节我们将跟大家介绍相机预览背景的绘制过程和原理。
一、OpenGL ES渲染管线
OpenGL ES渲染管线其实就是渲染流水线,实质上指的是一系列的绘制过程。这些过程输入的是待渲染3D物理的相关描述信息数据,经过渲染管线,输出一帧想要的图像。
OpenGL ES渲染管线,如下图所示:
1.基本处理
在该阶段设定3D空间中物体的顶点坐标、顶点对应的颜色、顶点纹理坐标等属性,并指定绘制方式,如点绘制、线段绘制或者三角形绘制等。
OpenGL ES支持2D和3D图形的绘制。当你希望在2D界面绘制一条任意的曲线的时候,把这边曲线放的足够大来看,会发现这条曲线其实是由许多足够短的直线连接起来的。那么绘制3D图形,即使你看见的是一个“圆滑曲面”的3D图形,实际上足够放大它依然是由多个小平面组成的。所以对于2D图形,可能是由很多“曲线”组成;而3D图形可能是由很多“曲面”组成。
2.顶点缓冲对象
该阶段在应用程序中是可选的。在整个场景中顶点的基本数据不变的情况。可以在初始化阶段将顶点数据经过基本处理后送入顶点缓冲对象,在绘制每一帧想要的图像就省去了顶点IO的麻烦,直接从顶点缓冲对象中获取顶点数据即可。
3.顶点着色器
顶点着色器是一个可编程处理单元,为执行顶点变换、光照、材质的应用与计算等顶点相关的操作,每个顶点执行一次。工作过程首先将原始的几何信息及其他属性传送到顶点着色器中,经过自己开发的顶点着色器处理后产生纹理坐标、颜色、点位置等后流程需要的各项顶点属性信息,然后将其传递给图元装配阶段。
顶点着色器输入主要为待处理顶点相应的attribute(属性)变量、uniform(一致)变量、采样器以及临时变量;输出主要为经过顶点着色器后生成的varying(易变)变量以及一些内建输出变量。
4.图元装配
该阶段主要任务是图元组装和图元处理。
图元组装是指顶点数据根据设置的绘制方式被结合成完整的图元。如点绘制每个顶点为一个图元;线段绘制方式每个图元则为两个顶点;三角形绘制方式下需要3个顶点构造成一个图元。
图元处理最重要的工作是剪裁,其任务是消除位于半空间之外的部分几何图元。之所以进行剪裁,是因为随着观察位置、角度不同,并不能总看到特定3D物体某个图元的全部。剪裁时,若图元完全位置视景体以及自定义裁剪平面的内部,则将图元传递到后面步骤进行处理;如果其完全位于视景体或者自定义剪裁平面的外部,则丢弃该图元。
5.光栅化
虚拟3D世界中的物体的几何信息一般采用连续的数学向量来表示,因此投影的平面结果也是用连续的数学向量表示的。但目前的显示设备都是离散化的(由一个一个的像素组成),因此还需要将投影的结果离散化。将其分解为一个一个离散化的小单元。
6.片元着色器
片元着色器是用于处理片元值以及相关数据的可编程单元,其可以执行纹理的采样、颜色的汇总、计算雾颜色等操作,每片元执行一次。片元着色器通过重复执行,将3D物体中的图元光栅化后产生的每个片元的颜色等属性计算出来后送入后续阶段,如剪裁测试、深度测试及模板测试等。
片元着色器输入是从顶点着色器传递到片元着色器的易变变量(Varying0~n),输出为内建变量(gl_FragColor)是片元的最终颜色。
7.剪裁测试
OpenGL ES会检查每个片元在帧缓冲中对应的位置,若对应位置在剪裁窗口中则将此片元送入下一阶段,否则丢弃此片元。
8.深度测试和模板测试
深度测试指将输入片元的深度值与缓冲区中存储的对应位置的深度值进行比较,若输入片元的深度值小于则将输入片元送入下一个阶段准备覆盖帧缓冲中的原片元或帧缓冲中的原片元缓冲,否则丢弃输入片元。
模板测试的主要功能是为将绘制区域限定在一定范围内,一般应用在湖面倒影、镜像等场合。
9.颜色缓冲混合
若程序开启的Alpha混合,则根据混合因子将上一阶段送来的片元帧缓冲对应的位置的片元进行Alpha混合,否则送入的片元将覆盖帧缓冲中对应位置的片元。
10.抖动
抖动是一种简单的操作,允许只使用少量的颜色模拟出更宽的颜色显示范围,从而使颜色视觉更丰富。
11.帧缓冲
OpenGL ES中的物体绘制并不是直接绘制在屏幕上进行的,而是预先在帧缓冲区中进行绘制,每绘制完一帧再将绘制的结果交换到屏幕上。因此,在每次绘制更新一帧都需要清除缓冲区中的相关数据。
二、纹理映射
除了基本基本图形的绘制,如果想要绘制更加真实、炫酷的3D物体,就需要用到纹理映射。它就是把一幅纹理应用到相应的几何图元,告知渲染系统如何进行纹理映射。告知的方式就是为图元中的每个顶点指定恰当纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中的纹理区域中的内容根据纹理坐标映射到指定的图元上。
三 、相机预览背景绘制步骤
1.绘制背景准备
在该过程,完成了顶点和纹理坐标数据的提供,纹理id的生成获取、绑定和设置,创建顶点坐标和纹理坐标顶点缓冲对象,加载并绑定背景顶点和片元着色器,获取顶点和纹理坐标位置属性等。
2.背景纹理和Session摄像头纹理绑定
在该过程中,将背景纹理id和Session摄像头纹理id进行绑定。
3.执行背景绘制
该阶段处理显示旋转纹理坐标的变换,设置深度测试,告知OpenGL应用的纹理id和使用的程序,将顶点和纹理坐标传送渲染管线,启动顶点和纹理id数据,执行背景绘制和禁用顶点数据等。
四、案例源码分析
1.绘制背景准备
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
private static final String TAG = BackgroundRenderer.class.getSimpleName();
private static final int COORDS_PER_VERTEX = 3;
private static final int TEXCOORDS_PER_VERTEX = 2;
private static final int FLOAT_SIZE = 4;
//顶点坐标数据
public static final float[] QUAD_COORDS = new float[]{
-1.0f, -1.0f, 0.0f, //左下顶点
-1.0f, +1.0f, 0.0f, //左上顶点
+1.0f, -1.0f, 0.0f, //右下顶点
+1.0f, +1.0f, 0.0f, //右上顶点
};
//纹理坐标数据
public static final float[] QUAD_TEXCOORDS = new float[]{
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 1.0f,
1.0f, 0.0f,
};
//顶点坐标,纹理坐标和顶点变换数据内存
private FloatBuffer mQuadVertices;
private FloatBuffer mQuadTexCoord;
private FloatBuffer mQuadTexCoordTransformed;
//背景着色器程序
private int mQuadProgram;
//背景绘制顶点和纹理属性
private int mQuadPositionParam;
private int mQuadTexCoordParam;
//背景纹理渲染id
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
public BackgroundRenderer() {
}
... ...
/**
* 分配和初始化背景渲染器的需要的OpenGL资源。必须在OpenGL线程中调用,通常在GLSurfaceView.Render
* (GL10,EGLConfig)
*/
public void createOnGlThread(Context context) {
//生成背景纹理
int textures[] = new int[1];
GLES20.glGenTextures(1, textures, 0);
//获取加载图像后OpenGL纹理id
mTextureId = textures[0];
//告诉OpenGL后面的纹理调用应该应用mTextureId这个纹理对象
GLES20.glBindTexture(mTextureTarget, mTextureId);
//当纹理大小被扩大或者缩小的时候,我们就会使用到纹理过滤
//为mTextureTarget纹理设置属性,纹理放大和缩小的滤波方式,横向和纵向平铺方式
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
GLES20.glTexParameteri(mTextureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
int numVertices = 4;
if (numVertices != QUAD_COORDS.length / COORDS_PER_VERTEX) {
throw new RuntimeException("Unexpected number of vertices in BackgroundRenderer.");
}
//OpenGL作为本地系统直接运行在硬件上,没有虚拟机。所以需要把Java虚拟机中的内存复制到本地堆中
//分配一块本地内存bbVertices,告诉缓冲区按照本地字节组织它的内容
ByteBuffer bbVertices = ByteBuffer.allocateDirect(QUAD_COORDS.length * FLOAT_SIZE);
bbVertices.order(ByteOrder.nativeOrder());
//获取一个可以反映底层字节的FloatBuffer类实例,把顶点QUAD_COORDS数据放到本地内存中
mQuadVertices = bbVertices.asFloatBuffer();
mQuadVertices.put(QUAD_COORDS);
mQuadVertices.position(0);
//同上
ByteBuffer bbTexCoords = ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoords.order(ByteOrder.nativeOrder());
mQuadTexCoord = bbTexCoords.asFloatBuffer();
mQuadTexCoord.put(QUAD_TEXCOORDS);
mQuadTexCoord.position(0);
//同上
ByteBuffer bbTexCoordsTransformed = ByteBuffer.allocateDirect(numVertices * TEXCOORDS_PER_VERTEX * FLOAT_SIZE);
bbTexCoordsTransformed.order(ByteOrder.nativeOrder());
mQuadTexCoordTransformed = bbTexCoordsTransformed.asFloatBuffer();
//加载背景顶点和片元着色器
int vertexShader = ShaderUtil.loadGLShader(TAG, context,GLES20.GL_VERTEX_SHADER, R.raw.screenquad_vertex);
int fragmentShader = ShaderUtil.loadGLShader(TAG, context,GLES20.GL_FRAGMENT_SHADER, R.raw.screenquad_fragment_oes);
//把顶点和Fragment着色器绑定放在一个单个的程序中一起工作:
//新建程序对象mQuadProgram,并把vertexShader和fragmentShader附加到程序中
mQuadProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mQuadProgram, vertexShader);
GLES20.glAttachShader(mQuadProgram, fragmentShader);
//把这些着色器联合起来,并且告诉OpenGL在绘制背景的时候使用这个定义的程序
GLES20.glLinkProgram(mQuadProgram);
GLES20.glUseProgram(mQuadProgram);
//获取顶点位置纹理坐标属性
ShaderUtil.checkGLError(TAG, "Program creation");
mQuadPositionParam = GLES20.glGetAttribLocation(mQuadProgram, "a_Position");
mQuadTexCoordParam = GLES20.glGetAttribLocation(mQuadProgram, "a_TexCoord");
ShaderUtil.checkGLError(TAG, "Program parameters");
}
... ...
}
com\google\ar\core\examples\java\helloar\rendering\ShaderUtil.java
public class ShaderUtil {
/**
* 将原始文本文件(保存为资源)转换为OpenGL ES着色程序。
*/
public static int loadGLShader(String tag, Context context, int type, int resId) {
//通过着色器代码原始资源文件id,读取代码为字符串
String code = readRawTextFile(context, resId);
//创建一个类型的着色器对象
int shader = GLES20.glCreateShader(type);
//把着色器源代码上传上着色器对象里,它与shader引用的着色器对象关联起来
GLES20.glShaderSource(shader, code);
//编译着色器
GLES20.glCompileShader(shader);
//取出编译状态,检查OpenGL是否能成功地编译这个着色器,如果编译失败则删除着色器
final int[] compileStatus = new int[1];
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0);
if (compileStatus[0] == 0) {
Log.e(tag, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shader));
GLES20.glDeleteShader(shader);
shader = 0;
}
//编译成功,返回着色器引用id
if (shader == 0) {
throw new RuntimeException("Error creating shader.");
}
return shader;
}
/**
* 将原始文本文件转换为字符串:从原始资源中通过流读取字符串
*/
private static String readRawTextFile(Context context, int resId) {
InputStream inputStream = context.getResources().openRawResource(resId);
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
java_arcore_hello_ar\app\src\main\res\raw\screenquad_vertex.shader
attribute vec4 a_Position; //3D物理每个顶点各自不同的顶点位置信息
attribute vec2 a_TexCoord; //3D物理每个顶点各自不同的纹理坐标信息
varying vec2 v_TexCoord; //从顶点着色器计算产生并传递到片元着色器的纹理坐标信息
void main() {
gl_Position = a_Position; //顶点着色器从渲染管线中获得原始顶点信息a_Position,写入gl_Position内建变量传递到渲染管线的后阶段继续处理
v_TexCoord = a_TexCoord; //顶点着色器从渲染管线中获得纹理坐标信息a_TexCoord,传递给片元着色器继续处理
}
java_arcore_hello_ar\app\src\main\res\raw\screenquad_fragment_oes.shader
precision mediump float; //给出默认的浮点精度
varying vec2 v_TexCoord; //从顶点着色器传来的纹理坐标
uniform samplerExternalOES sTexture; //纹理内容数据
void main() {
gl_FragColor = texture2D(sTexture, v_TexCoord); //根据纹理坐标采样出颜色值
}
2.背景纹理和Session摄像头纹理绑定
com\google\ar\core\examples\java\helloar\HelloArActivity.java
public class HelloArActivity extends AppCompatActivity implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//OpenGL环境被设置的时候,调用一次
//设置背景帧的颜色
GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
//准备背景渲染对象
mBackgroundRenderer.createOnGlThread(this);
//创建纹理,并且将它传递给ARCore Session,在update()的时候填充,允许GPU访问相机图像
mSession.setCameraTextureName(mBackgroundRenderer.getTextureId());
}
}
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
... ...
//背景纹理id
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
... ...
//获取背景纹理id
public int getTextureId() {
return mTextureId;
}
... ...
}
3.执行背景绘制
com\google\ar\core\examples\java\helloar\rendering\BackgroundRender.java
public class BackgroundRenderer {
private static final String TAG = BackgroundRenderer.class.getSimpleName();
private static final int COORDS_PER_VERTEX = 3;
private static final int TEXCOORDS_PER_VERTEX = 2;
private static final int FLOAT_SIZE = 4;
private FloatBuffer mQuadVertices;
private FloatBuffer mQuadTexCoord;
private FloatBuffer mQuadTexCoordTransformed;
private int mQuadProgram;
private int mQuadPositionParam;
private int mQuadTexCoordParam;
private int mTextureId = -1;
private int mTextureTarget = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
/**
* 绘制AR背景图片。这个图片将会使用Frame.getViewMatrix(float[], int)和Session.getProjectionMatrix(
* float[], int, float, float)提供的矩阵绘制,将准确的跟踪静态物理对象,它必须在绘制虚拟对象之前调用
* @param frame 通过Session.update()返回的最新Frame
*/
public void draw(Frame frame) {
//如果显示旋转(也包括尺寸的改变),我们需要重新查询屏幕rect的uv坐标,因为它们可能发生了变化
if (frame.isDisplayRotationChanged()) {
frame.transformDisplayUvCoords(mQuadTexCoord, mQuadTexCoordTransformed);
}
//不需要测试或者写入深度,屏幕顶点具有任意深度,预计将首先绘制
GLES20.glDisable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthMask(false);
//告诉OpenGL纹理调用应该应用mTextureId这个纹理对象
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
//告诉OpenGL在绘制背景的时候使用这个定义的程序
GLES20.glUseProgram(mQuadProgram);
//将顶点位置数据传送进渲染管线
GLES20.glVertexAttribPointer(mQuadPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, mQuadVertices);
//将顶点纹理坐标数据传送进渲染管线
GLES20.glVertexAttribPointer(mQuadTexCoordParam, TEXCOORDS_PER_VERTEX,GLES20.GL_FLOAT, false, 0, mQuadTexCoordTransformed);
//启动顶点位置和着色数据
GLES20.glEnableVertexAttribArray(mQuadPositionParam);
GLES20.glEnableVertexAttribArray(mQuadTexCoordParam);
//执行背景绘制
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
//禁用顶点数组
GLES20.glDisableVertexAttribArray(mQuadPositionParam);
GLES20.glDisableVertexAttribArray(mQuadTexCoordParam);
//恢复深度状态以作进一步绘图。
GLES20.glDepthMask(true);
GLES20.glEnable(GLES20.GL_DEPTH_TEST);
ShaderUtil.checkGLError(TAG, "Draw");
}
}
1.新技术,新未来!尽在1024工场。时刻关注最前沿技术资讯,发布最棒技术博文!(甭客气!尽情的扫描或者长按!)
2.完整和持续更新的《使用Android打开AR的开发大门—ARCore》文档,欢迎大家阅读!
https://www.kancloud.cn/p3243986735/arcore_develop/457951