Skip to content

Instantly share code, notes, and snippets.

@vkuchinov
Last active October 4, 2019 10:36
Show Gist options
  • Select an option

  • Save vkuchinov/1301e99cd2c10c1ed7d6a6d1ffe65fac to your computer and use it in GitHub Desktop.

Select an option

Save vkuchinov/1301e99cd2c10c1ed7d6a6d1ffe65fac to your computer and use it in GitHub Desktop.
THREE.JS / D3.JS : A vector-based HUD over THREE.JS scene + custom THREE.PylonGeometry
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js webgl - geometry - cube</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/98/three.min.js"></script>
<script src="OrbitControls.js"></script>
<script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<style>
body {
margin: 0px;
background-color: #000000;
overflow: hidden;
}
svg text{
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
#world {
width: 100%;
height: 150px;
}
#ui {
position:absolute;
z-index: 5;
}
</style>
</head>
<body>
<div id="threejs"><div id="ui"></div></div>
<script>
console.warn = function(){};
THREE.PylonGeometry = function(radius_, height_, offset_){
THREE.Geometry.call( this );
this.type = 'PylonGeometry';
this.parameters = {
raidus: radius_,
height: height_,
};
this.HUD = {label: "", p: new THREE.Vector3(1E-5, offset_ + height_ + 8.0, 1E-5) };
this.fromBufferGeometry( new THREE.PylonBufferGeometry(radius_, height_, offset_) );
this.mergeVertices();
this.computeVertexNormals();
this.computeFaceNormals();
this.faces.forEach(function(f_){ f_.materialIndex = 0; } );
this.faces[3].materialIndex = 1;
}
THREE.PylonGeometry.prototype = Object.create( THREE.Geometry.prototype );
THREE.PylonGeometry.prototype.constructor = THREE.PylonGeometry;
THREE.PylonBufferGeometry = function(radius_, height_, offset_){
THREE.BufferGeometry.call( this );
this.type = "PylonBufferGeometry";
this.parameters = {
raidus: radius_,
height: height_,
};
var indices = [];
var vertices = [];
var normals = [];
var uvs = [];
vertices.push(0, offset_, 0);
for(var i = 0; i < 3; i++){
var dx = radius_ * Math.cos(Math.PI * 2 / 3 * i);
var dy = offset_ + height_;
var dz = radius_ * Math.sin(Math.PI * 2 / 3 * i);
vertices.push(dx, dy, dz);
}
indices.push(0, 1, 2, 0, 2, 3, 0, 3, 1, 3, 2, 1);
for(var i = 0; i < indices.length / 3; i++){
var a = new THREE.Vector3(vertices[i], offset_ + vertices[i + 1], vertices[i + 2]);
var b = new THREE.Vector3(vertices[(i + 1)], offset_ + vertices[(i + 1) + 1], vertices[(i + 2) + 2]);
var c = new THREE.Vector3(vertices[(i + 2)], offset_ + vertices[(i + 2) + 1], vertices[(i + 2) + 2]);
var ab = b.sub(b);
var ac = b.sub(c);
var n = ab.cross(ac);
normals.push(n.x, n.y, n.z);
}
this.setIndex( indices );
this.addAttribute( "position", new THREE.Float32BufferAttribute( vertices, 3 ) );
this.addAttribute( "normal", new THREE.Float32BufferAttribute( normals, 3 ) );
this.addAttribute( "uv", new THREE.Float32BufferAttribute( uvs, 2 ) );
}
THREE.PylonBufferGeometry.prototype = Object.create( THREE.BufferGeometry.prototype );
THREE.PylonBufferGeometry.prototype.constructor = THREE.PylonBufferGeometry;
THREE.Mesh.prototype.setHUD = function(){
if(this.geometry.type == "PylonGeometry"){
this.geometry.HUD.label = this.name || "PylonGeometry_" + pylons.length;
var v = this.geometry.HUD.p.add(this.position);
this.geometry.HUD.p = [v, new THREE.Vector3(v.x - 16, v.y + 16, v.z), new THREE.Vector3(v.x - 64, v.y + 16, v.z)];
}
}
CanvasRenderingContext2D.prototype.clear =
CanvasRenderingContext2D.prototype.clear || function (preserveTransform) {
if (preserveTransform) {
this.save();
this.setTransform(1, 0, 0, 1, 0, 0);
}
this.clearRect(0, 0, this.canvas.width, this.canvas.height);
if (preserveTransform) { this.restore(); }
};
var camera, scene, renderer, pylons = [], w, h, svg;
var data = [
{ p: new THREE.Vector3(0, 0, 0), r: 32, h: 128, c: "#FF00FF" },
{ p: new THREE.Vector3(-64, 0, -64), r: 24, h: 96, c: "#00FFFF" },
{ p: new THREE.Vector3(64, 0, 64), r: 50, h: 160, c: "#FFFF00" }
];
init();
animate();
function init() {
w = window.innerWidth, h = window.innerHeight;
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( w, h );
renderer.setClearColor( 0xDEDEDE );
camera = new THREE.PerspectiveCamera( 35, w / h, 1, 2056 );
camera.position.y = 400;
camera.position.z = 1000;
camera.lookAt(0, 0, 0);
scene = new THREE.Scene();
data.forEach(function(pylon_){
var materials = [ new THREE.MeshBasicMaterial({ color: 0xA0A0A0 }) , new THREE.MeshBasicMaterial({ color: pylon_.c }) ];
var geometry = new THREE.PylonGeometry( pylon_.r, pylon_.h, 16 );
mesh = new THREE.Mesh(geometry, materials );
mesh.position.add(pylon_.p);
mesh.setHUD();
pylons.push( mesh );
scene.add(mesh);
var geometry = new THREE.CircleGeometry( 8, 32 );
var material = new THREE.MeshBasicMaterial( { color: 0xFFFFFF, transparent: true, opacity: 0.5 } );
var circle = new THREE.Mesh( geometry, material );
circle.rotation.x = -Math.PI / 2;
circle.position.add(pylon_.p);
scene.add( circle );
})
controls = new THREE.OrbitControls( camera, document );
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.screenSpacePanning = false;
controls.minDistance = 100;
controls.maxDistance = 1500;
controls.maxPolarAngle = Math.PI / 2;
var gridHelper = new THREE.GridHelper( 400, 16, 0x999999, 0x808080 );
gridHelper.position.y = 0;
scene.add( gridHelper );
//d3.js UI
svg = d3.select("#ui").append("svg").attr("width", w).attr("height", h);
svg.selectAll(".arrow")
.data(pylons)
.enter()
.append("path")
.attr("class", "arrow")
.attr("id", function(d_, i_){ return "pylon_" + i_; })
.attr("d", function(d_){ return generatePath(d_.geometry.HUD.p); })
.attr("fill", "none")
.attr("stroke", "#FFFFFF")
svg.selectAll(".label")
.data(pylons)
.enter()
.append("text")
.attr("class", "label")
.attr("id", function(d_, i_){ return "pylonLabel_" + i_; })
.attr("transform", function(d_){ return getScreenPosition(d_.geometry.HUD.p); })
.attr("fill", "#FFFFFF")
.attr("text-anchor", "start")
//.style("pointer-events", "none")
.text(function(d_, i_){ return "pylon_" + i_ })
.on("mouseover", function(d_, i_){
pylons[i_].material[1].color = new THREE.Color(0xFFFFFF);
pylons[i_].material[1].needsUpdate = true;
d3.select(this).attr("font-weight", "bolder")
})
.on("mouseout", function(d_, i_){
pylons[i_].material[1].color = new THREE.Color(data[i_].c);
pylons[i_].material[1].needsUpdate = true;
d3.select(this).attr("font-weight", "normal")
})
document.getElementById("threejs").appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize, false );
}
function onWindowResize() {
w = window.innerWidth, h = window.innerHeight;
svg.attr("width", w).attr("height", h);
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize( w, h );
}
function animate() {
for(var i = 0; i < pylons.length; i++){
d3.select("#pylon_" + i).attr("d", function(d_){ return generatePath(d_.geometry.HUD.p); });
d3.select("#pylonLabel_" + i).attr("transform", function(d_){ return getScreenPosition(d_.geometry.HUD.p); })
}
controls.update();
requestAnimationFrame( animate );
renderer.render( scene, camera );
}
function generatePath(vertices_){
w = window.innerWidth, h = window.innerHeight;
var d = "M ";
var xy = toScreenXY(vertices_[0], camera);
d += xy.x + " " + xy.y;
d += " L " + (xy.x - 32) + " " + (xy.y - 32);
d += " L " + (xy.x - 160) + " " + (xy.y - 32);
return d;
}
function getScreenPosition(vertices_){
w = window.innerWidth, h = window.innerHeight;
var xy = toScreenXY(vertices_[0], camera);
xy.x -= 160;
xy.y -= 35;
return "translate(" + xy.x + "," + xy.y + ")";
}
function toScreenXY(v_, camera_) {
var p = new THREE.Vector3(v_.x, v_.y, v_.z);
var vector = p.project(camera_);
vector.x = (vector.x + 1) / 2 * w;
vector.y = -(vector.y - 1) / 2 * h;
return { x: vector.x, y: vector.y };
}
</script>
</body>
</html>
/**
* @author qiao / https://github.com/qiao
* @author mrdoob / http://mrdoob.com
* @author alteredq / http://alteredqualia.com/
* @author WestLangley / http://github.com/WestLangley
*/
THREE.OrbitControls = function ( object, domElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
// API
this.enabled = true;
this.center = new THREE.Vector3();
this.userZoom = true;
this.userZoomSpeed = 1.0;
this.userRotate = true;
this.userRotateSpeed = 1.0;
this.userPan = true;
this.userPanSpeed = 2.0;
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
this.minDistance = 0;
this.maxDistance = Infinity;
// 65 /*A*/, 83 /*S*/, 68 /*D*/
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 };
// internals
var scope = this;
var EPS = 0.000001;
var PIXELS_PER_ROUND = 1800;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var zoomStart = new THREE.Vector2();
var zoomEnd = new THREE.Vector2();
var zoomDelta = new THREE.Vector2();
var phiDelta = 0;
var thetaDelta = 0;
var scale = 1;
var lastPosition = new THREE.Vector3();
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
var state = STATE.NONE;
// events
var changeEvent = { type: 'change' };
this.rotateLeft = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
thetaDelta -= angle;
};
this.rotateRight = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
thetaDelta += angle;
};
this.rotateUp = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
phiDelta -= angle;
};
this.rotateDown = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
phiDelta += angle;
};
this.zoomIn = function ( zoomScale ) {
if ( zoomScale === undefined ) {
zoomScale = getZoomScale();
}
scale /= zoomScale;
};
this.zoomOut = function ( zoomScale ) {
if ( zoomScale === undefined ) {
zoomScale = getZoomScale();
}
scale *= zoomScale;
};
this.pan = function ( distance ) {
distance.transformDirection( this.object.matrix );
distance.multiplyScalar( scope.userPanSpeed );
this.object.position.add( distance );
this.center.add( distance );
};
this.update = function () {
var position = this.object.position;
var offset = position.clone().sub( this.center );
// angle from z-axis around y-axis
var theta = Math.atan2( offset.x, offset.z );
// angle from y-axis
var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
if ( this.autoRotate ) {
this.rotateLeft( getAutoRotationAngle() );
}
theta += thetaDelta;
phi += phiDelta;
// restrict phi to be between desired limits
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
// restrict phi to be betwee EPS and PI-EPS
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
var radius = offset.length() * scale;
// restrict radius to be between desired limits
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
offset.x = radius * Math.sin( phi ) * Math.sin( theta );
offset.y = radius * Math.cos( phi );
offset.z = radius * Math.sin( phi ) * Math.cos( theta );
position.copy( this.center ).add( offset );
this.object.lookAt( this.center );
thetaDelta = 0;
phiDelta = 0;
scale = 1;
if ( lastPosition.distanceTo( this.object.position ) > 0 ) {
this.dispatchEvent( changeEvent );
lastPosition.copy( this.object.position );
}
};
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow( 0.95, scope.userZoomSpeed );
}
function onMouseDown( event ) {
if ( scope.enabled === false ) return;
if ( scope.userRotate === false ) return;
event.preventDefault();
if ( state === STATE.NONE )
{
if ( event.button === 0 )
state = STATE.ROTATE;
if ( event.button === 1 )
state = STATE.ZOOM;
if ( event.button === 2 )
state = STATE.PAN;
}
if ( state === STATE.ROTATE ) {
//state = STATE.ROTATE;
rotateStart.set( event.clientX, event.clientY );
} else if ( state === STATE.ZOOM ) {
//state = STATE.ZOOM;
zoomStart.set( event.clientX, event.clientY );
} else if ( state === STATE.PAN ) {
//state = STATE.PAN;
}
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
scope.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed );
scope.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed );
rotateStart.copy( rotateEnd );
} else if ( state === STATE.ZOOM ) {
zoomEnd.set( event.clientX, event.clientY );
zoomDelta.subVectors( zoomEnd, zoomStart );
if ( zoomDelta.y > 0 ) {
scope.zoomIn();
} else {
scope.zoomOut();
}
zoomStart.copy( zoomEnd );
} else if ( state === STATE.PAN ) {
var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );
}
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
if ( scope.userRotate === false ) return;
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
state = STATE.NONE;
}
function onMouseWheel( event ) {
if ( scope.enabled === false ) return;
if ( scope.userZoom === false ) return;
var delta = 0;
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
} else if ( event.detail ) { // Firefox
delta = - event.detail;
}
if ( delta > 0 ) {
scope.zoomOut();
} else {
scope.zoomIn();
}
}
function onKeyDown( event ) {
if ( scope.enabled === false ) return;
if ( scope.userPan === false ) return;
switch ( event.keyCode ) {
/*case scope.keys.UP:
scope.pan( new THREE.Vector3( 0, 1, 0 ) );
break;
case scope.keys.BOTTOM:
scope.pan( new THREE.Vector3( 0, - 1, 0 ) );
break;
case scope.keys.LEFT:
scope.pan( new THREE.Vector3( - 1, 0, 0 ) );
break;
case scope.keys.RIGHT:
scope.pan( new THREE.Vector3( 1, 0, 0 ) );
break;
*/
case scope.keys.ROTATE:
state = STATE.ROTATE;
break;
case scope.keys.ZOOM:
state = STATE.ZOOM;
break;
case scope.keys.PAN:
state = STATE.PAN;
break;
}
}
function onKeyUp( event ) {
switch ( event.keyCode ) {
case scope.keys.ROTATE:
case scope.keys.ZOOM:
case scope.keys.PAN:
state = STATE.NONE;
break;
}
}
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
this.domElement.addEventListener( 'mousedown', onMouseDown, false );
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
window.addEventListener( 'keydown', onKeyDown, false );
window.addEventListener( 'keyup', onKeyUp, false );
};
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment