Picker Framebuffer Issue with Camera Movement and Screen Resize in v1.68.0

Dear PlayCanvas Team,

After updating to PlayCanvas version 1.68.0, the picker framebuffer no longer consistently selects the correct entity when I click on it. This issue seems to occur after I move the camera or change the screen size. For example, when I attempt to select a capsule entity, the picker sometimes incorrectly identifies and selects a brick entity instead, even though the capsule was clearly clicked. This behavior is inconsistent, and the selected entity varies without apparent reason.

Steps to Reproduce:

  1. Update PlayCanvas project to version 1.68.0.
  2. Implement a basic picker framebuffer setup to select entities on click.
  3. Move the camera or change the screen size.
  4. Attempt to select an entity by clicking on it.
  5. Observe that the wrong entity is sometimes selected.

below is my project.
https://playcanvas.com/editor/scene/1950243

below is the link for the video
https://filetalk.jp/download.php?cd=RcwnW6TmPs

Dear PlayCanvas Team,

Any update in this topic. Please let me know if there is any solution on this topic.

Thank you in advance.

Hi … I have not looked at your project, but we have an example using it here:
https://playcanvas.github.io/#/graphics/area-picker

and I cannot reproduce the error by changing the screen size. The example calls this each frame to handle it, is this something you do in your code as well?

        // Make sure the picker is the right size, and prepare it, which renders meshes into its render target
        if (picker) {
            picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
            picker.prepare(camera.camera, app.scene, pickerLayers);
        }

Hello @mvaligursky , Thank you for the reply. below is my code.
it does not selects the correct entity when I click on it.



var PickerFramebuffer = pc.createScript('pickerFramebuffer');

PickerFramebuffer.attributes.add('pickAreaScale', {
    type: 'number',
    title: 'Pick Area Scale',
    description: '1 is more accurate but worse performance. 0.01 is best performance but least accurate. 0.25 is the default.',
    default: 0.25,
    min: 0.01,
    max: 1
});

PickerFramebuffer.attributes.add('layerNames', {
    type: 'string',
    title: 'Layers Names',
    array: true,
    description: 'Layer names from which objects will be picked from.',
    default: ['World']
});

// initialize code called once per entity
PickerFramebuffer.prototype.initialize = function () {
    // Create a frame buffer picker with a scaled resolution
    var canvas = this.app.graphicsDevice.canvas;
    var canvasWidth = parseInt(canvas.clientWidth, 10);
    var canvasHeight = parseInt(canvas.clientHeight, 10);

    this.picker = new pc.Picker(this.app, canvasWidth * this.pickAreaScale, canvasHeight * this.pickAreaScale);
    this.layers = [];
    for (var i = 0; i < this.layerNames.length; ++i) {
        var layer = this.app.scene.layers.getLayerByName(this.layerNames[i]);
        if (layer) {
            this.layers.push(layer);
        }
    }

    this.app.mouse.on(pc.EVENT_MOUSEDOWN, this.onSelect, this);

    this.on('destroy', function () {
        this.app.mouse.off(pc.EVENT_MOUSEDOWN, this.onSelect, this);
    }, this);
};

PickerFramebuffer.prototype.onSelect = function (event) {
    var canvas = this.app.graphicsDevice.canvas;
    var canvasWidth = parseInt(canvas.clientWidth, 10);
    var canvasHeight = parseInt(canvas.clientHeight, 10);

    var camera = this.entity.camera;
    var scene = this.app.scene;
    var picker = this.picker;

    if (picker) {
        picker.resize(canvasWidth * this.pickAreaScale, canvasHeight * this.pickAreaScale);
        picker.prepare(camera, scene, this.layers);
    }

    // Map the mouse coordinates into picker coordinates and 
    // query the selection
    var selected = picker.getSelection(
        Math.floor(event.x * (picker.width / canvasWidth)),
        picker.height - Math.floor(event.y * (picker.height / canvasHeight))
    );

    if (selected.length > 0) {
        // Get the graph node used by the selected mesh instance
        var entity = selected[0].node;
        console.log('selected entity :', entity);
    }
};

Can you log out what this gives you?
parseInt expects a string … but canvas.clientWidth is a number.

Have you found the issue?

I’m not certain if this is the most efficient approach, but it has proven effective for my needs.

// initialize code called once per entity
Picker.prototype.initialize = function () {
    // Initialize the picker component
    this.initPicker();

  const selectedEntity = this.handleEntityPicking(event);
};

Picker.prototype.initPicker = function () {
    try {
        // Define layer names
        this.layerNames = ['World', 'Front'];

        // Destructure canvas dimensions directly for picker initialization
        const { clientWidth, clientHeight } = this.app.graphicsDevice.canvas;

        // Initialize the picker with full canvas resolution
        this.picker = new pc.Picker(this.app, clientWidth, clientHeight);

        // Filter valid layers directly, logging warnings for missing layers
        this.layers = this.layerNames.reduce((validLayers, name) => {
            const layer = this.app.scene.layers.getLayerByName(name);
            if (!layer) {
                if (debugLevel > 0) console.log(`Layer with name ${name} not found.`);
                return validLayers;
            }
            validLayers.push(layer);
            return validLayers;
        }, []);

        // Handle canvas resize events to adjust picker dimensions accordingly
        this.app.graphicsDevice.on('resizecanvas', () => {
            const { canvas } = this.app.graphicsDevice;
            this.picker.resize(canvas.clientWidth, canvas.clientHeight);
        }, this);
    } catch (error) {
        if (debugLevel > 0) console.log('Error initializing picker:', error);
    }
};

Picker.prototype.handleEntityPicking = function (event) {
    // Simplify the assignment of camera and scene for readability
    const { camera } = this.entity;
    const { scene } = this.app;

    // Prepare the picker with necessary layers, if any
    this.picker.prepare(camera, scene, this.layers);

    // Get the selection based on the event's x and y coordinates
    const selection = this.picker.getSelection(event.x, event.y);
    // Check if there is a selection and the first item has a node
    if (selection && selection[0]?.node) {
        const { name, parent } = selection[0].node;

        // Log the selected entity's name and its parent
        //  if (debugLevel > 0) console.log(`Selected entity: ${name}`);
        //  if (debugLevel > 0) console.log(`Selected parent:`, parent);

        // Additional check to console if the selected entity has an object and has a node
        if (parent && parent.node) {
            if (debugLevel > 0) console.log('The selected entity\'s parent has a node');
        }
        return selection[0];

    } else {
        // Log when no entity is selected or the selected entity does not have a node
        if (debugLevel > 0) console.log('No valid selection or the selected entity does not have a node');
        // Return null when the selection doesn't meet the criteria
        return null;
    }
};