/**
* MLJLib
* MeshLabJS Library
*
* Copyright(C) 2015
* Paolo Cignoni
* Visual Computing Lab
* ISTI - CNR
*
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See theGNU General Public License
* (http://www.gnu.org/licenses/gpl.txt) for more details.
*
*/
/**
* @file
*
* @author Stefano Gabriele
*/
/**
* The MLJ.core.Scene namespace defines the functions to manage the scene,
* i.e. the set of mesh layers that constitute the ''document'' of the MeshLabJS system.
* This namespace also actually stores the set of meshes, the reference to current mesh,
* the threejs container for the scene, the threejs camera and the threejs renderer
* (e.g. the webgl context where the scene is rendered).
*
* @namespace MLJ.core.Scene
* @memberOf MLJ.core
* @author Stefano Gabriele
*
*/
MLJ.core.Scene = {};
(function () {
/**
* Associative Array that contains all the meshes in the scene
* @type MLJ.util.AssociativeArray
* @memberOf MLJ.core.Scene
*/
var _layers = new MLJ.util.AssociativeArray();
/**
* Associative array that contains all the scene level "background"
* decorators (axes, background grid etc..)
* @type MLJ.util.AssociativeArray
* @memberOf MLJ.core.Scene
*/
var _decorators = new MLJ.util.AssociativeArray();
/**
* Associative array that contains all the currently active post process
* rendering passes. For details see {@link MLJ.core.Scene.addPostProcessPass}.
* @type MLJ.util.AssociativeArray
* @memberOf MLJ.core.Scene
*/
var _postProcessPasses = new MLJ.util.AssociativeArray();
/**
* Reference to current layer
* @type MLJ.core.Layer
* @memberOf MLJ.core.Scene
*/
var _selectedLayer;
/**
* It contains the ThreeJs Representation of the current set of layers.
* Each Layer is associated to a ThreeJS mesh whose contained in the MLJ.core.MeshFile object.
* @type THREE.Scene
* @memberOf MLJ.core.Scene
*/
var _scene;
/**
* The ThreeJs group that contains all the layers.
* It also store the global transformation (scale + translation)
* that brings the global bbox of the scene
* in the origin of the camera reference system.
* @type THREE.Object
* @memberOf MLJ.core.Scene
*/
var _group;
var _camera;
/**
* This scene contains 2D overlays that are drawn on top of everything else
* @memberOf MLJ.core.Scene
*/
var _scene2D;
/**
* "Fake" camera object passed to the renderer when rendering the <code>_scene2D</code>
*/
var _camera2D;
var _stats;
var _controls;
/// @type {Object}
var _renderer;
var _this = this;
function get3DSize() {
var _3D = $('#_3D');
return {
width: _3D.innerWidth (),
height: _3D.innerHeight()
};
}
function initDragAndDrop() {
function FileDragHandler(e) {
e.stopPropagation();
e.preventDefault();
var files = e.target.files || e.dataTransfer.files;
MLJ.core.File.openMeshFile(files);
}
function FileDragHover(e) {
e.stopPropagation();
e.preventDefault();
}
$(window).ready(function () {
var ddd = document.getElementById("_3D");
ddd.addEventListener("dragover", FileDragHover, false);
ddd.addEventListener("dragleave", FileDragHover, false);
ddd.addEventListener("drop", FileDragHandler, false);
});
}
function initStats() {
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms
stats.active = false;
// Align top-right
stats.domElement.style.visibility = 'hidden';
stats.domElement.style.position = 'absolute';
stats.domElement.style.right = '0px';
stats.domElement.style.top = '0px';
stats.domElement.style.zIndex = 100;
$("#_3D").append( stats.domElement );
return stats;
}
//SCENE INITIALIZATION ________________________________________________________
function initScene() {
var _3DSize = get3DSize();
_scene = new THREE.Scene();
_camera = new THREE.PerspectiveCamera(45, _3DSize.width / _3DSize.height, 0.1, 1800);
_camera.position.z = 15;
_group = new THREE.Object3D();
_scene.add(_group);
_scene2D = new THREE.Scene();
_camera2D = new THREE.OrthographicCamera(0 , _3DSize.width / _3DSize.height, 1, 0, -1, 1);
_camera2D.position.z = -1;
_renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
preserveDrawingBuffer:true});
//_renderer.shadowMapEnabled = true;
//_renderer.context.getSupportedExtensions();
_renderer.context.getExtension("EXT_frag_depth");
_renderer.setPixelRatio( window.devicePixelRatio );
_renderer.setSize(_3DSize.width, _3DSize.height);
$('#_3D').append(_renderer.domElement);
_scene.add(_camera);
_stats = initStats();
/*
requestAnimationFrame(function updateStats() {
_stats.update();
requestAnimationFrame(updateStats); });
*/
//INIT CONTROLS
var container = document.getElementsByTagName('canvas')[0];
_controls = new THREE.TrackballControls(_camera, container);
_controls.rotateSpeed = 4.0;
_controls.zoomSpeed = 1.2;
_controls.panSpeed = 2.0;
_controls.noZoom = false;
_controls.noPan = false;
_controls.staticMoving = true;
_controls.dynamicDampingFactor = 0.3;
_controls.keys = [65, 83, 68];
$(document).keydown(function(event) {
if((event.ctrlKey || (event.metaKey && event.shiftKey)) && event.which === 72) {
event.preventDefault();
_controls.reset();
}
});
//INIT LIGHTS
_this.lights.AmbientLight = new MLJ.core.AmbientLight(_scene, _camera, _renderer);
_this.lights.Headlight = new MLJ.core.Headlight(_scene, _camera, _renderer);
//EVENT HANDLERS
var $canvas = $('canvas')[0];
$canvas.addEventListener('touchmove', _controls.update.bind(_controls), false);
$canvas.addEventListener('mousemove', _controls.update.bind(_controls), false);
$canvas.addEventListener('mousewheel', _controls.update.bind(_controls), false);
$canvas.addEventListener('DOMMouseScroll', _controls.update.bind(_controls), false ); // firefox
_controls.addEventListener('change', function () {
MLJ.core.Scene.render();
$($canvas).trigger('onControlsChange');
});
$(window).resize(function () {
var size = get3DSize();
_camera.aspect = size.width / size.height;
_camera.updateProjectionMatrix();
_renderer.setSize(size.width, size.height);
colorBuffer.setSize(size.width, size.height);
targetBuffer.setSize(size.width, size.height);
_camera2D.left = size.width / size.height;
_camera2D.updateProjectionMatrix;
MLJ.core.Scene.render();
});
$(document).on("MeshFileOpened",
function (event, layer) {
MLJ.core.Scene.addLayer(layer);
});
$(document).on("MeshFileReloaded",
function (event, layer) {
// Restore three geometry to reflect the new state of the vcg mesh
layer.updateThreeMesh();
/**
* Triggered when a layer is reloaded
* @event MLJ.core.Scene#SceneLayerReloaded
* @type {Object}
* @property {MLJ.core.Layer} layer The reloaded mesh file
* @example
* <caption>Event Interception:</caption>
* $(document).on("SceneLayerReloaded",
* function (event, layer) {
* //do something
* }
* );
*/
$(document).trigger("SceneLayerReloaded", [layer]);
});
}
/* Compute global bounding box and translate and scale every object in proportion
* of global bounding box. First translate every object into original position,
* then scale all by reciprocal value of scale factor (note that scale factor
* and original position are stored into mesh object). Then it computes
* global bbox, scale every object, recalculate global bbox and finally
* translate every object in a right position.
*/
function _computeGlobalBBbox()
{
console.time("Time to update bbox: ");
_group.scale.set(1,1,1);
_group.position.set(0,0,0);
_group.updateMatrixWorld();
if (_layers.size() === 0) // map to the canonical cube
BBGlobal = new THREE.Box3(new THREE.Vector3(-1,-1,-1), new THREE.Vector3(1,1,1));
else {
BBGlobal = new THREE.Box3();
BBGlobal.setFromObject(_group);
}
var scaleFac = 15.0 / (BBGlobal.min.distanceTo(BBGlobal.max));
var offset = BBGlobal.center().negate();
offset.multiplyScalar(scaleFac);
_group.scale.set(scaleFac,scaleFac,scaleFac);
_group.position.set(offset.x,offset.y,offset.z);
_group.updateMatrixWorld();
//console.log("Position:" + offset.x +" "+ offset.y +" "+ offset.z );
//console.log("ScaleFactor:" + _group.scale.x +" "+ _group.scale.x +" "+ _group.scale.x);
//console.timeEnd("Time to update bbox: ");
return BBGlobal;
}
this.getBBox = function () {
return _computeGlobalBBbox();
}
this.lights = {
AmbientLight: null,
Headlight: null
};
this.getCamera = function() {
return _camera;
};
this.getStats = function() {
return _stats;
}
this.getThreeJsGroup = function() {
return _group;
}
/**
* Selects the layer with the name <code>layerName</code>
* @param {String} layerName The name of the layer
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.selectLayerByName = function (layerName) {
_selectedLayer = _layers.getByKey(layerName);
/**
* Triggered when a layer is selected
* @event MLJ.core.Scene#SceneLayerSelected
* @type {Object}
* @property {MLJ.core.Layer} layer The selected mesh file
* @example
* <caption>Event Interception:</caption>
* $(document).on("SceneLayerSelected",
* function (event, layer) {
* //do something
* }
* );
*/
$(document).trigger("SceneLayerSelected", [_selectedLayer]);
};
/**
* Sets the visibility of layer with the name <code>layerName</code>
* @param {String} layerName The name of the layer
* @param {Boolean} visible <code>true</code> if the layers must be visible,
* <code>false</code> otherwise
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.setLayerVisible = function (layerName, visible) {
var layer = _layers.getByKey(layerName);
layer.getThreeMesh().visible = visible;
var iter = layer.overlays.iterator();
while(iter.hasNext()) {
iter.next().visible = visible;
}
// if histogram overlay is defined show/hide labels
if (layer.__mlj_histogram) {
if (visible) layer.__mlj_histogram.show();
else layer.__mlj_histogram.hide();
}
MLJ.core.Scene.render();
};
/**
* Adds a new layer in the scene
* @param {MLJ.core.Layer} layer The mesh file to add
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.addLayer = function (layer) {
if (!(layer instanceof MLJ.core.Layer)) {
console.error("The parameter must be an instance of MLJ.core.Layer");
return;
}
// Initialize the THREE geometry used by overlays and rendering params
layer.initializeRenderingAttributes();
_group.add(layer.getThreeMesh());
//Add new mesh to associative array _layers
_layers.set(layer.name, layer);
_selectedLayer = layer;
_computeGlobalBBbox();
/**
* Triggered when a layer is added
* @event MLJ.core.Scene#SceneLayerAdded
* @type {Object}
* @property {MLJ.core.Layer} layer The last mesh file added
* @property {Integer} layersNumber The number of layers in the scene
* @example
* <caption>Event Interception:</caption>
* $(document).on("SceneLayerAdded",
* function (event, layer, layersNumber) {
* //do something
* }
* );
*/
$(document).trigger("SceneLayerAdded", [layer, _layers.size()]);
//render the scene
_this.render();
};
this.addOverlayLayer = function(layer, name, mesh, overlay2D) {
if(!(mesh instanceof THREE.Object3D)) {
console.warn("mesh parameter must be an instance of THREE.Object3D");
return;
}
layer.overlays.set(name,mesh);
mesh.visible = layer.getThreeMesh().visible;
if (overlay2D) {
_scene2D.add(mesh);
} else {
// _group.add(mesh);
layer.getThreeMesh().add(mesh);
}
_this.render();
};
function disposeObject(obj) {
if (obj.geometry) obj.geometry.dispose();
if (obj.material) obj.material.dispose();
if (obj.texture) obj.texture.dispose();
}
this.removeOverlayLayer = function(layer, name, overlay2D) {
var mesh = layer.overlays.getByKey(name);
if (mesh !== undefined) {
mesh = layer.overlays.remove(name);
if (overlay2D) {
_scene2D.remove(mesh);
} else {
layer.getThreeMesh().remove(mesh);
// _group.remove(mesh);
}
mesh.traverse(disposeObject);
disposeObject(mesh);
_this.render();
}
};
/**
* Updates a layer. This function should be called if the <code>layer</code>
* geometry or properties was modified.
* @param {MLJ.core.Layer} layer The mesh file corresponding to the level
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
* @example
* //Apply Laplacian smooth filter
* Module.LaplacianSmooth(layer.ptrMesh, 1, false);
* //The filter has changed mesh geometry ...
* scene.updateLayer(layer);
*/
this.updateLayer = function (layer) {
if (layer instanceof MLJ.core.Layer) {
if (_layers.getByKey(layer.name) === undefined) {
console.warn("Trying to update a layer not in the scene.");
return;
}
layer.updateThreeMesh();
_computeGlobalBBbox();
//render the scene
this.render();
/**
* Triggered when a layer is updated
* @event MLJ.core.Scene#SceneLayerUpdated
* @type {Object}
* @property {MLJ.core.Layer} layer The updated mesh file
* @example
* <caption>Event Interception:</caption>
* $(document).on("SceneLayerUpdated",
* function (event, layer) {
* //do something
* }
* );
*/
$(document).trigger("SceneLayerUpdated", [layer]);
} else {
console.error("The parameter must be an instance of MLJ.core.Layer");
}
};
/**
* Returns the layer corresponding to the given name
* @param {String} name The name of the layer
* @memberOf MLJ.core.Scene
* @return {MLJ.core.Layer} The layer corresponding to the given name
* @author Stefano Gabriele
*/
this.getLayerByName = function (name) {
return _layers.getByKey(name);
};
function disambiguateName(meshName) {
var prefix, ext;
var ptIndex = meshName.lastIndexOf('.');
if (ptIndex > 0) {
prefix = meshName.substr(0, ptIndex);
ext = meshName.substr(ptIndex);
} else {
prefix = meshName;
ext = "";
}
if (/\[(\d+)\]$/.test(prefix)) {
prefix = prefix.substr(0, prefix.lastIndexOf("["));
}
var maxNumTag = 0;
while (true) {
var collision = false;
var layerIterator = MLJ.core.Scene.getLayers().iterator();
while (layerIterator.hasNext() && !collision) {
if (meshName === layerIterator.next().name) collision = true;
}
if (collision) meshName = prefix + "[" + ++maxNumTag + "]" + ext;
else break;
}
return meshName;
}
/**
* Creates a new mesh file using the c++ functions bound to JavaScript
* @param {String} name The name of the new mesh file
* @memberOf MLJ.core.File
* @returns {MLJ.core.Layer} The new layer
* @author Stefano Gabriele
*/
this.createLayer = function (name) {
var layerName = disambiguateName(name);
var layer = new MLJ.core.Layer(layerName, new Module.CppMesh());
return layer;
};
/**
* Removes the layer corresponding to the given name
* @param {String} name The name of the layer which must be removed
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.removeLayerByName = function (name) {
var layer = this.getLayerByName(name);
if (layer !== undefined) {
//remove layer from list
_layers.remove(name);
_group.remove(layer.getThreeMesh());
$(document).trigger("SceneLayerRemoved", [layer, _layers.size()]);
layer.dispose();
if(_layers.size() > 0) {
_this.selectLayerByName(_layers.getFirst().name);
} else {
_this._selectedLayer = undefined;
}
_computeGlobalBBbox();
MLJ.core.Scene.render();
}
};
/**
* Adds a scene decorator. A scene decorator differs fron an overlay layer in
* that it's not tied to a particular layer, but to the scene as a whole (for
* example an axes descriptor that highlights the direction of the x, y, and z
* coordinates).
* @param {String} name - The name of the decorator
* @param {THREE.Object3D} decorator - The decorator object
* @memberOf MLJ.core.Scene
*/
this.addSceneDecorator = function(name, decorator) {
if(!(decorator instanceof THREE.Object3D)) {
console.warn("MLJ.core.Scene.addSceneDecorator(): decorator parameter not an instance of THREE.Object3D");
return;
}
_decorators.set(name, decorator)
_group.add(decorator);
_this.render();
};
/**
* Removes a decorator object from the scene.
* @param {String} name - The name of the decorator to remove
* @memberOf MLJ.core.Scene
*/
this.removeSceneDecorator = function(name) {
var decorator = _decorators.getByKey(name)
if (decorator !== undefined) {
_decorators.remove(name);
_group.remove(decorator);
decorator.geometry.dispose();
decorator.material.dispose();
} else {
console.warn("Warning: " + name + " decorator not in the scene");
}
//render the scene
_this.render();
};
/**
* Returns the currently selected layer
* @returns {MLJ.core.Layer} The currently selected layer
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.getSelectedLayer = function () {
return _selectedLayer;
};
/**
* Returns the layers list
* @returns {MLJ.util.AssociativeArray} The layers list
* @memberOf MLJ.core.Scene
* @author Stefano Gabriele
*/
this.getLayers = function () {
return _layers;
};
this.get3DSize = function() { return get3DSize(); };
this.getRenderer = function() { return _renderer; };
this.getScene = function () {return _scene;};
/**
* Adds a post process pass to the rendering chain. As of now the interface
* to post process effects is minimal: an effect is simply a callable object
* that must accept two render buffers as parameters. The first buffer
* contains the color generated by the rendering chain of MeshLabJS up to
* the effect invocation, (this includes the basic scene rendering plus the
* result of any post process effect that was applied before the current
* one). The second buffer must be used as the render target of the pass,
* will be forwarded as input to the next effect, or will be transfered to
* the canvas if no other effects are active. Both buffers have the same
* size as the page canvas. Any other information that may be needed by an
* effect must be passed with closure variables or queried directly to
* {@link MLJ.core.Scene}.
*
* @param {String} name - The name of the pass
* @param {Object} pass - The callable (function) object that will apply the pass
* @memberOf MLJ.core.Scene
*/
this.addPostProcessPass = function (name, pass) {
if(!jQuery.isFunction(pass)) {
console.warn("MLJ.core.Scene.addPostProcessPass(): pass parameter must be callable");
return;
}
_postProcessPasses.set(name, pass);
}
/**
* Removes a post process effect from the rendering chain.
* @param {String} name - The name of the pass to remove
* @memberOf MLJ.core.Scene
*/
this.removePostProcessPass = function (name) {
var pass = _postProcessPasses.remove(name);
if (pass == undefined) {
console.warn("Warning: " + name + " pass not enabled");
}
_this.render();
}
var colorBuffer = new THREE.WebGLRenderTarget(0, 0, {
type: THREE.FloatType,
minFilter: THREE.NearestFilter
});
var targetBuffer = new THREE.WebGLRenderTarget(0, 0, {
type: THREE.FloatType,
minFilter: THREE.NearestFilter
});
var plane = new THREE.PlaneBufferGeometry(2, 2);
var quadMesh = new THREE.Mesh(
plane
);
var quadScene = new THREE.Scene();
quadScene.add(quadMesh);
quadMesh.material = new THREE.ShaderMaterial({
vertexShader:
"varying vec2 vUv; \
void main(void) \
{ \
vUv = uv; \
gl_Position = vec4(position.xyz, 1.0); \
}",
fragmentShader:
"uniform sampler2D offscreen; \
varying vec2 vUv; \
void main(void) { gl_FragColor = texture2D(offscreen, vUv.xy); }"
});
quadMesh.material.uniforms = {
offscreen: { type: "t", value: null }
};
/**
* Renders the scene. If there are no post process effects enabled, the
* THREE.js scene that contains all the scene decorators and the overlay
* layers is rendered straight to the canvas. Otherwise, the basic scene is
* rendered to an off screen render target, and post process effects are
* applied one after the other (according to the user's activation order)
* before displaying the result.
* @memberOf MLJ.core.Scene
*/
this.render = function (fromReqAnimFrame) {
if (_stats.active && !fromReqAnimFrame) {
return;
}
if (_postProcessPasses.size() > 0) {
_renderer.render(_scene, _camera, colorBuffer, true);
var it = _postProcessPasses.iterator();
while (it.hasNext()) {
var pass = it.next();
pass(colorBuffer, targetBuffer);
// Swap rendering targets for the next pass
var tmp = colorBuffer;
colorBuffer = targetBuffer;
targetBuffer = tmp;
}
// final pass, render colorBuffer to the screen
quadMesh.material.uniforms.offscreen.value = colorBuffer;
_renderer.render(quadScene, _camera2D);
} else {
_renderer.render(_scene, _camera);
}
// render the 2D overlays
_renderer.autoClear = false;
_renderer.render(_scene2D, _camera2D);
_renderer.autoClear = true;
};
this.takeSnapshot = function() {
var canvas = _renderer.context.canvas;
// draw to canvas...
canvas.toBlob(function(blob) {
saveAs(blob, "snapshot.png");
});
};
this.resetTrackball = function() {
_controls.reset();
};
//INIT
$(window).ready(function () {
initScene();
initDragAndDrop();
});
}).call(MLJ.core.Scene);