NDK系列之OpenGL渲染画面效果技术实战,本节主要是通过OpenGL Java库(谷歌对OpenGL C++库做了JIN封装,核心实现还是在Native层),实现页面渲染,自定义渲染特效。
实现效果:
实现逻辑:
1.创建Camera,绑定SurfaceTexture纹理画布对象;
2.将Camera预览画面数据传递给SurfaceTexture,缓存到buffer;
3.OpenGLES获取SurfaceTexture缓存的画面数据,渲染到GLSurfaceView(屏幕)。
本节主要内容:
1.OpenGL理论知识;
2.自定义GLSurfaceView;
3.自定义渲染器Renderer;
4.ScreenFilter封装OpenGL;
5.着色器代码工作。
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果
一、OpenGL理论知识
1)图形领域的工业标准,是一套跨编程语言、跨平台的、专业的图形编程(软件)接口。它用于二维、三维图 像,是一个功能强大,调用方便的底层图形库。 可以简单理解为是GPU显卡语言。针对手机、PDA和游戏主机等嵌入式设备而设计的OpenGL API 子集, GLSurfaceView(不仅仅有,SurfaceView的所有功能,还拥有了 OpenGL的处理)。
2)OpenGL绘制流程:
为什么是三角形:三角形是 图像领域最小单元
1.位置排版,顶点位置确定好【顶点位置】
2.根据各个的位置排版,细节的网格排版【光栅化】
3.根据各个的位置排版,细节的网格排版执行逻辑处理【光栅操作逻辑】
4.颜色色彩的准备工作【纹理过滤】
5.片元处理,其实就是上色,绘制成效画面【纹理填充】
6.输出结果 ---> GPU 人类就看到了画面
3)Shader着色器(重点)
着色器(Shader)是运行在GPU上的小程序。
顶点着色器(vertex shader)如何处理顶点、法线等数据的小程序。
片元着色器(fragment shader)如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一 副图像的小程序。
二、自定义GLSurfaceView
GLSurfaceView继承至SurfaceView,不仅仅有SurfaceView的所有功能,还拥有了OpenGL的处理;而MyGLSurfaceView继承至GLSurfaceView,拥有GLSurfaceView的所有能力,支持自定义渲染器(render),可以灵活的进行OpenGL渲染。
public class MyGLSurfaceView extends GLSurfaceView {
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// TODO 一:设置EGL版本
// 2 代表是 OpenGLES 2.0
setEGLContextClientVersion(2);
// TODO 二:设置渲染器
// 注意:
// EGL 开启一个 GLThread.start run { Renderer.onSurfaceCreated ...onSurfaceChanged onDrawFrame }
// 如果这三个函数,不让GLThread调用,会崩溃,所以他内部的设计,必须通过GLThread调用来调用三个函数
setRenderer(new MyGlRenderer(this)); // this 自定义渲染器 会回调回来做处理,所有传递this
// TODO 三:设置渲染器模式
// RENDERMODE_WHEN_DIRTY 按需渲染,有帧数据的时候,才会去渲染( 效率高,麻烦,后面需要手动调用一次才行)
// RENDERMODE_CONTINUOUSLY 每隔16毫秒,读取更新一次,(如果没有显示上一帧)
setRenderMode(RENDERMODE_WHEN_DIRTY); // 手动模式 - 效率高,麻烦,后面需要手动调用一次才行
}
}
三、自定义渲染器Renderer
1.实现GLSurfaceView.Renderer接口,实现接口的onSurfaceCreated()、onSurfaceChanged()和onDrawFrame()方法;
1)当Surface创建时,回调onSurfaceCreated()函数,创建CameraHelper对象;创建SurfaceTexture纹理对象,并绑定SurfaceTexture.OnFrameAvailableListener监听;创建ScreenFilter屏幕过滤器。
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.i(TAG, "onSurfaceCreated");
mCameraHelper = new CameraHelper((Activity) myGLSurfaceView.getContext(), // 上下文
Camera.CameraInfo.CAMERA_FACING_FRONT, // 前置摄像头
640, 480);
// 获取纹理ID【先理解成画布】
mTextureID = new int[1];
/**
* 1.长度 只有一个 1
* 2.纹理ID,是一个数组
* 3.offset:0 使用数组的0下标
*/
glGenTextures(mTextureID.length, mTextureID, 0);
mSurfaceTexture = new SurfaceTexture(mTextureID[0]); // 实例化纹理对象
mSurfaceTexture.setOnFrameAvailableListener(this); // 绑定好此监听 SurfaceTexture.OnFrameAvailableListener
mScreenFilter = new ScreenFilter(myGLSurfaceView.getContext());
}
2)当Surface改变时,回调onSurfaceChanged()函数,获得Camera对象,设置Camera绑定SurfaceTexture纹理对象,开启相机预览。
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.i(TAG, "onSurfaceChanged");
mCameraHelper.startPreview(mSurfaceTexture); // 开始预览
mScreenFilter.onReady(width, height);
}
实例化CameraHelper,打开Camera,设置Camera绑定SurfaceTexture纹理对象,开启相机预览。
public void startPreview(SurfaceTexture surfaceTexture) {
mSurfaceTexture = surfaceTexture; // TODO 重点
try {
// 获得camera对象
mCamera = Camera.open(mCameraID);
// 配置camera的属性
Camera.Parameters parameters = mCamera.getParameters();
// 设置预览数据格式为nv21
parameters.setPreviewFormat(ImageFormat.NV21);
// 这是摄像头宽、高
setPreviewSize(parameters);
// 设置摄像头 图像传感器的角度、方向
setPreviewOrientation(parameters);
mCamera.setParameters(parameters);
cameraBuffer = new byte[mWidth * mHeight * 3 / 2]; // 请看之前讲过的的细节
cameraBuffer_ = new byte[mWidth * mHeight * 3 / 2]; // 请看之前讲过的的细节
// 数据缓存区
mCamera.addCallbackBuffer(cameraBuffer);
mCamera.setPreviewCallbackWithBuffer(this);
// 设置预览画面(之前的方式:把显示的画面,渲染到SurfaceView屏幕上即可)
// mCamera.setPreviewDisplay(mSurfaceHolder); // SurfaceView 和 Camera绑定 ,以前音视频推流,就是这个干的
// 设置预览画面(离屏渲染) surfaceTexture纹理画布(仅仅只是缓存一份画布,不可见的画布) 配合 OpenGL渲染操作
// Camera相机预览数据 ----> surfaceTexture纹理画布(不可见的画布) ---> OpenGL TODO 重点,绑定纹理
mCamera.setPreviewTexture(surfaceTexture); // OpenGL无法直接访问到 Camera预览数据, 他只能访问 surfaceTexture
// 开启预览
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
3)绘制一帧图像时,回调onDrawFrame()函数,获取纹理对象的图像数据,通过OpenGL渲染到屏幕;
@Override
public void onDrawFrame(GL10 gl) {
Log.i(TAG, "onDrawFrame");
// 每次清空之前的:例子:上课擦黑白 是一个道理
glClearColor(255, 0, 0, 0); // 屏幕清理成颜色 红色,清理成红色的黑板一样
// mask 细节看看此文章:https://blog.csdn.net/z136411501/article/details/83273874
// GL_COLOR_BUFFER_BIT 颜色缓冲区
// GL_DEPTH_BUFFER_BIT 深度缓冲区
// GL_STENCIL_BUFFER_BIT 模型缓冲区
glClear(GL_COLOR_BUFFER_BIT);
// 绘制摄像头数据
mSurfaceTexture.updateTexImage(); // 将纹理图像更新为图像流中最新的帧数据【刷新一下】
// 画布,矩阵数据,通过Native层将数据存储到mtx
mSurfaceTexture.getTransformMatrix(mtx);
// 绘制一帧图像
mScreenFilter.onDrawFrame(mTextureID[0], mtx);
}
2.实现SurfaceTexture.OnFrameAvailableListener接口,实现接口的onFrameAvailable()方法;
当有可用的数据时,回调onFrameAvailable()函数,手动调用,触发屏幕渲染
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
Log.i(TAG, "onFrameAvailable");
myGLSurfaceView.requestRender(); // setRenderMode(RENDERMODE_WHEN_DIRTY); 配合用
}
四、ScreenFilter封装OpenGL
1.初始化ScreenFilter操作:
1)读取顶点着色器和片元着色器代码,转换为字符串
String vertexSource = readTextFileFromResource(context, R.raw.camera_vertex);
String fragmentSource = readTextFileFromResource(context, R.raw.camera_fragment);
2)配置顶点着色器
// 1.1 创建顶点着色器
int vShaderId = glCreateShader(GL_VERTEX_SHADER);
// 1.2 绑定着色器源代码到 着色器(加载着色器的代码)
glShaderSource(vShaderId, vertexSource);
// 1.3 编译着色器代码(编译阶段:编译成功就能拿到顶点着色器ID,编译失败基本上就是着色器代码字符串写错了)
glCompileShader(vShaderId);
// 1.4 获取着色器配置结果
int[] status = new int[1];
glGetShaderiv(vShaderId, GL_COMPILE_STATUS, status, 0);
if (status[0] != GL_TRUE) {
throw new IllegalStateException("顶点着色器配置失败!");
}
3)配置片元着色器
// 2.1 创建片元着色器
int fShaderId = glCreateShader(GL_FRAGMENT_SHADER);
// 2.2 绑定着色器源代码到 着色器(加载着色器的代码)
glShaderSource(fShaderId, fragmentSource);
// 2.3 编译着色器代码(编译阶段:编译成功就能拿到顶点着色器ID,编译失败基本上就是着色器代码字符串写错了)
glCompileShader(fShaderId);
// 2.4 获取着色器配置结果
glGetShaderiv(fShaderId, GL_COMPILE_STATUS, status, 0);
if (status[0] != GL_TRUE) {
throw new IllegalStateException("片元着色器配置失败!");
}
4)配置着色器程序
// 3.1 创建一个着色器程序
mProgram = glCreateProgram();
// 3.2 将前面配置的 顶点 和 片元 着色器 附加到新的程序 上
glAttachShader(mProgram, vShaderId); // 顶点
glAttachShader(mProgram, fShaderId); // 片元
// 3.3 链接着色器
glLinkProgram(mProgram); // mProgram着色器程序 是我们的成果
// 3.4 获取着色器配置结果
glGetShaderiv(mProgram, GL_LINK_STATUS, status, 0);
if (status[0] != GL_TRUE) {
throw new IllegalStateException("着色器程序链接失败!");
}
5)获取顶点着色器和片元着色器代码的变量的索引值,通过索引来赋值
// 顶点着色器里面的如下:
vPosition = glGetAttribLocation(mProgram, "vPosition"); // 顶点着色器:的索引值
vCoord = glGetAttribLocation(mProgram, "vCoord"); // 顶点着色器:纹理坐标,采样器采样图片的坐标 的索引值
vMatrix = glGetUniformLocation(mProgram, "vMatrix"); // 顶点着色器:变换矩阵 的索引值
// 片元着色器里面的如下:
vTexture = glGetUniformLocation(mProgram, "vTexture"); // 片元着色器:采样器
6)缓存顶点坐标(顶点:位置 排版)
mVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4) // 分配内存 坐标个数 * xy坐标数据类型 * float占几字节
.order(ByteOrder.nativeOrder()) // 使用本地字节序,例如:大端模式,小端模式,这里设置为:跟随OpenGL的变化二变化
.asFloatBuffer();
mVertexBuffer.clear(); // 清除一下
float[] v = { // OpenGL世界坐标
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
};
mVertexBuffer.put(v);
7)缓存纹理坐标(纹理:上色 成果)
mTexturBuffer = ByteBuffer.allocateDirect(4 * 2 * 4) // 分配内存 坐标个数 * xy坐标数据类型 * float占几字节
.order(ByteOrder.nativeOrder()) // 使用本地字节序,例如:大端模式,小端模式,这里设置为:跟随OpenGL的变化二变化
.asFloatBuffer();
mTexturBuffer.clear(); // 清除一下
// 旋转 180度 就纠正了
float[] t = { // 屏幕坐标系
1.0f, 0.0f,
0.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f,
};
mTexturBuffer.put(t);
2.绘制操作
通过OpenGL顶点着色器,规划位置;设置顶点坐标相当于,相机的四个点位置排版;设置纹理坐标,用来图形上色;设置图形矩阵数据;
通过OpenGL片元着色器,给图片上色;设置采样器,获取顶点着色器设置的纹理坐标和矩阵数据的最终计算成果,通知OpenGL绘制。
1)顶点着色器
// TODO 1.1 顶点坐标赋值 NIO的Buffer 用它就要归零,习惯
mVertexBuffer.position(0); // 养成好习惯,每次来用他时,归零(为了从坐标位置的起始开始)
/**
* 传值(把float[]值传递给顶点着色器)把mVertexBuffer传递到vPosition == size:每次两个xy, stride:0 不跳步
* 1.着色器代码里面的 标记变量 attribute vec4 vPosition;
* 2.xy 所以是两个
* 3.不用管
* 4.不用管
* 5.跳步 0 不跳步
* 6.顶点坐标nio的buffer缓存数据
*/
glVertexAttribPointer(vPosition, 2, GL_FLOAT, false, 0, mVertexBuffer);
// 激活
glEnableVertexAttribArray(vPosition);
// TODO 1.2 纹理坐标赋值
mTexturBuffer.position(0); // 养成好习惯,每次来用他时,归零(为了从坐标位置的起始开始)
// 传值(把float[]值传递给纹理)把mTexturBuffer传递到vCoord == size:每次两个xy, stride:不跳步
glVertexAttribPointer(vCoord, 2, GL_FLOAT, false, 0, mTexturBuffer);
// 激活
glEnableVertexAttribArray(vCoord);
// TODO 1.3 变换矩阵 把mtx矩阵数据 传递到 vMatrix
glUniformMatrix4fv(vMatrix, 1, false, mtx, 0);
2)片元着色器
// 激活图层
glActiveTexture(GL_TEXTURE0);
// TODO 2.1 绑定纹理ID --- glBindTexture(GL_TEXTURE_2D ,textureId); 如果在片元着色器中的vTexture,不是samplerExternalOES类型,就可以这样写
glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); // 由于我们的着色器代码是 使用了 samplerExternalOES
// TODO 2.2 采样器赋值
glUniform1i(vTexture, 0); // 传递参数 给 片元着色器:采样器
// TODO 2.3 通知 opengl 绘制 ,从0开始,共四个点绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
五、着色器代码工作
1)顶点着色器
attribute vec4 vPosition; // 顶点坐标,相当于:相机的四个点位置排版
attribute vec4 vCoord; // 纹理坐标,用来图形上色的
uniform mat4 vMatrix; // 变换矩阵,4*4的格式的
varying vec2 aCoord; // 把这个最终的计算成果,给片元着色器 【不需要Java传递,他是计算出来的】
void main() {
gl_Position = vPosition; // 确定好位置排版
aCoord = (vMatrix * vCoord).xy; // 兼容所有设备
}
2)片元着色器
// 导入 samplerExternalOES 的意思
#extension GL_OES_EGL_image_external : require
// float 数据的精度 (precision lowp = 低精度) (precision mediump = 中精度) (precision highp = 高精度)
precision mediump float;
// 根据上面的数据的精度,写下面的 采样器 相机的数据
// uniform sampler2D vTexture; 由于我们用的是 安卓的相机,就不能用他
uniform samplerExternalOES vTexture; // samplerExternalOES才能采样相机的数据
varying vec2 aCoord; // 把这个最终的计算成果,给片元着色器,拿到最终的成果,我才能上色
void main() {
// texture2D (采样器, 坐标) 正常效果
// gl_FragColor = texture2D(vTexture, aCoord);
// 305911公式:黑白电视效果,其实原理就是提取出Y分量
/*vec4 rgba =texture2D(vTexture, aCoord);
float gray = (0.30 * rgba.r + 0.59 * rgba.g + 0.11* rgba.b); // 其实原理就是提取出Y分量 ,就是黑白电视
gl_FragColor = vec4(gray, gray, gray, 1.0);*/
// 底片效果
vec4 rgba = texture2D(vTexture,aCoord); // rgba
gl_FragColor = vec4(1.-rgba.r, 1.-rgba.g, 1.-rgba.b, rgba.a);
}
至此,OpenGL渲染画面效果技术实战项目已完成;黑白电视和大眼萌等特效都可以通过OpenGL实现。
源码:
NdkOpenGLPlay: NDK OpenGL渲染画面效果