I have a Three.js scene with points and am trying to figure out the relationship between my points' positions and screen coordinates. I thought I could use the function @WestLangley provided to a previous question but implementing this function has raised some confusion.
In the scene below, I'm storing the x coordinates of the left and right-most points in world.bb.x
, and am logging the world coordinates of the cursor each time the mouse moves. However, when I mouse to the left and right-most points, the world coordinates do not match the min or max x-coordinate values in world.bb.x
, which is what I expected.
Do others know what I can do to figure out the world coordinates of my cursor at any given time? Any help others can offer is greatly appreciated!
function World() {
this.scene = this.getScene();
this.camera = this.getCamera();
this.renderer = this.getRenderer();
this.controls = this.getControls();
this.color = new THREE.Color();
this.addPoints();
this.render();
}
World.prototype.getScene = function() {
var scene = new THREE.Scene();
scene.background = new THREE.Color(0xefefef);
return scene;
}
World.prototype.getCamera = function() {
var renderSize = getRenderSize(),
aspectRatio = renderSize.w / renderSize.h,
camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 100000);
camera.position.set(0, 1, -10);
return camera;
}
World.prototype.getRenderer = function() {
var renderSize = getRenderSize(),
renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setPixelRatio(window.devicePixelRatio); // retina displays
renderer.setSize(renderSize.w, renderSize.h); // set w,h
find('#gl-target').appendChild(renderer.domElement);
return renderer;
}
World.prototype.getControls = function() {
var controls = new THREE.TrackballControls(this.camera, this.renderer.domElement);
controls.zoomSpeed = 0.4;
controls.panSpeed = 0.4;
return controls;
}
World.prototype.render = function() {
requestAnimationFrame(this.render.bind(this));
this.renderer.render(this.scene, this.camera);
this.controls.update();
}
World.prototype.getMouseWorldCoords = function(e) {
var vector = new THREE.Vector3(),
camera = world.camera,
x = (e.clientX / window.innerWidth) * 2 - 1,
y = (e.clientY / window.innerHeight) * 2 + 1;
vector.set(x, y, 0.5);
vector.unproject(camera);
var direction = vector.sub(camera.position).normalize(),
distance = - camera.position.z / direction.z,
scaled = direction.multiplyScalar(distance),
coords = camera.position.clone().add(scaled);
return {
x: coords.x, y: coords.y,
};
}
World.prototype.addPoints = function() {
// this geometry builds a blueprint and many copies of the blueprint
var IBG = THREE.InstancedBufferGeometry,
BA = THREE.BufferAttribute,
IBA = THREE.InstancedBufferAttribute,
Vec3 = THREE.Vector3,
Arr = Float32Array;
// add data for each observation; n = num observations
var geometry = new IBG(),
n = 10000,
rootN = n**(1/2),
// find max min for each dim to center camera
xMax = Number.NEGATIVE_INFINITY,
xMin = Number.POSITIVE_INFINITY,
yMax = Number.NEGATIVE_INFINITY,
yMin = Number.POSITIVE_INFINITY;
var translations = new Arr(n * 3),
colors = new Arr(n * 3),
uidColors = new Arr(n * 3),
translationIterator = 0,
colorIterator = 0,
uidColorIterator = 0;
var colorMap = this.getColorMap();
for (var i=0; i<n; i++) {
var x = Math.sin(i) * 4,
y = Math.floor(i / (n/20)) * 0.3,
color = colorMap[ Math.floor(i / (n/20)) ],
uidColor = this.color.setHex(i + 1);
if (x > xMax) xMax = x;
if (x < xMin) xMin = x;
if (y > yMax) yMax = y;
if (y < yMin) yMin = y;
translations[translationIterator++] = x;
translations[translationIterator++] = y;
translations[translationIterator++] = 0;
colors[colorIterator++] = color.r / 255;
colors[colorIterator++] = color.g / 255;
colors[colorIterator++] = color.b / 255;
uidColors[uidColorIterator++] = uidColor.r;
uidColors[uidColorIterator++] = uidColor.g;
uidColors[uidColorIterator++] = uidColor.b;
}
// store the min and max coords in each dimension
this.bb = {
x: {
min: xMin,
max: xMax,
},
y: {
min: yMin,
max: yMax,
}
}
// center the camera
this.center = {
x: (xMax + xMin) / 2,
y: (yMax + yMin) / 2
}
this.camera.position.set(this.center.x, this.center.y, -6);
this.camera.lookAt(this.center.x, this.center.y, 0);
this.controls.target = new Vec3(this.center.x, this.center.y, 0);
// add attributes
geometry.addAttribute('position', new BA( new Arr([0, 0, 0]), 3));
geometry.addAttribute('translation', new IBA(translations, 3, 1) );
geometry.addAttribute('color', new IBA(colors, 3, 1) );
geometry.addAttribute('uidColor', new IBA(uidColors, 3, 1) );
var material = new THREE.RawShaderMaterial({
vertexShader: find('#vertex-shader').textContent,
fragmentShader: find('#fragment-shader').textContent,
});
var mesh = new THREE.Points(geometry, material);
mesh.frustumCulled = false; // prevent the mesh from being clipped on drag
this.scene.add(mesh);
}
World.prototype.getColorMap = function() {
function toHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? '0' + hex : hex;
}
function rgbToHex(r, g, b) {
return '#' + toHex(r) + toHex(g) + toHex(b);
}
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16),
} : null;
}
var hexes = [
'#fe4445','#ff583b','#ff6a2f','#ff7a20','#ff8800',
'#ff9512','#ffa31f','#ffaf2a','#ffbb34',
'#cfc522','#99cc01',
'#91c14a','#85b66e','#73ac8f','#57a3ac','#0099cb',
'#14a0d1','#20a7d8','#2aaedf','#33b5e6'
]
var colorMap = {};
hexes.forEach(function(c, idx) { colorMap[idx] = hexToRgb(c) })
return colorMap;
}
/**
* Helpers
**/
function getRenderSize() {
var elem = find('#gl-target');
return {
w: elem.clientWidth,
h: elem.clientHeight,
}
}
function find(selector) {
return document.querySelector(selector);
}
/**
* Main
**/
var world = new World();
world.controls.enabled = false;
find('canvas').addEventListener('mousemove', function(e) {
find('#bar').style.left = e.clientX + 'px';
var coords = world.getMouseWorldCoords(e);
console.log(coords, world.bb.x);
})
html, body {
width: 100%;
height: 100%;
background: #000;
}
body {
margin: 0;
overflow: hidden;
}
canvas {
width: 100%;
height: 100%;
}
.gl-container {
position: relative;
}
#gl-target {
width:700px;
height:400px
}
#bar {
width: 1px;
height: 100%;
display: inline-block;
position: absolute;
left: 30px;
background: red;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/95/three.min.js'></script>
<script src='https://rawgit.com/YaleDHLab/pix-plot/master/assets/js/trackball-controls.js'></script>
<script type='x-shader/x-vertex' id='vertex-shader'>
precision highp float;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
attribute vec3 position;
attribute vec3 translation;
#ifdef PICKING
attribute vec3 uidColor;
varying vec3 vUidColor;
#else
attribute vec3 color;
#endif
varying vec3 vColor;
void main() {
#ifdef PICKING
vUidColor = uidColor;
#else
vColor = color;
#endif
// set point position
vec3 raw = position + translation;
vec4 pos = projectionMatrix * modelViewMatrix * vec4(raw, 1.0);
gl_Position = pos;
// set point size
gl_PointSize = 10.0;
}
</script>
<script type='x-shader/x-fragment' id='fragment-shader'>
precision highp float;
#ifdef PICKING
varying vec3 vUidColor;
#else
varying vec3 vColor;
#endif
void main() {
// make point circular
vec2 coord = gl_PointCoord - vec2(0.5);
if (length(coord) > 0.5) discard;
// color the point
#ifdef PICKING
gl_FragColor = vec4(vUidColor, 1.0);
#else
gl_FragColor = vec4(vColor, 1.0);
#endif
}
</script>
<div class='gl-container'>
<div id='bar'></div>
<div id='gl-target'></div>
</div>