Source: tissueRenderer.js

/**
 * Used for viewing 3D tissue image stacks, it may include clickable points which
 * may provide additional informations and trigger actions on other panels.
 * 
 * @param {String} PanelName - Id of the target element to create the  {@link PJP.TissueViewer} on.
 * @class
 * 
 * @author Alan Wu
 * @returns {PJP.TissueViewer}
 */
PJP.TissueViewer = function(PanelName)  {
	//ZincScene for this renderer.
	var textureScene;
	var sceneFirstPass, renderer;
	
	var rtTexture, transferTexture;
	var cubeTextures = ['collagen', 'crop', 'collagen_large', 'crop'];
	var materialFirstPass;
	var materialSecondPass;
	var tissueRenderer;
	var sphere, sphere2, pickerSphere, pickerSphere2;
	var tissueScene;
	var meshFirstPass, meshSecondPass;
	var cellPickerScene;
	//dat.gui container
	var gui;
	var guiControls;
	// a link to the constitutive laws of the cardiac cells
	var constitutiveLawsLink = "https://models.physiomeproject.org/mechanical_constitutive_laws";
	var UIIsReady = false;
	var cellPanel = undefined;
	var modelPanel = undefined;
	var _this = this;
	
	this.setCellPanel = function(CellPanelIn) {
		cellPanel = CellPanelIn;
	}
	
	this.setModelPanel = function(ModelPanelIn) {
		modelPanel = ModelPanelIn;
	}
	
	var showCellTooltip = function(id, x, y) {
		setToolTipText("Cell model " + id);
		tooltipcontainerElement.style.left = x +"px";
		tooltipcontainerElement.style.top = (y - 20) + "px";
		tipElement.style.visibility = "visible";
		tipElement.style.opacity = 1;
		tiptextElement.style.visibility = "visible";
		tiptextElement.style.opacity = 1;
		currentHoverId = id;
	}
	
	var hideCellTooltip = function() {
		tipElement.style.visibility = "hidden";
		tipElement.style.opacity = 0;
		tiptextElement.style.visibility = "hidden";
		tiptextElement.style.opacity = 0;
	}
	
	
	var openConstitutiveLawsLink = function() {
		window.open(constitutiveLawsLink, '');
	}
	
	//A cell has been picked, display something on the cell and model panels
	var openCellModelUI = function(id) {
		var cellTitle = "<strong>Cell: <span style='color:#FF4444'>" + id + "</span></strong>";
		if (cellPanel) {
			cellPanel.setCellPanelTitle(cellTitle);
			cellPanel.openCell();
		}
		if (modelPanel)
			modelPanel.openModel("Myocyte_v6_Grouped.svg");
	}
	
	/** 
	 * Callback function when a pickable object has been picked. It will then call functions in cell panel
	 * and model panel to show corresponding informations.
	 * 
	 * @callback
	 */
	var _pickingCellCallback = function() {
		return function(intersects, window_x, window_y) {
			if (intersects[0] !== undefined) {
				openCellModelUI("1");
			}
		}	
	};
	
	/**
	 * Callback function when a pickable object has been hovered over. It will show
	 * objecty id/name as tooltip text.
	 */
	var _hoverCellCallback = function() {
		return function(intersects, window_x, window_y) {
			if (intersects[0] !== undefined && intersects[0].object !== undefined &&
					(intersects[0].object.geometry instanceof THREE.SphereGeometry)) {
				showCellTooltip(1, window_x, window_y);
				document.getElementById("tissueDisplayArea").style.cursor = "pointer";
			}
			else {
				hideCellTooltip();
				document.getElementById("tissueDisplayArea").style.cursor = "auto";
			}
		}	
	};
	
	
	var changeModels = function(value) {
		materialSecondPass.uniforms.cubeTex.value =  cubeTextures[value];
		if (value.includes('crop')) {
			materialSecondPass.uniforms.black_flip.value = false;
		} else {
			materialSecondPass.uniforms.black_flip.value = true;
		}
		materialSecondPass.uniforms.slides_per_side.value = 16;
	}
	
	//Callback function for rendering the first pass scene
	var renderFirstPass = function() {
		return function() {
			//Render first pass and store the world space coords of the back face fragments into the texture.
			tissueRenderer.getThreeJSRenderer().render( sceneFirstPass, tissueRenderer.getCurrentScene().camera, rtTexture, true );
		}	
	}
	
	//Update dat.gui widget
	var updateDatGui = function()
	{
		for (var i in gui.__controllers) {
			gui.__controllers[i].updateDisplay();
		}
	}
	
	//Update the shader uniforms defining the boundaries 
	var updateBoundaryUniforms = function() {
		materialFirstPass.uniforms.min_x.value = guiControls.min_x;
		materialFirstPass.uniforms.max_x.value = guiControls.max_x;
		materialFirstPass.uniforms.min_y.value = guiControls.min_y;
		materialFirstPass.uniforms.max_y.value = guiControls.max_y;		
		materialFirstPass.uniforms.min_z.value = guiControls.min_z;
		materialFirstPass.uniforms.max_z.value = guiControls.max_z;		
		
		materialSecondPass.uniforms.min_x.value = guiControls.min_x;
		materialSecondPass.uniforms.max_x.value = guiControls.max_x;
		materialSecondPass.uniforms.min_y.value = guiControls.min_y;
		materialSecondPass.uniforms.max_y.value = guiControls.max_y;		
		materialSecondPass.uniforms.min_z.value = guiControls.min_z;
		materialSecondPass.uniforms.max_z.value = guiControls.max_z;		
	}
	
	var resetSlider = function() {
		guiControls.min_x = 0;
		guiControls.max_x = 1.0;
		guiControls.min_y = 0;
		guiControls.max_y = 1.0;
		guiControls.min_z = 0;
		guiControls.max_z = 1.0;
		updateBoundaryUniforms();
		updateDatGui();	
	}
	
	//boundary has been changed from dat.gui, update the values and send to the shaders
	var changeBoundary = function(name) {
		return function(value) {
			if (name == "min_x")
			{
				if (guiControls.min_x >= guiControls.max_x)
					guiControls.max_x = guiControls.min_x + 0.01;
			}
			if (name == "min_y")
			{
				if (guiControls.min_y >= guiControls.max_y)
					guiControls.max_y = guiControls.min_y + 0.01;
			}
			if (name == "min_z")
			{
				if (guiControls.min_z >= guiControls.max_z)
					guiControls.max_z = guiControls.min_z + 0.01;
			}
			if (name == "max_x")
			{pickerSphere
				if (guiControls.max_x <= guiControls.min_x)
					guiControls.min_x = guiControls.max_x - 0.01;
			}
			if (name == "max_y")
			{
				if (guiControls.max_y <= guiControls.min_y)
					guiControls.min_y = guiControls.max_y - 0.01;
			}
			if (name == "max_z")
			{
				if (guiControls.max_z <= guiControls.min_z)
					guiControls.min_z = guiControls.max_z - 0.01;
			}
			updateBoundaryUniforms();		
			updateDatGui();
		}	
	}
	
	var volumeRenderBackGroundChanged = function() {
		return function(value) {
			var redValue = parseInt(value[0]);
			var greenValue = parseInt(value[1]);
			var blueValue = parseInt(value[2]);
			
			var backgroundColourString = 'rgb(' + redValue + ',' + greenValue + ',' + blueValue + ')';
			var colour = new THREE.Color(backgroundColourString);
			var renderer = tissueRenderer.getThreeJSRenderer();
			renderer.setClearColor( colour, 1 );
		}
	}
	
	//Set default parameters for texture
	var setDefaultTextureParameters = function(textureName) {
		var texture = cubeTextures[textureName];
		texture.generateMipmaps = false;
		texture.minFilter = THREE.LinearFilter;
		texture.magFilter = THREE.LinearFilter;
		texture.wrapS = THREE.RepeatWrapping;
		texture.wrapT = THREE.RepeatWrapping;
	}
	
	//Setup volume renderer
	var volumeRenderStart = function(shaderText) {
	
		guiControls = new function() {
			this.model = 'collagen';
			this.steps = 256.0;
			this.alphaCorrection = 5.0;
			this.min_x = 0.01;
			this.max_x = 0.99;
			this.min_y = 0.01;
			this.max_y = 0.99;
			this.min_z = 0.01;
			this.max_z = 0.99;
			this.Background = [ 255, 255, 255 ]; // RGB array
		};
		
		activated = false;
		
		//create the renderer
		var container = document.createElement( 'div' );
		document.getElementById("tissueDisplayArea").appendChild( container );
		container.style.height = "100%"
		container.style.backgroundColor = "white";
		tissueRenderer = new Zinc.Renderer(container, window);
		tissueRenderer.initialiseVisualisation();
		tissueRenderer.playAnimation = false;
		
		//create the scene for visualisations
		tissueScene = tissueRenderer.getCurrentScene();
		var renderer = tissueRenderer.getThreeJSRenderer();
		renderer.setClearColor( 0xffffff, 1 );
		var camera = tissueScene.camera;
		camera.near = 0.01;
		camera.far = 3000.0;
		camera.position.z = 3.0;
	
		//Load the 2D texture containing the Z slices.
		cubeTextures['collagen'] = THREE.ImageUtils.loadTexture('textures/collagen.png');
		cubeTextures['crop'] = THREE.ImageUtils.loadTexture('textures/crop.jpg');
		cubeTextures['collagen_large'] = THREE.ImageUtils.loadTexture('textures/collagen_large.png');
		cubeTextures['crop_large'] = THREE.ImageUtils.loadTexture('textures/crop_large.jpg');
		
		//Don't let it generate mipmaps to save memory and apply linear filtering to prevent use of LOD.
		setDefaultTextureParameters('collagen');
		setDefaultTextureParameters('crop');
		setDefaultTextureParameters('collagen_large');
		setDefaultTextureParameters('crop_large');
	 
		//Create a render target for preprocessing, first pass material and mesh will be render into 
		//rtTexture
		var screenSize = new THREE.Vector2( container.clientWidth, container.clientHeight );
		rtTexture = new THREE.WebGLRenderTarget( screenSize.x, screenSize.y,
												{ 	minFilter: THREE.LinearFilter,
													magFilter: THREE.LinearFilter,
													wrapS:  THREE.ClampToEdgeWrapping,
													wrapT:  THREE.ClampToEdgeWrapping,
													format: THREE.RGBFormat,
													generateMipmaps: false} );
	
		//First pass material
		materialFirstPass = new THREE.ShaderMaterial( {
			vertexShader: shaderText[0],
			fragmentShader: shaderText[1],
			side: THREE.BackSide,
			uniforms: {	min_x : {type: "1f" , value: guiControls.min_x },
				max_x : {type: "1f" , value: guiControls.max_x },
				min_y : {type: "1f" , value: guiControls.min_y },
				max_y : {type: "1f" , value: guiControls.max_y },
				min_z : {type: "1f" , value: guiControls.min_z },
				max_z : {type: "1f" , value: guiControls.max_z }}
		} );
	
		//Second pass material
		materialSecondPass = new THREE.ShaderMaterial( {
			vertexShader: shaderText[2],
			fragmentShader: shaderText[3],
			transparent: true,
			depthTest: true,
			side: THREE.FrontSide,
			uniforms: {	tex:  { type: "t", value: rtTexture.texture },
						cubeTex:  { type: "t", value: cubeTextures['collagen'] },
						steps : {type: "1f" , value: guiControls.steps },
						alphaCorrection : {type: "1f" , value: guiControls.alphaCorrection },
						min_x : {type: "1f" , value: guiControls.min_x },
						max_x : {type: "1f" , value: guiControls.max_x },
						min_y : {type: "1f" , value: guiControls.min_y },
						max_y : {type: "1f" , value: guiControls.max_y },
						min_z : {type: "1f" , value: guiControls.min_z },
						max_z : {type: "1f" , value: guiControls.max_z },
						slides_per_side : {type: "1f" , value: 16 },
						black_flip : {value: true}}
		 });
	
		//First pass scene
		sceneFirstPass = new THREE.Scene();
	
		// Create a new box geometry in which the volume render is located
		var boxGeometry = new THREE.BoxGeometry(1.0, 1.0, 1.0);
		boxGeometry.doubleSided = true;
	
		meshFirstPass = new THREE.Mesh( boxGeometry, materialFirstPass );
		meshSecondPass = new THREE.Mesh( boxGeometry, materialSecondPass );
	
		sceneFirstPass.add( meshFirstPass );
		tissueScene.addObject( meshSecondPass );
		
		
		//Create sphere and add it to tissueScene for display
		var geometry = new THREE.SphereGeometry( 0.02, 16, 16 );
		var material = new THREE.MeshPhongMaterial( { color: 0x3920d9, shading: THREE.SmoothShading,  shininess: 0.5 } );
		
		sphere = new THREE.Mesh( geometry, material );
		sphere.position.x = 0.279;
		sphere.position.y = 0.14;
		sphere.position.z = 0.52;
		sphere.visible = false;
		tissueScene.addObject( sphere );
		
		sphere2 = new THREE.Mesh( geometry, material );
		sphere2.position.x = 0.279;
		sphere2.position.y = -0.2;
		sphere2.position.z = 0.32;
		sphere2.visible = false;
		tissueScene.addObject( sphere2 );
	
		//Create sphere and picker scene for object picking
		cellPickerScene = tissueRenderer.createScene("cell_picker_scene");
		var zincCameraControl = tissueScene.getZincCameraControls();
		zincCameraControl.setMouseButtonAction("AUXILIARY", "ZOOM");
		zincCameraControl.setMouseButtonAction("SECONDARY", "PAN");
		pickerSphere = new THREE.Mesh( geometry, material );
		pickerSphere.position.x = 0.279;
		pickerSphere.position.y = 0.14;
		pickerSphere.position.z = 0.52;
		pickerSphere.visible = false;
		
		pickerSphere2 = new THREE.Mesh( geometry, material );
		pickerSphere2.position.x = 0.279;
		pickerSphere2.position.y = -0.2;
		pickerSphere2.position.z = 0.32;
		pickerSphere2.visible = false;
		
		cellPickerScene.addObject( pickerSphere );
		cellPickerScene.addObject( pickerSphere2 );
		zincCameraControl.enableRaycaster(cellPickerScene, _pickingCellCallback(), _hoverCellCallback());
		//Disable autoclear
		tissueScene.autoClearFlag = false;
		
		//setup dat.gui
		gui = new dat.GUI({autoPlace: false});
		gui.domElement.id = 'gui';
		gui.close();
		var customContainer = document.getElementById("tissueGui").append(gui.domElement);
		var controller = gui.addColor(guiControls, 'Background');
		controller.onChange(volumeRenderBackGroundChanged());
		var modelSelected = gui.add(guiControls, 'model', [ 'collagen', 'crop', 'collagen_large', 'crop_large'] );
		var resetSliderButton = { 'Reset':function(){ resetSlider() }};
		gui.add(guiControls, 'min_x', 0.00, 0.99).step(0.01).onChange(changeBoundary("min_x"));
		gui.add(guiControls, 'max_x', 0.01, 1.0).step(0.01).onChange(changeBoundary("max_x"));
		gui.add(guiControls, 'min_y', 0.00, 0.99).step(0.01).onChange(changeBoundary("min_y"));
		gui.add(guiControls, 'max_y', 0.01, 1.0).step(0.01).onChange(changeBoundary("max_y"));
		gui.add(guiControls, 'min_z', 0.00, 0.99).step(0.01).onChange(changeBoundary("min_z"));
		gui.add(guiControls, 'max_z', 0.01, 1.0).step(0.01).onChange(changeBoundary("max_z"));
		gui.add(resetSliderButton,'Reset');
		
		modelSelected.onChange(function(value) {
			changeModels(value);
		} );
		
		resetSlider();
		materialSecondPass.visible = false;
		//add a prerender callback to always render the first pass before the second pass
		tissueRenderer.addPreRenderCallbackFunction(renderFirstPass());
		tissueRenderer.animate();
	}
	
	var addUICallback = function() {
		var callbackElement = document.getElementById("cellButton1");
		callbackElement.onclick = function() { openCellModelUI('Cardiac myocyte'); };
		callbackElement = document.getElementById("cellButton2");
		callbackElement.onclick = function() { openCellModelUI('Cardiac fibroblast'); };
		callbackElement = document.getElementById("cellButton3");
		callbackElement.onclick = function() { openConstitutiveLawsLink(); };
	}
	
	//initialising the loading of the volume renderer
	var volumeRenderInit = function() {
		loadExternalFiles(['shaders/tissueShaderFirstPass.vs', 'shaders/tissueShaderFirstPass.fs',
		                   'shaders/tissueShaderSecondPass.vs', 'shaders/tissueShaderSecondPass.fs'], 
		                   function (shaderText) {
								volumeRenderStart(shaderText);
							}, function (url) {
							alert('Failed to download "' + url + '"');});
	}
	
	var loadHTMLComplete = function(link) {
		return function(event) {
			var localDOM = document.getElementById(PanelName);
			var childNodes = null;
			if (link.import.body !== undefined)
				childNodes = link.import.body.childNodes;
			else if (link.childNodes !== undefined)
				childNodes = link.childNodes;
			for (i = 0; i < childNodes.length; i++) {
				localDOM.appendChild(childNodes[i]);
			}
			addUICallback();
			volumeRenderInit();
			document.head.removeChild(link);
			UIIsReady = true;
		}
	}
	
	
	/**
	 * Initialise loading of the page, this is called when 
	 * the {@link PJP.TissueViewer} is created.
	 * @async
	 */
	var initialise = function() {
		var link = document.createElement('link');
		link.rel = 'import';
		link.href = 'snippets/tissueViewer.html';
		link.onload = loadHTMLComplete(link);
		link.onerror = loadHTMLComplete(link);
		document.head.appendChild(link);	
	}
	
	/**
	 * Set the title string for {@link PJP.TissueViewer}.
	 * @param {String} text - Tissue viewer title to be set.
	 */
	this.setTissueTitleString = function(text) {
	 	var text_display = document.getElementById('TissueTitle');
	 	text_display.innerHTML = text;
	}
 
	this.showButtons = function(flag) {
		if (flag)
			document.getElementById("cellButtonContainer").style.visibility = "visible";
		else
			document.getElementById("cellButtonContainer").style.visibility = "hiddenlink ";
	}
	
	/**
	 * Display the volume rendering
	 */
	this.showCollagenVisible = function(flag) {
		changeModels(guiControls.model);
		materialSecondPass.visible = flag;
		tissueScene.getZincCameraControls().updateDirectionalLight();
		sphere.visible = flag;
		sphere2.visible = flag;
		pickerSphere.visible = flag;
		pickerSphere2.visible = flag;
	}
	
	/**
	 * Reset the panel and hide all displays
	 */
	this.resetTissuePanel = function() {
	 	var text_display = document.getElementById('TissueTitle');
	 	text_display.innerHTML = "<strong>Tissue</strong>";
	 	_this.showCollagenVisible(false);
		_this.showButtons(false);
	}
	
	initialise();
}