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

OpenGL ES 3.0实现Bokeh效果时缓冲区与着色器互传方法问询

Android OpenGL ES 3.0 散景(Bokeh)效果核心实现方案

1. Fragment Shader 写入Bokeh点到缓冲区的实现

OpenGL ES 没有原生DirectX风格的Append Buffer,通过两类GPU缓冲区组合即可实现无CPU参与的片元阶段追加写入,完全匹配你提到的bokeh点提取Pass逻辑:

  • 原子计数器缓冲区:4字节大小,初始值为0,用于原子性记录当前已写入的bokeh点总数,避免多线程写入冲突
  • 结构化存储缓冲区(SSBO):按屏幕规格预分配足够容量,按固定结构存储每个bokeh点的位置、颜色、CoC尺寸

Java层缓冲区初始化代码

// 常量定义
private final int MAX_BOKEH_POINTS = 200000; // 1080P屏幕预留20w点容量足够
private final int BOKEH_POINT_STRIDE = 6 * 4; // 单点位:2float + 颜色:3float + CoC:1float = 24字节
private int atomicCounterBuf;
private int bokehPointBuf;
private int indirectDrawBuf;

private void initBokehBuffers() {
    int[] buf = new int[1];
    // 1. 初始化原子计数器缓冲区
    GLES30.glGenBuffers(1, buf, 0);
    atomicCounterBuf = buf[0];
    GLES30.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, atomicCounterBuf);
    IntBuffer zeroBuf = IntBuffer.allocate(1).put(0, 0);
    GLES30.glBufferData(GLES31.GL_ATOMIC_COUNTER_BUFFER, 4, zeroBuf, GLES30.GL_DYNAMIC_COPY);
    GLES30.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0);

    // 2. 初始化Bokeh点存储SSBO
    GLES30.glGenBuffers(1, buf, 0);
    bokehPointBuf = buf[0];
    GLES30.glBindBuffer(GLES31.GL_SHADER_STORAGE_BUFFER, bokehPointBuf);
    GLES30.glBufferData(GLES31.GL_SHADER_STORAGE_BUFFER, MAX_BOKEH_POINTS * BOKEH_POINT_STRIDE, null, GLES30.GL_DYNAMIC_COPY);
    GLES30.glBindBuffer(GLES31.GL_SHADER_STORAGE_BUFFER, 0);

    // 3. 初始化间接绘制参数缓冲区,结构为:[每实例顶点数, 实例数, 起始顶点, 基础实例偏移] 共4个uint
    GLES30.glGenBuffers(1, buf, 0);
    indirectDrawBuf = buf[0];
    GLES30.glBindBuffer(GLES30.GL_DRAW_INDIRECT_BUFFER, indirectDrawBuf);
    IntBuffer indirectInit = IntBuffer.allocate(4);
    indirectInit.put(0, 4); // 每个点扩展为四边形,对应4个顶点
    indirectInit.put(1, 0); // 初始实例数为0,后续从原子计数器拷贝
    indirectInit.put(2, 0);
    indirectInit.put(3, 0);
    GLES30.glBufferData(GLES30.GL_DRAW_INDIRECT_BUFFER, 16, indirectInit, GLES30.GL_DYNAMIC_COPY);
    GLES30.glBindBuffer(GLES30.GL_DRAW_INDIRECT_BUFFER, 0);
}

Bokeh点提取Pass的Fragment Shader代码

#version 300 es
precision highp float;
layout(binding = 0, offset = 0) uniform atomic_uint bokehCounter;

struct BokehPoint {
    vec2 pos;
    vec3 color;
    float coc;
};
layout(binding = 1, std430) buffer BokehBuffer {
    BokehPoint points[];
};

uniform sampler2D inputTex;
uniform sampler2D cocTex;
uniform float brightnessThreshold;
uniform float cocThreshold;
out vec4 fragColor;

void main() {
    vec2 screenSize = vec2(textureSize(inputTex, 0));
    vec2 uv = gl_FragCoord.xy / screenSize;
    vec3 currentCol = texelFetch(inputTex, ivec2(gl_FragCoord.xy), 0).rgb;
    float currentLum = dot(currentCol, vec3(0.2126, 0.7152, 0.0722));
    float currentCoC = texelFetch(cocTex, ivec2(gl_FragCoord.xy), 0).r;

    // 计算5x5邻域平均亮度
    float avgLum = 0.0;
    for(int x = -2; x <= 2; x++) {
        for(int y = -2; y <= 2; y++) {
            vec2 sampleCoord = clamp(gl_FragCoord.xy + vec2(x,y), vec2(0), screenSize);
            vec3 sampleCol = texelFetch(inputTex, ivec2(sampleCoord), 0).rgb;
            avgLum += dot(sampleCol, vec3(0.2126, 0.7152, 0.0722));
        }
    }
    avgLum /= 25.0;

    if (currentLum - avgLum > brightnessThreshold && currentCoC > cocThreshold) {
        uint writeIdx = atomicCounterIncrement(bokehCounter);
        if(writeIdx < MAX_BOKEH_POINTS) {
            points[writeIdx].pos = gl_FragCoord.xy;
            points[writeIdx].color = currentCol;
            points[writeIdx].coc = currentCoC;
        }
        fragColor = vec4(0.0);
        return;
    }
    fragColor = vec4(currentCol, 1.0);
}

若设备仅支持纯GLES3.0无SSBO扩展,可将存储缓冲区替换为R32UI格式的2D图像,通过imageStore按偏移写入数据,逻辑完全一致。


2. 缓冲区数据传递给Geometry Shader、间接绘制实现

Bokeh点提取Pass执行完成后,不需要将任何数据读回CPU,直接通过GPU内置的缓冲区拷贝即可完成绘制参数准备,再通过实例化绘制+Geometry Shader将点扩展为对应尺寸的四边形。

绘制前的缓冲区准备(Java层)

private void prepareBokehDraw() {
    // 将原子计数器存储的点数量,拷贝到间接绘制缓冲区的「实例数」偏移位置(偏移4字节)
    GLES30.glBindBuffer(GLES30.GL_COPY_READ_BUFFER, atomicCounterBuf);
    GLES30.glBindBuffer(GLES30.GL_COPY_WRITE_BUFFER, indirectDrawBuf);
    GLES30.glCopyBufferSubData(GLES30.GL_COPY_READ_BUFFER, GLES30.GL_COPY_WRITE_BUFFER, 0, 4, 4);
    GLES30.glBindBuffer(GLES30.GL_COPY_READ_BUFFER, 0);
    GLES30.glBindBuffer(GLES30.GL_COPY_WRITE_BUFFER, 0);

    // 重置原子计数器为0,供下一帧使用
    GLES30.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, atomicCounterBuf);
    GLES30.glBufferSubData(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0, 4, IntBuffer.wrap(new int[]{0}));
    GLES30.glBindBuffer(GLES31.GL_ATOMIC_COUNTER_BUFFER, 0);
}

绘制用Shader代码

顶点着色器:直接通过实例ID读取SSBO中的bokeh点数据

#version 300 es
#extension GL_OES_geometry_shader : enable
precision highp float;

struct BokehPoint {
    vec2 pos;
    vec3 color;
    float coc;
};
layout(binding = 1, std430) buffer BokehBuffer {
    BokehPoint points[];
};

out V2G {
    vec3 color;
    float coc;
} v2g;

void main() {
    BokehPoint pt = points[gl_InstanceID];
    gl_Position = vec4(pt.pos, 0.0, 1.0);
    v2g.color = pt.color;
    v2g.coc = pt.coc;
}

几何着色器:将点扩展为CoC尺寸的四边形

#extension GL_OES_geometry_shader : enable
precision highp float;

layout(points) in;
layout(triangle_strip, max_vertices = 4) out;

in V2G {
    vec3 color;
    float coc;
} v2g[];

out vec2 pUV;
out vec3 pColor;

uniform vec2 screenSize;

void main() {
    vec2 pixelPos = gl_in[0].gl_Position.xy;
    float coc = v2g[0].coc;
    vec3 col = v2g[0].color;
    vec2 ndcPos = (pixelPos / screenSize) * 2.0 - 1.0;
    vec2 ndcOffset = vec2(coc) / screenSize * 2.0;

    pUV = vec2(0.0, 0.0);
    pColor = col;
    gl_Position = vec4(ndcPos + vec2(-ndcOffset.x, -ndcOffset.y), 0.0, 1.0);
    EmitVertex();

    pUV = vec2(1.0, 0.0);
    gl_Position = vec4(ndcPos + vec2(ndcOffset.x, -ndcOffset.y), 0.0, 1.0);
    EmitVertex();

    pUV = vec2(0.0, 1.0);
    gl_Position = vec4(ndcPos + vec2(-ndcOffset.x, ndcOffset.y), 0.0, 1.0);
    EmitVertex();

    pUV = vec2(1.0, 1.0);
    gl_Position = vec4(ndcPos + vec2(ndcOffset.x, ndcOffset.y), 0.0, 1.0);
    EmitVertex();

    EndPrimitive();
}

片段着色器:采样光圈形状、加法混合输出

precision highp float;
in vec2 pUV;
in vec3 pColor;
uniform sampler2D apertureTex;
out vec4 fragColor;

void main() {
    float mask = texture(apertureTex, pUV).r;
    fragColor = vec4(pColor * mask, 1.0);
}

Java层绘制调用代码

private void drawBokehLayer() {
    GLES30.glUseProgram(bokehDrawProgram);
    // 开启加法混合
    GLES30.glEnable(GLES30.GL_BLEND);
    GLES30.glBlendFunc(GLES30.GL_ONE, GLES30.GL_ONE);

    // 绑定缓冲区到Shader约定的绑定点
    GLES30.glBindBuffer(GLES30.GL_DRAW_INDIRECT_BUFFER, indirectDrawBuf);
    GLES30.glBindBufferBase(GLES31.GL_SHADER_STORAGE_BUFFER, 1, bokehPointBuf);

    // 传Uniform
    int screenLoc = GLES30.glGetUniformLocation(bokehDrawProgram, "screenSize");
    GLES30.glUniform2f(screenLoc, screenWidth, screenHeight);
    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, apertureShapeTex);
    int texLoc = GLES30.glGetUniformLocation(bokehDrawProgram, "apertureTex");
    GLES30.glUniform1i(texLoc, 0);

    // 执行间接绘制,无需传入任何顶点/索引数据
    GLES30.glDrawArraysIndirect(GLES30.GL_TRIANGLE_STRIP, 0);

    // 状态恢复
    GLES30.glDisable(GLES30.GL_BLEND);
    GLES30.glBindBuffer(GLES30.GL_DRAW_INDIRECT_BUFFER, 0);
    GLES30.glBindBufferBase(GLES31.GL_SHADER_STORAGE_BUFFER, 1, 0);
}

3. 关键实现说明

  • 缓冲区与着色器双向数据传递:所有GPU缓冲区(SSBO/原子计数器/VBO/PBO)仅需绑定到对应绑定点,所有着色器阶段都可直接读写,无需CPU参与。调试阶段可通过glMapBufferRange映射缓冲区到Java内存读取数据,正式版本禁止该操作,会阻塞GPU管线。
  • 大量顶点输入性能:上述方案全程GPU生成、消费bokeh点数据,无CPU-GPU数据传输开销,单帧30w以内的点数量在中端安卓设备上可稳定60帧。若设备不支持Geometry Shader,可直接在顶点着色器中通过gl_VertexID计算四边形顶点偏移,移除GS阶段即可兼容。
  • DOF Pass优化:选择1/4分辨率高斯模糊方案时,需用CoC值做采样权重计算避免边缘渗色,最终模糊层与原图用CoC值作为插值因子融合即可;选择Poisson圆盘采样方案时,大尺寸CoC可搭配一次降采样Pass减少采样次数,16次采样可覆盖绝大多数场景。
  • Bokeh层分辨率选择:1/4分辨率的bokeh渲染目标可降低75%的带宽开销,最终做双线性上采样与主场景融合,肉眼几乎无法分辨差异,推荐中低端设备使用。

内容的提问来源于stack exchange,提问作者oHuflying

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 10:15:32