在 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