@adhiban/three-mesh-ui
Version:
a library on top of three.js to help in creating 3D user interfaces, with minor changes ;)
426 lines (287 loc) • 10.2 kB
JavaScript
import * as THREE from 'three';
import { VRButton } from 'three/examples/jsm/webxr/VRButton.js';
import { BoxLineGeometry } from 'three/examples/jsm/geometries/BoxLineGeometry.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
import ThreeMeshUI from '../src/three-mesh-ui.js';
import VRControl from './utils/VRControl.js';
import ShadowedLight from './utils/ShadowedLight.js';
import FontJSON from './assets/Roboto-msdf.json';
import FontImage from './assets/Roboto-msdf.png';
let scene, camera, renderer, controls, vrControl;
let meshContainer, meshes, currentMesh;
const objsToTest = [];
window.addEventListener( 'load', init );
window.addEventListener( 'resize', onWindowResize );
// compute mouse position in normalized device coordinates
// (-1 to +1) for both directions.
// Used to raycasting against the interactive elements
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
mouse.x = mouse.y = null;
let selectState = false;
window.addEventListener( 'pointermove', ( event ) => {
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = -( event.clientY / window.innerHeight ) * 2 + 1;
} );
window.addEventListener( 'pointerdown', () => {
selectState = true;
} );
window.addEventListener( 'pointerup', () => {
selectState = false;
} );
window.addEventListener( 'touchstart', ( event ) => {
selectState = true;
mouse.x = ( event.touches[ 0 ].clientX / window.innerWidth ) * 2 - 1;
mouse.y = -( event.touches[ 0 ].clientY / window.innerHeight ) * 2 + 1;
} );
window.addEventListener( 'touchend', () => {
selectState = false;
mouse.x = null;
mouse.y = null;
} );
//
function init() {
////////////////////////
// Basic Three Setup
////////////////////////
scene = new THREE.Scene();
scene.background = new THREE.Color( 0x505050 );
camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.xr.enabled = true;
document.body.appendChild( VRButton.createButton( renderer ) );
document.body.appendChild( renderer.domElement );
// Orbit controls for no-vr
controls = new OrbitControls( camera, renderer.domElement );
camera.position.set( 0, 1.6, 0 );
controls.target = new THREE.Vector3( 0, 1, -1.8 );
/////////
// Room
/////////
const room = new THREE.LineSegments(
new BoxLineGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ),
new THREE.LineBasicMaterial( { color: 0x808080 } )
);
const roomMesh = new THREE.Mesh(
new THREE.BoxGeometry( 6, 6, 6, 10, 10, 10 ).translate( 0, 3, 0 ),
new THREE.MeshBasicMaterial( { side: THREE.BackSide } )
);
scene.add( room );
objsToTest.push( roomMesh );
//////////
// Light
//////////
const light = ShadowedLight( {
z: 10,
width: 6,
bias: -0.0001
} );
const hemLight = new THREE.HemisphereLight( 0x808080, 0x606060 );
scene.add( light, hemLight );
////////////////
// Controllers
////////////////
vrControl = VRControl( renderer, camera, scene );
scene.add( vrControl.controllerGrips[ 0 ], vrControl.controllers[ 0 ] );
vrControl.controllers[ 0 ].addEventListener( 'selectstart', () => {
selectState = true;
} );
vrControl.controllers[ 0 ].addEventListener( 'selectend', () => {
selectState = false;
} );
////////////////////
// Primitive Meshes
////////////////////
meshContainer = new THREE.Group();
meshContainer.position.set( 0, 1, -1.9 );
scene.add( meshContainer );
//
const sphere = new THREE.Mesh(
new THREE.IcosahedronBufferGeometry( 0.3, 1 ),
new THREE.MeshStandardMaterial( { color: 0x3de364, flatShading: true } )
);
const box = new THREE.Mesh(
new THREE.BoxBufferGeometry( 0.45, 0.45, 0.45 ),
new THREE.MeshStandardMaterial( { color: 0x643de3, flatShading: true } )
);
const cone = new THREE.Mesh(
new THREE.ConeBufferGeometry( 0.28, 0.5, 10 ),
new THREE.MeshStandardMaterial( { color: 0xe33d4e, flatShading: true } )
);
//
sphere.visible = box.visible = cone.visible = false;
meshContainer.add( sphere, box, cone );
meshes = [ sphere, box, cone ];
currentMesh = 0;
showMesh( currentMesh );
//////////
// Panel
//////////
makePanel();
//
renderer.setAnimationLoop( loop );
}
// Shows the primitive mesh with the passed ID and hide the others
function showMesh( id ) {
meshes.forEach( ( mesh, i ) => {
mesh.visible = i === id ? true : false;
} );
}
///////////////////
// UI contruction
///////////////////
function makePanel() {
// Container block, in which we put the two buttons.
// We don't define width and height, it will be set automatically from the children's dimensions
// Note that we set contentDirection: "row-reverse", in order to orient the buttons horizontally
const container = new ThreeMeshUI.Block( {
justifyContent: 'center',
contentDirection: 'row-reverse',
fontFamily: FontJSON,
fontTexture: FontImage,
fontSize: 0.07,
padding: 0.02,
borderRadius: 0.11
} );
container.position.set( 0, 0.6, -1.2 );
container.rotation.x = -0.55;
scene.add( container );
// BUTTONS
// We start by creating objects containing options that we will use with the two buttons,
// in order to write less code.
const buttonOptions = {
width: 0.4,
height: 0.15,
justifyContent: 'center',
offset: 0.05,
margin: 0.02,
borderRadius: 0.075
};
// Options for component.setupState().
// It must contain a 'state' parameter, which you will refer to with component.setState( 'name-of-the-state' ).
const hoveredStateAttributes = {
state: 'hovered',
attributes: {
offset: 0.035,
backgroundColor: new THREE.Color( 0x999999 ),
backgroundOpacity: 1,
fontColor: new THREE.Color( 0xffffff )
},
};
const idleStateAttributes = {
state: 'idle',
attributes: {
offset: 0.035,
backgroundColor: new THREE.Color( 0x666666 ),
backgroundOpacity: 0.3,
fontColor: new THREE.Color( 0xffffff )
},
};
// Buttons creation, with the options objects passed in parameters.
const buttonNext = new ThreeMeshUI.Block( buttonOptions );
const buttonPrevious = new ThreeMeshUI.Block( buttonOptions );
// Add text to buttons
buttonNext.add(
new ThreeMeshUI.Text( { content: 'next' } )
);
buttonPrevious.add(
new ThreeMeshUI.Text( { content: 'previous' } )
);
// Create states for the buttons.
// In the loop, we will call component.setState( 'state-name' ) when mouse hover or click
const selectedAttributes = {
offset: 0.02,
backgroundColor: new THREE.Color( 0x777777 ),
fontColor: new THREE.Color( 0x222222 )
};
buttonNext.setupState( {
state: 'selected',
attributes: selectedAttributes,
onSet: () => {
currentMesh = ( currentMesh + 1 ) % 3;
showMesh( currentMesh );
}
} );
buttonNext.setupState( hoveredStateAttributes );
buttonNext.setupState( idleStateAttributes );
//
buttonPrevious.setupState( {
state: 'selected',
attributes: selectedAttributes,
onSet: () => {
currentMesh -= 1;
if ( currentMesh < 0 ) currentMesh = 2;
showMesh( currentMesh );
}
} );
buttonPrevious.setupState( hoveredStateAttributes );
buttonPrevious.setupState( idleStateAttributes );
//
container.add( buttonNext, buttonPrevious );
objsToTest.push( buttonNext, buttonPrevious );
}
// Handle resizing the viewport
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
//
function loop() {
// Don't forget, ThreeMeshUI must be updated manually.
// This has been introduced in version 3.0.0 in order
// to improve performance
ThreeMeshUI.update();
controls.update();
meshContainer.rotation.z += 0.01;
meshContainer.rotation.y += 0.01;
renderer.render( scene, camera );
updateButtons();
}
// Called in the loop, get intersection with either the mouse or the VR controllers,
// then update the buttons states according to result
function updateButtons() {
// Find closest intersecting object
let intersect;
if ( renderer.xr.isPresenting ) {
vrControl.setFromController( 0, raycaster.ray );
intersect = raycast();
// Position the little white dot at the end of the controller pointing ray
if ( intersect ) vrControl.setPointerAt( 0, intersect.point );
} else if ( mouse.x !== null && mouse.y !== null ) {
raycaster.setFromCamera( mouse, camera );
intersect = raycast();
}
// Update targeted button state (if any)
if ( intersect && intersect.object.isUI ) {
if ( selectState ) {
// Component.setState internally call component.set with the options you defined in component.setupState
intersect.object.setState( 'selected' );
} else {
// Component.setState internally call component.set with the options you defined in component.setupState
intersect.object.setState( 'hovered' );
}
}
// Update non-targeted buttons state
objsToTest.forEach( ( obj ) => {
if ( ( !intersect || obj !== intersect.object ) && obj.isUI ) {
// Component.setState internally call component.set with the options you defined in component.setupState
obj.setState( 'idle' );
}
} );
}
//
function raycast() {
return objsToTest.reduce( ( closestIntersection, obj ) => {
const intersection = raycaster.intersectObject( obj, true );
if ( !intersection[ 0 ] ) return closestIntersection;
if ( !closestIntersection || intersection[ 0 ].distance < closestIntersection.distance ) {
intersection[ 0 ].object = obj;
return intersection[ 0 ];
}
return closestIntersection;
}, null );
}