Source: lib/sys/gpupicker.js

import * as THREE from 'three';
import * as ECS from '../../packages/ecs-js/index';
import {x} from '../xapp/xworld';
import {ramTexture} from '../xutils/xcommon';
import AssetKeepr from '../xutils/assetkeepr';

const iffall = ['Obj3', 'GpuPickable'];

/**Uuid for picking object's id
 * @class pickuuid */
const pickuuid = {
	uuid: 1,
	/**
	 * Get an increase pickable's uuid.
	 * @property inc
	 */
	inc: function () {
		return pickuuid.uuid++;
	},
}

/**Helper for picking scene object.
 * For example, see <a href='https://threejsfundamentals.org/threejs/lessons/threejs-picking.html'>
 * Three.js Tutorial</a>
 * Can only work with Obj3.mesh.
 * @class GpuPicker
 */
export default class GpuPicker extends ECS.System {
	/** If any entity has a GpuPickable component, add it to my picking scene.
	 * @constructor GpuPicker
	 */
	constructor(ecs, x) {
		super(ecs);
		this.ecs = ecs;
		// create a 1x1 pixel render target
		this.pickingTexture = new THREE.WebGLRenderTarget(1, 1);
		this.pixelBuffer = new Uint8Array(4);
		this.pickedObject = undefined;
		this.pickedObjectSavedColor = 0;

		this.scene = new THREE.Scene();
		// this.scene = x.scene;
		this.scene.background = new THREE.Color(0);

		this.camera = x.xcam.XCamera.cam;
		this.renderer = x.renderer;
		if (this.renderer === undefined) {
			if (x.log >= 5)
				console.warn("[log 5] GpuPicker: Can't find a THREE.renderer. Testing?");
		}

		this.initMyPickings(x.ecs, this.scene);
	}

	/** If any entity has a GpuPickable component, add it to my picking scene.
	 * @param {ECS} ecs
	 * @param {THREE.Scene} scene
	 * @member GpuPicker#initMyPickings
	 * @function
	 */
	initMyPickings(ecs, scene) {
		// var scn = scene; // picking scene
		this.idToObject = {0: undefined};

		// create meshes
		var pickables = ecs.queryEntities({iffall});
		if (pickables) {
			var ids = this.idToObject;
			pickables.forEach( function(p, ix) {
				if (!p.Obj3 || !p.Obj3.mesh || !p.Obj3.mesh.geometry)
					return; // such as THREE.ArrowHelper

				const pickable = p.GpuPickable;
				pickable.pickid = pickuuid.inc();

				var tex;
				if (p.Visual && p.Visual.asset || pickable.tex) {
					if (p.Visual.asset && p.Visual.asset.startsWith('data:'))
						tex = new THREE.TextureLoader().load(p.Visual.asset);
					else
						tex = new THREE.TextureLoader().load(
							`assets/${p.Visual && p.Visual.asset ?
							p.Visual.asset : pickable.tex}`);
				}
				// else tex = new ramTexture(1, 1, 1);
				else
					tex = new THREE.TextureLoader().load(
						// 1x1 grey png
						AssetKeepr.greyPixel
						// 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AQSDjsH5tmdcQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAMSURBVAjXY2hoaAAAAwQBgZrwzU8AAAAASUVORK5CYII='
					);

				// TODO: simplify
				pickable.material = new THREE.MeshPhongMaterial({
					emissive: new THREE.Color(pickable.pickid),
					color: new THREE.Color(0, 0, 0),
					specular: new THREE.Color(0, 0, 0),
					// map: p.Obj3.mesh.map,
					// map: new THREE.TextureLoader().load('assets/tex/gpupick-debug.png'),
					// map: new THREE.TextureLoader().load('assets/tex/ruler256.png'),
					map: tex,
					transparent: true,
					side: THREE.DoubleSide,
					alphaTest: 0.5,
					blending: THREE.NoBlending,
				});

				pickable.mesh = new THREE.Mesh(p.Obj3.mesh.geometry.clone(),
								pickable.material);
				pickable.mesh.matrix = p.Obj3.mesh.matrix;
				pickable.mesh.matrixWorld = p.Obj3.mesh.matrixWorld;
				pickable.mesh.matrixAutoUpdate = false;

				scene.add(pickable.mesh);
				ids[pickable.pickid] = p;
			});
		}
	}

	/** If mouse moved, try pick the object
	 * @param {number} tick
	 * @param {array<Entity>} entities
	 * @member GpuPicker#update
	 * @function
	 */
	update(tick, entities) {
		var flag = false;

		if (x.xview.flag === 0 || entities.size === 0)
			return;

		// find mouse position (in xview.cmds)
		var clientxy;
		var v = x.xview;
		if (v.cmds) {
			for (var cx = v.cmds.length - 1; cx >= 0; cx--) {
				var c = v.cmds[cx];
				if (c && c.code === 'mouse' && c.e.type === 'mousemove') {
					clientxy = c.client;
					break;
				}
			}
		}

		if (clientxy) {
			var e = this.pickTest(clientxy, this.camera);
			if (e && e.GpuPickable) {
				e.GpuPickable.picktick = tick;
				e.GpuPickable.picked = true;
				// this.pickedEnt = e;
				x.xview.picked = e;
			}
			// else if(this.pickedEnt)
			// 	this.pickedEnt.GpuPickable.picked = false;
			else if (x.xview.picked) {
				x.xview.picked.GpuPickable.picked = false;
				x.xview.picked = undefined;
			}
		}
	}

	/**Picking test, check is the randered object?
	 * @param {array} canvPos [x, y] canvas position, in pixel
	 * @param {THREE.Camera} camera
	 * @member GpuPicker#pickTest
	 * @function
	 */
	pickTest(canvPos, camera) {
		const {scene, pickingTexture, pixelBuffer} = this;

		// reset picked
		if (this.pickedObject) {
			this.pickedObject.GpuPickable.picked = false;
			this.pickedObject = undefined;
			this.pickedId = undefined;
		}

		// set the view offset to represent just a single pixel under the mouse
		var renderer = this.renderer;
        var ctx = renderer.getContext();
		const pixelRatio = renderer.getPixelRatio();
		const aspRatio = camera.aspect;
		camera.setViewOffset(
			ctx.drawingBufferWidth,         // full width
			ctx.drawingBufferHeight,        // full top
			canvPos[0] * pixelRatio | 0,    // rect x
			canvPos[1] * pixelRatio | 0,    // rect y
			1, 1                            // w, h
		);
		// render the scene
		renderer.setRenderTarget(pickingTexture);
		renderer.render(scene, camera);
		renderer.setRenderTarget(null);

		// clear the view offset so rendering returns to normal
		camera.clearViewOffset();
		camera.aspect = aspRatio;

		//read the pixel
		renderer.readRenderTargetPixels(
					pickingTexture,
					0, 0, 1, 1,   // x y width height
					pixelBuffer);
		const id =  (pixelBuffer[0] << 16) |
					(pixelBuffer[1] <<  8) |
					(pixelBuffer[2]      );

		if (x.log >= 6) console.log(`[6] pickid ${id}`);
		this.pickedId = id;

		const intersectedObject = this.idToObject[id];
		if (intersectedObject) {
			// pick the first object. It's the closest one
			this.pickedObject = intersectedObject;
		}

		return this.idToObject[id];
	}
}

GpuPicker.query = {iffall};