Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -168,15 +168,20 @@ assetListLoader.load(() => {
const pickerScale = 0.25;
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);

// render the ID texture
// render the ID texture — scissor to a single pixel around the click so only that
// fragment is rasterized into the pick buffer
const worldLayer = app.scene.layers.getLayerByName('World');
picker.prepare(camera.camera, app.scene, [worldLayer]);
const px = x * pickerScale;
const py = y * pickerScale;
picker.prepare(camera.camera, app.scene, [worldLayer], {
x: px, y: py, width: 1, height: 1
});

// get the world position at the clicked point
picker.getWorldPointAsync(x * pickerScale, y * pickerScale).then((worldPoint) => {
picker.getWorldPointAsync(px, py).then((worldPoint) => {
if (worldPoint) {
// get the meshInstance of the picked object
picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((meshInstances) => {
picker.getSelectionAsync(px, py, 1, 1).then((meshInstances) => {

if (meshInstances.length > 0) {
const meshInstance = meshInstances[0];
Expand Down
22 changes: 9 additions & 13 deletions examples/src/examples/gaussian-splatting/paint.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,6 @@ assetListLoader.load(() => {
// Paint state
let isPainting = false;

// Track if picker needs re-preparation (after camera moves)
let pickerDirty = true;

// Disable context menu for RMB
app.mouse.disableContextMenu();

Expand All @@ -266,13 +263,13 @@ assetListLoader.load(() => {
const picker = new pc.Picker(app, 1, 1, true);
const worldLayer = app.scene.layers.getLayerByName('World');

// Prepare picker (re-prepare when camera moves)
const preparePicker = () => {
if (pickerDirty) {
picker.resize(canvas.clientWidth, canvas.clientHeight);
picker.prepare(camera.camera, app.scene, [worldLayer]);
pickerDirty = false;
}
// Prepare picker for a single pixel at the brush position. Scissoring to 1x1 keeps the
// pick render trivially small even when called every mousemove during a paint drag.
const preparePicker = (x, y) => {
picker.resize(canvas.clientWidth, canvas.clientHeight);
picker.prepare(camera.camera, app.scene, [worldLayer], {
x: x, y: y, width: 1, height: 1
});
};

// Pending paint requests - processed in update loop for consistent frame timing
Expand Down Expand Up @@ -306,8 +303,8 @@ assetListLoader.load(() => {

// Request paint at a specific screen position - queues for processing in update loop
const paintAt = (x, y) => {
// Prepare picker if needed (after camera moved)
preparePicker();
// Re-prepare each call so the 1x1 scissor follows the brush
preparePicker(x, y);

// Get world position for the paint brush
picker.getWorldPointAsync(x, y).then((worldPoint) => {
Expand All @@ -324,7 +321,6 @@ assetListLoader.load(() => {
app.mouse.on(pc.EVENT_MOUSEDOWN, (e) => {
if (e.button === pc.MOUSEBUTTON_RIGHT) {
isPainting = true;
pickerDirty = true;
orbitInput.enabled = false;
orbitInput.panButtonDown = false; // Cancel pan that orbit-camera started
paintAt(e.x, e.y);
Expand Down
13 changes: 9 additions & 4 deletions examples/src/examples/gaussian-splatting/picking.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -185,15 +185,20 @@ assetListLoader.load(() => {
const pickerScale = 0.25;
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);

// render the ID texture
// render the ID texture — scissor to a single pixel around the click so only that
// fragment is rasterized into the pick buffer
const worldLayer = app.scene.layers.getLayerByName('World');
picker.prepare(camera.camera, app.scene, [worldLayer]);
const px = x * pickerScale;
const py = y * pickerScale;
picker.prepare(camera.camera, app.scene, [worldLayer], {
x: px, y: py, width: 1, height: 1
});

// get the world position at the clicked point
picker.getWorldPointAsync(x * pickerScale, y * pickerScale).then((worldPoint) => {
picker.getWorldPointAsync(px, py).then((worldPoint) => {
if (worldPoint) {
// get the meshInstance of the picked object
picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((meshInstances) => {
picker.getSelectionAsync(px, py, 1, 1).then((meshInstances) => {

if (meshInstances.length > 0) {
// Unified mode: picker returns the GSplatComponent directly
Expand Down
39 changes: 33 additions & 6 deletions examples/src/examples/graphics/area-picker.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,6 @@ assetListLoader.load(() => {
camera.setLocalPosition(40 * Math.sin(time), 0, 40 * Math.cos(time));
camera.lookAt(pc.Vec3.ZERO);

// 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);
}

// areas we want to sample - two larger rectangles, one small square, and one pixel at a mouse position
// assign them different highlight colors as well
const areas = [
Expand All @@ -252,6 +246,39 @@ assetListLoader.load(() => {
}
];

// compute the union bounding rect of all query areas (in pick-buffer pixels) and use it
// as the picker scissor so the GPU only rasterizes fragments that will actually be read.
// include any pending click point so getWorldPointAsync can still read a valid pixel.
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (let a = 0; a < areas.length; a++) {
const ap = areas[a].pos;
const asz = areas[a].size;
minX = Math.min(minX, ap.x);
minY = Math.min(minY, ap.y);
maxX = Math.max(maxX, ap.x + asz.x);
maxY = Math.max(maxY, ap.y + asz.y);
}
if (pendingPickRequest) {
const cx = pendingPickRequest.x / pickerScale;
const cy = pendingPickRequest.y / pickerScale;
minX = Math.min(minX, cx);
minY = Math.min(minY, cy);
maxX = Math.max(maxX, cx + 1);
maxY = Math.max(maxY, cy + 1);
}
const scissor = {
x: minX * pickerScale,
y: minY * pickerScale,
width: (maxX - minX) * pickerScale,
height: (maxY - minY) * pickerScale
};

// 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, scissor);
}

// process all areas every frame
const promises = [];
for (let a = 0; a < areas.length; a++) {
Expand Down
15 changes: 14 additions & 1 deletion examples/src/examples/misc/editor.controls.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as pc from 'playcanvas';
* @returns {JSX.Element} The returned JSX Element.
*/
export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
const { BindingTwoWay, LabelGroup, Panel, SliderInput, SelectInput, ColorPicker } = ReactPCUI;
const { BindingTwoWay, BooleanInput, LabelGroup, Panel, SliderInput, SelectInput, ColorPicker } = ReactPCUI;
const { useState } = React;

const [type, setType] = useState('translate');
Expand Down Expand Up @@ -215,6 +215,19 @@ export const controls = ({ observer, ReactPCUI, React, jsx, fragment }) => {
max: 200
})
)
),
jsx(
Panel,
{ headerText: 'Picking' },
jsx(
LabelGroup,
{ text: 'Show Pick Point' },
jsx(BooleanInput, {
type: 'toggle',
binding: new BindingTwoWay(),
link: { observer, path: 'picking.showAxes' }
})
)
)
);
};
38 changes: 38 additions & 0 deletions examples/src/examples/misc/editor.example.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ setGizmoControls();
// view cube
const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0));
viewCube.dom.style.margin = '20px';
data.set('picking', {
showAxes: false
});
data.set('viewCube', {
colorX: Object.values(viewCube.colorX),
colorY: Object.values(viewCube.colorY),
Expand All @@ -212,6 +215,21 @@ app.on('prerender', () => {
viewCube.update(camera.getWorldTransform());
});

// pick hit visualization — three orthogonal axes anchored at the clicked surface point.
// PlayCanvas is Y-up, so the derived surface normal is the local +Y (green) axis.
// Tangent and bitangent become local +X (red) and local +Z (blue) respectively.
/** @type {{ point: pc.Vec3 | null, normal: pc.Vec3 }} */
const pickHit = {
point: null,
normal: new pc.Vec3()
};
const pickTangent = new pc.Vec3();
const pickBitangent = new pc.Vec3();
const pickTip = new pc.Vec3();
const worldUp = new pc.Vec3(0, 1, 0);
const worldRight = new pc.Vec3(1, 0, 0);
const AXIS_LEN = 1.0;

// selector
const layers = app.scene.layers;
const selector = new Selector(app, camera.camera, [layers.getLayerByName('World')]);
Expand All @@ -230,6 +248,26 @@ selector.on('deselect', () => {
}
gizmoHandler.clear();
outlineRenderer.removeAllEntities();
pickHit.point = null;
});
selector.on('pick', (/** @type {pc.Vec3} */ point, /** @type {pc.Vec3} */ normal) => {
pickHit.point = (pickHit.point ?? new pc.Vec3()).copy(point);
pickHit.normal.copy(normal);
});
app.on('prerender', () => {
if (!pickHit.point || !data.get('picking.showAxes')) return;
const p = pickHit.point;
const n = pickHit.normal;

// choose a reference vector not (anti)parallel to the normal, then build a right-handed
// orthonormal basis with X = tangent, Y = normal, Z = bitangent = cross(X, Y)
const ref = Math.abs(n.y) < 0.99 ? worldUp : worldRight;
pickTangent.cross(ref, n).normalize();
pickBitangent.cross(pickTangent, n).normalize();

app.drawLine(p, pickTip.copy(pickTangent).mulScalar(AXIS_LEN).add(p), pc.Color.RED);
app.drawLine(p, pickTip.copy(n).mulScalar(AXIS_LEN).add(p), pc.Color.GREEN);
app.drawLine(p, pickTip.copy(pickBitangent).mulScalar(AXIS_LEN).add(p), pc.Color.BLUE);
});

// ensure canvas is resized when window changes size + keep gizmo size consistent to canvas size
Expand Down
23 changes: 19 additions & 4 deletions examples/src/examples/misc/editor.selector.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ class Selector extends pc.EventHandler {
this._camera = camera;
this._scene = app.scene;
const device = app.graphicsDevice;
this._picker = new pc.Picker(app, device.canvas.width, device.canvas.height);
// depth enabled so we can also recover the world point + surface normal at the click
this._picker = new pc.Picker(app, device.canvas.width, device.canvas.height, true);
this._layers = layers;

this._onPointerDown = this._onPointerDown.bind(this);
Expand Down Expand Up @@ -70,16 +71,30 @@ class Selector extends pc.EventHandler {

const device = this._picker.device;
this._picker.resize(device.canvas.clientWidth, device.canvas.clientHeight);
this._picker.prepare(this._camera, this._scene, this._layers);

const selection = await this._picker.getSelectionAsync(e.clientX - 1, e.clientY - 1, 2, 2);
// scissor the render to a 2x2 rect around the click so only those fragments rasterize
const px = e.clientX - 1;
const py = e.clientY - 1;
this._picker.prepare(this._camera, this._scene, this._layers, {
x: px, y: py, width: 2, height: 2
});

// run selection and normal queries in parallel — both read the same prepared pick buffer
const [selection, hit] = await Promise.all([
this._picker.getSelectionAsync(px, py, 2, 2),
this._picker.getWorldPointAndNormalAsync(px, py)
]);

if (!selection[0]) {
this.fire('deselect');
return;
}

this.fire('select', selection[0].node, !e.ctrlKey && !e.metaKey);
const clear = !e.ctrlKey && !e.metaKey;
this.fire('select', selection[0].node, clear);
if (hit) {
this.fire('pick', hit.point, hit.normal, selection[0].node, clear);
}
}

bind() {
Expand Down
Loading