device.getRenderTarget always returns null

According to documentation, we can get renderTarget, but it always null.

https://developer.playcanvas.com/en/api/pc.GraphicsDevice.html#getRenderTarget

So my question is - how can I get render target? Camera has null too.
Okay, I can set it by hands, but is there a default one?

And, the second question. Is it acceptable to get colorBuffer from renderTarget and pass it into a shader as a texture. Will it slow down my performance a lot?

And how can I do that?

I can’t even lock inputTarget.colorBuffer in post-effect in order to get it’s pixels.

If I do

inputTarget.colorBuffer.lock();
inputTarget.colorBuffer.unlock();

in pc.PostEffect.prototype.render I get just a black screen. Bug?

Okay, I see, nobody loves me here =\

1 Like

I’d be glad to help but I don’t know about it, and I’m interested too to see a solution for this.

Thanks!

I guess, core team guys could help with that, but I think they are too busy with new features which were announced last week. Probably, this features even can solve my problem as well.

That’s what I hope for.

Looking at the documentation, if the renderTarget is null, than the backbuffer is the render target.

https://developer.playcanvas.com/en/api/pc.GraphicsDevice.html#setRenderTarget

What are you looking to do?

I want to get last frame as a texture. is it possible? How can I get backBuffer?

You would have to set the renderTarget as a new pc.RenderTarget, render the frame and unset it effectively.

The pc.Picker has an example of this in getSelection https://github.com/playcanvas/engine/blob/master/src/scene/pick.js#L90 (you may have to look at an older revision as GitHub has been updated to use the layer system)

Older revision below:

pc.extend(pc, function () {

    function sortDrawCalls(drawCallA, drawCallB) {

        if (drawCallA.layer === drawCallB.layer) {
            if (drawCallA.drawOrder && drawCallB.drawOrder) {
                return drawCallA.drawOrder - drawCallB.drawOrder;
            }
        }

        return drawCallB.key - drawCallA.key;
    }

    /**
     * @constructor
     * @name pc.Picker
     * @classdesc Picker object used to select mesh instances from screen coordinates.
     * @description Create a new instance of a Picker object
     * @param {pc.GraphicsDevice} device Graphics device used to manage internal graphics resources.
     * @param {Number} width The width of the pick buffer in pixels.
     * @param {Number} height The height of the pick buffer in pixels.
     * @property {Number} width Width of the pick buffer in pixels (read-only).
     * @property {Number} height Height of the pick buffer in pixels (read-only).
     * @property {pc.RenderTarget} renderTarget The render target used by the picker internally (read-only).
     */
    var Picker = function(device, width, height) {
        this.device = device;
        this.library = device.getProgramLibrary();

        this.pickColor = new Float32Array(4);

        this.scene = null;
        this.drawCalls = [ ];

        this.clearOptions = {
            color: [1, 1, 1, 1],
            depth: 1,
            flags: pc.CLEARFLAG_COLOR | pc.CLEARFLAG_DEPTH
        };
        this.resize(width, height);

        this._ignoreOpacityFor = null; // meshInstance
    };

    /**
     * @function
     * @name pc.Picker#getSelection
     * @description Return the list of mesh instances selected by the specified rectangle in the
     * previously prepared pick buffer.The rectangle using top-left coordinate system.
     * @param {Number} x The left edge of the rectangle
     * @param {Number} y The top edge of the rectangle
     * @param {Number} [width] The width of the rectangle
     * @param {Number} [height] The height of the rectangle
     * @returns {pc.MeshInstance[]} An array of mesh instances that are in the selection
     * @example
     * // Get the selection at the point (10,20)
     * var selection = picker.getSelection(10, 20);
     *
     * // Get all models in rectangle with corners at (10,20) and (20,40)
     * var selection = picker.getSelection(10, 20, 10, 20);
     */
    Picker.prototype.getSelection = function (x, y, width, height) {
        var device = this.device;

        if (typeof x === 'object') {
            // #ifdef DEBUG
            console.warn("Picker.getSelection:param 'rect' is deprecated, use 'x, y, width, height' instead.");
            // #endif

            var rect = x;
            x = rect.x;
            y = rect.y;
            width = rect.width;
            height = rect.height;
        } else {
            y = this._pickBufferTarget.height - (y + (height || 1));
        }

        width = width || 1;
        height = height || 1;

        // Cache active render target
        var prevRenderTarget = device.renderTarget;

        // Ready the device for rendering to the pick buffer
        device.setRenderTarget(this._pickBufferTarget);
        device.updateBegin();

        var pixels = new Uint8Array(4 * width * height);
        device.readPixels(x, y, width, height, pixels);

        device.updateEnd();

        // Restore render target
        device.setRenderTarget(prevRenderTarget);

        var selection = [];

        for (var i = 0; i < width * height; i++) {
            var r = pixels[4 * i + 0];
            var g = pixels[4 * i + 1];
            var b = pixels[4 * i + 2];
            var index = r << 16 | g << 8 | b;
            // White is 'no selection'
            if (index !== 0xffffff) {
                var selectedMeshInstance = this.drawCalls[index];
                if (selection.indexOf(selectedMeshInstance) === -1) {
                    selection.push(selectedMeshInstance);
                }
            }
        }

        return selection;
    };

    /**
     * @function
     * @name pc.Picker#prepare
     * @description Primes the pick buffer with a rendering of the specified models from the point of view
     * of the supplied camera. Once the pick buffer has been prepared, pc.Picker#getSelection can be
     * called multiple times on the same picker object. Therefore, if the models or camera do not change
     * in any way, pc.Picker#prepare does not need to be called again.
     * @param {pc.Camera} camera The camera used to render the scene, note this is the CameraNode, not an Entity
     * @param {pc.Scene} scene The scene containing the pickable mesh instances.
     */
    Picker.prototype.prepare = function (camera, scene) {
        var device = this.device;

        this.scene = scene;

        // Cache active render target
        var prevRenderTarget = device.renderTarget;

        // Ready the device for rendering to the pick buffer
        device.setRenderTarget(this._pickBufferTarget);
        device.updateBegin();
        device.setViewport(0, 0, this._pickBufferTarget.width, this._pickBufferTarget.height);
        device.setScissor(0, 0, this._pickBufferTarget.width, this._pickBufferTarget.height);
        device.clear(this.clearOptions);

        // Build mesh instance list (ideally done by visibility query)
        var i;
        var mesh, meshInstance, material;
        var type;
        var shader;
        var scope = device.scope;
        var modelMatrixId = scope.resolve('matrix_model');
        var boneTextureId = scope.resolve('texture_poseMap');
        var boneTextureSizeId = scope.resolve('texture_poseMapSize');
        var poseMatrixId = scope.resolve('matrix_pose[0]');
        var pickColorId = scope.resolve('uColor');
        var projId = scope.resolve('matrix_projection');
        var viewProjId = scope.resolve('matrix_viewProjection');
        var opacityMapId = scope.resolve('texture_opacityMap');
        var alphaTestId = scope.resolve('alpha_ref');
        var nineOuterScaleId = scope.resolve('outerScale');

        var wtm = camera._node.getWorldTransform();
        var projMat = camera.getProjectionMatrix();
        var viewMat = wtm.clone().invert();
        var viewProjMat = new pc.Mat4();
        viewProjMat.mul2(projMat, viewMat);

        projId.setValue(projMat.data);
        viewProjId.setValue(viewProjMat.data);

        // copy scene drawCalls
        this.drawCalls = scene.drawCalls.slice(0);
        // sort same as forward renderer
        this.drawCalls.sort(sortDrawCalls);

        for (i = 0; i < this.drawCalls.length; i++) {
            if (this.drawCalls[i].command) {
                this.drawCalls[i].command();
            } else {
                if (!this.drawCalls[i].pick) continue;
                meshInstance = this.drawCalls[i];
                mesh = meshInstance.mesh;
                if (! mesh) continue;
                material = meshInstance.material;

                type = mesh.primitive[pc.RENDERSTYLE_SOLID].type;
                var isSolid = (type === pc.PRIMITIVE_TRIANGLES) || (type === pc.PRIMITIVE_TRISTRIP) || (type === pc.PRIMITIVE_TRIFAN);
                var isPickable = (material instanceof pc.StandardMaterial) || (material instanceof pc.BasicMaterial);
                if (isSolid && isPickable) {

                    device.setBlending(false);
                    device.setCullMode(material.cull);
                    device.setDepthWrite(material.depthWrite);
                    device.setDepthTest(material.depthTest);

                    modelMatrixId.setValue(meshInstance.node.worldTransform.data);
                    if (meshInstance.skinInstance) {
                        if (device.supportsBoneTextures) {
                            boneTextureId.setValue(meshInstance.skinInstance.boneTexture);
                            var w = meshInstance.skinInstance.boneTexture.width;
                            var h = meshInstance.skinInstance.boneTexture.height;
                            boneTextureSizeId.setValue([w, h]);
                        } else {
                            poseMatrixId.setValue(meshInstance.skinInstance.matrixPalette);
                        }
                    }

                    if (material.opacityMap) {
                        opacityMapId.setValue(material.opacityMap);
                        alphaTestId.setValue(meshInstance===this._ignoreOpacityFor? 0 : material.alphaTest);
                    }

                    if (meshInstance.nineSlice) {
                        nineOuterScaleId.setValue(meshInstance.parameters.outerScale.data);
                    }

                    this.pickColor[0] = ((i >> 16) & 0xff) / 255;
                    this.pickColor[1] = ((i >> 8) & 0xff) / 255;
                    this.pickColor[2] = (i & 0xff) / 255;
                    this.pickColor[3] = 1;
                    pickColorId.setValue(this.pickColor);

                    shader = meshInstance._shader[pc.SHADER_PICK];
                    if (!shader) {
                        shader = this.library.getProgram('pick', {
                                skin: !!meshInstance.skinInstance,
                                screenSpace: meshInstance.screenSpace,
                                nineSlice: meshInstance.nineSlice,
                                opacityMap: !!material.opacityMap,
                                opacityChannel: material.opacityMap? (material.opacityMapChannel || 'r') : null
                            });
                        meshInstance._shader[pc.SHADER_PICK] = shader;
                    }
                    device.setShader(shader);

                    device.setVertexBuffer((meshInstance.morphInstance && meshInstance.morphInstance._vertexBuffer) ?
                        meshInstance.morphInstance._vertexBuffer : mesh.vertexBuffer, 0);
                    device.setIndexBuffer(mesh.indexBuffer[pc.RENDERSTYLE_SOLID]);
                    device.draw(mesh.primitive[pc.RENDERSTYLE_SOLID]);
                }
            }
        }

        device.setViewport(0, 0, device.width, device.height);
        device.setScissor(0, 0, device.width, device.height);
        device.updateEnd();

        // Restore render target
        device.setRenderTarget(prevRenderTarget);
    };

    /**
     * @function
     * @name pc.Picker#resize
     * @description Sets the resolution of the pick buffer. The pick buffer resolution does not need
     * to match the resolution of the corresponding frame buffer use for general rendering of the
     * 3D scene. However, the lower the resolution of the pick buffer, the less accurate the selection
     * results returned by pc.Picker#getSelection. On the other hand, smaller pick buffers will
     * yield greater performance, so there is a trade off.
     * @param {Number} width The width of the pick buffer in pixels.
     * @param {Number} height The height of the pick buffer in pixels.
     */
    Picker.prototype.resize = function (width, height) {
        var colorBuffer = new pc.Texture(this.device, {
            format: pc.PIXELFORMAT_R8_G8_B8_A8,
            width: width,
            height: height,
            mipmaps: false,
            minFilter: pc.FILTER_NEAREST,
            magFilter: pc.FILTER_NEAREST
        });
        this._pickBufferTarget = new pc.RenderTarget(this.device, colorBuffer, { depth: true });
    };

    Object.defineProperty(Picker.prototype, 'renderTarget', {
        get: function() { return this._pickBufferTarget; }
    });

    Object.defineProperty(Picker.prototype, 'width', {
        get: function() { return this._pickBufferTarget.width; }
    });

    Object.defineProperty(Picker.prototype, 'height', {
        get: function() { return this._pickBufferTarget.height; }
    });

    return {
        Picker: Picker
    };
}());

Yes, but it renders scene one more time. At least, specified objects.

But I think it should be possible to get already rendered scene. It sounds quite simple - just copy texture from renderTarget before clean up and that’s all.

I can get colorBuffer (that texture) in post-effect. But when I lock-unlock it, screen gets black. I don’t know why.

There is another method where you set the flag for preserving the drawing buffer in rendering options. Project example of capturing a screenshot here: https://developer.playcanvas.com/en/tutorials/capturing-a-screenshot/

This may have an impact on performance though.

Nope, doesn’t help.

I found a dirty way to do that directly via webGL render context.


var pixels = null;

var gl = this.app.graphicsDevice.gl;
gl.readPixels(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
    
if (pixels) {
    var pixels = this.transparentTexture.lock();
    pixels.set(this.pixels);
    this.transparentTexture.unlock();
}

But I guess it harms performance a lot, maybe even more than double render… I don’t know how can I check it?

Anyway, Im gonna wait for pc.Layer because now I can’t control draw order.

My idea was to render whole scene without one mesh. Then get current renderBuffer as a texture and pass it into a shader, and then render this left mesh.