/**
* 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) );
}
}