View/SurfaceView/GLSurfaceView的异同
参考
参考二
View的特点暂不多言。主要看一下SurfaceView与GLSurfaceView的特色与区别
SurfaceView
SurfaceView继承自View,并提供了一个可以嵌入到View结构树中的独立的绘图层,你可以完全控制这个绘图层,比如说设定它的大小。它与正常View最大的区别在于拥有自己独立的Surface。SurfaceView负责将surface放置在屏幕上的正确位置。 这个Surface的Z轴方向允许窗体在它后面持有该SurfaceView,就是在窗体上打了个洞允许surface显示。
SurfaceView就是通过SurfaceHolder来对Surface进行管理控制的。比如控件大小, 格式,编辑像素,以及监视其变化。
我们使用SurfaceView可能目的就是为了使用相机、播放视频这些对页面刷新有很高的要求的场景。但实际上,相机、播放视频等之所以要用SurfaceView根本目的就是:为了开启第二个线程去在片屏幕上做渲染。
SurfaceView牵扯的类主要有:
①Surface
Android中的Surface就是一个用来画图形(graphics)或图像(image)的地方,对于View及其子类,都是画在Surface上,各Surface对象通过Surfaceflinger合成到frameBuffer,每个Surface都是双缓冲(实际上就是两个线程,一个渲染线程,一个UI更新线程),它有一个backBuffer和一个frontBuffer,Surface中创建了Canvas对象,用来管理Surface绘图操作,Canvas对应Bitmap,存储Surface中的内容。
②SurfaceView
内嵌了一个专门用于绘制的Surface,SurfaceView可以控制这个Surface的格式和尺寸,以及Surface的绘制位置。可以理解为Surface就是管理数据的地方,SurfaceView就是展示数据的地方
③SurfaceHolder
一个管理SurfaceHolder的容器。SurfaceHolder是一个接口,可理解为一个Surface的监听器。 通过回调方法addCallback(SurfaceHolder.Callback callback )监听Surface的创建 通过获取Surface中的Canvas对象,并锁定之。
简单总结
SurfaceView中调用getHolder方法,可以获得当前SurfaceView中的Surface对应的SurfaceHolder,SurfaceHolder开始对Surface进行管理操作。这里其实按MVC模式理解的话,可以更好理解。M:Surface(图像数据),V:SurfaceView(图像展示),C:SurfaceHolder(图像数据管理)。
GLSurfaceView
针对SurfaceView做的扩展,专门用于OpenGL rendering使用。
1> 提供并且管理一个独立的Surface。
2>** 提供并且管理一个EGL display,它能让opengl把内容渲染到上述的Surface上。**
3> 支持用户自定义渲染器(Render),通过setRenderer设置一个自定义的Renderer。
4>让渲染器在独立的GLThread线程里运作,和UI线程分离。
5> 支持按需渲染(on-demand)和连续渲染(continuous)两种模式。
6> 另外还针对OpenGL调用进行追踪和错误检查。
另外:
GPU加速:GLSurfaceView的效率是SurfaceView的30倍以上,SurfaceView使用画布进行绘制,GLSurfaceView利用GPU加速提高了绘制效率。
View的绘制onDraw(Canvas canvas)使用Skia渲染引擎渲染,而GLSurfaceView的渲染器Renderer的onDrawFrame(GL10 gl)使用opengl绘制引擎进行渲染。
最后贴上大佬总结的图:
自定义SurfaceView
参考
先简单说一下自定义View要做的工作:
①继承SurfaceView(废话...)
②实现SurfaceHolder.Callback接口,实现接口的三个方法
③在onSurfaceCreate中创建绘制线程,在onSurfaceDestroyed中停止线程
④在子线程中通过lockCanvas,锁定画布,开始我们的绘制
一个简单的自定义SurfaceView就需要这么简单的四步,接下来我们来实现吧:
package com.rye.opengl.about;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import androidx.annotation.NonNull;
public class CustomSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = "Rye";
private SurfaceHolder mSurfaceHolder;
private Paint mPaint;
private String[] colors = new String[]{"#ff4ab1", "#e84626", "#3e18e8"};
private SurfaceRunnable mSurfaceRunnable;
private boolean mRendering = false;
public CustomSurfaceView(Context context) {
this(context, null);
}
public CustomSurfaceView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//添加回调监听,这样surfaceCrated/surfaceChanged/surfaceDestroyed才能监听到
mSurfaceHolder.addCallback(this);
}
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
//初始画笔
mPaint = new Paint();
mPaint.setColor(Color.parseColor(colors[0]));
mPaint.setStrokeWidth(10f);
startRenderThread();
}
private void startRenderThread() {
mSurfaceRunnable = new SurfaceRunnable();
new Thread(mSurfaceRunnable).start();
mRendering = true;
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
Log.i(TAG, "onSurfaceChanged");
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
Log.i(TAG, "surfaceDestroyed");
mRendering = false;
}
public void drawSth() {
Canvas canvas = mSurfaceHolder.lockCanvas();
if (null != canvas) {
synchronized (mSurfaceHolder) {
//清空画布
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
mPaint.setColor(Color.parseColor(colors[(int) (Math.random() * 3)]));
//绘制一个变色方块
canvas.drawRect(new Rect(0, 0, 100, 100), mPaint);
}
}
if (null != mSurfaceHolder && null != canvas) {
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
class SurfaceRunnable implements Runnable {
@Override
public void run() {
while (mRendering) {
drawSth();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
然后在xml中添加此SurfaceView即可
自定义GLSurfaceView
简单使用GLSurfaceView
public class FirstOpenglActivity extends AppCompatActivity {
private GLSurfaceView glSurfaceView;
private boolean renderSet = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_first_opengl);
glSurfaceView = new GLSurfaceView(this);
setRenderer();
setContentView(glSurfaceView);
}
private void setRenderer() {
if (isSupportEs2()) {
glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setRenderer(new FirstRenderer());
renderSet = true;
} else {
Log.e("Rye", "not support opengl es 2.0");
}
}
private boolean isSupportEs2() {
ActivityManager activityManager =
(ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo deviceConfigurationInfo =
activityManager.getDeviceConfigurationInfo();
return deviceConfigurationInfo.reqGlEsVersion >= 0x20000;
}
@Override
protected void onResume() {
super.onResume();
if (renderSet) {
glSurfaceView.onResume();
}
if (renderSet) {
glSurfaceView.onPause();
}
}
}
public class FirstRenderer implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
GLES20.glClearColor(1.0f,0.0f,0.0f,0.0f);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0,0,width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
}
}
主要说一下Renderer的三个方法:
①onSurfaceCreated(GL10 gl, EGLConfig config)
当Surface被创建的时候,GLSurfaceView会调用这个方法;这发生在应用程序第一次运行的时候,并且当设备长时间休眠,系统回收资源后重新被唤醒,这个方法也可能会被调用。这意味着,本方法可能会被调用多次。
我们调用GLES20.glClearColor(float red, float green, float blue, float alpha); 设置清空屏幕用的颜色;前三个参数分别对应红,绿和蓝,最后的参数对应透明度.这个方法我们以后会经常看到。
②onSurfaceChanged(GL10 gl, int width, int height)
在Surface被创建以后,每次Surface尺寸变化时,在横竖屏切换的时候,这个方法都会被调用到。
我们调用 GLES20.glViewport(int x, int y, int width, int height); 设置视口的尺寸,就是锁定你操作的渲染区域是哪部分,整个屏幕那就是 (0.0)点开始,宽度为widht,长度为height。如果你只想渲染左半个屏幕,那就(0,0,width/2,长height/2)。这样设置viewport大小后,你之后的GL画图操作,都只作用这部分区域,右半屏幕是不会有任何反应的。
③onDrawFrame(GL10 gl)
当绘制一帧时,这个方法会被调用。在这个方法中,我们一定要绘制一些东西,即使只是清空屏幕;因为在这方法返回之后,渲染缓冲区会被交换(前人的源码分析),并显示在屏幕上,如果什么都没画,可能会看到糟糕的闪烁效果。
我们调用 GLES20.glClear(GL_COLOR_BUFFER_BIT); 清空屏幕,这会擦除屏幕上的所有颜色,并用之前glClearColor调用定义的颜色填充整个屏幕。
GL10,它是OpenGL.ES 1.0的API遗留下来的,我们使用OpenGL.ES 2.0。所以直接忽略就可以了,GLES20类提供了静态方法来存取.
GLSurfaceView实际上为它自己创建了一个窗口(window),并在视图层次(View Hierarchy)上穿了个“洞”,让底层的OpenGL surface显示出来。
实战
一、利用OpenGL绘制一个三角形
绘制三角形
楼主博客里虽然解析的很清楚,但是还是自己敲一遍,总结一下适合自己的记忆思路。
实现步骤
1.首先需要编写顶点着色器和片段着色器资源文件,指定位置和颜色信息
顶点着色器:
attribute vec4 vPosition;
void main()
{
gl_Position = vPosition;
}
片段着色器:
precision mediump float;
uniform vec4 vColor;
void main(){
gl_FragColor = vColor;
}
简单说一下这里涉及的两个修饰符的作用:
①attribute:表示只读的顶点数据,只用在顶点着色器中。数据来自当前的顶点状态或者顶点数组。它必须是全局范围声明的,不能再函数内部。一个attribute可以是浮点数类型的标量,向量,或者矩阵。不可以是数组或则结构体
②uniform:一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。一致变量在顶点着色器和片段着色器之间是共享的。它也只能在全局范围进行声明。
还有两个内置变量:
顶点着色器内置变量:
①gl_Position:输出属性,变换后的顶点的位置,在main()中必须将顶点坐标赋值给系统变量gl_Position
片段着色器内置变量:
②gl_FragColor:输出的颜色,用于随后的像素操作
还有一个:precision mediump float
用于定义数据精度,Opengl中可以设置三种类型的精度(lowp,medium 和 highp),对于Vertex Shader(顶点着色器)来说,Opengl使用的是默认最高精度级别(highp),因此没有定义,所以基本上都是在片段着色器中声明。
着色器声明中顶点的位置信息和颜色信息,是必不可缺的第一步。
2.创建、加载、编译着色器
创建好着色器资源文件后,还需要创建着色器、将资源文件加载到着色器中、编译着色器。
private int loadShader(int type, int shaderResource) {
//根据type创建顶点或片段着色器
int shader = GLES20.glCreateShader(type);
//将资源加载到着色器中
GLES20.glShaderSource(shader, readTextFileFromResource(mContext, shaderResource));
//编译着色器
GLES20.glCompileShader(shader);
return shader;
}
这个方法包括readTextFileFromResource都是可以复用的,不用每次都写。
3.创建顶点数组,改变内存分配方式
有一点我们需要清楚,Android应用层的代码运行在Dalvik虚拟机上,不能直接访问本地环境。而OpenGL作为本地系统库,运行在本地环境。所以我们需要寻找一种java代码和本地环境构造的渠道,这个渠道就是ByteBuffer
首先我们先创建一个float数组,用来存储我们要绘制点的集合。
同时创建一个颜色数组,用来指定我们绘制的颜色。
//顶点坐标
private float triangleCoords[] = {
0.5f, 0.5f, 0f, //顶点
-0.5f, -0.5f, -0.5f, //左侧点
0.5f, -0.5f, 0f //右侧点
};
//绘制颜色
private float color[] = {1.0f, 1.0f, 1.0f, 1.0f};//白色
然后我们创建FloatBuffer,用于这个顶点数组和OpenGL交互。
private static final int BYTE_PER_VERTEX = 4;
private FloatBuffer vertexData = ByteBuffer.allocateDirect(triangleCoords.length * BYTE_PER_VERTEX)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
因为我们指定的位置数组是float,每个float占4个字节,所以我们指定此FloatBuffer的空间大小为数组length*4;
3.初始化操作
初始化操作目前有以下几个:
①加载着色器
②创建项目
③获取着色器信息句柄
④将位置信息存储到ByteBuffer中
这些初始化操作可以放在Renderer的构造方法中,也可以放在onSurfaceCreate方法中。
private void init() {
//这句必不可少,否则会出现黑屏!!!
GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, R.raw.triangle_vertex_shader);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, R.raw.triangle_fragment_shader);
//创建项目
mProgram = GLES20.glCreateProgram();
//将着色器附着到项目中
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
//连接到着色器程序
GLES20.glLinkProgram(mProgram);
//获取着色器中位置、颜色句柄
aPosition = GLES20.glGetAttribLocation(mProgram, "vPosition"/*要与顶点着色器中声明的相同*/);
aColor = GLES20.glGetUniformLocation(mProgram, "vColor");
//将位置信息存入byteBuffer中
vertexData.put(triangleCoords);
vertexData.position(0);
}
初始化的流程基本类似,最简单的流程就是上面几个步骤,获取着色器id,创建OpenGL程序,并将着色器绑定到程序中,然后获取着色器资源文件中声明的位置、颜色等信息。将位置数组存入ByteBuffer中,以保证java虚拟机环境可以和本地环境的数据交互。
项目id、着色器资源文件中位置、颜色句柄和ByteBuffer是我们draw的时候需要用到的信息,所以需要保留。
4.绘制操作
在onDrawFrame中开始我们具体的绘制操作,就是将我们上面定义的点绘制成我们的图形。这里的点涉及到好几个地方,包括:着色器资源文件中声明的点的信息,java代码中定义的点的位置,opengl本地环境中点的坐标信息。
这些点就是通过GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer)
这一行代码连接起来的。
然后设置绘制的颜色信息,最后通过GLES20.glDrawArrays将三角形绘制出来。
//将程序加入到OpenGL ES 2.0环境中
GLES20.glUseProgram(mProgram);
//启用三角形顶点句柄
GLES20.glEnableVertexAttribArray(aPosition);
//准备三角形坐标数据,为glDrawArrays做准备
GLES20.glVertexAttribPointer(aPosition, COORDS_PER_VERTEX, GLES20.GL_FLOAT,
false, vertexStride, vertexData);
//设置三角形的颜色
GLES20.glUniform4fv(aColor, 1, color, 0);
//设置好坐标数据和颜色后,开始绘制三角形
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
//禁用顶点数组句柄
GLES20.glDisableVertexAttribArray(aPosition);
【5.涉及的OpenGL Api】
OpenGL ES详解II
【着色器相关】
①GLES20.glCreateShader(type)
创建一个着色器对象。 参数shaderType:指定要创建的着色器类型。只能是GL_VERTEX_SHADER或GL_FRAGMENT_SHADER,也就是只能创建顶点着色器或者片段着色器。
② GLES20.glShaderSource(int shader, String )
加载shader的源代码。这里的两个参数 :
shader:就是我们通过glCreateShader创建的shader的id
string:就是着色器的源代码,可以是资源文件,通过io操作转变为字符串。也可以直接用字符串表示。
③GLES20.glCompileShader(shader)
编译着色器源码。
④GLES20.glCreateProgram()
创建一个空的OpenGL程序。
⑤GLES20.glAttachShader( int program, int shader)
将着色器对象附加到program对象。两个参数:
program:指定着色器对象将附加到的program对象
shader:要附加的着色器对象。
⑥GLES20.glLinkProgram(int program)
连接一个program对象.
以目前经验来看,就是创建程序、绑定着色器之后调用此方法。
⑦GLES20.glGetAttribLocation(int program,String name)
获取着色器中,指定为attribute类型的变量的id。注意这个和此program前面绑定的shader有关,取得是绑定的shader的属性id,如果不是此项目绑定的shader里的属性,会出现奇怪的问题的。
⑧GLES20.glGetUniformLocation(int program,String name)
获取着色器程序中,指定为uniform类型变量的id。
【onDrawFrame相关API】
①GLES20.glUseProgram(int program)
将程序加入到OpenGL ES 2.0环境中,作为渲染的一部分。
②GLES20.glEnableVertexAttribArray(int index)
启用该顶点位置属性。
获取了 attribute 的 location 之后,在 OpenGL ES 以及 GPU 真正使用这个 attribute 之前,还需要通过 glEnableVertexAttribArray 这个 API,对这个 attribute 进行 enable。如果不 enable 的话,这个 attribute 的值无法被访问,比如无法通过 OpenGL ES 给这个 Attribute 赋值。更严重的是,如果不 enable 的话,由于 attribute 的值无法访问,GPU 甚至在通过 glDrawArray 或者 glDrawElement 这 2 个 API 进行绘制的时候都无法使用这个 attribute。
【所以这个api很重要很重要,要使用一个属性的时候,一定要enable!!】
③GLES20.glDisableVertexAttribArray(int index);
有enable,就一定要disable。当绘制结束后,就可以把没用了的attribute通过此api关闭。
【重点】④GLES20.glVertexAttribPointer(int indx, int size, int type, boolean normalized, int stride, java.nio.Buffer ptr)
glVertexAttribPointer详解
这个api很重要很重要,指定索引index处的通用顶点属性数组的位置和数据格式。可以看到此方法将着色器属性和ByteBuffer即java层与本地环境数据通信渠道打通了。
size:指定每个通用顶点属性的组件数。 必须为1,2,3或4.初始值为4。(因为坐标顶多有四个,xyzw)
type:指定每个组件的数据类型;
stride:指定从一个属性到下一个属性的【字节跨度】,这里是3*4,因为三个数据定义一个点的位置,一个点用float存储占4个字节。允许将顶点和属性打包到单个数组中或存储在单独的数组中;
ptr:ByteBuffer数据源,之前定义的点的数组数据存放在此,而且此属性实现了java层与本地环境的数据交互。
⑤GLES20.glUniform4fv( int location, int count, float[] v, int offset )
【为当前程序对象指定uniform变量的值。】由于OpenGL ES由C语言编写,但是C语言不支持函数的重载,所以会有很多名字相同后缀不同的函数版本存在。其中函数名中包含数字(1、2、3、4)表示接受这个数字个用于更改uniform变量的值,i表示32位整形,f表示32位浮点型,ub表示8位无符号byte,ui表示32位无符号整形,v表示接受相应的指针类型。
Java虽然支持重载,但看来还是和C保持一致了,命名是一样的。
参数:
location:指明要更改的uniform变量的位置
count:指明要更改的元素个数。如果目标uniform变量不是一个数组,那么这个值应该设为1;如果是数组,则应该设置为>=1。
v:颜色值
offset:偏移值。因为我们数组里就是四个点,代表RGBA,所以一般设置为0.
【重点】⑥GLES20.glDrawArrays( int mode, int first, int count )
OpenGLES中函数 glDrawArrays 详解
提供绘制功能,从数据数据中提取数据渲染基本单元。这就是真正绘制的地方。
mode:需要渲染的图元类型。包括:GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN ,GL_TRIANGLES。
具体每一个值的操作,可以参考博客。
first:从数组缓存中的哪一位开始绘制,一般为0.
count:数组中顶点的数量。一般取值就是数组的长度除以代表一个点的数据个数即顶点的偏移量。
总结
我们先理一下关于着色器我们依次做了什么工作:
①创建着色器资源文件 xxx.glsl。当然也可以直接用一个字符串表示,资源文件也得转为字符串。
②创建、加载、编译着色器。(glCreateShader/glShaderSource/glCompileShader)
③创建项目并将着色器绑定到项目中。(glCreateProgram/glAttachShader)
④从着色器中获取attribute、uniform等属性(glGetAttribLocation/glGetUniformLocation)
⑤绘制时启用/禁用着色器attribute属性,为uniform属性赋值。(glEnableVertexAttribArray/glDisableVertexAttribArray/glUniform4fv)
所以可以看到我们一个简单的OpenGL项目,基本很多操作都是关乎着色器,着色器十分重要。
利用投影绘制等腰、彩色三角形
TODO,搜了一下相关资料,但都看不明白,不敢贸然写上,现加个TODO,等找到好的资料再从头分析,继续跟下面课程。
绘制正方形、圆形
绘制正方形之前,我们需要知道一个新的API
【glDrawElements】
glDrawElements介绍
glDrawArrays和glDrawElements异同
glDrawArrays在opengl es中是不支持绘制除三角形外的多边形的。