Skip to content
本页内容

拾取事件

在三维应用中时常需要点击场景中的物体,引擎支持射线包围盒拾取和帧缓冲区拾取。

拾取支持的事件类型:

名称解释
PICK_OVER当触控点进入碰撞体范围时触发一次
PICK_OUT当触控点离开碰撞体范围时触发一次
PICK_CLICK当触控点在碰撞体范围内按下并松开,在松开时触发一次
PICK_MOVE当触控点在碰撞体范围内移动时触发
PICK_UP当触控点在碰撞体范围内松开时触发一次
PICK_DOWN当触控点在碰撞体范围内按下时触发一次

监听事件

拾取事件依赖 Collider 组件,我们可以直接对 Object3D 进行 PointerEvent3D 事件监听。引擎统一封装了两种拾取类型的用法,可以通过简单配置进行切换

ts
//引擎启动前需要配置开启拾取和拾取类型
Engine3D.setting.pick.enable = true;
// Bound: 包围盒拾取, pixel: 帧缓冲区拾取
Engine3D.setting.pick.mode = `bound`; // or 'pixel'

await Engine3D.init()
// 拾取检测依赖 Collider 碰撞组件
let obj = Object3D();
obj.addComponent(Collider);

// 在节点上添加 PickEvent 事件监听,在回调函数可以获取到对应的事件
obj.addEventListener(PointerEvent3D.PICK_CLICK, onPick, this);

// 或者通过 Engine3D.pickFire 全局监听所有物体点击事件
Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, onPick, this);

//回调函数中获取到事件信息
function onPick(e: PointerEvent3D) {
    ...
}

包围盒拾取

射线包围盒是一种常用的 CPU 计算拾取方法,需要计算 Collider 组件的 shape 和鼠标射线的交集,在物体数量不多的场景中性能较好,但是精度较差,因为包围盒往往不能够精准的表达物体的真实形状。

为了保持 cpu 性能,目前包围盒拾取只支持主动 pick 点击拾取,暂不支持 over/hover 状态拾取。

ts
import {Object3D, Collider, BoxColliderShape, Vector3} from '@orillusion/core';

let box = new Object3D();
let mr = box.addComponent(MeshRenderer);
// 设置 box geometry
mr.geometry = new BoxGeometry(1,1,1);
// 添加碰撞盒检测
let collider = box.addComponent(Collider);
// bound 模式需要手动设置碰撞盒样式和大小
// 拾取精度取决于 box.geometry 和 collider.shape 的匹配程度
collider.shape = new BoxColliderShape().setFromCenterAndSize(new Vector3(0, 0, 0), new Vector3(1, 1, 1));
  • 左面的 box 使用同形状的 BoxColliderShape 进行检测,精度较好
  • 右边的 sphere 也使用 BoxColliderShape,但可点击区域就会比实际模型要大,精度较差


Orillusion powered by WebGPU on Chrome/Edge 113+
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Vector3, Object3D, Camera3D, ForwardRenderJob, LitMaterial, MeshRenderer, BoxColliderShape, Collider, BoxGeometry, ComponentBase, Color, PointerEvent3D, SphereGeometry } from "@orillusion/core";

export default class TouchDemo {
    scene: Scene3D;

    cameraObj: Object3D;

    camera: Camera3D;

    constructor() { }

    async run() {
        console.log('start demo');

        Engine3D.setting.pick.enable = true;
        Engine3D.setting.pick.mode = `bound`;

        await Engine3D.init();

        this.scene = new Scene3D();
        this.cameraObj = new Object3D();
        this.camera = this.cameraObj.addComponent(Camera3D)
        this.scene.addChild(this.cameraObj);
        this.camera.lookAt(new Vector3(0, 0, 10), new Vector3(0, 0, 0));
        this.camera.perspective(60, window.innerWidth / window.innerHeight, 1, 10000.0);

        let box = this.createBox(-2, 0, 0);
        let sphere = this.createSphere(2, 0, 0);

        let renderJob = new ForwardRenderJob(this.scene);
        Engine3D.startRender(renderJob);
        
        // 统一监听点击事件
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, this.onPick, this);
    }

    createBox(x: number, y: number, z: number) {
        let boxObj = new Object3D();
        boxObj.transform.localPosition = new Vector3(x, y, z);

        let size: number = 2;
        let shape: BoxColliderShape = new BoxColliderShape().setFromCenterAndSize(new Vector3(0, 0, 0), new Vector3(size, size, size));
        //加一个碰撞盒子。
        let collider = boxObj.addComponent(Collider);
        collider.shape = shape;
        // 为对象添 MeshRenderer
        let mr: MeshRenderer = boxObj.addComponent(MeshRenderer);
        // 设置几何体
        mr.geometry = new BoxGeometry(size, size, size);
        // 设置材质
        mr.material = new LitMaterial();
        this.scene.addChild(boxObj);
        return boxObj;
    }

    createSphere(x: number, y: number, z: number){
        let sphereObj = new Object3D();
        sphereObj.transform.localPosition = new Vector3(x, y, z);

        let size: number = 2;
        let shape: BoxColliderShape = new BoxColliderShape().setFromCenterAndSize(new Vector3(0, 0, 0), new Vector3(size, size, size));
        //加一个碰撞盒子。
        let collider = sphereObj.addComponent(Collider);
        collider.shape = shape;
        // 为对象添 MeshRenderer
        let mr: MeshRenderer = sphereObj.addComponent(MeshRenderer);
        // 设置几何体
        mr.geometry = new SphereGeometry(size/2, 20, 20);
        // 设置材质
        mr.material = new LitMaterial();
        this.scene.addChild(sphereObj);
        return sphereObj;
    }

    onPick(e: PointerEvent3D) {
        console.log('onClick:', e);
        let mr: MeshRenderer = e.target.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random();
    }
}
new TouchDemo().run();


帧缓冲拾取

pixel 模式和 bound 模式不同,帧缓冲拾取 利用的是 GPU 的像素检测,几乎不消耗 CPU 性能,可以无视场景中交互对象的数量和形状复杂度,支持所有触控事件。当场景模型形状复杂或物体数量众多的时候,我们推荐使用 pixel 模式进行拾取检测。


Orillusion powered by WebGPU on Chrome/Edge 113+
Please upgrade to latest Chrome/Edge

<
ts
import {
    BoxColliderShape, Camera3D, CameraUtil, Collider, Color, defaultTexture, DirectLight, Engine3D, ForwardRenderJob, GUIHelp, HDRBloomPost, LitMaterial, HoverCameraController, KelvinUtil, MeshRenderer, Object3D, PointerEvent3D, Scene3D, SphereGeometry, Vector3, webGPUContext,
} from '@orillusion/core';

export class Sample_MousePick {
    lightObj: Object3D;
    cameraObj: Camera3D;
    scene: Scene3D;
    hover: HoverCameraController;

    constructor() { }

    async run() {
        Engine3D.setting.pick.enable = true;
        Engine3D.setting.pick.mode = `pixel`;
        Engine3D.setting.shadow.debug = false;

        await Engine3D.init({});

        GUIHelp.init();

        this.scene = new Scene3D();
        let camera = CameraUtil.createCamera3DObject(this.scene);
        camera.perspective(60, webGPUContext.aspect, 1, 5000.0);

        this.hover = camera.object3D.addComponent(HoverCameraController);
        this.hover.setCamera(-45, -45, 120);

        let wukong = await Engine3D.res.loadGltf('https://cdn.orillusion.com/gltfs/wukong/wukong.gltf');
        wukong.transform.y = 30;
        wukong.transform.scaleX = 20;
        wukong.transform.scaleY = 20;
        wukong.transform.scaleZ = 20;
        wukong.forChild((node) => {
            if (node.hasComponent(MeshRenderer)){ 
                node.addComponent(Collider)
            }
        });
        this.scene.addChild(wukong);

        this.initPickObject(this.scene);

        let renderJob = new ForwardRenderJob(this.scene);
        renderJob.addPost(new HDRBloomPost());
        Engine3D.startRender(renderJob);

        // 统一监听鼠标拾取
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_UP, this.onUp, this);
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_DOWN, this.onDow, this);
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, this.onPick, this);
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_OVER, this.onOver, this);
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_OUT, this.onOut, this);
        Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_MOVE, this.onMove, this);
    }

    private initPickObject(scene: Scene3D): void {
        /******** light *******/
        {
            this.lightObj = new Object3D();
            this.lightObj.rotationX = 125;
            this.lightObj.rotationY = 0;
            this.lightObj.rotationZ = 40;
            let lc = this.lightObj.addComponent(DirectLight);
            lc.lightColor = KelvinUtil.color_temperature_to_rgb(5355);
            lc.castShadow = true;
            lc.intensity = 10;
            lc.debug()
            scene.addChild(this.lightObj);
        }

        let size: number = 9;
        let shape = new BoxColliderShape();
        shape.setFromCenterAndSize(new Vector3(), new Vector3(size, size, size));

        let geometry = new SphereGeometry(size / 2, 20, 20);
        for (let i = 0; i < 10; i++) {
            let obj = new Object3D();
            obj.name = 'sphere ' + i;
            scene.addChild(obj);
            obj.x = (i - 5) * 10;

            let mat = new LitMaterial();
            mat.emissiveMap = defaultTexture.grayTexture;
            mat.emissiveIntensity = 0.0;

            let renderer = obj.addComponent(MeshRenderer);
            renderer.geometry = geometry;
            renderer.material = mat;
            obj.addComponent(Collider);
        }
    }

    private onUp(e: PointerEvent3D) {
        console.log('onUp', e.target.name, e.data.pickInfo)
        let obj = e.target as Object3D;
        let mr = obj.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random()
    }

    private onDow(e: PointerEvent3D) {
        console.log('onDown', e.target.name, e.data.pickInfo)
        let obj = e.target as Object3D;
        let mr = obj.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random()
    }

    private onPick(e: PointerEvent3D) {
        console.log('onPick', e.target.name, e.data.pickInfo)
        let obj = e.target as Object3D;
        let mr = obj.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random()
    }

    private onOver(e: PointerEvent3D) {
        console.log('onOver', e.target.name, e.data.pickInfo)
        let obj = e.target as Object3D;
        let mr = obj.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random()
    }

    private onOut(e: PointerEvent3D) {
        console.log('onOut', e.target.name, e.data.pickInfo)
        let obj = e.target as Object3D;
        let mr = obj.getComponent(MeshRenderer);
        mr.material.baseColor = Color.random()
    }

    private onMove(e: PointerEvent3D) {
        console.log('onMove', e.target.name, e.data.pickInfo)
    }
}
new Sample_MousePick().run();