Android GLSurfaceView.Renderer glReadPixels前绘制Bitmap/文字水印方案
问题根因
直接调用GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, waterMarkBmp, 0);不生效,是因为这行代码仅完成了Bitmap数据到GPU纹理内存的上传,没有执行任何图元绘制操作,默认帧缓冲区中不会写入水印内容,后续glReadPixels自然只能读到原始渲染画面。
正确实现水印叠加的方案
需要在主画面绘制完成后、调用glReadPixels之前,走完整的纹理绑定、着色器渲染流程把水印画到当前帧缓冲区,具体实现步骤如下:
1. 提前初始化水印相关GPU资源
在onSurfaceCreated阶段完成所有水印资源的初始化,不要每帧重复创建浪费性能:
- 生成普通2D纹理,一次性上传水印Bitmap(水印内容不变的话后续无需重复上传)
- 准备适配普通
sampler2D的水印专用着色器(水印是普通2D纹理,不需要OES外部纹理扩展) - 准备水印的顶点坐标、UV坐标缓冲,根据水印想要放置的位置(比如右上角、左下角)调整顶点范围即可,例如要在右上角放占屏幕1/4大小的水印,顶点坐标就对应右上角的视口区域
相关代码示例:
// 水印相关全局变量 private int watermarkProgram; private int watermarkTexture; private FloatBuffer watermarkVertexBuffer; private FloatBuffer watermarkUvBuffer; private ShortBuffer watermarkDrawBuffer; private final short[] watermarkDrawOrder = {0,1,2,0,2,3}; // 右上角水印顶点坐标示例,宽高各占屏幕1/4 private final float[] watermarkCoords = { 0.5f, 1.0f, 0.0f, 0.5f, 0.5f, 0.0f, 1.0f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f }; // 水印UV坐标,需要上下翻转可调整顺序 private final float[] watermarkUv = { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f }; // 水印专用着色器,无需OES扩展 private final String watermarkVertexShader = "attribute vec4 vPosition;" + "attribute vec2 vUv;" + "varying vec2 uv;" + "void main() {" + " gl_Position = vPosition;" + " uv = vUv;" + "}"; private final String watermarkFragShader = "precision mediump float;" + "varying vec2 uv;" + "uniform sampler2D sWatermark;" + "void main() {" + " vec4 color = texture2D(sWatermark, uv);" + " if(color.a < 0.1) discard;" + " gl_FragColor = color;" + "}";
在onSurfaceCreated中补充资源初始化逻辑:
// 初始化水印顶点/UV/索引缓冲 ByteBuffer wVbb = ByteBuffer.allocateDirect(watermarkCoords.length*4); wVbb.order(ByteOrder.nativeOrder()); watermarkVertexBuffer = wVbb.asFloatBuffer(); watermarkVertexBuffer.put(watermarkCoords); watermarkVertexBuffer.position(0); ByteBuffer wUbb = ByteBuffer.allocateDirect(watermarkUv.length*4); wUbb.order(ByteOrder.nativeOrder()); watermarkUvBuffer = wUbb.asFloatBuffer(); watermarkUvBuffer.put(watermarkUv); watermarkUvBuffer.position(0); ByteBuffer wDlb = ByteBuffer.allocateDirect(watermarkDrawOrder.length*2); wDlb.order(ByteOrder.nativeOrder()); watermarkDrawBuffer = wDlb.asShortBuffer(); watermarkDrawBuffer.put(watermarkDrawOrder); watermarkDrawBuffer.position(0); // 链接着水印着色器程序 int wVs = loadShader(GLES20.GL_VERTEX_SHADER, watermarkVertexShader); int wFs = loadShader(GLES20.GL_FRAGMENT_SHADER, watermarkFragShader); watermarkProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(watermarkProgram, wVs); GLES20.glAttachShader(watermarkProgram, wFs); GLES20.glLinkProgram(watermarkProgram); // 生成水印纹理并上传Bitmap int[] wTex = new int[1]; GLES20.glGenTextures(1, wTex, 0); watermarkTexture = wTex[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, watermarkTexture); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, waterMarkBmp, 0);
2. 在渲染流程中插入水印绘制逻辑
修改onDrawFrame逻辑,主画面绘制完成后先绘制水印,再调用glReadPixels读帧:
// 原有主画面绘制完成、glUseProgram(0)之后,开始绘制水印 GLES20.glEnable(GLES20.GL_BLEND); // 开启混合,正确处理水印透明通道 GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA); GLES20.glUseProgram(watermarkProgram); int wPosHandle = GLES20.glGetAttribLocation(watermarkProgram, "vPosition"); GLES20.glEnableVertexAttribArray(wPosHandle); GLES20.glVertexAttribPointer(wPosHandle, 3, GLES20.GL_FLOAT, false, 12, watermarkVertexBuffer); int wUvHandle = GLES20.glGetAttribLocation(watermarkProgram, "vUv"); GLES20.glEnableVertexAttribArray(wUvHandle); GLES20.glVertexAttribPointer(wUvHandle, 2, GLES20.GL_FLOAT, false, 8, watermarkUvBuffer); int wSampler = GLES20.glGetUniformLocation(watermarkProgram, "sWatermark"); GLES20.glUniform1i(wSampler, 0); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, watermarkTexture); GLES20.glDrawElements(GLES20.GL_TRIANGLES, watermarkDrawOrder.length, GLES20.GL_UNSIGNED_SHORT, watermarkDrawBuffer); GLES20.glDisableVertexAttribArray(wPosHandle); GLES20.glDisableVertexAttribArray(wUvHandle); GLES20.glDisable(GLES20.GL_BLEND); GLES20.glUseProgram(0); // 所有绘制完成后再读帧,此时读到的数据已经包含水印 GLES20.glReadPixels(x, 0, w, y + h, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, buffer); sentRtmp(buffer);
3. 文字水印实现方式
文字水印不需要引入额外渲染库,提前通过Android原生Canvas把文字绘制到空白Bitmap上即可,后续流程和普通Bitmap水印完全一致:
public static Bitmap createTextWatermark(String text, int textSize, int textColor, int bmpWidth, int bmpHeight) { Bitmap bmp = Bitmap.createBitmap(bmpWidth, bmpHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); paint.setColor(textColor); paint.setTextSize(textSize); paint.setAntiAlias(true); // 文字位置可自行调整,示例为左上角偏移20px canvas.drawText(text, 20, textSize + 20, paint); return bmp; }
注意事项
- 所有OpenGL相关操作必须在GL渲染线程执行,禁止跨线程调用纹理上传、绘制类API
- 纹理、着色器、顶点缓冲这类GPU资源仅需初始化一次,不要每帧重复创建,否则会造成明显的性能卡顿
- 如果需要动态更新水印内容(比如实时时间戳水印),仅需在内容变化时重新调用
GLUtils.texImage2D把新Bitmap上传到已有水印纹理即可,无需重复生成纹理对象 - 开启混合模式是为了正确渲染带透明通道的PNG水印,如果水印完全不透明,可以关闭混合降低性能开销
内容的提问来源于stack exchange,提问作者youteng li
相关产品推荐
相关产品推荐

