Camera Api 2 和 OPEN GL ES 使用(显示滤镜效果)
Camera Api 2 和 OPEN GL ES 使用(显示滤镜效果)
相机预览和open GL 使用实现滤镜效果
代码 https://github.com/loggerBill/camera
相机预览
1.相机动态权限
<uses-permission android:name="android.permission.CAMERA" /><uses-featureandroid:name="android.hardware.camera"android:required="true" />
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {ActivityCompat.requestPermissions(this, new String[]{android.Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);}
2.打开相机
private void openCamera() {
...manager.openCamera(cameraId, new CameraDevice.StateCallback() {@Overridepublic void onOpened(@NonNull CameraDevice camera) {cameraDevice = camera;createCameraPreviewSession();}
...
}
3.创建session和CaptureRequest
private void createCameraPreviewSession() {
...try {final CaptureRequest.Builder previewRequestBuilder =cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);previewRequestBuilder.addTarget(previewSurface);cameraDevice.createCaptureSession(Arrays.asList(previewSurface),new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {captureSession = session;try {// 设置重复请求captureSession.setRepeatingRequest(previewRequestBuilder.build(),null, null);} catch (CameraAccessException e) {e.printStackTrace();}}...}
创建需要告诉相机有那些可以输出的surface,和CaptureRequest 中addTarget surface.
surface 中会带有size。
要是正常使用TextureView 显示预览代码:
public class CameraPreview extends TextureView implements TextureView.SurfaceTextureListener {@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {openCamera(width, height);}private void openCamera(int width, int height) {
...texture.setDefaultBufferSize(largest.getWidth(), largest.getHeight());Surface surface = new Surface(texture);previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);previewRequestBuilder.addTarget(surface);
...}
这样创建surface 并且与Request绑定等待预览上来就会显示到TextureView中。
但我们相实现滤镜效果,TextureView 做不到修改效果。
所以我们用到了GLSurfaceView。
OPENGL
GLSurfaceView
1.创建自定义view 继承自GLSurfaceView。
public class CameraGLSurfaceView extends GLSurfaceView {private final CameraGLRenderer renderer;public CameraGLSurfaceView(Context context, AttributeSet attrs) {super(context, attrs);setEGLContextClientVersion(2); // 使用 OpenGL ES 2.0renderer = new CameraGLRenderer(context,this);setRenderer(renderer);setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 有帧时渲染,需要手动调用requestRender}public Surface getSurface() {return renderer.getSurface();}public void setPreviewSize(Size size) {renderer.setPreviewSize(size);}public CameraGLRenderer getRenderer(){return renderer;}
}
setEGLContextClientVersion(2); // 使用 OpenGL ES 2.0
- OpenGL ES 2.0 支持可编程渲染管线(使用 GLSL 着色器),而 1.x 是固定管线。
- 2.0 版本提供更灵活的图形控制(如自定义滤镜、特效等),适合相机图像处理。
创建自定义渲染器
- 作用:实例化自定义渲染器
CameraGLRenderer
。 - 参数:
context
:传递上下文给渲染器(可能用于资源加载)。this
:将当前GLSurfaceView
实例传给渲染器(便于渲染器与视图交互)。
- 渲染器职责:
- 实现
GLSurfaceView.Renderer
接口。 - 重写
onSurfaceCreated()
,onSurfaceChanged()
,onDrawFrame()
方法。 - 处理 OpenGL 初始化、相机帧绘制等逻辑。
- 实现
setRenderer(renderer);
- 作用:将自定义渲染器绑定到
GLSurfaceView
。 - 触发行为:
- 系统自动创建 OpenGL ES 上下文和渲染线程。
- 首次调用渲染器的
onSurfaceCreated()
和onSurfaceChanged()
。 - 启动渲染循环(根据渲染模式触发
onDrawFrame()
)。
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 有帧时渲染
- 作用:设置为 按需渲染 模式(仅在主动请求时重绘)。
- 对比默认模式:
- 默认模式
RENDERMODE_CONTINUOUSLY
:连续渲染(60 FPS),浪费资源。 WHEN_DIRTY
模式:仅在调用requestRender()
时渲染。
- 默认模式
- 适用场景:
- 相机预览:当新帧到达时手动调用
requestRender()
。 - 节省 CPU/GPU 资源,避免无效渲染。
- 相机预览:当新帧到达时手动调用
自定义渲染器–>CameraGLRenderer
实现自定义渲染器需要继承GLSurfaceView.Renderer。
public class CameraGLRenderer implements GLSurfaceView.Renderer {@Overridepublic void onSurfaceCreated(GL10 gl, EGLConfig config) {//首次回调 onSurfaceCreated()// 此处执行一次性初始化操作// 例如:设置清屏颜色、编译着色器、创建纹理等}@Overridepublic void onSurfaceChanged(GL10 gl, int width, int height) {// 响应视图尺寸变化gl.glViewport(0, 0, width, height); // 必须设置视口// 可在此处更新投影矩阵等}@Overridepublic void onDrawFrame(GL10 gl) {// 每帧重复执行}
}
大致明白渲染器的功能,下面我们来实现预览功能。
1.创建纹理对象。
OpenGL ES 2.0 中生成纹理对象 的标准操作
int[] textures = new int[1];GLES20.glGenTextures(1, textures, 0);textureId = textures[0];
- 创建一个长度为1的整型数组
textures
- 作用:作为容器接收 OpenGL 生成的纹理 ID
- 为什么需要数组:因为
glGenTextures
方法需要传入数组来接收生成的纹理 ID(支持一次性生成多个纹理)
2.创建 SurfaceTexture 和 Surface
surfaceTexture = new SurfaceTexture(textureId);surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {glSurfaceView.requestRender();}});
...if (preViewSize != null) {surfaceTexture.setDefaultBufferSize(preViewSize.getWidth(),preViewSize.getHeight());}mSurface = new Surface(surfaceTexture);
SurfaceTexture
- 纹理消费者
-
核心作用:将图像流转换为 OpenGL ES 纹理
-
关键特性:
- 内部维护一个
BufferQueue
(缓冲区队列) - 绑定到 OpenGL 纹理(通过构造函数传入
textureId
) - 当新帧到达时通知监听器
工作流程:
- 内部维护一个
// 创建并绑定到OpenGL纹理
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
// 设置新帧监听器
surfaceTexture.setOnFrameAvailableListener(listener);
// 在OpenGL线程更新纹理
surfaceTexture.updateTexImage(); // 将最新帧数据同步到纹理
- 典型使用者:
- OpenGL ES 渲染器
- 需要处理图像流的图形应用
Surface
- 图像生产者
-
核心作用:提供图像数据的写入接口
-
关键特性:
- 是
SurfaceTexture
的生产者端 - 实现了
Parcelable
,可跨进程传递 - 提供
Canvas
或BufferQueue
写入接口
创建方式:-
Surface surface = new Surface(surfaceTexture); // 绑定到SurfaceTexture
- 是
-
典型生产者:
- 相机 (
Camera
/Camera2
) - 视频解码器 (
MediaPlayer
) - View 的渲染表面
- 相机 (
3. 两者关系图解
┌─────────────┐ ┌───────────────────┐ ┌───────────────┐
│ │ writes │ │ updates │ │
│ 生产者 ├─────────►│ Surface ├─────────►│ SurfaceTexture │
│ (Camera/解码器)│ │ (生产者接口) │ │ (消费者接口) │
└─────────────┘ └───────────────────┘ └───────┬───────┘│▼┌─────────────────┐│ OpenGL ES Texture││ (textureId) │└─────────────────┘
简单流程::
// 步骤1: 创建OpenGL纹理
int textureId = createGLTexture();// 步骤2: 创建SurfaceTexture并绑定纹理
SurfaceTexture surfaceTexture = new SurfaceTexture(textureId);
surfaceTexture.setOnFrameAvailableListener(() -> {// 新帧到达时请求渲染glSurfaceView.requestRender();
});// 步骤3: 创建Surface作为生产者目标
Surface surface = new Surface(surfaceTexture);// 步骤4: 配置相机输出到Surface
camera.setPreviewSurface(surface); // Camera1 API
// 或
session.createCaptureSession(..., surface); // Camera2 API// 步骤5: 在渲染器中处理帧
@Override
public void onDrawFrame(GL10 gl) {surfaceTexture.updateTexImage(); // 同步数据到纹理surfaceTexture.getTransformMatrix(texMatrix); // 获取变换矩阵
// 使用纹理渲染
renderTexture(textureId, texMatrix);}
关键区别对比表
特性 | SurfaceTexture | Surface |
---|---|---|
角色 | 纹理消费者 | 图像生产者 |
数据去向 | OpenGL 纹理 | 绑定到的 SurfaceTexture |
核心方法 | updateTexImage() , getTransformMatrix() | lockCanvas() , unlockCanvasAndPost() |
是否跨进程 | 否 | 是 (实现 Parcelable) |
图形API关联 | 直接关联 OpenGL | 不依赖特定图形API |
主要使用者 | GL渲染器 | 相机/解码器/系统渲染服务 |
-
零拷贝机制:
- 数据从生产者→Surface→SurfaceTexture→OpenGL纹理的传输不经过CPU内存复制
- 通过 Android 的
BufferQueue
直接传递图形缓冲区
-
纹理坐标系处理:
- 相机帧可能有旋转/镜像
- 需调用
surfaceTexture.getTransformMatrix()
获取变换矩阵 - 在着色器中应用矩阵校正:
attribute vec2 aPosition; attribute vec2 aTexCoord; uniform mat4 uTexMatrix; varying vec2 vTexCoord;void main() {gl_Position = vec4(aPosition, 0.0, 1.0);vTexCoord = (uTexMatrix * vec4(aTexCoord, 0.0, 1.0)).xy; }
-
生命周期管理:
// 正确释放顺序 camera.stopPreview(); surface.release(); // 先释放Surface surfaceTexture.release(); // 再释放SurfaceTexture deleteGLTexture(textureId);
创建 SurfaceTexture 并绑定纹理
surfaceTexture = new SurfaceTexture(textureId);
- 作用:创建一个
SurfaceTexture
实例并将其绑定到指定的 OpenGL 纹理 - 参数:
textureId
- 之前通过glGenTextures()
生成的 OpenGL 纹理 ID - 关键机制:
SurfaceTexture
内部创建一个BufferQueue
- 将该队列与指定的 OpenGL 纹理关联
- 后续相机数据将直接流入此纹理
设置帧可用监听器
surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {@Overridepublic void onFrameAvailable(SurfaceTexture surfaceTexture) {glSurfaceView.requestRender();}
});
- 作用:注册回调,当新相机帧到达时触发渲染
- 工作流程:
- 相机填充新帧到
SurfaceTexture
的缓冲区 SurfaceTexture
触发onFrameAvailable()
回调- 回调中调用
requestRender()
请求 OpenGL 渲染
- 相机填充新帧到
- 渲染模式配合:
- 前面设置了
setRenderMode(RENDERMODE_WHEN_DIRTY)
- 此回调确保有新帧时才渲染,节省资源
- 前面设置了
设置缓冲区尺寸
if (preViewSize != null) {surfaceTexture.setDefaultBufferSize(preViewSize.getWidth(),preViewSize.getHeight());
}
- 作用:配置
SurfaceTexture
的缓冲区尺寸以匹配相机预览分辨率 - 参数:
preViewSize
- 相机支持的预览尺寸(如 1920x1080) - 为什么重要:
- 确保分配的图形缓冲区大小正确
- 避免图像拉伸/裁剪
- 优化内存使用和性能
创建 Surface 生产者接口
mSurface = new Surface(surfaceTexture);
- 作用:创建
Surface
对象作为相机数据输出的目标 - 关键连接:
Surface
是SurfaceTexture
的生产者端- 相机系统会将帧数据写入此
Surface
- 数据自动传递到关联的 OpenGL 纹理
Surface 和 surfaceTexture 已经创建完成,我们还需要设置GL 的相关。
顶点着色器代码
String vertexShaderSource ="attribute vec4 aPosition;\n" +"attribute vec2 aTexCoord;\n" +"varying vec2 vTexCoord;\n" +"uniform mat4 uTexMatrix;\n" +"uniform mat4 uMvpMatrix;\n" + // 新增MVP矩阵"void main() {\n" +" gl_Position = uMvpMatrix * aPosition;\n" + // 应用MVP矩阵" vTexCoord = (uTexMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;\n" +"}";
attribute vec4 aPosition;
attribute
:声明顶点属性(每个顶点特有的数据)vec4
:4维向量(x,y,z,w)- 作用:接收从CPU传递的顶点坐标数据
- 典型值:屏幕四角的NDC坐标(如[-1,1]范围)
attribute vec2 aTexCoord;
vec2
:2维向量(s,t)- 作用:接收从CPU传递的纹理坐标
- 典型值:
[0,0]
(左下),[1,0]
(右下),[0,1]
(左上),[1,1]
(右上)
varying vec2 vTexCoord;
varying
:声明插值变量(顶点→片元着色器)- 作用:将处理后的纹理坐标传递给片元着色器
- 关键特性:在光栅化过程中自动插值
uniform mat4 uTexMatrix;
uniform
:声明全局常量(所有顶点共享)mat4
:4x4矩阵- 作用:校正相机纹理的方向(旋转/镜像)
- 数据来源:
SurfaceTexture.getTransformMatrix()
uniform mat4 uMvpMatrix; // 新增MVP矩阵
- 作用:将顶点从模型空间→裁剪空间
- 组成:
- Model:物体自身变换
- View:摄像机视角
- Projection:投影方式(正交/透视)
- 应用场景:实现2D/3D变换效果
gl_Position = uMvpMatrix * aPosition;
gl_Position
:内置变量,输出裁剪空间坐标- 计算:
- 应用MVP矩阵变换
- 可实现旋转/缩放/3D效果
vTexCoord = (uTexMatrix * vec4(aTexCoord, 0.0, 1.0)).xy;
- 步骤分解:
- 将2D纹理坐标扩展为4D向量:
vec4(aTexCoord.s, aTexCoord.t, 0.0, 1.0)
- 应用纹理变换矩阵
uTexMatrix
- 取结果的xy分量 (
.xy
)
- 将2D纹理坐标扩展为4D向量:
- 为什么需要:
- 前置摄像头需要水平翻转
- 不同设备旋转方向不同(0°/90°/180°/270°)
- 解决纹理坐标与屏幕方向不匹配问题
┌───────────────────────┐ ┌───────────────────┐
│ 顶点属性 (CPU传入) │ │ Uniform矩阵 │
│ aPosition: vec4 ├─┬─► │ uMvpMatrix: mat4 │
│ aTexCoord: vec2 │ │ │ uTexMatrix: mat4 │
└───────────────────────┘ │ └───────────────────┘│▼
┌───────────────────────────────────────────────┐
│ 顶点着色器处理流程 │
│ gl_Position = uMvpMatrix * aPosition │
│ vTexCoord = (uTexMatrix * [aTexCoord]).xy │
└───────────────────────┬───────────────────────┘│▼
┌───────────────────────────────────────────────┐
│ 光栅化插值 │
│ 自动计算每个片元的vTexCoord (插值后的坐标) │
└───────────────────────┬───────────────────────┘│▼
┌───────────────────────────────────────────────┐
│ 片元着色器采样纹理 │
│ texture2D(uTexture, vTexCoord) │
└───────────────────────────────────────────────┘
fragmentShaderSource片段着色器代码
String fragmentShaderSource ="precision mediump float;\n" +"uniform sampler2D uTextureUnit;\n" +"varying vec2 vTexCoord;\n" +"void main() {\n" +" vec4 color = texture2D(uTextureUnit, vTexCoord);\n" +" float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));\n" +" gl_FragColor = vec4(gray, gray, gray, color.a);\n" +"}";
主要是varying uniform 等变量属性写完着色器代码 下面就是编译着色器
编译着色器
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexShaderSource);int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentShaderSource);private int loadShader(int type, String shaderSource) {int shader = GLES20.glCreateShader(type);GLES20.glShaderSource(shader, shaderSource);GLES20.glCompileShader(shader);// 检查编译状态int[] compiled = new int[1];GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);if (compiled[0] == 0) {String errorMsg = GLES20.glGetShaderInfoLog(shader);GLES20.glDeleteShader(shader);throw new RuntimeException("Shader compile error: " + errorMsg);}return shader;}
创建着色器程序
// 创建着色器程序programHandle = GLES20.glCreateProgram();GLES20.glAttachShader(programHandle, vertexShader);GLES20.glAttachShader(programHandle, fragmentShader);GLES20.glLinkProgram(programHandle);// 检查链接状态int[] linkStatus = new int[1];GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);if (linkStatus[0] != GLES20.GL_TRUE) {String errorMsg = GLES20.glGetProgramInfoLog(programHandle);GLES20.glDeleteProgram(programHandle);throw new RuntimeException("Shader program link error: " + errorMsg);}
变量位置查询
获取句柄
positionHandle = GLES20.glGetAttribLocation(programHandle, "aPosition");texCoordHandle = GLES20.glGetAttribLocation(programHandle, "aTexCoord");filterTypeHandle = GLES20.glGetUniformLocation(programHandle, "uFilterType");texMatrixHandle = GLES20.glGetUniformLocation(programHandle, "uTexMatrix");textureHandle = GLES20.glGetUniformLocation(programHandle, "uTexture");mvpMatrixHandle = GLES20.glGetUniformLocation(programHandle, "uMvpMatrix");
3. 具体变量解释
3.1 顶点属性 (Attributes)
positionHandle = GLES20.glGetAttribLocation(programHandle, "aPosition");
- 作用:获取顶点位置属性的位置句柄
- 对应着色器变量:
attribute vec4 aPosition;
- 使用场景:
GLES20.glEnableVertexAttribArray(positionHandle);// 绑定顶点缓冲区数据
GLES20.glVertexAttribPointer(positionHandle, // 位置句柄3, // 每个顶点分量数 (x,y,z)GLES20.GL_FLOAT,// 数据类型false, // 是否归一化12, // 步长 (3个float * 4字节)vertexBuffer // 顶点缓冲区
);
texCoordHandle = GLES20.glGetAttribLocation(programHandle, "aTexCoord");
-
作用:获取纹理坐标属性的位置句柄
-
对应着色器变量:
attribute vec2 aTexCoord;
-
使用场景:
GLES20.glEnableVertexAttribArray(texCoordHandle); GLES20.glVertexAttribPointer(texCoordHandle,2, // 两个纹理坐标分量 (s,t)GLES20.GL_FLOAT,false,8, // 2个float * 4字节texCoordBuffer );
统一变量 (Uniforms)
filterTypeHandle = GLES20.glGetUniformLocation(programHandle, "uFilterType");
- 作用:获取滤镜类型统一变量的位置句柄
- 对应着色器变量:
uniform int uFilterType;
- 使用场景:
// 设置滤镜类型 (0=正常, 1=黑白, 2=反色等)
GLES20.glUniform1i(filterTypeHandle, currentFilterType);
texMatrixHandle = GLES20.glGetUniformLocation(programHandle, "uTexMatrix");
- 作用:获取纹理变换矩阵的位置句柄
- 对应着色器变量:
uniform mat4 uTexMatrix;
- 使用场景:
// 从SurfaceTexture获取变换矩阵
surfaceTexture.getTransformMatrix(texMatrix);// 传递给着色器
GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0);
textureHandle = GLES20.glGetUniformLocation(programHandle, "uTexture");
-
作用:获取纹理采样器的位置句柄
-
对应着色器变量:
uniform sampler2D uTexture;
-
使用场景:
// 激活纹理单元0 GLES20.glActiveTexture(GLES20.GL_TEXTURE0);// 绑定纹理到当前单元 GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);// 告诉着色器使用0号纹理单元 GLES20.glUniform1i(textureHandle, 0);
mvpMatrixHandle = GLES20.glGetUniformLocation(programHandle, "uMvpMatrix");
-
作用:获取模型-视图-投影矩阵的位置句柄
-
对应着色器变量:
uniform mat4 uMvpMatrix;
-
使用场景:
// 计算MVP矩阵 Matrix.setIdentityM(mvpMatrix, 0); Matrix.scaleM(mvpMatrix, 0, scaleX, scaleY, 1.0f); // 缩放 Matrix.translateM(mvpMatrix, 0, transX, transY, 0); // 平移// 传递给着色器 GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);
-
位置句柄的有效期:
- 只在当前着色器程序链接后有效
- 重新链接程序后需要重新获取
-
性能优化:
- 位置句柄只需获取一次(通常在程序链接后)
- 存储在成员变量中避免重复查询
-
错误处理:
if (positionHandle == -1) {throw new RuntimeException("找不到aPosition属性"); }
-
变量名匹配:
- 必须与着色器代码中的变量名完全一致
- 大小写敏感
-
着色器优化:
- 未使用的变量可能被编译器优化掉
- 返回-1表示变量不存在
在相机预览渲染中,这些句柄特别重要:
texMatrixHandle
:校正前置摄像头镜像问题textureHandle
:绑定相机帧纹理filterTypeHandle
:实时切换滤镜效果mvpMatrixHandle
:处理屏幕旋转适配
设置纹理参数
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
OpenGL ES 中配置外部纹理(用于相机/视频)的关键步骤,专门用于处理 Android 的相机预览帧或视频流。
在 Android 相机/视频处理中,不能使用普通的 GL_TEXTURE_2D
,而必须使用特殊的 GL_TEXTURE_EXTERNAL_OES
扩展纹理:
- 特殊性质:直接接收来自 SurfaceTexture 的流数据
- 着色器要求:必须声明扩展
#extension GL_OES_EGL_image_external : require
- 采样器类型:
uniform samplerExternalOES uTexture
(不是 sampler2D)
-
激活纹理单元
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
- 作用:激活0号纹理单元(OpenGL ES 有多个纹理单元)
- 为什么重要:
- OpenGL ES 支持同时使用多个纹理(如 GL_TEXTURE0, GL_TEXTURE1…)
- 默认激活0号单元(但显式声明更安全)
-
绑定外部纹理
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);
- 关键参数:
GLES11Ext.GL_TEXTURE_EXTERNAL_OES
:特殊纹理目标,用于相机/视频流textureId
:之前通过glGenTextures()
生成的纹理ID
- 作用:
- 将纹理绑定到当前激活的纹理单元
- 后续操作将作用于这个纹理
- 关键参数:
-
设置缩小过滤器
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
- 参数解析:
GL_TEXTURE_MIN_FILTER
:纹理缩小过滤方式GL_LINEAR
:线性插值(双线性过滤)
- 应用场景:当纹理被渲染得比原始尺寸小时(如缩略图)
- 参数解析:
-
设置放大过滤器
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
- 参数解析:
GL_TEXTURE_MAG_FILTER
:纹理放大过滤方式
- 为什么用线性过滤:
- 提供平滑的图像质量
- 最适合相机预览(比
GL_NEAREST
锯齿感少)
- 参数解析:
-
设置S方向(水平)环绕模式
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
- 参数解析:
GL_TEXTURE_WRAP_S
:水平方向(U坐标)GL_CLAMP_TO_EDGE
:边缘像素延伸
- 为什么不用重复:
- 相机帧不需要平铺重复
- 防止边缘出现异常颜色
- 参数解析:
-
设置T方向(垂直)环绕模式
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
- 同上,作用于垂直方向(V坐标)
外部纹理 vs 普通2D纹理
特性 | GL_TEXTURE_EXTERNAL_OES | GL_TEXTURE_2D |
---|---|---|
来源 | 相机/视频流 | 图像数据 |
创建 | glGenTextures() + 特殊绑定 | 常规创建 |
数据上传 | 通过 SurfaceTexture 自动更新 | glTexImage2D() |
着色器声明 | #extension... + samplerExternalOES | sampler2D |
Mipmap | 不支持 | 支持 |
坐标范围 | 必须 [0,1] | 可重复 |
性能 | 零拷贝,高效 | 需要数据复制 |
为什么需要这些设置?
- 过滤模式 (GL_LINEAR)
- 相机帧常需要缩放(如适配屏幕)
- 线性过滤提供最自然的视觉效果
- 避免
GL_NEAREST
产生的像素化锯齿
- 环绕模式 (GL_CLAMP_TO_EDGE)
- 相机帧是连续视频流,不是平铺纹理
- 防止边缘采样错误(尤其旋转时)
- 兼容所有 Android 设备(某些设备严格需要)
- 外部纹理的特殊性
- 直接映射到 SurfaceTexture 的缓冲区
- 避免 CPU-GPU 数据拷贝(零拷贝)
- 支持 YUV 到 RGB 的硬件转换
常见问题解决
纹理显示绿色或扭曲?
- 检查着色器是否正确定义
samplerExternalOES
- 确认调用了
surfaceTexture.updateTexImage()
- 验证纹理坐标变换矩阵的使用:
surfaceTexture.getTransformMatrix(texMatrix);
性能优化提示
- 纹理配置只需一次(放在初始化时)
- 避免每帧重复调用这些参数设置
- 使用
RENDERMODE_WHEN_DIRTY
模式
setIdentityM(float[] sm, int smOffset)
方法将数组 sm
中从偏移量 smOffset
开始的16个元素(代表4x4矩阵)设置为单位矩阵。
在图形编程中的作用:
- 初始化MVP矩阵:在OpenGL渲染前,通常将模型视图投影矩阵(MVP)初始化为单位矩阵,作为变换计算的起点。
- 重置变换:单位矩阵表示“无变换”状态,后续的平移、旋转、缩放操作会基于此矩阵累积。
- 矩阵运算基准:类似于乘法中的"1",确保矩阵操作从初始状态开始。
// 初始化MVP矩阵为单位矩阵
float[] mvpMatrix = new float[16];
Matrix.setIdentityM(mvpMatrix, 0); // 后续操作(例如平移)会基于此单位矩阵
Matrix.translateM(mvpMatrix, 0, 0, 0, -5); // 沿z轴平移-5
onSurfaceChanged
GLSurfaceView.Renderer
接口中的一个方法,当Surface尺寸改变时系统自动调用。
设置OpenGL视口
GLES20.glViewport(0, 0, width, height);
- 作用:定义OpenGL的渲染区域(窗口坐标系)
- 参数:
- 前两个参数:视口左下角坐标(0,0表示从屏幕左下角开始)
- 后两个参数:视口宽度和高度(使用新的窗口尺寸)
- 重要性:当屏幕旋转或窗口大小改变时,必须重新设置视口,否则渲染会变形或错位
为什么需要这个回调?
- 设备方向变化:当手机旋转时(竖屏↔横屏)
- 窗口大小改变:分屏模式、折叠屏切换等
- 初始化时:Surface首次创建时也会调用
后续关键应用:
这个宽高比主要用于投影矩阵的计算,确保3D场景正确显示:
// 在onDrawFrame中通常会这样使用
Matrix.perspectiveM(projectionMatrix, 0, 45f, // 视野角度viewAspectRatio, // 这里使用计算的宽高比0.1f, 100f); // 近/远裁剪平面
典型工作流程:
onSurfaceCreated (初始化) → onSurfaceChanged (尺寸确定) → onDrawFrame (渲染循环)
不处理的后果:
- 竖屏转横屏时:物体会被压缩变扁
- 分屏模式:只渲染部分区域
- 折叠屏展开:画面只显示在部分屏幕
onDrawFrame
OpenGL ES渲染循环的核心部分,主要负责每一帧的渲染准备工作
设置清除颜色
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
- 作用:设置清除屏幕时使用的背景颜色
- 参数:RGBA颜色值(红、绿、蓝、透明度)
- 本例:黑色(RGB=0)且完全不透明(Alpha=1.0)
清除颜色缓冲区
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
- 作用:用预设的清除颜色填充整个屏幕
- GL_COLOR_BUFFER_BIT:指定清除颜色缓冲区
- 效果:将屏幕重置为纯黑色,擦除上一帧内容
更新纹理数据
surfaceTexture.updateTexImage();
- 作用:从SurfaceTexture获取最新的图像帧并更新到OpenGL纹理
- 典型应用:用于显示相机预览、视频流或动态生成的图像
- 工作原理:从Android的SurfaceTexture中提取最新的图像数据,将其绑定到OpenGL纹理
获取纹理变换矩阵
4.
surfaceTexture.getTransformMatrix(texMatrix);
- 作用:获取纹理坐标变换矩阵
- 为什么需要:相机/视频源的图像方向可能与设备方向不一致
- 功能:
- 校正图像旋转(如手机竖屏时相机横屏拍摄)
- 处理镜像翻转(前置摄像头通常需要)
- 调整UV坐标映射
整体流程说明:
- 重置画布:用黑色清屏(准备绘制新帧)
- 获取新帧:从视频源/相机获取最新图像
- 准备纹理:将新图像转换为OpenGL可用的纹理
- 校正显示:计算纹理变换矩阵,确保图像正确显示
private void drawTexture() {// 更新MVP矩阵updateMvpMatrix();// 使用着色器程序GLES20.glUseProgram(programHandle);// 启用顶点属性数组GLES20.glEnableVertexAttribArray(positionHandle);GLES20.glEnableVertexAttribArray(texCoordHandle);// 传递顶点数据vertexBuffer.position(0);GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);// 传递纹理坐标数据texCoordBuffer.position(0);GLES20.glVertexAttribPointer(texCoordHandle, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer);// 传递MVP矩阵if (mvpMatrixHandle != -1) {GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);}// 传递纹理变换矩阵GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0);// 设置纹理单元if (filterTypeHandle != -1) {GLES20.glUniform1i(filterTypeHandle, filterType);}// 绘制GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);// 禁用顶点属性数组GLES20.glDisableVertexAttribArray(positionHandle);GLES20.glDisableVertexAttribArray(texCoordHandle);}private void updateMvpMatrix() {// 重置为单位矩阵Matrix.setIdentityM(mvpMatrix, 0);if (previewAspectRatio > viewAspectRatio) {// 预览比视图宽,缩放高度float scale = viewAspectRatio / previewAspectRatio;Matrix.scaleM(mvpMatrix, 0, 1f, scale, 1f);} else {// 预览比视图高,缩放宽度float scale = previewAspectRatio / viewAspectRatio;Matrix.scaleM(mvpMatrix, 0, scale, 1f, 1f);}}
updateMvpMatrix()
作用是根据预览内容(如相机画面)和视图区域的宽高比差异,计算并更新模型视图投影矩阵(MVP Matrix),以实现画面自适应缩放,保持原始比例不变形。
代码根据两种宽高比的关系动态调整缩放:
previewAspectRatio
:预览内容的宽高比(宽度/高度)viewAspectRatio
:视图区域的宽高比(宽度/高度)
矩阵操作详解:
初始化单位矩阵:
Matrix.setIdentityM(mvpMatrix, 0);
应用缩放变换:
Matrix.scaleM(mvpMatrix, 0, scaleX, scaleY, scaleZ);
实际应用场景:
// 在渲染前调用
updateMvpMatrix();// 将矩阵传入着色器
GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);
顶点着色器中使用:
gl_Position = uMVPMatrix * aPosition;
为什么需要这样做?
- 避免拉伸变形:直接拉伸会扭曲图像(圆形变椭圆)
- 保持内容完整性:完整显示原始画面内容
- 自适应不同屏幕:处理手机旋转/分屏/折叠屏等场景
这种处理是视频播放器、相机预览等应用的标配逻辑,数学上属于保持原始比例的仿射变换。通过调整MVP矩阵而非直接修改顶点坐标,可以利用GPU的并行计算优势提高性能。
GLES20.glUseProgram(programHandle);
GLES20.glUseProgram(programHandle);
是OpenGL ES 2.0中一个关键的函数调用,用于激活指定的着色器程序。以下是详细解释:
作用与功能
- 激活着色器程序:
- 将指定的着色器程序设置为当前渲染管线使用的程序
- 所有后续的绘制操作都将使用这个程序中的着色器
- 参数说明:
programHandle
:指向着色器程序的整数句柄(ID)- 这个句柄是通过之前
glCreateProgram()
和glLinkProgram()
创建的
工作原理
// 创建着色器程序
int programHandle = GLES20.glCreateProgram();// 附加着色器(顶点+片段)
GLES20.glAttachShader(programHandle, vertexShader);
GLES20.glAttachShader(programHandle, fragmentShader);// 链接程序
GLES20.glLinkProgram(programHandle);// 使用程序
GLES20.glUseProgram(programHandle); // <-- 关键调用
底层机制
当调用glUseProgram()
时:
- GPU驱动加载指定程序的字节码
- 配置渲染管线阶段:
- 顶点处理器使用顶点着色器
- 片段处理器使用片段着色器
- 重置所有uniform变量为默认值
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glEnableVertexAttribArray(texCoordHandle);
功能详解:
- 启用顶点属性数组:
- 告诉OpenGL:“请使用我提供的数组数据,而不是默认的常量值”
- 默认情况下,所有顶点属性都是禁用的,使用常量值(0,0,0,1)
- 参数说明:
positionHandle
:顶点位置属性的句柄(从着色器获取)texCoordHandle
:纹理坐标属性的句柄(从着色器获取)
完整工作流程:
// 1. 获取属性位置(通常在初始化时完成)
positionHandle = GLES20.glGetAttribLocation(program, "aPosition");
texCoordHandle = GLES20.glGetAttribLocation(program, "aTexCoord");// 2. 指定顶点数据来源(在绘制前调用)
GLES20.glVertexAttribPointer(positionHandle, // 属性位置3, // 每个顶点的分量数(x,y,z)GLES20.GL_FLOAT, // 数据类型false, // 是否归一化5 * 4, // 步长(每个顶点占20字节:3位置+2纹理坐标)*4字节/floatvertexBuffer // 顶点缓冲区
);GLES20.glVertexAttribPointer(texCoordHandle, 2, // 每个纹理坐标的分量数(u,v)GLES20.GL_FLOAT, false, 5 * 4, vertexBuffer.position(3) // 从缓冲区第12字节开始(3个float后)
);// 3. 启用属性数组
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glEnableVertexAttribArray(texCoordHandle);// 4. 绘制图形
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);// 5. 可选:禁用属性数组(减少资源占用)
GLES20.glDisableVertexAttribArray(positionHandle);
GLES20.glDisableVertexAttribArray(texCoordHandle);
概念 | 说明 |
---|---|
顶点属性(Attribute) | 着色器中每个顶点的输入数据 |
属性句柄(Location) | 着色器中属性的唯一标识符 |
顶点缓冲区(VBO) | 存储顶点数据的GPU内存区域 |
glVertexAttribPointer | 定义如何从缓冲区读取数据 |
vertexBuffer.position(0);
GLES20.glVertexAttribPointer(positionHandle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
-
重置缓冲区位置:
vertexBuffer.position(0);
- 将顶点缓冲区的读取位置重置到开头
- 确保从缓冲区的起始位置读取数据
-
设置顶点属性指针:
GLES20.glVertexAttribPointer(positionHandle, // 着色器中顶点位置属性的句柄3, // 每个顶点包含的分量数 (x,y,z)GLES20.GL_FLOAT, // 数据类型为浮点数false, // 不需要归一化处理0, // 连续顶点间的字节步长 (0表示紧密排列)vertexBuffer // 包含顶点数据的缓冲区 );
参数 | 说明 |
---|---|
属性句柄 | 通过glGetAttribLocation 获取的着色器属性位置 |
分量数 | 位置属性需要3个值(x,y,z),纹理坐标需要2个值(u,v) |
数据类型 | GL_FLOAT 表示使用32位浮点数 |
归一化 | false 表示保持原始值范围,不压缩到[0,1] |
步长(Stride) | 0 表示数据紧密排列,无间隔 |
缓冲区 | 包含实际数据的Java NIO缓冲区 |
典型的顶点数据结构:
// 顶点位置数据 (每个顶点3个float)
float[] vertexData = {-1.0f, -1.0f, 0.0f, // 左下1.0f, -1.0f, 0.0f, // 右下-1.0f, 1.0f, 0.0f, // 左上1.0f, 1.0f, 0.0f // 右上
};// 纹理坐标数据 (每个坐标2个float)
float[] texCoordData = {0.0f, 1.0f, // 左下1.0f, 1.0f, // 右下0.0f, 0.0f, // 左上1.0f, 0.0f // 右上
};
工作流程
-
准备数据:
// 创建顶点缓冲区 FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(VERTEX_DATA.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); vertexBuffer.put(vertexData); vertexBuffer.position(0);// 创建纹理坐标缓冲区(类似)
-
设置属性指针(如上述代码)
-
启用属性数组:
GLES20.glEnableVertexAttribArray(positionHandle); GLES20.glEnableVertexAttribArray(texCoordHandle);
-
绘制图形:
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
矩阵传递给顶点着色器
// 传递MVP矩阵if (mvpMatrixHandle != -1) {GLES20.glUniformMatrix4fv(mvpMatrixHandle, 1, false, mvpMatrix, 0);}// 传递纹理变换矩阵GLES20.glUniformMatrix4fv(texMatrixHandle, 1, false, texMatrix, 0);
-
作用:将模型-视图-投影(Model-View-Projection)矩阵传递给顶点着色器
-
参数解析:
mvpMatrixHandle
:着色器中MVP矩阵的uniform位置句柄1
:传递的矩阵数量false
:不转置矩阵(OpenGL ES默认列主序)mvpMatrix
:包含16个float值的矩阵数组0
:矩阵数据在数组中的偏移量
-
检查
!= -1
:确保着色器中实际存在这个uniform变量 -
在渲染中的作用:
- 将3D顶点从模型空间转换到裁剪空间
- 综合了模型变换、相机视图和投影变换
-
作用:将纹理坐标变换矩阵传递给着色器
-
参数解析:
texMatrixHandle
:纹理变换矩阵的uniform位置句柄texMatrix
:从SurfaceTexture获取的纹理变换矩阵
-
特殊用途:
- 校正相机预览的方向(如前置摄像头镜像)
- 处理设备旋转时的图像方向
- 调整不同宽高比的纹理映射
顶点着色器示例:
uniform mat4 uMVPMatrix; // MVP矩阵
uniform mat4 uTexMatrix; // 纹理变换矩阵attribute vec4 aPosition; // 顶点位置
attribute vec4 aTextureCoord; // 原始纹理坐标varying vec2 vTextureCoord; // 传递给片段着色器的纹理坐标void main() {gl_Position = uMVPMatrix * aPosition;vTextureCoord = (uTexMatrix * aTextureCoord).xy;
}
概念 | 说明 |
---|---|
MVP矩阵 | 综合模型、视图、投影变换的矩阵 |
纹理变换矩阵 | 校正纹理坐标的特殊变换 |
glUniformMatrix4fv | 传递4x4矩阵到着色器的函数 |
uniform变量 | 着色器中保持不变的值(每帧设置一次) |
常见问题解决方案
- 图像显示不正确:
- 检查矩阵计算逻辑
- 确保矩阵传递顺序正确
- 验证着色器中的矩阵运算
- 纹理方向错误:
- 确保
texMatrix
从SurfaceTexture正确获取 - 检查设备方向处理逻辑
- 确保
- 性能优化:
- 只在矩阵变化时更新
- 避免每帧重复计算不变矩阵
提示:在相机预览等实时应用中,texMatrix
通常每帧变化(处理设备旋转),而mvpMatrix
在视图尺寸不变时可缓存复用。这两个矩阵的协同工作确保了3D空间正确投影和纹理正确映射。
选择滤镜
if (filterTypeHandle != -1) {GLES20.glUniform1i(filterTypeHandle, filterType);
}
- 作用:将当前选择的滤镜类型传递给片段着色器
- 参数:
filterTypeHandle
:着色器中滤镜类型uniform的句柄filterType
:整数表示的滤镜类型(如0=正常,1=黑白,2=复古等)
- 技术细节:
glUniform1i
:传递单个整数值到着色器!= -1
检查:确保着色器中存在该uniform变量
执行绘制命令
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
- 作用:命令GPU执行实际的渲染操作
- 参数详解:
GL_TRIANGLE_STRIP
:三角形条带绘制模式0
:从顶点数组的第一个索引开始4
:总共绘制4个顶点(两个三角形)
- 渲染结果:
- 绘制一个矩形(两个三角形组成)
- 每个顶点包含位置和纹理坐标
- 应用当前设置的滤镜效果
0---2
| / | 顶点顺序:0→1→2→3
| / | 三角形1:0-1-2
1---3 三角形2:1-2-3
禁用顶点属性数组
GLES20.glDisableVertexAttribArray(positionHandle);
GLES20.glDisableVertexAttribArray(texCoordHandle);
- 作用:释放顶点属性数组资源
- 为什么需要:
- 避免资源泄露
- 防止后续绘制操作意外使用当前配置
- 减少GPU状态保持开销
- 专业实践:
- 在绘制完成后立即禁用
- 与
glEnableVertexAttribArray
成对出现 - 特别是多对象渲染时必不可少
性能优化提示
- 滤镜切换优化:
- 只在滤镜改变时更新uniform
- 避免每帧重复设置相同值
- 批处理绘制:
- 相同滤镜的物体一起绘制
- 减少着色器切换次数
- 资源复用:
- 顶点缓冲区对象(VBO)长期保留
- 着色器程序预编译
- 避免冗余调用:
- 检查uniform句柄有效性(-1检查)
- 只在必要时更新矩阵
目前从重写GLSurfaceView 到自定义Rander中onCreate ->onSurfaceChanged ->onDrawFrame 已完全实现,所以整体上已完成。我们需要将生成的surface 传递到相机中。
也就是创建session 中需要配置的surface 列表中,和请求预览时 addTarget 中。后续预览就会显示到我们设置view中了。
final CaptureRequest.Builder previewRequestBuilder =cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);previewRequestBuilder.addTarget(previewSurface);cameraDevice.createCaptureSession(Arrays.asList(previewSurface),new CameraCaptureSession.StateCallback() {@Overridepublic void onConfigured(@NonNull CameraCaptureSession session) {captureSession = session;try {// 设置重复请求captureSession.setRepeatingRequest(previewRequestBuilder.build(),null, null);} catch (CameraAccessException e) {e.printStackTrace();}}···
}