Skip to content

Instantly share code, notes, and snippets.

@duhaime
Last active November 8, 2023 21:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save duhaime/1eafa293e7ce16b074a6d55cac67badc to your computer and use it in GitHub Desktop.
Save duhaime/1eafa293e7ce16b074a6d55cac67badc to your computer and use it in GitHub Desktop.
GPU Picking (Three.js)
<html>
<head>
<style>
html, body { width: 100%; height: 100%; background: #000; }
body { margin: 0; overflow: hidden; }
canvas { width: 100vw; height: 100vh; }
#selected { position: absolute; top: 10; left: 10; font-size: 40; color: black; background: #fff; width: 100%;}
</style>
</head>
<body>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js'></script>
<script src='https://rawgit.com/YaleDHLab/pix-plot/master/pixplot/web/assets/vendor/src/trackball-controls.js'></script>
<div id='selected'></div>
<canvas />
<script type='x-shader/x-vertex' id='vertex-shader'>
precision mediump float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec3 cameraPosition;
attribute vec3 position; // blueprint's vertex positions
attribute vec3 color; // only used for raycasting
attribute vec3 translation; // x y translation offsets for an instance
varying vec3 vColor;
void main() {
vColor = color;
vec3 pos = position + translation;
vec4 mvPos = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPos;
gl_PointSize = 10000.0 / -mvPos.z;
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
</script>
<script>
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
camera.position.set(0, 1, -600);
var canvas = document.querySelector('canvas');
var renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: canvas,
});
var controls = new THREE.TrackballControls(camera, canvas);
var geometry = new THREE.InstancedBufferGeometry();
var BA = THREE.BufferAttribute;
var IBA = THREE.InstancedBufferAttribute;
// add data for each observation
var n = 10000; // number of observations
var rootN = n**(1/2);
var cellSize = 20;
var color = new THREE.Color();
var translations = new Float32Array( n * 3 );
var colors = new Float32Array( n * 3 );
var translationIterator = 0;
var colorIterator = 0;
for (var i=0; i<n; i++) {
var rgb = color.setHex(i+1);
translations[translationIterator++] = (i % rootN) * cellSize;
translations[translationIterator++] = Math.floor(i / rootN) * cellSize;
translations[translationIterator++] = 0;
colors[colorIterator++] = rgb.r;
colors[colorIterator++] = rgb.g;
colors[colorIterator++] = rgb.b;
}
// picking scene
var pickingScene = new THREE.Scene();
pickingScene.background = new THREE.Color(0x000000);
// must be identical to the size of the drawn scene
var pickingTexture = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
var pixelBuffer = new Uint8Array(4);
canvas.addEventListener('mousemove', function(e) {
// render the picking scene
renderer.setRenderTarget(pickingTexture)
renderer.render(pickingScene, camera);
renderer.setRenderTarget(null);
var x = e.clientX * window.devicePixelRatio;
var y = pickingTexture.height - e.clientY * window.devicePixelRatio;
// read the selected pixel
renderer.readRenderTargetPixels(pickingTexture, x, y, 1, 1, pixelBuffer);
var id =(pixelBuffer[0] << 16) | (pixelBuffer[1] << 8) | (pixelBuffer[2]);
document.querySelector('#selected').textContent = id-1 >= 0
? 'You are hovering point ' + (id-1).toString()
: 'You are hovering point';
})
// rendered scene
var positionAttr = new BA( new Float32Array( [0, 0, 0] ), 3);
var translationAttr = new IBA(translations, 3, true, 1);
var colorAttr = new IBA(colors, 3, true, 1);
geometry.setAttribute('position', positionAttr);
geometry.setAttribute('translation', translationAttr);
geometry.setAttribute('color', colorAttr);
var material = new THREE.RawShaderMaterial({
vertexShader: document.getElementById('vertex-shader').textContent,
fragmentShader: document.getElementById('fragment-shader').textContent,
});
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
scene.add(mesh);
pickingScene.add(mesh.clone());
function onWindowResize() {
var width = canvas.clientWidth * window.devicePixelRatio;
var height = canvas.clientHeight * window.devicePixelRatio;
camera.aspect = width / height;
camera.updateProjectionMatrix();
// pass false to prevent three.js from tampering with css
renderer.setSize(width, height, false);
pickingTexture.setSize(width, height);
}
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
};
window.addEventListener('resize', onWindowResize)
// set the initial renderer sizes
onWindowResize();
render();
</script>
</body>
</html>
@jonobr1
Copy link

jonobr1 commented Oct 27, 2023

What’s the max number n can be?

@duhaime
Copy link
Author

duhaime commented Oct 29, 2023

The highest number of n is kind of a function of the machine on which the visualization is loaded, but essentially most graphics cards should be able to handle up to roughly 1M points :)

@jonobr1
Copy link

jonobr1 commented Oct 30, 2023

Thanks for the response :) I guess put another way, is the limit (not necessarily the performance) the combinations of pixel colors? E.g pulling the rgba from a canvas are discrete integers 0-255.

@duhaime
Copy link
Author

duhaime commented Oct 30, 2023

Ah yes, the pixel reading logic can be run in either RGB mode (=256**3 possibilities) or RGBA mode [docs]. I'm not sure how large the alpha buffer needs to be quantized in order to allow successful picking within that channel. In practice though unless you have a powerful graphics card running the visualization you'll run out of vertices before you fill up even the RGB picking texture space.

@jonobr1
Copy link

jonobr1 commented Oct 30, 2023

Makes sense, thank you so much for the explanation and example!

@duhaime
Copy link
Author

duhaime commented Oct 31, 2023

❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment