Skip to content

Commit bf1905e

Browse files
Add enhancements to picker, adding scissor command for optimisation; added support for getting normals from pick point; updated examples; added pick point to editor example
1 parent d78ee4b commit bf1905e

9 files changed

Lines changed: 294 additions & 53 deletions

File tree

examples/src/examples/gaussian-splatting-legacy/picking.example.mjs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,20 @@ assetListLoader.load(() => {
169169
const pickerScale = 0.25;
170170
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
171171

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

176181
// get the world position at the clicked point
177-
picker.getWorldPointAsync(x * pickerScale, y * pickerScale).then((worldPoint) => {
182+
picker.getWorldPointAsync(px, py).then((worldPoint) => {
178183
if (worldPoint) {
179184
// get the meshInstance of the picked object
180-
picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((meshInstances) => {
185+
picker.getSelectionAsync(px, py, 1, 1).then((meshInstances) => {
181186

182187
if (meshInstances.length > 0) {
183188
const meshInstance = meshInstances[0];

examples/src/examples/gaussian-splatting/paint.example.mjs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,6 @@ assetListLoader.load(() => {
243243
// Paint state
244244
let isPainting = false;
245245

246-
// Track if picker needs re-preparation (after camera moves)
247-
let pickerDirty = true;
248-
249246
// Disable context menu for RMB
250247
app.mouse.disableContextMenu();
251248

@@ -270,13 +267,13 @@ assetListLoader.load(() => {
270267
const picker = new pc.Picker(app, 1, 1, true);
271268
const worldLayer = app.scene.layers.getLayerByName('World');
272269

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

282279
// Pending paint requests - processed in update loop for consistent frame timing
@@ -310,8 +307,8 @@ assetListLoader.load(() => {
310307

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

316313
// Get world position for the paint brush
317314
picker.getWorldPointAsync(x, y).then((worldPoint) => {
@@ -328,7 +325,6 @@ assetListLoader.load(() => {
328325
app.mouse.on(pc.EVENT_MOUSEDOWN, (e) => {
329326
if (e.button === pc.MOUSEBUTTON_RIGHT) {
330327
isPainting = true;
331-
pickerDirty = true;
332328
orbitInput.enabled = false;
333329
orbitInput.panButtonDown = false; // Cancel pan that orbit-camera started
334330
paintAt(e.x, e.y);

examples/src/examples/gaussian-splatting/picking.example.mjs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,20 @@ assetListLoader.load(() => {
184184
const pickerScale = 0.25;
185185
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
186186

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

191196
// get the world position at the clicked point
192-
picker.getWorldPointAsync(x * pickerScale, y * pickerScale).then((worldPoint) => {
197+
picker.getWorldPointAsync(px, py).then((worldPoint) => {
193198
if (worldPoint) {
194199
// get the meshInstance of the picked object
195-
picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((meshInstances) => {
200+
picker.getSelectionAsync(px, py, 1, 1).then((meshInstances) => {
196201

197202
if (meshInstances.length > 0) {
198203
// Unified mode: picker returns the GSplatComponent directly

examples/src/examples/graphics/area-picker.example.mjs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -220,12 +220,6 @@ assetListLoader.load(() => {
220220
camera.setLocalPosition(40 * Math.sin(time), 0, 40 * Math.cos(time));
221221
camera.lookAt(pc.Vec3.ZERO);
222222

223-
// Make sure the picker is the right size, and prepare it, which renders meshes into its render target
224-
if (picker) {
225-
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
226-
picker.prepare(camera.camera, app.scene, pickerLayers);
227-
}
228-
229223
// areas we want to sample - two larger rectangles, one small square, and one pixel at a mouse position
230224
// assign them different highlight colors as well
231225
const areas = [
@@ -252,6 +246,39 @@ assetListLoader.load(() => {
252246
}
253247
];
254248

249+
// compute the union bounding rect of all query areas (in pick-buffer pixels) and use it
250+
// as the picker scissor so the GPU only rasterizes fragments that will actually be read.
251+
// include any pending click point so getWorldPointAsync can still read a valid pixel.
252+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
253+
for (let a = 0; a < areas.length; a++) {
254+
const ap = areas[a].pos;
255+
const asz = areas[a].size;
256+
minX = Math.min(minX, ap.x);
257+
minY = Math.min(minY, ap.y);
258+
maxX = Math.max(maxX, ap.x + asz.x);
259+
maxY = Math.max(maxY, ap.y + asz.y);
260+
}
261+
if (pendingPickRequest) {
262+
const cx = pendingPickRequest.x / pickerScale;
263+
const cy = pendingPickRequest.y / pickerScale;
264+
minX = Math.min(minX, cx);
265+
minY = Math.min(minY, cy);
266+
maxX = Math.max(maxX, cx + 1);
267+
maxY = Math.max(maxY, cy + 1);
268+
}
269+
const scissor = {
270+
x: minX * pickerScale,
271+
y: minY * pickerScale,
272+
width: (maxX - minX) * pickerScale,
273+
height: (maxY - minY) * pickerScale
274+
};
275+
276+
// Make sure the picker is the right size, and prepare it, which renders meshes into its render target
277+
if (picker) {
278+
picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
279+
picker.prepare(camera.camera, app.scene, pickerLayers, scissor);
280+
}
281+
255282
// process all areas every frame
256283
const promises = [];
257284
for (let a = 0; a < areas.length; a++) {

examples/src/examples/misc/editor.controls.jsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
BindingTwoWay,
3+
BooleanInput,
34
LabelGroup,
45
Panel,
56
SliderInput,
@@ -191,6 +192,15 @@ export function Controls({ observer }) {
191192
/>
192193
</LabelGroup>
193194
</Panel>
195+
<Panel headerText='Picking'>
196+
<LabelGroup text='Show Pick Point'>
197+
<BooleanInput
198+
type='toggle'
199+
binding={new BindingTwoWay()}
200+
link={{ observer, path: 'picking.showAxes' }}
201+
/>
202+
</LabelGroup>
203+
</Panel>
194204
</>
195205
);
196206
}

examples/src/examples/misc/editor.example.mjs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ setGizmoControls();
184184
// view cube
185185
const viewCube = new pc.ViewCube(new pc.Vec4(0, 1, 1, 0));
186186
viewCube.dom.style.margin = '20px';
187+
data.set('picking', {
188+
showAxes: false
189+
});
187190
data.set('viewCube', {
188191
colorX: Object.values(viewCube.colorX),
189192
colorY: Object.values(viewCube.colorY),
@@ -207,6 +210,21 @@ app.on('prerender', () => {
207210
viewCube.update(camera.getWorldTransform());
208211
});
209212

213+
// pick hit visualization — three orthogonal axes anchored at the clicked surface point.
214+
// PlayCanvas is Y-up, so the derived surface normal is the local +Y (green) axis.
215+
// Tangent and bitangent become local +X (red) and local +Z (blue) respectively.
216+
/** @type {{ point: pc.Vec3 | null, normal: pc.Vec3 }} */
217+
const pickHit = {
218+
point: null,
219+
normal: new pc.Vec3()
220+
};
221+
const pickTangent = new pc.Vec3();
222+
const pickBitangent = new pc.Vec3();
223+
const pickTip = new pc.Vec3();
224+
const worldUp = new pc.Vec3(0, 1, 0);
225+
const worldRight = new pc.Vec3(1, 0, 0);
226+
const AXIS_LEN = 1.0;
227+
210228
// selector
211229
const layers = app.scene.layers;
212230
const selector = new Selector(app, camera.camera, [layers.getLayerByName('World')]);
@@ -225,6 +243,26 @@ selector.on('deselect', () => {
225243
}
226244
gizmoHandler.clear();
227245
outlineRenderer.removeAllEntities();
246+
pickHit.point = null;
247+
});
248+
selector.on('pick', (/** @type {pc.Vec3} */ point, /** @type {pc.Vec3} */ normal) => {
249+
pickHit.point = (pickHit.point ?? new pc.Vec3()).copy(point);
250+
pickHit.normal.copy(normal);
251+
});
252+
app.on('prerender', () => {
253+
if (!pickHit.point || !data.get('picking.showAxes')) return;
254+
const p = pickHit.point;
255+
const n = pickHit.normal;
256+
257+
// choose a reference vector not (anti)parallel to the normal, then build a right-handed
258+
// orthonormal basis with X = tangent, Y = normal, Z = bitangent = cross(X, Y)
259+
const ref = Math.abs(n.y) < 0.99 ? worldUp : worldRight;
260+
pickTangent.cross(ref, n).normalize();
261+
pickBitangent.cross(pickTangent, n).normalize();
262+
263+
app.drawLine(p, pickTip.copy(pickTangent).mulScalar(AXIS_LEN).add(p), pc.Color.RED);
264+
app.drawLine(p, pickTip.copy(n).mulScalar(AXIS_LEN).add(p), pc.Color.GREEN);
265+
app.drawLine(p, pickTip.copy(pickBitangent).mulScalar(AXIS_LEN).add(p), pc.Color.BLUE);
228266
});
229267

230268
// ensure canvas is resized when window changes size + keep gizmo size consistent to canvas size

examples/src/examples/misc/editor.selector.mjs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ class Selector extends pc.EventHandler {
4242
this._camera = camera;
4343
this._scene = app.scene;
4444
const device = app.graphicsDevice;
45-
this._picker = new pc.Picker(app, device.canvas.width, device.canvas.height);
45+
// depth enabled so we can also recover the world point + surface normal at the click
46+
this._picker = new pc.Picker(app, device.canvas.width, device.canvas.height, true);
4647
this._layers = layers;
4748

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

7172
const device = this._picker.device;
7273
this._picker.resize(device.canvas.clientWidth, device.canvas.clientHeight);
73-
this._picker.prepare(this._camera, this._scene, this._layers);
7474

75-
const selection = await this._picker.getSelectionAsync(e.clientX - 1, e.clientY - 1, 2, 2);
75+
// scissor the render to a 2x2 rect around the click so only those fragments rasterize
76+
const px = e.clientX - 1;
77+
const py = e.clientY - 1;
78+
this._picker.prepare(this._camera, this._scene, this._layers, {
79+
x: px, y: py, width: 2, height: 2
80+
});
81+
82+
// run selection and normal queries in parallel — both read the same prepared pick buffer
83+
const [selection, hit] = await Promise.all([
84+
this._picker.getSelectionAsync(px, py, 2, 2),
85+
this._picker.getWorldPointAndNormalAsync(px, py)
86+
]);
7687

7788
if (!selection[0]) {
7889
this.fire('deselect');
7990
return;
8091
}
8192

82-
this.fire('select', selection[0].node, !e.ctrlKey && !e.metaKey);
93+
const clear = !e.ctrlKey && !e.metaKey;
94+
this.fire('select', selection[0].node, clear);
95+
if (hit) {
96+
this.fire('pick', hit.point, hit.normal, selection[0].node, clear);
97+
}
8398
}
8499

85100
bind() {

0 commit comments

Comments
 (0)