Three.js 拾取GPUPick的理解和思考

673 阅读4分钟

      今天,分享的是项目中真实会用到的东西,Three.js GPU Picking,这是什么意思呢,不同于Three.js 的Raycaster,它是利用颜色的6位16进制表示,以颜色作为ID,在后台渲染出纹理后,根据鼠标坐标下的纹理颜色,进行ID的查询进行拾取操作,哈哈,如果你不是很了解,应该不明白我在说什么,下面我们一步一步来说明这个所谓的GPU Picking。

      在Three.js r102版本中,这两个案例用到了这种拾取方式,webgl_interactive_instances_gpu和webgl_interactive_cubes_gpu中用到了GPU Picking,我们先来看一下效果:

               webgl_interactive_cubes_gpu                                                                     webgl_interactive_cubes_gpu

   上面两幅图中的黑色箭头就是拾取的效果,那下面我就webgl_interactive_cubes_gpu的instance部分来说说拾取,instance方式渲染,后面的博客会有。首先,来看一下webgl_interactive_cubes_gpu创建InstanceMesh部分的代码。

function makeInstanced( geo ) {
	//实例渲染的着色器
	var vert = document.getElementById( 'vertInstanced' ).textContent;
	var frag = document.getElementById( 'fragInstanced' ).textContent;
	//创建实例渲染所用的RawShaderMaterial
	var material = new THREE.RawShaderMaterial( {
		vertexShader: vert,
		fragmentShader: frag,
	} );
	//将material放进材质List方便后期释放资源
	materialList.push( material );
	//创建后台渲染的拾取材质----------GPU Picking
	var pickingMaterial = new THREE.RawShaderMaterial( {
		vertexShader: "#define PICKING\n" + vert,
		fragmentShader: "#define PICKING\n" + frag
	} );
	//将pickingMaterial放进材质List方便后期释放资源
	materialList.push( pickingMaterial );
	//创建InstancedBufferGeometry----------既用于GPU Picking,也用于渲染呈现
	var igeo = new THREE.InstancedBufferGeometry();
	//将igeo其放进几何体列表
	geometryList.push( igeo );
	.....//省略克隆顶点的代码
	.....//省略创建模型矩阵BufferAttribute的代码
	//创建矩阵
	var matrix = new THREE.Matrix4();
	//列主序的matrix
	var me = matrix.elements;
	//根据实例数量循环
	for ( var i = 0, ul = mcol0.count; i < ul; i ++ ) {
		//随机产生模型矩阵
		randomizeMatrix( matrix );
		//创建Object3D,主要是将每个实例的矩阵信息保存下来----------GPU Picking
		var object = new THREE.Object3D();
		objectCount ++;
		//object应用此矩阵----------GPU Picking
		object.applyMatrix( matrix );
		//拾取数组中按照----------GPU Picking*
		pickingData[ i + 1 ] = object;
		//设置各个实例的矩阵
		....//省略
	}
	......//省略设置模型矩阵相关的代码
	......//省略随机生成颜色,并存放到实例渲染的color中的代码
	//创建颜色BufferAttribute----------GPU Picking
	var col = new THREE.Color();
	//用于拾取渲染的颜色----------GPU Picking
	var pickingColors = new THREE.InstancedBufferAttribute(
		new Float32Array( instanceCount * 3 ), 3
	);
	//根据实例数量,逐渐增加colors的颜色并按照索引增加添加到pickingColors-GPU Picking
	for ( var i = 0, ul = pickingColors.count; i < ul; i ++ ) {
		//设置颜色,此处是10进制,此方法通过右移操作和按位与设置颜色-GPU Picking*
		col.setHex( i + 1 );
		//将颜色按照索引添加---GPU Picking*
		pickingColors.setXYZ( i, col.r, col.g, col.b );
	}
	//添加拾取颜色Attribute
	igeo.addAttribute( 'pickingColor', pickingColors );
	//创建用于渲染的Mesh 
	var mesh = new THREE.Mesh( igeo, material );
	scene.add( mesh );
	//创建后台用于渲染拾取的Mesh---GPU Picking*
	var pickingMesh = new THREE.Mesh( igeo, pickingMaterial );
	pickingScene.add( pickingMesh );
}

   上面是创建Intance方式渲染Mesh的代码,和用于拾取计算的Mesh的代码,那它之间有什么关联呢

    1.在为每个实例渲染添加模型矩阵的时候,会创建一个object3D,并将对应实例的模型矩阵应用到此Object3D,那么此Object3D就具有了和对应实例一样的矩阵信息,然后我们按照i+1的索引将其放入PickData数组中。

    2.后面有按照i+1设置pickcolor颜色,然后创建用于渲染Mesh添加进scene,创建用于拾取的mesh,添加进pickscene用于后台渲染。

       看到这里,两个标红的地方应该有种联系,那这种联系是什么呢,其实就是在用pickscene在后台渲染出纹理,然后根据鼠标点击,获取到鼠标点击位置的像素颜色rgba,并用rgb按位进行或操作算出十进制颜色值,这个十进制颜色值就是上面说的i+1,也就是ID,那就可以按照这个ID作为索引找到PickData数组中的Object3D,此Object3D的模型矩阵和对应实例的矩阵一样。

       我们继续看pick函数中的代码:

function pick() {
	//离屏渲染
    //显示选中的黄色Box设为不渲染
	highlightBox.visible = false;
    //设置渲染目标 
	renderer.setRenderTarget( pickingRenderTarget );
    //渲染pickingScene
	renderer.render( pickingScene, camera );

	//读取鼠标点击位置的像素,像素的rgba值会存到pixelBuffer中
	renderer.readRenderTargetPixels(
		pickingRenderTarget,
		mouse.x,
		pickingRenderTarget.height - mouse.y,
		1,
		1,
		pixelBuffer
	);
	//将像素转换为ID,rgb先右移在按位或
	var id =
		( pixelBuffer[ 0 ] << 16 ) |
		( pixelBuffer[ 1 ] << 8 ) |
		( pixelBuffer[ 2 ] );
    //根据ID从PickData中获取到Object
	var object = pickingData[ id ];
	if ( object ) {
		// move the highlightBox so that it surrounds the picked object
		if ( object.position && object.rotation && object.scale ) {
            //根据Object的参数设置选中BOX的位置,旋转
			highlightBox.position.copy( object.position );
			highlightBox.rotation.copy( object.rotation );
            //根据Object的参数设置缩放,并乘上包围盒的尺寸和缩放系数,以保证能够足够包围 
			highlightBox.scale.copy( object.scale )
				.multiply( geometrySize )
				.multiplyScalar( scale );
            //选中BOX开始渲染
			highlightBox.visible = true;
		}
	} else {
		highlightBox.visible = false;
	}
}

        到这里呢,整个的拾取就结束了,那其实所谓的GPU Picking就是在创建一个与渲染呈现的Mesh位置相同的pickingMesh,并将pickingMesh中的各个实例的颜色按照从1开始增加进行设置,同时设置按照对应索引设置特定的数据,后台渲染出纹理,将鼠标点击位置下的像素颜色转换回颜色值,根据颜色值,找到特定的数据进行处理。在这个Demo中,是将Object3D作为模型矩阵信息存储的介质,然后根据后台渲染鼠标拾取到像素的10进制颜色值获取到指定的Object,并将此Object的信息复制给选中BOX实现选中效果。

       留一个简单的问题:上面提到的两个Demo都用到了这种GPU Picking的思想,在这两个Demo中的具体实现中有什么相同,又有什么不同呢。好啦,笔者不才,也许并未表达出Three.js 中这种拾取方式精髓,请多多指教,可以加入QQ群,我们一起交流,这里有WebGL、Vulkan、OpenGL,也有Three.js、Unity、UE4,还有前端框架Vue等!当然也有图形图像处理大佬哈!期待你的加入!

                              ​      

\