Three.js-GPU拾取
解决什么问题
Three.js中常用的处理用户交互的方式,是RayCaster,这是CPU拾取方案。
但是如果遇到下面2个场景,常规RayCaster的方案就不够用了:
场景中的物体数量非常多/模型面数很多,RayCaster发射的每一根射线要判断的三角形数量巨大
在无用户交互接入的情况下,我们想判断下场景中当前哪些物体是可见的
这2种场景都需要巨大的计算量,CPU会不够看,此时,就需要借助GPU的计算能力了。
原理
我们先思考下:GPU处理的是什么?是像素。
因此基于GPU的处理,一定是围绕像素进行的。而像素能携带的信息,最常用的就是颜色RGB值。
因此我们构建一个离屏拾取层(_pickingScene),里面的物体和最终需要渲染的物体一一对应,并且给离屏场景中每一个物体,设置一个唯一的颜色值,那么我们只需要判断最终渲染的2D离屏图片的像素中,携带有哪些颜色,就可以知道当前哪些对象被渲染出来了。
这和分层Canvas处理交互是一样的道理。
所以我们要做的就很简单了:
给每个物体设置一个唯一的颜色值(一般是基于该对象的唯一标记,比如索引ID,来生成颜色值)
将物体通过离屏渲染,渲染到内存中的纹理上
读取离屏纹理上的像素的颜色值,映射到对象
注意渲染和最后的采样,针对不同的需求,方式会有所不同:
用户交互
如果是用于判断用户的鼠标交互触发了那个对象,那么渲染和采样时,只需要对用户鼠标当前位置的一个像素进行采样即可,这种场景基本没有性能问题。
全屏判断
如果是针对非用户交互的全屏判断,比如判断场景中当前哪些物体是可见的,这种情况,就需要渲染整个屏幕的内容(一次),然后针对屏幕内容,根据预设的采样范围大小(比如按照20*20像素的矩形),进行多次采样。
注意:获取像素的颜色值,这是耗费的CPU,不是GPU,因此要控制这个采样的操作数量,操作次数太多了,也是会卡住的。
这里有很大的优化空间,比如可以先获取模型的包围盒,然后算出一个涵盖所有模型的包围盒,只针对这一个或者有限的几个包围盒范围做采样。
遇到的问题
构建离屏层对象
这个其实不是问题,只是作为前置环节,这里做个记录:
1 | |
离屏对象和非离屏对象的Transform同步问题
因为我们构建了一个离屏层,那么就需要保证离屏层里面的对象和非离屏层的对象,其Transform信息(position、rotation、scale)一定是相同的。这就需要我们对其做一个同步操作:
1 | |
DPR问题
这里有一个必须先要理清楚的内容,就是DPR:devicePixelRatio,设备像素比。
先弄清楚几个概念:
DP = Device Pixels = 设备物理像素/分辨率
DIP = Device-Independent Pixel = 设备独立像素、CSS像素、逻辑像素
DPR = DP / DIP
以我的显示器为例:
DP = 3840 x 2160
我设置了150%缩放,因此DPR = 1.5
那么可以计算得出DIP = DP / DPR = (3840 x 2160) / 1.5 = 2560 x 1440
这里要注意:
离屏渲染,最终渲染的是一个纹理,也就是图片,一般我们会将其设置为物理像素,即DP
用户的鼠标交互,获取的坐标点,是逻辑像素,即DIP
因此我们在采样的时候,需要将用户的鼠标位置,乘以DPR,以获取物理像素位置,再进行纹理采样。
设置离屏纹理的大小,将其设置为物理像素(TODO:如果设置为逻辑像素会怎么样?):
1 | |
获取用户的鼠标位置信息:
1 | |
采样时对鼠标坐标进行的处理:
1 | |
(待确认)Y坐标反向问题
注意:这个想法只是我根据现象进行的猜测,不一定准确。
离屏渲染的纹理,其Y坐标从上到下,是从大到小的;这和常规的CSS里的坐标系的Y坐标是相反的,因此采样时,需要对Y坐标做一个反向处理。
1 | |
像素颜色还原为模型id
因为像素颜色是将模型id作为十六进制颜色值来设置的,通过readRenderTargetPixels()取得像素信息后,得到的实际上是范围为255的number类型颜色值,因此反向将像素颜色转为模型id时,需要用到位运算:
1 | |
R左移16位,R左移8位,B不用左移;然后进行位或运算。
比如以红色为例:
1、我们获取的pixelBuffer是(255, 0, 0)
R = 1111 1111
G = 0000 0000
B = 0000 0000
2、左移操作后:
R = 1111 1111 0000 0000 0000 0000
G = 0000 0000 0000 0000
B = 0000 0000
3、位或操作后:
1111 1111 0000 0000 0000 0000
4、转为十进制:
16,711,680
因此红色模型,其id就是16,711,680。
基于RGB,我们理论上能标记的模型数量是16,777,215个,即FFFFFF的10进制范围。
当模型数量比较少的时候,我们如果将离屏层打印出来,可以看到基本都是蓝色的,因为1-255对应的是RGB中的B范围。
toneMapping导致的颜色RGB范围异常问题
WebGLRenderer默认的色调映射(renderer.toneMapping = THREE.NoToneMapping)下,RGB值的范围是0-255,但是如果我们设置了toneMapping为其他值,RGB的范围就可能会发生变化(比如设置为THREE.LinearToneMapping,则RGB的上限就只有153了)。
这会导致我们通过像素的颜色还原模型id时,会和模型id对应不上。因此需要做如下处理:
1、离屏处理开始前,先去除toneMapping:
1 | |
2、当离屏处理结束后,恢复toneMapping:
1 | |
调试技巧
离屏层默认对我们是不可见的,如果出现问题,很难调试,因此我们可以考虑在需要的时候,将离屏层渲染出来:
1 | |
看到的效果类似这样(为了便于区分,将车顶和侧后方的玻璃手动设置成了红色):