Source: Rendering.js

/**
 * The rendering object, which has two main functionalities:
 * - render the surface
 * - offer drag-able elements, to control light and seedline.
 * @class
 */
function Rendering(canvas) {
	this.renderer = null;
	this.camera = null;
	this.controls = null;
	this.scene = null;
	this.cloudScene = null;
	
	this.mouse = new THREE.Vector2();
	this.raycaster = new THREE.Raycaster();
	this.offset = new THREE.Vector3();
	
	this.material = null;
	this.lightSource = null;
	this.materialDepth = null;
	
	this.INTERSECTED = null;
	this.SELECTED = null;
	
	this.plane = null;
	this.objects = [];
	
	this.canvas = canvas;
	
	this.rtAttribute = null;
	this.rtDepth = null;
	this.sceneSQ = null;
	this.cameraSQ = null;
	this.materialSQ = null;
	
	this.surface = null;
	this.seedLine = null;	
	this.seedLineStartSphere = null;	
	this.seedLineEndSphere = null;
	
	this.seedLineMesh = null;
	
	this.colored = false;
	
	this.animate = function() {
		requestAnimationFrame( this.animate.bind(this) );
		this.controls.update();
	}

	/**
	 * Renders the the surface, the seed line and the scene.
	 */
	this.render = function() {
		var needUpdate = false;
	
		if (!this.rtDepth || 
			this.rtDepth.width != this.canvas.clientWidth || 
			this.rtDepth.height != this.canvas.clientHeight) {
			
			if (this.rtDepth)
				this.rtDepth.dispose();
			
			this.rtDepth = new THREE.WebGLRenderTarget( 
				this.canvas.clientWidth, 
				this.canvas.clientHeight, { 
				minFilter: THREE.NearestFilter, 
				magFilter: THREE.NearestFilter, 
				format: THREE.RGBAFormat, 
				type: THREE.FloatType
			} );
			
			needUpdate = true;
		}
	
		if (!this.rtAttribute || 
			this.rtAttribute.width != this.canvas.clientWidth || 
			this.rtAttribute.height != this.canvas.clientHeight) {
			
			if (this.rtAttribute)
				this.rtAttribute.dispose();
			
			this.rtAttribute = new THREE.WebGLRenderTarget( 
				this.canvas.clientWidth, 
				this.canvas.clientHeight, { 
				minFilter: THREE.NearestFilter, 
				magFilter: THREE.NearestFilter, 
				format: THREE.RGBAFormat, 
				type: THREE.FloatType
			} );
			
			needUpdate = true;
		}
		
		this.renderer.clear();
		this.renderer.clearTarget(this.rtDepth, true, true, true);
		this.renderer.clearTarget(this.rtAttribute, true, true, true);
	
		// cloud to texture	
		this.cloud.material = this.materialDepth;
		this.renderer.render(this.cloudScene, this.camera, this.rtDepth);
		
		this.cloud.material = this.material;
		this.material.uniforms.screenWidth.value = this.canvas.clientWidth;
		this.material.uniforms.screenHeight.value = this.canvas.clientHeight;
		this.material.uniforms.depthMap.value = this.rtDepth;
		this.material.uniforms.lightPosition.value = this.lightSource.position.clone();
		this.material.uniforms.colored.value = this.colored;
		this.renderer.render(this.cloudScene, this.camera, this.rtAttribute);
		
		this.materialSQ.uniforms.texture.value = this.rtAttribute;
		
		this.renderer.render(this.sceneSQ, this.cameraSQ);
		
		// rest
		this.renderer.render( this.scene, this.camera );
	}
	
	/**
	 * Callback when the window is resized.
	 */
	this.onWindowResize = function() {
		this.camera.aspect = window.innerWidth / window.innerHeight;
		this.camera.updateProjectionMatrix();
		this.renderer.setSize( window.innerWidth, window.innerHeight );
		this.render();
	}
	
	/**
	 * Callback, when the mouse is moved. If there is a selected object, the object is moved.
	 */
	this.onDocumentMouseMove = function(event) {
		event.preventDefault();

		this.mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
		this.mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;

		this.raycaster.setFromCamera(this.mouse, this.camera);

		if ( this.SELECTED ) {
			var intersects = this.raycaster.intersectObject( this.plane );
			var newPosition = intersects[ 0 ].point.sub( this.offset );
			
			newPosition.x = Math.max(newPosition.x, -0.5);
			newPosition.y = Math.max(newPosition.y, -0.5);
			newPosition.z = Math.max(newPosition.z, -0.5);
			newPosition.x = Math.min(newPosition.x, 0.5);
			newPosition.y = Math.min(newPosition.y, 0.5);
			newPosition.z = Math.min(newPosition.z, 0.5);
			
			this.SELECTED.position.copy( newPosition );
			
			if (this.SELECTED === this.seedLineStartSphere) {
				this.seedLine.start.copy(this.seedLineStartSphere.position);
				this.seedLine.start.addScalar(0.5);
				this.surface.calculate(this.surface.volume, this.seedLine, this.surface.numIt);
				this.refresh(this.surface);
			}
			
			if (this.SELECTED === this.seedLineEndSphere) {
				this.seedLine.end.copy(this.seedLineEndSphere.position);
				this.seedLine.end.addScalar(0.5);
				this.surface.calculate(this.surface.volume, this.seedLine, this.surface.numIt);
				this.refresh(this.surface);
			}
			
			this.render();
			return;
		}

		var intersects = this.raycaster.intersectObjects( this.objects );

		if ( intersects.length > 0 ) {
			if ( this.INTERSECTED != intersects[ 0 ].object ) {
				if ( this.INTERSECTED ) 
					this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex );

				this.INTERSECTED = intersects[ 0 ].object;
				this.INTERSECTED.currentHex = this.INTERSECTED.material.color.getHex();

				this.plane.position.copy( this.INTERSECTED.position );
				this.plane.lookAt( this.camera.position );
			}

			this.canvas.style.cursor = 'pointer';
		} else {
			if ( this.INTERSECTED ) 
				this.INTERSECTED.material.color.setHex( this.INTERSECTED.currentHex );

			this.INTERSECTED = null;
			this.canvas.style.cursor = 'auto';
		}
		
		this.render();
	}
	
	/**
	 * Callback for mouse down event. If there is an object under the mouse, it is marked 
	 * as selected, and will be moved, when the mouse is moved.
	 */
	this.onDocumentMouseDown = function( event ) {
		event.preventDefault();

		var vector = new THREE.Vector3( this.mouse.x, this.mouse.y, 0.5 ).unproject( this.camera );
		var raycaster = new THREE.Raycaster( this.camera.position, vector.sub( this.camera.position ).normalize() );
		var intersects = raycaster.intersectObjects( this.objects );

		if ( intersects.length > 0 ) {		
			this.controls.enabled = false;
			this.SELECTED = intersects[ 0 ].object;
			var intersects = raycaster.intersectObject( this.plane );			
			this.offset.copy( intersects[ 0 ].point ).sub( this.plane.position );
			this.canvas.style.cursor = 'move';
		}
	}
	
	/**
	 * Callback for mouse up event. Removes an object from being selected.
	 */
	this.onDocumentMouseUp = function( event ) {
		event.preventDefault();
		this.controls.enabled = true;
		if ( this.INTERSECTED ) {
			this.plane.position.copy( this.INTERSECTED.position );
			this.SELECTED = null;
		}

		this.canvas.style.cursor = 'auto';
	}

	/**
	 * This method creates quads for each particle, which are normal aligned to the surface normal.
	 *
	 * TODO replace by - not yet supported - geometry shader.
	 */
	this.points2quads = function(positions, normals, d) {
		var result = {
			positions: new Float32Array(positions.length * 6 * 3),
			uvs: new Float32Array(positions.length * 6 * 2)
		};
		
		var up = new THREE.Vector3(0, 0, 1);
		var side = new THREE.Vector3(1, 0, 0);
		
		var size = d*2;
		
		for (var i = 0; i < positions.length; i++) {
			var normal = normals[i];
			var position = positions[i];
			
			var v1 = new THREE.Vector3();
			var v2 = new THREE.Vector3();
			
			if (normal.z <= normal.x || normal.z <= normal.y) {
				v1.crossVectors(normal, up);
			} else {
				v1.crossVectors(normal, side);
			}
			v1 = v1.normalize();
			v2.crossVectors(v1, normal);
			
			v2 = v2.normalize();
			
			v1.multiplyScalar(size);
			v2.multiplyScalar(size);
			
			var p1 = new THREE.Vector3();
			var p2 = new THREE.Vector3();
			var p3 = new THREE.Vector3();
			var p4 = new THREE.Vector3();
			
			p1.addVectors(position, v1);
			p2.addVectors(position, v2);
			p3.subVectors(position, v1);
			p4.subVectors(position, v2);
			
			result.positions[i*18 +  0] = p1.x;
			result.positions[i*18 +  1] = p1.y;
			result.positions[i*18 +  2] = p1.z;
			result.positions[i*18 +  3] = p2.x;
			result.positions[i*18 +  4] = p2.y;
			result.positions[i*18 +  5] = p2.z;
			result.positions[i*18 +  6] = p3.x;
			result.positions[i*18 +  7] = p3.y;
			result.positions[i*18 +  8] = p3.z;
			result.positions[i*18 +  9] = p1.x;
			result.positions[i*18 + 10] = p1.y;
			result.positions[i*18 + 11] = p1.z;
			result.positions[i*18 + 12] = p3.x;
			result.positions[i*18 + 13] = p3.y;
			result.positions[i*18 + 14] = p3.z;
			result.positions[i*18 + 15] = p4.x;
			result.positions[i*18 + 16] = p4.y;
			result.positions[i*18 + 17] = p4.z;
			
			result.uvs[i*12 +  0] = 0;
			result.uvs[i*12 +  1] = 1;
			result.uvs[i*12 +  2] = 1;
			result.uvs[i*12 +  3] = 1;
			result.uvs[i*12 +  4] = 1;
			result.uvs[i*12 +  5] = 0;
			
			result.uvs[i*12 +  6] = 1;
			result.uvs[i*12 +  7] = 1;
			result.uvs[i*12 +  8] = 0;
			result.uvs[i*12 +  9] = 0;
			result.uvs[i*12 + 10] = 0;
			result.uvs[i*12 + 11] = 1;
		}
		
		return result;
	}
	
	this.array2buffered = function(array, n) {
		var result = new Float32Array(array.length * n * 3);
		for (var i = 0; i < array.length; i++) {
			for (var j = 0; j < n; j++) {
				result[i*3*n + j*3 + 0] = array[i].x;
				result[i*3*n + j*3 + 1] = array[i].y;
				result[i*3*n + j*3 + 2] = array[i].z;
			}
		}
		return result;
	}
	
	/**
	 * Initializes the rendering of the surface.
	 */
	this.initSurface = function() {
		this.cloudScene = new THREE.Scene();
	
		this.materialDepth = new THREE.ShaderMaterial( {
			attributes: {
				customUV: { type: 'v2', value: [] }
			},
			vertexShader: document.getElementById( 'vertexShaderDepth' ).textContent,
			fragmentShader: document.getElementById( 'fragmentShaderDepth' ).textContent,
			side: THREE.DoubleSide,
			blending: THREE.NoBlending,
			transparent: false,
			depthWrite: true,
			depthTest: true
		} );
	
		this.material = new THREE.ShaderMaterial( {
			attributes: {
				customNormal: { type: 'v3', value: [] },
				customColor: { type: 'v3', value: [] },
				customUV: { type: 'v2', value: [] }
			},
			uniforms: {
				lightPosition: { type: 'v3', value: new THREE.Vector3() },
				depthMap: { type: 't', value: this.rtDepth },
				screenWidth: { type: 'f', value: null },
				screenHeight: { type: 'f', value: null },
				colored: { type: 'f', value: this.colored * 1.0 }
			},
			vertexShader: document.getElementById( 'vertexShader' ).textContent,
			fragmentShader: document.getElementById( 'fragmentShader' ).textContent,
			side: THREE.DoubleSide,
			blending: THREE.AdditiveBlending,
			transparent: true,
			depthWrite: false,
			depthTest: false
		} );

		var quadsResult = this.points2quads(this.surface.positions, this.surface.normals, this.seedLine.interval);
		var quadPositions = quadsResult.positions;
		var quadUVs = quadsResult.uvs;
		var quadNormals = this.array2buffered(this.surface.normals, 6);
		var quadColors = this.array2buffered(this.surface.colors, 6);
		
		var geometry = new THREE.BufferGeometry();
		geometry.addAttribute( 'position', new THREE.BufferAttribute(quadPositions, 3 ));
		geometry.addAttribute( 'customUV', new THREE.BufferAttribute(quadUVs, 2 ));
		geometry.addAttribute( 'customColor', new THREE.BufferAttribute(quadColors, 3 ));
		geometry.addAttribute( 'customNormal', new THREE.BufferAttribute(quadNormals, 3 ));
		
		this.cloud = new THREE.Mesh(geometry, this.material);
		this.cloud.position.addScalar(-0.5);
		this.cloudScene.add(this.cloud);
	}
	
	/**
	 * Initializes the rendering of the seed line.
	 */
	this.initSeedLine = function() {
		
		if(this.seedLineMesh)
    		this.scene.remove( this.seedLineMesh );
		
		var seedLineGeometry = new THREE.Geometry();
		seedLineGeometry.vertices.push(
			this.seedLine.start,
			this.seedLine.end
		);
		var seedLineMaterial = new THREE.LineBasicMaterial({color: 0x00ff00, depthTest: false });
		this.seedLineMesh = new THREE.Line(seedLineGeometry, seedLineMaterial);
		this.seedLineMesh.position.addScalar(-0.5);
		
		this.scene.add(this.seedLineMesh);
	}
	
	/**
	 * Initializes the scene (except surface and seed line).
	 */
	this.initScene = function() {
		this.scene = new THREE.Scene();
		this.objects = [];
	
		// sq
		this.materialSQ = new THREE.ShaderMaterial({
			uniforms: {
				texture: { type: 't', value: this.rtAttribute }
			},
			vertexShader: document.getElementById( 'vertexShaderSQ' ).textContent,
			fragmentShader: document.getElementById( 'fragmentShaderSQ' ).textContent
		});

		var SQ = new THREE.Mesh(new THREE.PlaneBufferGeometry(2,2,0), this.materialSQ);
		this.sceneSQ = new THREE.Scene();
		this.sceneSQ.add(SQ);
		this.cameraSQ = new THREE.Camera();
	
		// box
		var boxGeometry = new THREE.BoxGeometry( 1, 1, 1 );
		var boxMesh = new THREE.Mesh( boxGeometry, new THREE.MeshBasicMaterial({color: 0xffffff, depthTest: false}) );
		var boxEdges = new THREE.EdgesHelper( boxMesh, 0xffffff );
		//boxEdges.material.linewidth = 2;
		boxEdges.material = boxMesh.material;
		this.scene.add( boxEdges );
		
		//plane
		this.plane = new THREE.Mesh(
					new THREE.PlaneBufferGeometry( 200, 200, 8, 8),
					new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0.25, transparent: true, side: THREE.DoubleSide } )
		);
		this.plane.visible = false;
		this.plane.rotation.x = -Math.PI/2;
		this.scene.add( this.plane );
		
		// light source
		if (!this.lightSource) {
			var lightSourceGeometry = new THREE.SphereGeometry(0.02,20,20);
			var lightSourceMaterial = new THREE.MeshBasicMaterial( {color: 0xffffff, depthTest: false} );
			this.lightSource = new THREE.Mesh(lightSourceGeometry, lightSourceMaterial);
			this.lightSource.position.x = -0.5;
		}
		this.scene.add(this.lightSource);
		this.objects.push(this.lightSource);
		
		// seedLine controls
		var seedLineStartSphereGeometry = new THREE.SphereGeometry(0.01,20,20);
		var seedLineStartSphereMaterial = new THREE.MeshBasicMaterial( {color: 0x00ff00, depthTest: false} );
		this.seedLineStartSphere = new THREE.Mesh(seedLineStartSphereGeometry, seedLineStartSphereMaterial);
		this.seedLineStartSphere.position.copy(this.seedLine.start).subScalar(0.5);
		this.scene.add(this.seedLineStartSphere);
		this.objects.push(this.seedLineStartSphere);
		
		var seedLineEndSphereGeometry = new THREE.SphereGeometry(0.01,20,20);
		var seedLineEndSphereMaterial = new THREE.MeshBasicMaterial( {color: 0x00ff00, depthTest: false} );
		this.seedLineEndSphere = new THREE.Mesh(seedLineEndSphereGeometry, seedLineEndSphereMaterial);
		this.seedLineEndSphere.position.copy(this.seedLine.end).subScalar(0.5);
		this.scene.add(this.seedLineEndSphere);
		this.objects.push(this.seedLineEndSphere);
	}

	/**
	 * Initializes the renderer.
	 * @param {StreamSurface} surface The surface to be rendered.
	 * @param {SeedLine} seedLine The seed line, which was used to sample the particles (also rendered).
	 */
	this.init = function(surface, seedLine) {
		this.surface = surface;
		this.seedLine = seedLine;
		
		this.resetCamera();
		this.initSurface();
		this.initScene();
		this.initSeedLine();
	
		// renderer
	
		this.renderer = new THREE.WebGLRenderer( { canvas: this.canvas, alpha: true, preserveDrawingBuffer: true } );
		this.renderer.autoClear = false;
		this.renderer.setPixelRatio( window.devicePixelRatio );
		this.renderer.setSize( this.canvas.width, this.canvas.height );
		this.renderer.sortObjects = false;

		this.render();
	
		window.addEventListener( 'resize', this.onWindowResize.bind(this), false );
		
		this.renderer.domElement.addEventListener( 'mousemove', this.onDocumentMouseMove.bind(this), false );
		this.renderer.domElement.addEventListener( 'mousedown', this.onDocumentMouseDown.bind(this), false );
		this.renderer.domElement.addEventListener( 'mouseup', this.onDocumentMouseUp.bind(this), false );
	}
	
	/**
	 * Resets the scene according to the new surface.
	 * @param {StreamSurface} surface The new surface.
	 */
	this.refresh = function(surface) {
		this.initSurface();
		this.initSeedLine();
		this.render();
	}
	
	/**
	 * Sets the camera back to its initial position.
	 */
	this.resetCamera = function() {
		this.camera = new THREE.PerspectiveCamera( 60, this.canvas.width/this.canvas.height, 0.001, 1000 );
	
		this.camera.position.y = 2;
		this.camera.up = new THREE.Vector3(0,0,-1);
		
		this.controls = new THREE.TrackballControls( this.camera, this.canvas );

		this.controls.rotateSpeed = 1.0;
		this.controls.zoomSpeed = 1.2;
		this.controls.panSpeed = 0.8;

		this.controls.noZoom = false;
		this.controls.noPan = false;

		this.controls.staticMoving = true;
		this.controls.dynamicDampingFactor = 0.3;

		this.controls.keys = [ 65, 83, 68 ];

		this.controls.addEventListener( 'change', this.render.bind(this) );
	}
	
}