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();}}···
}