|
| 1 | +/* |
| 2 | + * cgeHistogramFilter.cpp |
| 3 | + * |
| 4 | + * Luminance histogram overlay rendered entirely on the GPU using three |
| 5 | + * GLES 3.1 compute-shader passes: |
| 6 | + * |
| 7 | + * Pass 1 (clear) — zero the 256×1 r32ui atomic-counter texture |
| 8 | + * Pass 2 (count) — imageAtomicAdd per pixel into the counter texture |
| 9 | + * Pass 3 (draw) — write bar-chart pixels into the 256×256 display texture |
| 10 | + * |
| 11 | + * The display texture is then blended onto the frame at the configured |
| 12 | + * position / size, exactly like CGEWaveformFilter. |
| 13 | + * |
| 14 | + * Requirements: OpenGL ES 3.1 (compute shaders + image load/store). |
| 15 | + */ |
| 16 | + |
| 17 | +#include "cgeHistogramFilter.h" |
| 18 | + |
| 19 | +#define USING_ALPHA 1 /// 80 % semi-transparent blend, same as WaveformFilter |
| 20 | + |
| 21 | +// --------------------------------------------------------------------------- |
| 22 | +// Pass 1 — clear the 256 histogram bins |
| 23 | +// Dispatched as glDispatchCompute(256, 1, 1) |
| 24 | +// --------------------------------------------------------------------------- |
| 25 | +static CGEConstString s_cshHistClear = "#version 310 es\n" CGE_SHADER_STRING( |
| 26 | + precision highp float; |
| 27 | + precision highp int; |
| 28 | + layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; |
| 29 | + layout(r32ui, binding = 2) writeonly uniform highp uimage2D histogramImage; |
| 30 | + |
| 31 | + void main() { |
| 32 | + imageStore(histogramImage, ivec2(int(gl_GlobalInvocationID.x), 0), uvec4(0u, 0u, 0u, 0u)); |
| 33 | + }); |
| 34 | + |
| 35 | +// --------------------------------------------------------------------------- |
| 36 | +// Pass 2 — accumulate luminance histogram via atomic add |
| 37 | +// Dispatched as glDispatchCompute(frameWidth, frameHeight, 1) |
| 38 | +// --------------------------------------------------------------------------- |
| 39 | +static CGEConstString s_cshHistCount = "#version 310 es\n" CGE_SHADER_STRING( |
| 40 | + precision highp float; |
| 41 | + precision highp int; |
| 42 | + layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; |
| 43 | + layout(rgba8ui, binding = 0) uniform readonly highp uimage2D inputImageTexture; |
| 44 | + // coherent volatile restrict are required for imageAtomicAdd |
| 45 | + layout(r32ui, binding = 2) coherent volatile restrict uniform highp uimage2D histogramImage; |
| 46 | + |
| 47 | + void main() { |
| 48 | + ivec2 texCoord = ivec2(gl_GlobalInvocationID.xy); |
| 49 | + uvec3 color = imageLoad(inputImageTexture, texCoord).rgb; |
| 50 | + |
| 51 | + // BT.601 luminance (integer approximation kept in float for clarity) |
| 52 | + float lum = dot(vec3(color.rgb), vec3(0.299, 0.587, 0.114)); |
| 53 | + uint bin = clamp(uint(lum), 0u, 255u); |
| 54 | + imageAtomicAdd(histogramImage, ivec2(bin, 0), 1u); |
| 55 | + }); |
| 56 | + |
| 57 | +// --------------------------------------------------------------------------- |
| 58 | +// Pass 3 — render bar chart into the 256×256 display texture |
| 59 | +// Dispatched as glDispatchCompute(256, 256, 1) |
| 60 | +// |
| 61 | +// u_scaleInv = 256.0 / (frameWidth * frameHeight) |
| 62 | +// → a bin containing 1/256 of all pixels exactly fills the bar height |
| 63 | +// --------------------------------------------------------------------------- |
| 64 | +static CGEConstString s_cshHistDraw = "#version 310 es\n" CGE_SHADER_STRING( |
| 65 | + precision highp float; |
| 66 | + precision highp int; |
| 67 | + layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; |
| 68 | + layout(r32ui, binding = 2) readonly uniform highp uimage2D histogramImage; |
| 69 | + layout(rgba8ui, binding = 1) writeonly uniform highp uimage2D displayImage; |
| 70 | + |
| 71 | + uniform float u_scaleInv; // 256.0 / (frameWidth * frameHeight) |
| 72 | + |
| 73 | + void main() { |
| 74 | + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); |
| 75 | + uint count = imageLoad(histogramImage, ivec2(coord.x, 0)).r; |
| 76 | + |
| 77 | + // Normalise: full height when the bin contains 1/256 of total pixels |
| 78 | + uint barHeight = uint(clamp(float(count) * u_scaleInv, 0.0, 1.0) * 256.0); |
| 79 | + |
| 80 | + // coord.y == 0 is the bottom of the bar (low luminance side after flip) |
| 81 | + if (uint(coord.y) < barHeight) { |
| 82 | + imageStore(displayImage, coord, uvec4(255u, 255u, 255u, 255u)); |
| 83 | + } else { |
| 84 | + imageStore(displayImage, coord, uvec4(0u, 0u, 0u, 255u)); |
| 85 | + } |
| 86 | + }); |
| 87 | + |
| 88 | +// =========================================================================== |
| 89 | + |
| 90 | +namespace CGE |
| 91 | +{ |
| 92 | +CGEHistogramFilter::~CGEHistogramFilter() |
| 93 | +{ |
| 94 | + if (m_histogramTex != 0) |
| 95 | + { |
| 96 | + glDeleteTextures(1, &m_histogramTex); |
| 97 | + m_histogramTex = 0; |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +bool CGEHistogramFilter::init() |
| 102 | +{ |
| 103 | + if (!m_clearProgram.initWithComputeShader(s_cshHistClear) || |
| 104 | + !m_countProgram.initWithComputeShader(s_cshHistCount) || |
| 105 | + !m_drawProgram.initWithComputeShader(s_cshHistDraw)) |
| 106 | + { |
| 107 | + CGE_LOG_ERROR( |
| 108 | + "CGEHistogramFilter::init failed.\n" |
| 109 | + "This filter requires GLES 3.1+ (compute shaders + imageAtomicAdd).\n"); |
| 110 | + CGE_LOG_ERROR(" clear shader : %s\n", s_cshHistClear); |
| 111 | + return false; |
| 112 | + } |
| 113 | + |
| 114 | + setFormPosition(0.1f, 0.1f); |
| 115 | + setFormSize(0.3f, 0.3f); |
| 116 | + |
| 117 | + m_drawer.reset(TextureDrawer::create()); |
| 118 | + m_drawer->setFlipScale(1.0f, -1.0f); // flip to match GL coordinate system |
| 119 | + |
| 120 | + m_displayTexture = std::make_unique<TextureObject>(); |
| 121 | + // Allocate the 256×256 display texture (RGBA8, same as waveform uses). |
| 122 | + m_displayTexture->resize(256, 256); |
| 123 | + |
| 124 | + // Create 256×1 r32ui texture for atomic luminance counters. |
| 125 | + glGenTextures(1, &m_histogramTex); |
| 126 | + glBindTexture(GL_TEXTURE_2D, m_histogramTex); |
| 127 | + glTexStorage2D(GL_TEXTURE_2D, 1, GL_R32UI, 256, 1); |
| 128 | + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| 129 | + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| 130 | + glBindTexture(GL_TEXTURE_2D, 0); |
| 131 | + |
| 132 | + return true; |
| 133 | +} |
| 134 | + |
| 135 | +void CGEHistogramFilter::render2Texture(CGEImageHandlerInterface* handler, |
| 136 | + GLuint /*srcTexture*/, |
| 137 | + GLuint /*vertexBufferID*/) |
| 138 | +{ |
| 139 | + auto&& sz = handler->getOutputFBOSize(); |
| 140 | + |
| 141 | + // scale factor: a uniform histogram bin (1/256 of pixels) → full bar height |
| 142 | + const float scaleInv = 256.0f / static_cast<float>(sz.width * sz.height); |
| 143 | + |
| 144 | + // ----------------------------------------------------------------------- |
| 145 | + // Bind image units |
| 146 | + // binding 0 : source frame (rgba8ui, read-only) |
| 147 | + // binding 1 : display texture (rgba8ui, write-only) |
| 148 | + // binding 2 : histogram bins (r32ui, read-write for atomic) |
| 149 | + // ----------------------------------------------------------------------- |
| 150 | + glBindImageTexture(0, handler->getTargetTextureID(), 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA8UI); |
| 151 | + glBindImageTexture(1, m_displayTexture->texture(), 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA8UI); |
| 152 | + glBindImageTexture(2, m_histogramTex, 0, GL_FALSE, 0, GL_READ_WRITE, GL_R32UI); |
| 153 | + |
| 154 | + // ----------------------------------------------------------------------- |
| 155 | + // Pass 1 — clear bins |
| 156 | + // ----------------------------------------------------------------------- |
| 157 | + m_clearProgram.bind(); |
| 158 | + glDispatchCompute(256, 1, 1); |
| 159 | + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); |
| 160 | + |
| 161 | + // ----------------------------------------------------------------------- |
| 162 | + // Pass 2 — count luminance values |
| 163 | + // ----------------------------------------------------------------------- |
| 164 | + m_countProgram.bind(); |
| 165 | + glDispatchCompute(sz.width, sz.height, 1); |
| 166 | + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); |
| 167 | + |
| 168 | + // ----------------------------------------------------------------------- |
| 169 | + // Pass 3 — render bar chart |
| 170 | + // ----------------------------------------------------------------------- |
| 171 | + m_drawProgram.bind(); |
| 172 | + m_drawProgram.sendUniformf("u_scaleInv", scaleInv); |
| 173 | + glDispatchCompute(256, 256, 1); |
| 174 | + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); |
| 175 | + |
| 176 | + // ----------------------------------------------------------------------- |
| 177 | + // Composite the display texture onto the frame |
| 178 | + // ----------------------------------------------------------------------- |
| 179 | +#if USING_ALPHA |
| 180 | + glEnable(GL_BLEND); |
| 181 | + glBlendColor(1, 1, 1, 0.8f); |
| 182 | + glBlendFunc(GL_ONE, GL_ONE_MINUS_CONSTANT_ALPHA); |
| 183 | +#endif |
| 184 | + |
| 185 | + handler->setAsTarget(); |
| 186 | + glViewport( |
| 187 | + static_cast<GLint>(m_position[0] * sz.width), |
| 188 | + static_cast<GLint>(m_position[1] * sz.height), |
| 189 | + static_cast<GLsizei>(m_size[0] * sz.width), |
| 190 | + static_cast<GLsizei>(m_size[1] * sz.height)); |
| 191 | + |
| 192 | + m_drawer->drawTexture(m_displayTexture->texture()); |
| 193 | + |
| 194 | +#if USING_ALPHA |
| 195 | + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); |
| 196 | + glDisable(GL_BLEND); |
| 197 | +#endif |
| 198 | +} |
| 199 | + |
| 200 | +void CGEHistogramFilter::setFormPosition(float left, float top) |
| 201 | +{ |
| 202 | + m_position = { left, top }; |
| 203 | +} |
| 204 | + |
| 205 | +void CGEHistogramFilter::setFormSize(float width, float height) |
| 206 | +{ |
| 207 | + m_size = { width, height }; |
| 208 | +} |
| 209 | + |
| 210 | +} // namespace CGE |
0 commit comments