Read pixels from texture asynchronously

First time posting on the forums ^-^

I’m making a game where there are millions of instanced meshes with their positions being controlled by what’s effectively a compute shader Link if anyone’s interested in how that’s been set up

The issue I’m having is that texture.readPixels is really slow, I found an async alternative for reading pixels in the source code for the default engine engine.ts#L1772.

Sadly, I don’t know how to adopt this solution to reading from a texture, and also In my attempts at doing so, WebGL complains that RGBA and FLOAT are an invalid combination for readPixels.

I’ve found something about needing to bind the texture to COLOR_ATTACHMENT0 then call gl.readBuffer but I’m way far out of my depths.

If anyone has any ideas it would be greatly appreciated!

Welcome aboard!

Adding @sebavan, I don’t know if the method can be used.

[…] More info: Speeding up readPixels - #4 by sebavan

@Shahara_Seiun this method might still be slow but it would be async meaning cpu/gpu can fall out of sync easily.

The main question I would have is why do you need the read back on the CPU ? could you not reuse the data directly on GPU ?

I’m calculating the collision of objects and the player on the gpu (collision code hasn’t yet been pushed to the code I shared), I have to read it back to affect the game state. And speed is not really what I’m concerned about, I just need it to be non-blocking.

Perfect makes sense, so yup _readPixels should be ok but I am not sure it can work with float texture as I am not seeing any conformance tests related to floats.

The issue I’m having is that _readPixelsAsync reads pixels from the screen, Not sure how to change it to work with reading from a texture instead.

You would need to associate it with a framebuffer as readPixels would only work from a frame buffer.

I’m seeing that in the engine.readTexturePixels code, I just haven’t been able to get the code working for it. Do you have a pseudo code example for what webgl calls I’d need to make and in what order to end up with:

Texture → framebuffer
Framebuffer → readPixels
readPixels → PBO
Fencesync wait till PBO ready
Read from PBO

Or somewhere I can look for an example?

I’ve been googling this for days and have made little progress

Did it!

I’m very proud of myself

Code for anyone who comes across this issue themselves

const _clientWaitAsync = function (engine, sync, flags, interval_ms) {

    if (flags === void 0) { flags = 0; }

    if (interval_ms === void 0) { interval_ms = 10; }

    var gl = engine._gl;

    return new Promise(function (resolve, reject) {

        var check = function () {

            var res = gl.clientWaitSync(sync, flags, 0);

            if (res == gl.WAIT_FAILED) {

                reject();

                return;

            }

            if (res == gl.TIMEOUT_EXPIRED) {

                setTimeout(check, interval_ms);

                return;

            }

            resolve();

        };

        check();

    });

};

const _readTexturePixels = function (engine, texture, width, height, faceIndex, level, buffer) {

    if (faceIndex === void 0) { faceIndex = -1; }

    if (level === void 0) { level = 0; }

    if (buffer === void 0) { buffer = null; }

    var gl = engine._gl;

    if (!gl) {

        throw new Error("Engine does not have gl rendering context.");

    }

    if (!engine._dummyFramebuffer) {

        var dummy = gl.createFramebuffer();

        if (!dummy) {

            throw new Error("Unable to create dummy framebuffer");

        }

        engine._dummyFramebuffer = dummy;

    }

    gl.bindFramebuffer(gl.FRAMEBUFFER, engine._dummyFramebuffer);

    if (faceIndex > -1) {

        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + faceIndex, texture._webGLTexture, level);

    }

    else {

        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture._webGLTexture, level);

    }

    var readType = (texture.type !== undefined) ? engine._getWebGLTextureType(texture.type) : gl.UNSIGNED_BYTE;

    switch (readType) {

        case gl.UNSIGNED_BYTE:

            if (!buffer) {

                buffer = new Uint8Array(4 * width * height);

            }

            readType = gl.UNSIGNED_BYTE;

            break;

        default:

            if (!buffer) {

                buffer = new Float32Array(4 * width * height);

            }

            readType = gl.FLOAT;

            break;

    }

    var buf = gl.createBuffer();

    gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf);

    gl.bufferData(gl.PIXEL_PACK_BUFFER, buffer.byteLength, gl.STREAM_READ);

    gl.readPixels(0, 0, width, height, gl.RGBA, readType, 0);

    gl.bindFramebuffer(gl.FRAMEBUFFER, engine._currentFramebuffer);

    var sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);

    if (!sync) {

        return null;

    }

    gl.flush();

    return _clientWaitAsync(engine, sync, 0, 10).then(function () {

        gl.deleteSync(sync);

        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, buf);

        gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, buffer);

        gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);

        gl.deleteBuffer(buf);

        return buffer;

    });

};

const readPixels = function (texture, faceIndex, level, buffer) {

    if (faceIndex === void 0) { faceIndex = 0; }

    if (level === void 0) { level = 0; }

    if (buffer === void 0) { buffer = null; }

    if (!texture._texture) {

        return null;

    }

    var size = texture.getSize();

    var width = size.width;

    var height = size.height;

    var engine = texture._getEngine();

    if (!engine) {

        return null;

    }

    if (level != 0) {

        width = width / Math.pow(2, level);

        height = height / Math.pow(2, level);

        width = Math.round(width);

        height = Math.round(height);

    }

    try {

        if (texture._texture.isCube) {

            return _readTexturePixels(engine, texture._texture, width, height, faceIndex, level, buffer);

        }

        return _readTexturePixels(engine, texture._texture, width, height, -1, level, buffer);

    }

    catch (e) {

        console.log(e)

        return null;

    }

};
4 Likes

looks good !!!