Better

Ethan的博客,欢迎访问交流

Three.js Layers 工作机制

在 Raycaster 实现中,不会过滤 visible 为 false 的物体的,也就是说不可见物体依旧可以被拾取到,那如果想要不可见对象不参与拾取,该怎么实现呢,可以利用 Object3D 的 layers 属性实现。从源码中可以了解一下基础的位运算操作。

背景

关于不可见物体是否需要参与拾取,在 layers 机制引入之初,也有很多的讨论,有人认为不可见物体不应该参与讨论,但否定的声音也不少,理由是我不可以通过添加不可见物体来扩展选择区域,可见我们需要引入另一种机制,将可见性从光线拾取中解耦出来,同时支持过滤掉不想要的物体。

最终投票后采取的方式是新增 Layers 机制,即使这会给用户侧带来破坏更新,参考 Unity 的实现,具体讨论见 Raycaster intersects with invisible objects

常用场景

我们现在源码层面哪些模块用到了 Layers 类,除了 Object3D 外,主要还有 Raycaster 和 WebGLRenderer 类。

Raycaster 类中具体如下

function intersect( object, raycaster, intersects, recursive ) {

    if ( object.layers.test( raycaster.layers ) ) {

        object.raycast( raycaster, intersects );

    }

    if ( recursive === true ) {

        const children = object.children;

        for ( let i = 0, l = children.length; i < l; i ++ ) {

            intersect( children[ i ], raycaster, intersects, true );

        }

    }

}

由此可见,object.layers 必须和 ratcaster.layers 属于相同的一组图层,才会参与拾取判断,因此我们可以通过控制 layers 属性来过滤掉不参加拾取的物体。

在 WebGLRenderer 类中使用到的地方较多,总结下来都是与 camera.layers 做判断,如果不属于同一组图层,会直接跳过渲染,部分源码如下

function renderObjects( renderList, scene, camera ) {

    const overrideMaterial = scene.isScene === true ? scene.overrideMaterial : null;

    for ( let i = 0, l = renderList.length; i < l; i ++ ) {

        const renderItem = renderList[ i ];

        const object = renderItem.object;
        const geometry = renderItem.geometry;
        const material = overrideMaterial === null ? renderItem.material : overrideMaterial;
        const group = renderItem.group;

        if ( object.layers.test( camera.layers ) ) {

            renderObject( object, scene, camera, geometry, material, group );

        }

    }

}

源码分析

Layers 类实现简单,但比较有意思,主要使用各种位运算。

class Layers {

    constructor() {

        this.mask = 1 | 0;

    }

    // 指定图层开启,并关闭其他图层,很好理解:1 不断左移
    set( channel ) {

        this.mask = ( 1 << channel | 0 ) >>> 0;

    }

    // 开启指定图层。很好理解:1 不断左移,且与当前做按为或
    enable( channel ) {

        this.mask |= 1 << channel | 0;

    }

    // 开启所有图层,很好理解:全部设置为 1
    enableAll() {

        this.mask = 0xffffffff | 0;

    }

    // 切换指定图层的开启状态,很好理解:异或判断,不同为 1
    toggle( channel ) {

        this.mask ^= 1 << channel | 0;

    }

    // 关闭指定图层,得到指定图层取反后做按位与
    disable( channel ) {

        this.mask &= ~ ( 1 << channel | 0 );

    }

    // 关闭所有图层,很好理解:全部设置为 1
    disableAll() {

        this.mask = 0;

    }

    // 传入图层对象与当前对象是否属于相同的一组图层
    test( layers ) {

        return ( this.mask & layers.mask ) !== 0;

    }

    // 某个图层是否开启
    isEnabled( channel ) {

        return ( this.mask & ( 1 << channel | 0 ) ) !== 0;

    }

}

export { Layers };

可以看到很多的 | 0 运算和一个 >>> 运算。

更多位运算

按位与判断奇偶数

  • 奇数:num & 1 === 1
  • 偶数:num & 1 === 0

按位或取整:位运算会将数转换为 32 位整型

  • Num | 0

按位非

  • 判断数组中是否包含某个元素:~arr.indexOf(val) === 0,因为~x=== -x-1
  • 取整:~~x

按位异或 ^,实现值交换

let a = 10;
let b = 20;
a^=b;
b^=a;
a^=b;

无符号右移确保为数值且为有效正整数

// 将左操作数视为为无符号数,并将该数字的二进制表示形式移位为右操作数指定的位数(取模 32)。
// 向右移动的多余位将被丢弃,零位从左移入。其符号位变为 0,因此结果始终为非负数。
5 >>> 0; // 5
5.5 >>> 0; // 5.5
-5 >>> 0; // 4294967291


留言