实体拾取
碰撞拾取:点击以选择形状。
帧缓冲区拾取:点击以选择灰色形状,红色形状已设为不可拾取。
你可以在编辑器中打开教程项目进行尝试。
本教程介绍从三维场景选择实体的两种方法,例如在鼠标点击时进行选择。
碰撞拾取
基于碰撞的拾取使用碰撞组件,为每个需要拾取的实体添加形状,再通过刚体系统的 raycastFirst() 方法,从鼠标点击位置向屏幕内部发射射线。如果射线命中碰撞组件,就选中对应实体。
- ESM
- 传统脚本
import { Script, EVENT_MOUSEDOWN } from 'playcanvas';
export class PickerRaycast extends Script {
static scriptName = "pickerRayCast";
// initialize code called once per entity
initialize() {
this.app.mouse.on(EVENT_MOUSEDOWN, this.onSelect, this);
this.on('destroy', () => {
this.app.mouse.off(EVENT_MOUSEDOWN, this.onSelect, this);
});
}
onSelect (e) {
const from = this.entity.camera.screenToWorld(e.x, e.y, this.entity.camera.nearClip);
const to = this.entity.camera.screenToWorld(e.x, e.y, this.entity.camera.farClip);
const result = this.app.systems.rigidbody.raycastFirst(from, to);
if (result) {
var pickedEntity = result.entity;
pickedEntity.script.pulse.pulse();
}
}
}
var PickerRaycast = pc.createScript('pickerRaycast');
// initialize code called once per entity
PickerRaycast.prototype.initialize = function() {
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);
};
PickerRaycast.prototype.onSelect = function (e) {
var from = this.entity.camera.screenToWorld(e.x, e.y, this.entity.camera.nearClip);
var to = this.entity.camera.screenToWorld(e.x, e.y, this.entity.camera.farClip);
var result = this.app.systems.rigidbody.raycastFirst(from, to);
if (result) {
var pickedEntity = result.entity;
pickedEntity.script.pulse.pulse();
}
};
帧缓冲区拾取
基于帧缓冲区的拾取使用 pc.Picker 类,将场景渲染到内部缓冲区。鼠标点击时,根据缓冲区中鼠标位置处的颜色判断选中的网格实例。与碰撞拾取相比,此方法既有优点,也有缺点。
优点包括:
- 可以使用矩形区域一次选中场景中的多个对象。
- 无需引入物理库,减少预加载时间。
主要缺点是需要调用 readPixels,这会阻塞图形流水线,可能显著影响渲染性能,尤其是在移动设备和 GPU 负载较高的应用中。
可以缩小缓冲区尺寸,以降低精度换取性能。下面的示例脚本使用 pickAreaScale 属性控制这一点:数值越小,拾取速度越快,但精度越低。
还可以限制参与拾取的图层,脚本通过 layerNames 数组支持此功能。在数组中填入目标图层名称后,仅将所需内容渲染到内部缓冲区,也能提高性能。
- ESM
- 传统脚本
import { Script, Picker, Vec3, Entity, EVENT_MOUSEDOWN } from 'playcanvas';
export class PickerFramebuffer extends Script {
static scriptName = "pickerFrameBuffer";
/**
* 1 is more accurate but worse performance. 0.01 is best performance but least accurate. 0.25 is the default.
*
* @attribute
* @title Pick Area Scale
* @type {number}
* @range [0.01, 1]
*/
pickAreaScale = 0.25;
/**
* Layer names from which objects will be picked from.
*
* @attribute
* @title Layers Names
* @type {string[]}
*/
layerNames = ['World'];
initialize() {
// Create a frame buffer picker with a scaled resolution
const canvas = this.app.graphicsDevice.canvas;
const canvasWidth = parseInt(canvas.clientWidth, 10);
const canvasHeight = parseInt(canvas.clientHeight, 10);
this.picker = new Picker(this.app, canvasWidth * this.pickAreaScale, canvasHeight * this.pickAreaScale);
this.layers = [];
for (let i = 0; i < this.layerNames.length; ++i) {
const layer = this.app.scene.layers.getLayerByName(this.layerNames[i]);
if (layer) {
this.layers.push(layer);
}
}
this.app.mouse.on(EVENT_MOUSEDOWN, this.onSelect, this);
this.on('destroy', () => {
this.app.mouse.off(EVENT_MOUSEDOWN, this.onSelect, this);
});
}
onSelect(event) {
const canvas = this.app.graphicsDevice.canvas;
const canvasWidth = parseInt(canvas.clientWidth, 10);
const canvasHeight = parseInt(canvas.clientHeight, 10);
const camera = this.entity.camera.camera;
const scene = this.app.scene;
const picker = this.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
const selected = picker.getSelection({
x: Math.floor(event.x * (picker.width / canvasWidth)),
y: picker.height - Math.floor(event.y * (picker.height / canvasHeight))
});
if (selected.length > 0) {
// Get the graph node used by the selected mesh instance
let entity = selected[0].node;
// Bubble up the hierarchy until we find an actual Entity
while (entity !== null && !(entity instanceof Entity) && (entity.script && entity.script.pulse)) {
entity = entity.getParent();
}
if (entity) {
entity.script.pulse.pulse();
}
}
}
}
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.camera;
var scene = this.app.scene;
var picker = this.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({
x: Math.floor(event.x * (picker.width / canvasWidth)),
y: 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;
// Bubble up the hierarchy until we find an actual Entity
// that has the script we are looking for
while (entity !== null && !(entity instanceof pc.Entity) && (entity.script && entity.script.pulse)) {
entity = entity.getParent();
}
if (entity) {
entity.script.pulse.pulse();
}
}
};