001/* 002 * Copyright (C) Photon Vision. 003 * 004 * This program is free software: you can redistribute it and/or modify 005 * it under the terms of the GNU General Public License as published by 006 * the Free Software Foundation, either version 3 of the License, or 007 * (at your option) any later version. 008 * 009 * This program is distributed in the hope that it will be useful, 010 * but WITHOUT ANY WARRANTY; without even the implied warranty of 011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 012 * GNU General Public License for more details. 013 * 014 * You should have received a copy of the GNU General Public License 015 * along with this program. If not, see <https://www.gnu.org/licenses/>. 016 */ 017 018package org.photonvision.vision.pipe.impl; 019 020import static com.jogamp.opengl.GL.*; 021import static com.jogamp.opengl.GL2ES2.*; 022 023import com.jogamp.opengl.*; 024import com.jogamp.opengl.util.GLBuffers; 025import com.jogamp.opengl.util.texture.Texture; 026import com.jogamp.opengl.util.texture.TextureData; 027import java.nio.ByteBuffer; 028import java.nio.FloatBuffer; 029import java.nio.IntBuffer; 030import jogamp.opengl.GLOffscreenAutoDrawableImpl; 031import org.opencv.core.CvType; 032import org.opencv.core.Mat; 033import org.photonvision.common.logging.LogGroup; 034import org.photonvision.common.logging.Logger; 035import org.photonvision.vision.pipe.CVPipe; 036 037public class GPUAcceleratedHSVPipe extends CVPipe<Mat, Mat, HSVPipe.HSVParams> { 038 private static final String k_vertexShader = 039 String.join( 040 "\n", 041 "#version 100", 042 "", 043 "attribute vec4 position;", 044 "", 045 "void main() {", 046 " gl_Position = position;", 047 "}"); 048 private static final String k_fragmentShader = 049 String.join( 050 "\n", 051 "#version 100", 052 "", 053 "precision highp float;", 054 "precision highp int;", 055 "", 056 "uniform vec3 lowerThresh;", 057 "uniform vec3 upperThresh;", 058 "uniform vec2 resolution;", 059 "uniform sampler2D texture0;", 060 "", 061 "vec3 rgb2hsv(vec3 c) {", 062 " vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);", 063 " vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));", 064 " vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));", 065 "", 066 " float d = q.x - min(q.w, q.y);", 067 " float e = 1.0e-10;", 068 " return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);", 069 "}", 070 "", 071 "bool inRange(vec3 hsv) {", 072 " bvec3 botBool = greaterThanEqual(hsv, lowerThresh);", 073 " bvec3 topBool = lessThanEqual(hsv, upperThresh);", 074 " return all(botBool) && all(topBool);", 075 "}", 076 "", 077 "void main() {", 078 " vec2 uv = gl_FragCoord.xy/resolution;", 079 // Important! We do this .bgr swizzle because the image comes in as BGR, but we pretend 080 // it's RGB for convenience+speed 081 " vec3 col = texture2D(texture0, uv).bgr;", 082 // Only the first value in the vec4 gets used for GL_RED, and only the last value gets 083 // used for GL_ALPHA 084 " gl_FragColor = inRange(rgb2hsv(col)) ? vec4(1.0, 0.0, 0.0, 1.0) : vec4(0.0, 0.0, 0.0, 0.0);", 085 "}"); 086 private static final int k_startingWidth = 1280, k_startingHeight = 720; 087 private static final float[] k_vertexPositions = { 088 // Set up a quad that covers the screen 089 -1f, +1f, +1f, +1f, -1f, -1f, +1f, -1f 090 }; 091 private static final int k_positionVertexAttribute = 092 0; // ID for the vertex shader position variable 093 094 public enum PBOMode { 095 NONE, 096 SINGLE_BUFFERED, 097 DOUBLE_BUFFERED 098 } 099 100 private final IntBuffer vertexVBOIds = GLBuffers.newDirectIntBuffer(1), 101 unpackPBOIds = GLBuffers.newDirectIntBuffer(2), 102 packPBOIds = GLBuffers.newDirectIntBuffer(2); 103 104 private final GL2ES2 gl; 105 private final GLProfile profile; 106 private final int outputFormat; 107 private final PBOMode pboMode; 108 private final GLOffscreenAutoDrawableImpl.FBOImpl drawable; 109 private final Texture texture; 110 // The texture uniform holds the image that's being processed 111 // The resolution uniform holds the current image resolution 112 // The lower and upper uniforms hold the lower and upper HSV limits for thresholding 113 private final int textureUniformId, resolutionUniformId, lowerUniformId, upperUniformId; 114 115 private final Logger logger = new Logger(GPUAcceleratedHSVPipe.class, LogGroup.General); 116 117 private byte[] inputBytes, outputBytes; 118 private Mat outputMat = new Mat(k_startingHeight, k_startingWidth, CvType.CV_8UC1); 119 private int previousWidth = -1, previousHeight = -1; 120 private int unpackIndex = 0, unpackNextIndex = 0, packIndex = 0, packNextIndex = 0; 121 122 public GPUAcceleratedHSVPipe(PBOMode pboMode) { 123 this.pboMode = pboMode; 124 125 // Set up GL profile and ask for specific capabilities 126 profile = GLProfile.get(pboMode == PBOMode.NONE ? GLProfile.GLES2 : GLProfile.GL4ES3); 127 final var capabilities = new GLCapabilities(profile); 128 capabilities.setHardwareAccelerated(true); 129 capabilities.setFBO(true); 130 capabilities.setDoubleBuffered(false); 131 capabilities.setOnscreen(false); 132 capabilities.setRedBits(8); 133 capabilities.setBlueBits(0); 134 capabilities.setGreenBits(0); 135 capabilities.setAlphaBits(0); 136 137 // Set up the offscreen area we're going to draw to 138 final var factory = GLDrawableFactory.getFactory(profile); 139 drawable = 140 (GLOffscreenAutoDrawableImpl.FBOImpl) 141 factory.createOffscreenAutoDrawable( 142 factory.getDefaultDevice(), 143 capabilities, 144 new DefaultGLCapabilitiesChooser(), 145 k_startingWidth, 146 k_startingHeight); 147 drawable.display(); 148 drawable.getContext().makeCurrent(); 149 150 // Get an OpenGL context; OpenGL ES 2.0 and OpenGL 2.0 are compatible with all the coprocs we 151 // care about but not compatible with PBOs. Open GL ES 3.0 and OpenGL 4.0 are compatible with 152 // select coprocs *and* PBOs 153 gl = pboMode == PBOMode.NONE ? drawable.getGL().getGLES2() : drawable.getGL().getGL4ES3(); 154 final int programId = gl.glCreateProgram(); 155 156 if (pboMode == PBOMode.NONE && !gl.glGetString(GL_EXTENSIONS).contains("GL_EXT_texture_rg")) { 157 logger.warn( 158 "OpenGL ES 2.0 implementation does not have the 'GL_EXT_texture_rg' extension, falling back to GL_ALPHA instead of GL_RED output format"); 159 outputFormat = GL_ALPHA; 160 } else { 161 outputFormat = GL_RED; 162 } 163 164 // JOGL creates a framebuffer color attachment that has RGB set as the format, which is not 165 // appropriate for us because we want a single-channel format 166 // We make ourown FBO color attachment to remedy this 167 // Detach and destroy the FBO color attachment that JOGL made for us 168 drawable.getFBObject(GL_FRONT).detachColorbuffer(gl, 0, true); 169 // Equivalent to calling glBindFramebuffer 170 drawable.getFBObject(GL_FRONT).bind(gl); 171 if (true) { // For now renderbuffers are disabled 172 // Create a color attachment texture to hold our rendered output 173 var colorBufferIds = GLBuffers.newDirectIntBuffer(1); 174 gl.glGenTextures(1, colorBufferIds); 175 gl.glBindTexture(GL_TEXTURE_2D, colorBufferIds.get(0)); 176 gl.glTexImage2D( 177 GL_TEXTURE_2D, 178 0, 179 outputFormat == GL_RED ? GL_R8 : GL_ALPHA8, 180 k_startingWidth, 181 k_startingHeight, 182 0, 183 outputFormat, 184 GL_UNSIGNED_BYTE, 185 null); 186 gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 187 gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 188 // Attach the texture to the framebuffer 189 gl.glBindTexture(GL_TEXTURE_2D, 0); 190 gl.glFramebufferTexture2D( 191 GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBufferIds.get(0), 0); 192 // Cleanup 193 gl.glBindTexture(GL_TEXTURE_2D, 0); 194 } else { 195 // Create a color attachment texture to hold our rendered output 196 var renderBufferIds = GLBuffers.newDirectIntBuffer(1); 197 gl.glGenRenderbuffers(1, renderBufferIds); 198 gl.glBindRenderbuffer(GL_RENDERBUFFER, renderBufferIds.get(0)); 199 gl.glRenderbufferStorage( 200 GL_RENDERBUFFER, 201 outputFormat == GL_RED ? GL_R8 : GL_ALPHA8, 202 k_startingWidth, 203 k_startingHeight); 204 // Attach the texture to the framebuffer 205 gl.glFramebufferRenderbuffer( 206 GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderBufferIds.get(0)); 207 // Cleanup 208 gl.glBindRenderbuffer(GL_RENDERBUFFER, 0); 209 } 210 drawable.getFBObject(GL_FRONT).unbind(gl); 211 212 // Check that the FBO is attached 213 int fboStatus = gl.glCheckFramebufferStatus(GL_FRAMEBUFFER); 214 if (fboStatus == GL_FRAMEBUFFER_UNSUPPORTED) { 215 throw new RuntimeException( 216 "GL implementation does not support rendering to internal format '" 217 + String.format("0x%08X", outputFormat == GL_RED ? GL_R8 : GL_ALPHA8) 218 + "' with type '" 219 + String.format("0x%08X", GL_UNSIGNED_BYTE) 220 + "'"); 221 } else if (fboStatus != GL_FRAMEBUFFER_COMPLETE) { 222 throw new RuntimeException( 223 "Framebuffer is not complete; framebuffer status is " 224 + String.format("0x%08X", fboStatus)); 225 } 226 227 logger.debug( 228 "Created an OpenGL context with renderer '" 229 + gl.glGetString(GL_RENDERER) 230 + "', version '" 231 + gl.glGetString(GL.GL_VERSION) 232 + "', and profile '" 233 + profile 234 + "'"); 235 236 var fmt = GLBuffers.newDirectIntBuffer(1); 237 gl.glGetIntegerv(GLES3.GL_IMPLEMENTATION_COLOR_READ_FORMAT, fmt); 238 var type = GLBuffers.newDirectIntBuffer(1); 239 gl.glGetIntegerv(GLES3.GL_IMPLEMENTATION_COLOR_READ_TYPE, type); 240 241 // Tell OpenGL that the attribute in the vertex shader named position is bound to index 0 (the 242 // index for the generic position input) 243 gl.glBindAttribLocation(programId, 0, "position"); 244 245 // Compile and set up our two shaders with our program 246 final int vertexId = createShader(gl, programId, k_vertexShader, GL_VERTEX_SHADER); 247 final int fragmentId = createShader(gl, programId, k_fragmentShader, GL_FRAGMENT_SHADER); 248 249 // Link our program together and check for errors 250 gl.glLinkProgram(programId); 251 IntBuffer status = GLBuffers.newDirectIntBuffer(1); 252 gl.glGetProgramiv(programId, GL_LINK_STATUS, status); 253 if (status.get(0) == GL_FALSE) { 254 IntBuffer infoLogLength = GLBuffers.newDirectIntBuffer(1); 255 gl.glGetProgramiv(programId, GL_INFO_LOG_LENGTH, infoLogLength); 256 257 ByteBuffer bufferInfoLog = GLBuffers.newDirectByteBuffer(infoLogLength.get(0)); 258 gl.glGetProgramInfoLog(programId, infoLogLength.get(0), null, bufferInfoLog); 259 byte[] bytes = new byte[infoLogLength.get(0)]; 260 bufferInfoLog.get(bytes); 261 String strInfoLog = new String(bytes); 262 263 throw new RuntimeException("Linker failure: " + strInfoLog); 264 } 265 gl.glValidateProgram(programId); 266 267 // Cleanup shaders that are now compiled in 268 gl.glDetachShader(programId, vertexId); 269 gl.glDetachShader(programId, fragmentId); 270 gl.glDeleteShader(vertexId); 271 gl.glDeleteShader(fragmentId); 272 273 // Tell OpenGL to use our program 274 gl.glUseProgram(programId); 275 276 // Set up our texture 277 texture = new Texture(GL_TEXTURE_2D); 278 texture.setTexParameteri(gl, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 279 texture.setTexParameteri(gl, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 280 texture.setTexParameteri(gl, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 281 texture.setTexParameteri(gl, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 282 283 // Set up a uniform holding our image as a texture 284 textureUniformId = gl.glGetUniformLocation(programId, "texture0"); 285 gl.glUniform1i(textureUniformId, 0); 286 287 // Set up a uniform to hold image resolution 288 resolutionUniformId = gl.glGetUniformLocation(programId, "resolution"); 289 290 // Set up uniforms for the HSV thresholds 291 lowerUniformId = gl.glGetUniformLocation(programId, "lowerThresh"); 292 upperUniformId = gl.glGetUniformLocation(programId, "upperThresh"); 293 294 // Set up a quad that covers the entire screen so that our fragment shader draws onto the entire 295 // screen 296 gl.glGenBuffers(1, vertexVBOIds); 297 298 FloatBuffer vertexBuffer = GLBuffers.newDirectFloatBuffer(k_vertexPositions); 299 gl.glBindBuffer(GL_ARRAY_BUFFER, vertexVBOIds.get(0)); 300 gl.glBufferData( 301 GL_ARRAY_BUFFER, 302 (long) vertexBuffer.capacity() * Float.BYTES, 303 vertexBuffer, 304 GL_STATIC_DRAW); 305 306 // Set up pixel unpack buffer (a PBO to transfer image data to the GPU) 307 if (pboMode != PBOMode.NONE) { 308 gl.glGenBuffers(2, unpackPBOIds); 309 310 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(0)); 311 gl.glBufferData( 312 GLES3.GL_PIXEL_UNPACK_BUFFER, 313 k_startingHeight * k_startingWidth * 3, 314 null, 315 GLES3.GL_STREAM_DRAW); 316 if (pboMode == PBOMode.DOUBLE_BUFFERED) { 317 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(1)); 318 gl.glBufferData( 319 GLES3.GL_PIXEL_UNPACK_BUFFER, 320 k_startingHeight * k_startingWidth * 3, 321 null, 322 GLES3.GL_STREAM_DRAW); 323 } 324 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, 0); 325 } 326 327 // Set up pixel pack buffer (a PBO to transfer the processed image back from the GPU) 328 if (pboMode != PBOMode.NONE) { 329 gl.glGenBuffers(2, packPBOIds); 330 331 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(0)); 332 gl.glBufferData( 333 GLES3.GL_PIXEL_PACK_BUFFER, 334 k_startingHeight * k_startingWidth, 335 null, 336 GLES3.GL_STREAM_READ); 337 if (pboMode == PBOMode.DOUBLE_BUFFERED) { 338 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(1)); 339 gl.glBufferData( 340 GLES3.GL_PIXEL_PACK_BUFFER, 341 k_startingHeight * k_startingWidth, 342 null, 343 GLES3.GL_STREAM_READ); 344 } 345 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, 0); 346 } 347 } 348 349 private static int createShader(GL2ES2 gl, int programId, String glslCode, int shaderType) { 350 int shaderId = gl.glCreateShader(shaderType); 351 if (shaderId == 0) throw new RuntimeException("Shader ID is zero"); 352 353 IntBuffer length = GLBuffers.newDirectIntBuffer(new int[] {glslCode.length()}); 354 gl.glShaderSource(shaderId, 1, new String[] {glslCode}, length); 355 gl.glCompileShader(shaderId); 356 357 IntBuffer intBuffer = IntBuffer.allocate(1); 358 gl.glGetShaderiv(shaderId, GL_COMPILE_STATUS, intBuffer); 359 360 if (intBuffer.get(0) != 1) { 361 gl.glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, intBuffer); 362 int size = intBuffer.get(0); 363 if (size > 0) { 364 ByteBuffer byteBuffer = ByteBuffer.allocate(size); 365 gl.glGetShaderInfoLog(shaderId, size, intBuffer, byteBuffer); 366 System.err.println(new String(byteBuffer.array())); 367 } 368 throw new RuntimeException("Couldn't compile shader"); 369 } 370 371 gl.glAttachShader(programId, shaderId); 372 373 return shaderId; 374 } 375 376 @Override 377 protected Mat process(Mat in) { 378 if (in.width() != previousWidth && in.height() != previousHeight) { 379 logger.debug("Resizing OpenGL viewport, byte buffers, and PBOs"); 380 381 drawable.setSurfaceSize(in.width(), in.height()); 382 gl.glViewport(0, 0, in.width(), in.height()); 383 384 previousWidth = in.width(); 385 previousHeight = in.height(); 386 387 inputBytes = new byte[in.width() * in.height() * 3]; 388 389 outputBytes = new byte[in.width() * in.height()]; 390 outputMat = new Mat(k_startingHeight, k_startingWidth, CvType.CV_8UC1); 391 392 if (pboMode != PBOMode.NONE) { 393 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(0)); 394 gl.glBufferData( 395 GLES3.GL_PIXEL_PACK_BUFFER, 396 (long) in.width() * in.height(), 397 null, 398 GLES3.GL_STREAM_READ); 399 400 if (pboMode == PBOMode.DOUBLE_BUFFERED) { 401 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(1)); 402 gl.glBufferData( 403 GLES3.GL_PIXEL_PACK_BUFFER, 404 (long) in.width() * in.height(), 405 null, 406 GLES3.GL_STREAM_READ); 407 } 408 } 409 } 410 411 if (pboMode == PBOMode.DOUBLE_BUFFERED) { 412 unpackIndex = (unpackIndex + 1) % 2; 413 unpackNextIndex = (unpackIndex + 1) % 2; 414 } 415 416 // Reset the fullscreen quad 417 gl.glBindBuffer(GL_ARRAY_BUFFER, vertexVBOIds.get(0)); 418 gl.glEnableVertexAttribArray(k_positionVertexAttribute); 419 gl.glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0); 420 gl.glBindBuffer(GL_ARRAY_BUFFER, 0); 421 422 // Load and bind our image as a 2D texture 423 gl.glActiveTexture(GL_TEXTURE0); 424 texture.enable(gl); 425 texture.bind(gl); 426 427 // Load our image into the texture 428 in.get(0, 0, inputBytes); 429 if (pboMode == PBOMode.NONE) { 430 ByteBuffer buf = ByteBuffer.wrap(inputBytes); 431 // (We're actually taking in BGR even though this says RGB; it's much easier and faster to 432 // switch it around in the fragment shader) 433 texture.updateImage( 434 gl, 435 new TextureData( 436 profile, 437 GL_RGB8, 438 in.width(), 439 in.height(), 440 0, 441 GL_RGB, 442 GL_UNSIGNED_BYTE, 443 false, 444 false, 445 false, 446 buf, 447 null)); 448 } else { 449 // Bind the PBO to the texture 450 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(unpackIndex)); 451 452 // Copy pixels from the PBO to the texture object 453 gl.glTexSubImage2D( 454 GLES3.GL_TEXTURE_2D, 455 0, 456 0, 457 0, 458 in.width(), 459 in.height(), 460 GLES3.GL_RGB8, 461 GLES3.GL_UNSIGNED_BYTE, 462 0); 463 464 // Bind (potentially) another PBO to update the texture source 465 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, unpackPBOIds.get(unpackNextIndex)); 466 467 // This call with a nullptr for the data arg tells OpenGL *not* to wait to be in sync with the 468 // GPU 469 // This causes the previous data in the PBO to be discarded 470 gl.glBufferData( 471 GLES3.GL_PIXEL_UNPACK_BUFFER, 472 (long) in.width() * in.height() * 3, 473 null, 474 GLES3.GL_STREAM_DRAW); 475 476 // Map the buffer of GPU memory into a place that's accessible by us 477 var buf = 478 gl.glMapBufferRange( 479 GLES3.GL_PIXEL_UNPACK_BUFFER, 480 0, 481 (long) in.width() * in.height() * 3, 482 GLES3.GL_MAP_WRITE_BIT); 483 buf.put(inputBytes); 484 485 gl.glUnmapBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER); 486 gl.glBindBuffer(GLES3.GL_PIXEL_UNPACK_BUFFER, 0); 487 } 488 489 // Put values in a uniform holding the image resolution 490 gl.glUniform2f(resolutionUniformId, in.width(), in.height()); 491 492 // Put values in threshold uniforms 493 var lowr = params.getHsvLower().val; 494 var upr = params.getHsvUpper().val; 495 gl.glUniform3f(lowerUniformId, (float) lowr[0], (float) lowr[1], (float) lowr[2]); 496 gl.glUniform3f(upperUniformId, (float) upr[0], (float) upr[1], (float) upr[2]); 497 498 // Draw the fullscreen quad 499 gl.glDrawArrays(GL_TRIANGLE_STRIP, 0, k_vertexPositions.length); 500 501 // Cleanup 502 texture.disable(gl); 503 gl.glDisableVertexAttribArray(k_positionVertexAttribute); 504 gl.glUseProgram(0); 505 506 if (pboMode == PBOMode.NONE) { 507 return saveMatNoPBO(gl, in.width(), in.height()); 508 } else { 509 return saveMatPBO((GLES3) gl, in.width(), in.height(), pboMode == PBOMode.DOUBLE_BUFFERED); 510 } 511 } 512 513 private Mat saveMatNoPBO(GL2ES2 gl, int width, int height) { 514 ByteBuffer buffer = GLBuffers.newDirectByteBuffer(width * height); 515 // We use GL_RED/GL_ALPHA to get things in a single-channel format 516 // Note that which pixel format you use is *very* important to performance 517 // E.g. GL_ALPHA is super slow in this case 518 gl.glReadPixels(0, 0, width, height, outputFormat, GL_UNSIGNED_BYTE, buffer); 519 520 return new Mat(height, width, CvType.CV_8UC1, buffer); 521 } 522 523 private Mat saveMatPBO(GLES3 gl, int width, int height, boolean doubleBuffered) { 524 if (doubleBuffered) { 525 packIndex = (packIndex + 1) % 2; 526 packNextIndex = (packIndex + 1) % 2; 527 } 528 529 // Set the target framebuffer attachment to read 530 gl.glReadBuffer(GLES3.GL_COLOR_ATTACHMENT0); 531 532 // Read pixels from the framebuffer to the PBO 533 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(packIndex)); 534 // We use GL_RED (which is always supported in GLES3) to get things in a single-channel format 535 // Note that which pixel format you use is *very* important to performance 536 // E.g. GL_ALPHA is super slow in this case 537 gl.glReadPixels(0, 0, width, height, GLES3.GL_RED, GLES3.GL_UNSIGNED_BYTE, 0); 538 539 // Map the PBO into the CPU's memory 540 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, packPBOIds.get(packNextIndex)); 541 var buf = 542 gl.glMapBufferRange( 543 GLES3.GL_PIXEL_PACK_BUFFER, 0, (long) width * height, GLES3.GL_MAP_READ_BIT); 544 buf.get(outputBytes); 545 outputMat.put(0, 0, outputBytes); 546 gl.glUnmapBuffer(GLES3.GL_PIXEL_PACK_BUFFER); 547 gl.glBindBuffer(GLES3.GL_PIXEL_PACK_BUFFER, 0); 548 549 return outputMat; 550 } 551}