// Global ATON import AppState from "./state.js"; import { config } from "../config.js"; import UI from "./ui.js"; const Scene = {}; Scene.UI = UI; /** * @todo Experimental... * @param {THREE.Object3D} object - A THREE Object3D instance */ Scene.showEdges = function(object) { const edgeMaterial = new THREE.LineBasicMaterial( { color: 0x000000 } ); object.traverse(function(child) { if (child.isMesh) { let edges = new THREE.EdgesGeometry(child.geometry, 45); let line = new THREE.LineSegments(edges, edgeMaterial); child.add(line); console.log(child); } }); } /** * Calculate bounding box for the scene root object * and return it along with its center and size (bad?). * It only uses meshes to prevent "empty" nodes in the scene * from being included in the calculation. * @todo Use ATON.Node.getBound()? [bounding sphere] */ Scene.getRootBoundingBox = function() { const meshes = []; AppState.root.traverse(obj => { if (obj.isMesh) meshes.push(obj); }); if (meshes.length === 0) return null; const bbox = new THREE.Box3().setFromObject(meshes[0]); for (let i = 1; i < meshes.length; i++) { bbox.union(new THREE.Box3().setFromObject(meshes[i])); } const center = bbox.getCenter(new THREE.Vector3()); const size = bbox.getSize(new THREE.Vector3()); return { bbox, center, size }; } /** * * @param {THREE.Sphere} boundingSphere - The bounding sphere for the main node * @returns {THREE.Mesh} */ Scene.createClippingPlaneMesh = function (boundingSphere) { const planeSize = boundingSphere.radius * 1.5; const mesh = new THREE.Mesh( new THREE.PlaneGeometry(planeSize, planeSize), new THREE.MeshBasicMaterial({ color: 0xffff00, opacity: 0.1, side: THREE.DoubleSide, transparent: true }) ); return mesh; } /** * * @param {THREE.Mesh} planeMesh * @param {THREE.ArrowHelper} arrowHelper * @param {String} axis */ Scene.dragClipper = function(planeMesh, axis) { const controls = new THREE.DragControls( [planeMesh], ATON.Nav._camera, ATON._renderer.domElement, ); const startPosition = new THREE.Vector3(); // Only move along the selected axis (exlude the others) const excludedAxes = ['x', 'y', 'z'].filter(a => a != axis); if (AppState.clipping.enabled && AppState.clipping.vector) { controls.addEventListener('dragstart', function (event) { startPosition.copy(event.object.position); ATON.Nav.setUserControl(false); }); controls.addEventListener('drag', function(event) { const point = event.object.position; Scene.updateClipper(AppState.clipping.vector, point); for (const a of excludedAxes) { event.object.position[a] = startPosition[a]; } }); controls.addEventListener('dragend', function (event) { ATON.Nav.setUserControl(true); }); AppState.clipping.controls = controls; } } /** * @param {String} axis - The axis along which the plane's normal should be directed, * one of 'x', 'y', 'z' * @param {Number} orientation - Positive (1) or negative (-1) orientation on the axis */ Scene.addClippingPlane = function(axis, orientation = -1) { axis = axis.toLowerCase(); const bound = AppState.clipping.boundingSphere; if (!bound) return; const vector = [ axis === 'x' ? orientation : 0, axis === 'y' ? orientation : 0, axis === 'z' ? orientation : 0, ]; AppState.clipping.vector = vector; // First, add a default clipping plane // at a default point (calculated...) const defaultPoint = bound.center.clone(); Scene.activateClipper(vector, axis, defaultPoint); } /** * @todo WIP! * Activate clipping plane * @param {Number[]} vector - The vector array to direct the plane * @param {String} axis - The x,y,z axis * @param {?THREE.Vector3} point - The queried scene point */ Scene.activateClipper = function(vector, axis, point = null) { ATON.enableClipPlanes(); Scene.updateClipper(vector, point); Scene.dragClipper(AppState.clipping.helper, axis); } /** * * @param {THREE.Vector3} vector * @param {THREE.Vector3} point */ Scene.updateClipper = function(vector, point) { // Useless guard... if (vector) { // Normal of the clipping plane along the axis facing down const normal = new THREE.Vector3(...vector).normalize(); const plane = AppState.clipping.plane ?? ATON.addClipPlane(normal, point); // Add a visible plane helper for the clipping plane const visiblePlane = AppState.clipping.helper ?? Scene.createClippingPlaneMesh(AppState.clipping.boundingSphere); if (!AppState.clipping.helper) { AppState.root.add(visiblePlane); AppState.clipping.helper = visiblePlane; } visiblePlane.position.copy(point); visiblePlane.lookAt(point.clone().add(normal)); plane.setFromNormalAndCoplanarPoint(normal, point); AppState.clipping.plane = plane; } } /** * * @param {THREE.Vector3} vector - An object with x,y,z coordinates */ Scene.changeLightDirection = function(vector) { ATON.setMainLightDirection(vector); } /** * * @param {Boolean} isEnabled */ Scene.toggleAmbientOcclusion = function(isEnabled) { ATON.FX.togglePass(ATON.FX.PASS_AO, isEnabled); console.log('Ambient occlusion', isEnabled ? 'ON' : 'OFF'); } Scene.init = function() { ATON.realize(); ATON.UI.addBasicEvents(); ATON.UI.init(); // All assets for this app are stored here ATON.setPathCollection('/a/scaenae/assets/'); // Initial light direction ATON.setMainLightDirection(new THREE.Vector3(0.2,-0.3,-0.7)); ATON.toggleShadows(true); ATON.setExposure(config.scene.initialExposure); // Open settings side panel when clicking on settings btn Scene.UI.toggleSettingsPanel('settings'); Scene.UI.toggleContentMenu('menu'); AppState.camera = ATON.Nav._camera; AppState.renderer = ATON._renderer; ATON.Nav.setUserControl(true); } /** * Reset clipping state after disabling from UI * @todo DragControls errors!! */ Scene.resetClipping = function () { AppState.clipping.enabled = false; ATON.disableClipPlanes(); AppState.clipping.controls.deactivate(); // Manually remove event listeners from DragControls!! AppState.renderer.domElement.removeEventListener( 'pointermove', AppState.clipping.controls.onPointerMove ); AppState.renderer.domElement.removeEventListener( 'pointerdown', AppState.clipping.controls.onPointerDown ); AppState.renderer.domElement.removeEventListener( 'pointerup', AppState.clipping.controls.onPointerCancel ); AppState.renderer.domElement.removeEventListener( 'pointerleave', AppState.clipping.controls.onPointerCancel ) AppState.clipping.controls = null; AppState.clipping.helper.removeFromParent(); AppState.root.remove(AppState.clipping.helper); AppState.clipping.helper = null; AppState.clipping.plane = null; AppState.clipping.vector = null; // Ensure nav controls are reactivated! ATON.Nav.setUserControl(true); } /** * @param {Object} marker - The marker object from config */ Scene.openScene = function(marker) { Scene.init(); Scene.UI.toggleClipperBar('#clipper', '#clipper-bar'); // Load 3D models and create nodes Scene.loadNodes(marker.nodes); ATON.setMainPanorama(marker.pano); // TODO: hardcoded... AppState.initialRotation = new THREE.Vector3(0, 1.5, 0); //Scene.showEdges(mainNode); ATON.setAutoLP(config.scene.autoLP); AppState.lightProbe = config.scene.autoLP; Scene.toggleAmbientOcclusion(true); AppState.ambientOcclusion = true; AppState.root = ATON.getRootScene(); } /** * * @param {Array} nodes */ Scene.loadNodes = function (nodes) { nodes.forEach(n => { let node = ATON.createSceneNode(n.label); node.load(n.model); node.setRotation(0, 1.5, 0); node.attachToRoot(); if (n.isMain) { AppState.mainNodeId = n.label; // ATON.Node.getBound() returns a THREE.Sphere object AppState.clipping.boundingSphere = node.getBound(); } AppState.nodes.push({id: n.label, active: true}); }); } export default Scene;