You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 22:24:13