Camera Api 2 和 OPEN GL ES 使用(显示滤镜效果)

发布于:2025-06-26 ⋅ 阅读:(17) ⋅ 点赞:(0)

Camera Api 2 和 OPEN GL ES 使用(显示滤镜效果)

相机预览和open GL 使用实现滤镜效果
代码 https://github.com/loggerBill/camera

在这里插入图片描述

相机预览

1.相机动态权限

    <uses-permission android:name="android.permission.CAMERA" />

    <uses-feature
        android: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() {
                @Override
                public 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() {
                        @Override
                        public 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 {

    @Override
    public 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.0
        renderer = 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 {
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    //首次回调 onSurfaceCreated()
            // 此处执行一次性初始化操作
    // 例如:设置清屏颜色、编译着色器、创建纹理等
    }
    
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
    // 响应视图尺寸变化
        gl.glViewport(0, 0, width, height);  // 必须设置视口
    // 可在此处更新投影矩阵等
    }
    
    @Override
    public 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() {
            @Override
            public 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,可跨进程传递
    • 提供 CanvasBufferQueue 写入接口
      创建方式:-
    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渲染器 相机/解码器/系统渲染服务
  1. 零拷贝机制

    • 数据从生产者→Surface→SurfaceTexture→OpenGL纹理的传输不经过CPU内存复制
    • 通过 Android 的 BufferQueue 直接传递图形缓冲区
  2. 纹理坐标系处理

    • 相机帧可能有旋转/镜像
    • 需调用 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;
    }
    
  3. 生命周期管理

    // 正确释放顺序
    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() {
    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        glSurfaceView.requestRender();
    }
});
  • 作用:注册回调,当新相机帧到达时触发渲染
  • 工作流程
    1. 相机填充新帧到 SurfaceTexture 的缓冲区
    2. SurfaceTexture 触发 onFrameAvailable() 回调
    3. 回调中调用 requestRender() 请求 OpenGL 渲染
  • 渲染模式配合
    • 前面设置了 setRenderMode(RENDERMODE_WHEN_DIRTY)
    • 此回调确保有新帧时才渲染,节省资源

设置缓冲区尺寸

if (preViewSize != null) {
    surfaceTexture.setDefaultBufferSize(
        preViewSize.getWidth(),
        preViewSize.getHeight()
    );
}
  • 作用:配置 SurfaceTexture 的缓冲区尺寸以匹配相机预览分辨率
  • 参数preViewSize - 相机支持的预览尺寸(如 1920x1080)
  • 为什么重要
    • 确保分配的图形缓冲区大小正确
    • 避免图像拉伸/裁剪
    • 优化内存使用和性能

创建 Surface 生产者接口

mSurface = new Surface(surfaceTexture);
  • 作用:创建 Surface 对象作为相机数据输出的目标
  • 关键连接
    • SurfaceSurfaceTexture 的生产者端
    • 相机系统会将帧数据写入此 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;
  • 步骤分解
    1. 将2D纹理坐标扩展为4D向量:vec4(aTexCoord.s, aTexCoord.t, 0.0, 1.0)
    2. 应用纹理变换矩阵 uTexMatrix
    3. 取结果的xy分量 (.xy)
  • 为什么需要
    • 前置摄像头需要水平翻转
    • 不同设备旋转方向不同(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);
    
  1. 位置句柄的有效期

    • 只在当前着色器程序链接后有效
    • 重新链接程序后需要重新获取
  2. 性能优化

    • 位置句柄只需获取一次(通常在程序链接后)
    • 存储在成员变量中避免重复查询
  3. 错误处理

    if (positionHandle == -1) {
        throw new RuntimeException("找不到aPosition属性");
    }
    
  4. 变量名匹配

    1. 必须与着色器代码中的变量名完全一致
    2. 大小写敏感
  5. 着色器优化

    • 未使用的变量可能被编译器优化掉
    • 返回-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)
  1. 激活纹理单元

    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

    • 作用:激活0号纹理单元(OpenGL ES 有多个纹理单元)
    • 为什么重要
      • OpenGL ES 支持同时使用多个纹理(如 GL_TEXTURE0, GL_TEXTURE1…)
      • 默认激活0号单元(但显式声明更安全)
  2. 绑定外部纹理

    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId);

    • 关键参数
      • GLES11Ext.GL_TEXTURE_EXTERNAL_OES:特殊纹理目标,用于相机/视频流
      • textureId:之前通过 glGenTextures() 生成的纹理ID
    • 作用
      • 将纹理绑定到当前激活的纹理单元
      • 后续操作将作用于这个纹理
  3. 设置缩小过滤器

    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);

    • 参数解析
      • GL_TEXTURE_MIN_FILTER:纹理缩小过滤方式
      • GL_LINEAR:线性插值(双线性过滤)
    • 应用场景:当纹理被渲染得比原始尺寸小时(如缩略图)
  4. 设置放大过滤器

    GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

    • 参数解析
      • GL_TEXTURE_MAG_FILTER:纹理放大过滤方式
    • 为什么用线性过滤
      • 提供平滑的图像质量
      • 最适合相机预览(比 GL_NEAREST 锯齿感少)
  5. 设置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:边缘像素延伸
    • 为什么不用重复
      • 相机帧不需要平铺重复
      • 防止边缘出现异常颜色
  6. 设置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] 可重复
性能 零拷贝,高效 需要数据复制

为什么需要这些设置?

  1. 过滤模式 (GL_LINEAR)
    • 相机帧常需要缩放(如适配屏幕)
    • 线性过滤提供最自然的视觉效果
    • 避免 GL_NEAREST 产生的像素化锯齿
  2. 环绕模式 (GL_CLAMP_TO_EDGE)
    • 相机帧是连续视频流,不是平铺纹理
    • 防止边缘采样错误(尤其旋转时)
    • 兼容所有 Android 设备(某些设备严格需要)
  3. 外部纹理的特殊性
    • 直接映射到 SurfaceTexture 的缓冲区
    • 避免 CPU-GPU 数据拷贝(零拷贝)
    • 支持 YUV 到 RGB 的硬件转换

常见问题解决

纹理显示绿色或扭曲?

  1. 检查着色器是否正确定义 samplerExternalOES
  2. 确认调用了 surfaceTexture.updateTexImage()
  3. 验证纹理坐标变换矩阵的使用:
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表示从屏幕左下角开始)
    • 后两个参数:视口宽度和高度(使用新的窗口尺寸)
  • 重要性:当屏幕旋转或窗口大小改变时,必须重新设置视口,否则渲染会变形或错位
    为什么需要这个回调?
  1. 设备方向变化:当手机旋转时(竖屏↔横屏)
  2. 窗口大小改变:分屏模式、折叠屏切换等
  3. 初始化时: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坐标映射

整体流程说明:

  1. 重置画布:用黑色清屏(准备绘制新帧)
  2. 获取新帧:从视频源/相机获取最新图像
  3. 准备纹理:将新图像转换为OpenGL可用的纹理
  4. 校正显示:计算纹理变换矩阵,确保图像正确显示
    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;

为什么需要这样做?

  1. 避免拉伸变形:直接拉伸会扭曲图像(圆形变椭圆)
  2. 保持内容完整性:完整显示原始画面内容
  3. 自适应不同屏幕:处理手机旋转/分屏/折叠屏等场景

这种处理是视频播放器、相机预览等应用的标配逻辑,数学上属于保持原始比例的仿射变换。通过调整MVP矩阵而非直接修改顶点坐标,可以利用GPU的并行计算优势提高性能。

GLES20.glUseProgram(programHandle);

GLES20.glUseProgram(programHandle); 是OpenGL ES 2.0中一个关键的函数调用,用于激活指定的着色器程序。以下是详细解释:

作用与功能

  1. 激活着色器程序
    • 将指定的着色器程序设置为当前渲染管线使用的程序
    • 所有后续的绘制操作都将使用这个程序中的着色器
  2. 参数说明
    • programHandle:指向着色器程序的整数句柄(ID)
    • 这个句柄是通过之前glCreateProgram()glLinkProgram()创建的

工作原理

// 创建着色器程序
int programHandle = GLES20.glCreateProgram();

// 附加着色器(顶点+片段)
GLES20.glAttachShader(programHandle, vertexShader);
GLES20.glAttachShader(programHandle, fragmentShader);

// 链接程序
GLES20.glLinkProgram(programHandle);

// 使用程序
GLES20.glUseProgram(programHandle); // <-- 关键调用

底层机制

当调用glUseProgram()时:

  1. GPU驱动加载指定程序的字节码
  2. 配置渲染管线阶段:
    • 顶点处理器使用顶点着色器
    • 片段处理器使用片段着色器
  3. 重置所有uniform变量为默认值
GLES20.glEnableVertexAttribArray(positionHandle);
GLES20.glEnableVertexAttribArray(texCoordHandle);

功能详解:

  1. 启用顶点属性数组
    • 告诉OpenGL:“请使用我提供的数组数据,而不是默认的常量值”
    • 默认情况下,所有顶点属性都是禁用的,使用常量值(0,0,0,1)
  2. 参数说明
    • 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字节/float
    vertexBuffer    // 顶点缓冲区
);

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);
  1. 重置缓冲区位置

    vertexBuffer.position(0);
    
    • 将顶点缓冲区的读取位置重置到开头
    • 确保从缓冲区的起始位置读取数据
  2. 设置顶点属性指针

    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   // 右上
};

工作流程

  1. 准备数据

    // 创建顶点缓冲区
    FloatBuffer vertexBuffer = ByteBuffer.allocateDirect(VERTEX_DATA.length * 4)
                    .order(ByteOrder.nativeOrder())
                    .asFloatBuffer();
    vertexBuffer.put(vertexData);
    vertexBuffer.position(0);
    
    // 创建纹理坐标缓冲区(类似)
    
  2. 设置属性指针(如上述代码)

  3. 启用属性数组

    GLES20.glEnableVertexAttribArray(positionHandle);
    GLES20.glEnableVertexAttribArray(texCoordHandle);
    
  4. 绘制图形

    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变量 着色器中保持不变的值(每帧设置一次)

常见问题解决方案

  1. 图像显示不正确
    • 检查矩阵计算逻辑
    • 确保矩阵传递顺序正确
    • 验证着色器中的矩阵运算
  2. 纹理方向错误
    • 确保texMatrix从SurfaceTexture正确获取
    • 检查设备方向处理逻辑
  3. 性能优化
    • 只在矩阵变化时更新
    • 避免每帧重复计算不变矩阵

提示:在相机预览等实时应用中,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成对出现
    • 特别是多对象渲染时必不可少

性能优化提示

  1. 滤镜切换优化
    • 只在滤镜改变时更新uniform
    • 避免每帧重复设置相同值
  2. 批处理绘制
    • 相同滤镜的物体一起绘制
    • 减少着色器切换次数
  3. 资源复用
    • 顶点缓冲区对象(VBO)长期保留
    • 着色器程序预编译
  4. 避免冗余调用
    • 检查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() {
                        @Override
                        public void onConfigured(@NonNull CameraCaptureSession session) {
                            captureSession = session;
                            try {
                                // 设置重复请求
                                captureSession.setRepeatingRequest(
                                        previewRequestBuilder.build(),
                                        null, null);
                            } catch (CameraAccessException e) {
                                e.printStackTrace();
                            }
                        }
                        
                        
                      ···
}                        

网站公告

今日签到

点亮在社区的每一天
去签到