一、3D场景下拾取的思路:
| 思路 | 几何思路 | 渲染思路 |
|---|---|---|
| 介绍 | 通过连接摄像机和屏幕坐标生成射线,然后与场景中的物体做相交判断 | 对渲染的每个物体给与编号,将编号转换成颜色,然后通过拾取framebuffer的颜色来判断拾取到了哪个物体 |
| 优点 | 使用较为简单,无需额外渲染开销 | 1.充分利用GPU运算 2.由于拾取颜色,自动可以把深度过滤,只取最前面的 |
| 缺点 | 1.需要给每一个需要检测的物体配置碰撞体,有些场景多余,特别是编辑器里面,场景对象有些是不需要碰撞的 2.使用cpu计算相交性,如果场景较大CPU开销较高,会卡顿(需要额外的八叉树、BVH树优化拾取) 3.解决不了精度问题:当物体特别靠近摄像机的时候;当物体的中心在另一个物体内部时;当两个物体相互穿过,且有较大部分重合时 | 1.增加了渲染调度的复杂度 2.增加了渲染的开销 |
二、射线追踪法Raycaster
2.1原理
从鼠标处发射一条射线,穿透场景的视锥体,通过计算,找出与射线相交的对象。
2.2 坐标变换
与图形流水线反过来,窗口坐标系 ——> NDC坐标系 ———> 世界坐标系
2.3 常用方法
.setFromCamera(coords,camera)更新点坐标和相机视椎。coords点坐标(归一化),camera相机。.intersectObject(scenes,recursive,optionalTarget)检查射线与场景对象和其子集是否有交集。scenes 要检查的场景对象数组格式。recursive为true检查所有后代。optionalTarget返回交集结果,可选。
2.4 代码示例
// 实现效果:选中的网络模型变为半透明效果
function ray() {
const Sx = event.clientX;//鼠标单击位置横坐标
const Sy = event.clientY;//鼠标单击位置纵坐标
//屏幕坐标转标准设备坐标
const x = ( Sx / window.innerWidth ) * 2 - 1;//标准设备横坐标
const y = -( Sy / window.innerHeight ) * 2 + 1;//标准设备纵坐标
const standardVector = new THREE.Vector3(x, y, 0.5);//标准设备坐标
//标准设备坐标转世界坐标
const worldVector = standardVector.unproject(camera);
//射线投射方向单位向量(worldVector坐标减相机位置坐标)
const ray = worldVector.sub(camera.position).normalize(); // 相机位置 指向 鼠标位置 的射线的 方向向量
//创建射线投射器对象
const raycaster = new THREE.Raycaster(camera.position, ray);
//返回射线选中的对象
const intersects = raycaster.intersectObjects([boxMesh,sphereMesh,cylinderMesh]);
if (intersects.length > 0) {
intersects[0].object.material.transparent = true;
intersects[0].object.material.opacity = 0.6;
}
}
三、颜色/GPU拾取Framebuffer Picker
3.1 原理
OpenGL 提供了一个 glReadPixels 函数,利用颜色的6位16进制表示,以颜色作为ID,在后台渲染出纹理后,检查鼠标位置关联的像素的颜色,通过颜色来确认相交的对象。该技术用空间(多一份数据)换时间(拾取快),对象拾取仅在用户单击鼠标按钮时完成,因此性能损失仅在该时间发生。
3.2 步骤
- 准备好两份数据,一份渲染输出到屏幕,一份渲染到FBO帧缓存对象,同时把每个物体的信息存起来
- 创建一个webglRenderTarget 离屏渲染()(FBO,不直接输出到屏幕)。
- 渲染FBO,通过获取到的颜色位换算回ID值,判断点击了那个物体。
- 通过ID值获取到点击的物体的信息,在生成一个正方体套在点击物体外面,表示高亮。
- 最后正常渲染场景,输出到颜色缓冲区(屏幕)
3.3 代码实现
参考文档
全局变量
除了基本的,增加多了几个 缓存对象相关的 变量:
let pickingTexture, pickingScene;
let highlightBox;
const pickingData = [];
const pointer = new THREE.Vector2();
const offset = new THREE.Vector3( 10, 10, 10 );
const clearColor = new THREE.Color();
初始化
除了基本的初始化,增加scene、camera、light等,init函数还做了其他处理:
- 生成5000个
BoxGeometry,位置、颜色随机,并合成mergedGeometry,add进scene中 - 为每一个几何体赋值id=索引值i,同时将它的位置、旋转和缩放信息缓存在
pickingData数组中 - 离屏渲染(缓存中)也备份一份数据
pickingScene:备份的场景对象pickingTexture:THREE.WebGLRenderTarget实例,缓冲,用于在一个图像显示在屏幕上之前先做一些处理pickingMaterialhighlightBox:为鼠标经过时做高亮效果预备,叠加绘制在ViewOffset区域pickingData:存储5000个几何体的位置、缩放、旋转信息等
function init () {
// 1、增加scene、camera、light ....
for ( let i = 0; i < 5000; i ++ ) {
// 2、生成5000个BoxGeometry,位置、颜色随机,并合成 mergedGeometry,add进scene中...
// 为每一个几何体赋值id=索引值i,同时将它的位置、旋转和缩放信息缓存在pickingData数组中
applyId( geometry, i );
// 3、 离屏渲染(缓存中)也备份一份数据
pickingData[ i ] = {
position: position,
rotation: rotation,
scale: scale
};
}
// set up the picking texture to use a 32 bit integer so we can write and read integer ids from it
pickingScene = new THREE.Scene();
pickingTexture = new THREE.WebGLRenderTarget( 1, 1, {
type: THREE.IntType, // 纹理的type属性,THREE.IntType与格式THREE.RGIntegerFormat对应
format: THREE.RGBAIntegerFormat,
internalFormat: 'RGBA32I',
} );
const pickingMaterial = new THREE.ShaderMaterial( {
glslVersion: THREE.GLSL3,
vertexShader: /* glsl */`
attribute int id;
flat varying int vid;
void main() {
vid = id;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}
`,
fragmentShader: /* glsl */`
layout(location = 0) out int out_id;
flat varying int vid;
void main() {
out_id = vid;
}
`,
} );
pickingScene.add( new THREE.Mesh( mergedGeometry, pickingMaterial ) );
highlightBox = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshLambertMaterial( { color: 0xffff00 } )
);
scene.add( highlightBox );
}
拾取
拾取函数考虑到性能优化,只对局部做了重新擦除、绘制处理,
- 先对针对鼠标位置下的区域:
camera.setViewOffset(...) - 清除:
camera.clearViewOffset() - 绘制:
highlightBox.visible = true
function pick() {
// render the picking scene off-screen
// set the view offset to represent just a single pixel under the mouse
// 官方api .setViewOffset(多视图的全宽设置, 多视图的全高设置, 副摄像机的水平偏移, 副摄像机的垂直偏移, 副摄像机的宽度, 副摄像机的高度)
// 针对鼠标下的设置相机的有效显示部分,即,只针对子视图进行处理:清除、渲染
const dpr = window.devicePixelRatio;
camera.setViewOffset(
renderer.domElement.width, renderer.domElement.height,
Math.floor( pointer.x * dpr ),
Math.floor( pointer.y * dpr ),
1, 1
);
// render the scene
renderer.setRenderTarget( pickingTexture );
// clear the background to - 1 meaning no item was hit
clearColor.setRGB( - 1, - 1, - 1 );
renderer.setClearColor( clearColor );
renderer.render( pickingScene, camera );
// clear the view offset so rendering returns to normal
camera.clearViewOffset();
// create buffer for reading single pixel
const pixelBuffer = new Int32Array( 4 );
// read the pixel
// 将renderTarget的像素数据读取到传入的缓冲区中
renderer.readRenderTargetPixels( pickingTexture, 0, 0, 1, 1, pixelBuffer );
const id = pixelBuffer[ 0 ];
if ( id !== - 1 ) {
// move our highlightBox so that it surrounds the picked object
const data = pickingData[ id ];
highlightBox.position.copy( data.position );
highlightBox.rotation.copy( data.rotation );
highlightBox.scale.copy( data.scale ).add( offset );
highlightBox.visible = true;
} else {
highlightBox.visible = false;
}
}
为几何体赋予id
function applyId( geometry, id ) {
const position = geometry.attributes.position;
const array = new Int16Array( position.count );
array.fill( id );
const bufferAttribute = new THREE.Int16BufferAttribute( array, 1, false );
bufferAttribute.gpuType = THREE.IntType;
geometry.setAttribute( 'id', bufferAttribute );
}