Browse Source

Merge pull request #7671 from zz85/raytracing_workers

Raytracing Renderer using Web Workers Sandbox
Mr.doob 9 years ago
parent
commit
023ed81cd4

+ 144 - 401
examples/js/renderers/RaytracingRenderer.js

@@ -1,6 +1,9 @@
 /**
 /**
- * @author mrdoob / http://mrdoob.com/
- * @author alteredq / http://alteredqualia.com/
+ * RaytracingRenderer renders by raytracing it's scene. However, it does not
+ * compute the pixels itself but it hands off and coordinates the taks for workers.
+ * The workers compute the pixel values and this renderer simply paints it to the Canvas.
+ *
+ * @author zz85 / http://github.com/zz85
  */
  */
 
 
 THREE.RaytracingRenderer = function ( parameters ) {
 THREE.RaytracingRenderer = function ( parameters ) {
@@ -10,6 +13,8 @@ THREE.RaytracingRenderer = function ( parameters ) {
 	parameters = parameters || {};
 	parameters = parameters || {};
 
 
 	var scope = this;
 	var scope = this;
+	var pool = [];
+	var renderering = false;
 
 
 	var canvas = document.createElement( 'canvas' );
 	var canvas = document.createElement( 'canvas' );
 	var context = canvas.getContext( '2d', {
 	var context = canvas.getContext( '2d', {
@@ -23,469 +28,200 @@ THREE.RaytracingRenderer = function ( parameters ) {
 
 
 	var clearColor = new THREE.Color( 0x000000 );
 	var clearColor = new THREE.Color( 0x000000 );
 
 
-	var origin = new THREE.Vector3();
-	var direction = new THREE.Vector3();
-
-	var cameraPosition = new THREE.Vector3();
-
-	var raycaster = new THREE.Raycaster( origin, direction );
-	var raycasterLight = new THREE.Raycaster();
-
-	var perspective;
-	var modelViewMatrix = new THREE.Matrix4();
-	var cameraNormalMatrix = new THREE.Matrix3();
-
-	var objects;
-	var lights = [];
-	var cache = {};
-
-	var animationFrameId = null;
-
 	this.domElement = canvas;
 	this.domElement = canvas;
 
 
 	this.autoClear = true;
 	this.autoClear = true;
 
 
-	this.setClearColor = function ( color, alpha ) {
-
-		clearColor.set( color );
-
-	};
-
-	this.setPixelRatio = function () {};
-
-	this.setSize = function ( width, height ) {
-
-		canvas.width = width;
-		canvas.height = height;
-
-		canvasWidth = canvas.width;
-		canvasHeight = canvas.height;
-
-		canvasWidthHalf = Math.floor( canvasWidth / 2 );
-		canvasHeightHalf = Math.floor( canvasHeight / 2 );
-
-		context.fillStyle = 'white';
-
-	};
-
-	this.setSize( canvas.width, canvas.height );
-
-	this.clear = function () {
-
-	};
-
-	//
-
-	var spawnRay = ( function () {
-
-		var diffuseColor = new THREE.Color();
-		var specularColor = new THREE.Color();
-		var lightColor = new THREE.Color();
-		var schlick = new THREE.Color();
-
-		var lightContribution = new THREE.Color();
-
-		var eyeVector = new THREE.Vector3();
-		var lightVector = new THREE.Vector3();
-		var normalVector = new THREE.Vector3();
-		var halfVector = new THREE.Vector3();
-
-		var localPoint = new THREE.Vector3();
-		var reflectionVector = new THREE.Vector3();
-
-		var tmpVec = new THREE.Vector3();
-
-		var tmpColor = [];
-
-		for ( var i = 0; i < maxRecursionDepth; i ++ ) {
-
-			tmpColor[ i ] = new THREE.Color();
-
-		}
-
-		return function spawnRay( rayOrigin, rayDirection, outputColor, recursionDepth ) {
-
-			var ray = raycaster.ray;
-
-			ray.origin = rayOrigin;
-			ray.direction = rayDirection;
-
-			//
-
-			var rayLight = raycasterLight.ray;
-
-			//
-
-			outputColor.setRGB( 0, 0, 0 );
-
-			//
-
-			var intersections = raycaster.intersectObjects( objects, true );
-
-			// ray didn't find anything
-			// (here should come setting of background color?)
-
-			if ( intersections.length === 0 ) {
-
-				return;
-
-			}
-
-			// ray hit
-
-			var intersection = intersections[ 0 ];
-
-			var point = intersection.point;
-			var object = intersection.object;
-			var material = object.material;
-			var face = intersection.face;
-
-			var vertices = object.geometry.vertices;
-
-			//
-
-			var _object = cache[ object.id ];
-
-			localPoint.copy( point ).applyMatrix4( _object.inverseMatrix );
-			eyeVector.subVectors( raycaster.ray.origin, point ).normalize();
-
-			// resolve pixel diffuse color
+	var workers = parameters.workers;
+	var blockSize = parameters.blockSize || 64;
+	this.randomize = parameters.randomize;
 
 
-			if ( material instanceof THREE.MeshLambertMaterial ||
-				 material instanceof THREE.MeshPhongMaterial ||
-				 material instanceof THREE.MeshBasicMaterial ) {
-
-				diffuseColor.copyGammaToLinear( material.color );
-
-			} else {
-
-				diffuseColor.setRGB( 1, 1, 1 );
-
-			}
-
-			if ( material.vertexColors === THREE.FaceColors ) {
-
-				diffuseColor.multiply( face.color );
-
-			}
-
-			// compute light shading
-
-			rayLight.origin.copy( point );
-
-			if ( material instanceof THREE.MeshBasicMaterial ) {
-
-				for ( var i = 0, l = lights.length; i < l; i ++ ) {
-
-					var light = lights[ i ];
-
-					lightVector.setFromMatrixPosition( light.matrixWorld );
-					lightVector.sub( point );
-
-					rayLight.direction.copy( lightVector ).normalize();
-
-					var intersections = raycasterLight.intersectObjects( objects, true );
-
-					// point in shadow
-
-					if ( intersections.length > 0 ) continue;
-
-					// point visible
-
-					outputColor.add( diffuseColor );
-
-				}
+	var toRender = [], workerId = 0, sceneId = 0;
 
 
-			} else if ( material instanceof THREE.MeshLambertMaterial ||
-						material instanceof THREE.MeshPhongMaterial ) {
+	console.log( '%cSpinning off ' + workers + ' Workers ', 'font-size: 20px; background: black; color: white; font-family: monospace;' );
 
 
-				var normalComputed = false;
+	this.setWorkers = function( w ) {
 
 
-				for ( var i = 0, l = lights.length; i < l; i ++ ) {
+		workers = w || navigator.hardwareConcurrency || 4;
 
 
-					var light = lights[ i ];
+		while ( pool.length < workers ) {
+			var worker = new Worker( parameters.workerPath );
+			worker.id = workerId++;
 
 
-					lightColor.copyGammaToLinear( light.color );
+			worker.onmessage = function( e ) {
 
 
-					lightVector.setFromMatrixPosition( light.matrixWorld );
-					lightVector.sub( point );
+				var data = e.data;
 
 
-					rayLight.direction.copy( lightVector ).normalize();
+				if ( ! data ) return;
 
 
-					var intersections = raycasterLight.intersectObjects( objects, true );
+				if ( data.blockSize && sceneId == data.sceneId ) { // we match sceneId here to be sure
 
 
-					// point in shadow
+					var imagedata = new ImageData( new Uint8ClampedArray( data.data ), data.blockSize, data.blockSize );
+					context.putImageData( imagedata, data.blockX, data.blockY );
 
 
-					if ( intersections.length > 0 ) continue;
+					// completed
 
 
-					// point lit
+					console.log( 'Worker ' + this.id, data.time / 1000, ( Date.now() - reallyThen ) / 1000 + ' s' );
 
 
-					if ( normalComputed === false ) {
+					if ( pool.length > workers ) {
 
 
-						// the same normal can be reused for all lights
-						// (should be possible to cache even more)
-
-						computePixelNormal( normalVector, localPoint, material.shading, face, vertices );
-						normalVector.applyMatrix3( _object.normalMatrix ).normalize();
-
-						normalComputed = true;
-
-					}
-
-					// compute attenuation
-
-					var attenuation = 1.0;
-
-					if ( light.physicalAttenuation === true ) {
-
-						attenuation = lightVector.length();
-						attenuation = 1.0 / ( attenuation * attenuation );
+						pool.splice( pool.indexOf( this ), 1 );
+						return this.terminate();
 
 
 					}
 					}
 
 
-					lightVector.normalize();
-
-					// compute diffuse
-
-					var dot = Math.max( normalVector.dot( lightVector ), 0 );
-					var diffuseIntensity = dot * light.intensity;
-
-					lightContribution.copy( diffuseColor );
-					lightContribution.multiply( lightColor );
-					lightContribution.multiplyScalar( diffuseIntensity * attenuation );
-
-					outputColor.add( lightContribution );
-
-					// compute specular
-
-					if ( material instanceof THREE.MeshPhongMaterial ) {
-
-						halfVector.addVectors( lightVector, eyeVector ).normalize();
-
-						var dotNormalHalf = Math.max( normalVector.dot( halfVector ), 0.0 );
-						var specularIntensity = Math.max( Math.pow( dotNormalHalf, material.shininess ), 0.0 ) * diffuseIntensity;
-
-						var specularNormalization = ( material.shininess + 2.0 ) / 8.0;
-
-						specularColor.copyGammaToLinear( material.specular );
-
-						var alpha = Math.pow( Math.max( 1.0 - lightVector.dot( halfVector ), 0.0 ), 5.0 );
-
-						schlick.r = specularColor.r + ( 1.0 - specularColor.r ) * alpha;
-						schlick.g = specularColor.g + ( 1.0 - specularColor.g ) * alpha;
-						schlick.b = specularColor.b + ( 1.0 - specularColor.b ) * alpha;
-
-						lightContribution.copy( schlick );
-
-						lightContribution.multiply( lightColor );
-						lightContribution.multiplyScalar( specularNormalization * specularIntensity * attenuation );
-						outputColor.add( lightContribution );
-
-					}
+					renderNext( this );
 
 
 				}
 				}
 
 
 			}
 			}
 
 
-			// reflection / refraction
-
-			var reflectivity = material.reflectivity;
-
-			if ( ( material.mirror || material.glass ) && reflectivity > 0 && recursionDepth < maxRecursionDepth ) {
-
-				if ( material.mirror ) {
-
-					reflectionVector.copy( rayDirection );
-					reflectionVector.reflect( normalVector );
-
-				} else if ( material.glass ) {
+			worker.color = new THREE.Color().setHSL( Math.random() , 0.8, 0.8 ).getHexString();
+			pool.push( worker );
 
 
-					var eta = material.refractionRatio;
+			if ( renderering ) {
 
 
-					var dotNI = rayDirection.dot( normalVector );
-					var k = 1.0 - eta * eta * ( 1.0 - dotNI * dotNI );
+				updateSettings( worker );
 
 
-					if ( k < 0.0 ) {
+				worker.postMessage( {
+					scene: sceneJSON,
+					camera: cameraJSON,
+					annex: materials,
+					sceneId: sceneId
+				} );
 
 
-						reflectionVector.set( 0, 0, 0 );
-
-					} else {
-
-						reflectionVector.copy( rayDirection );
-						reflectionVector.multiplyScalar( eta );
-
-						var alpha = eta * dotNI + Math.sqrt( k );
-						tmpVec.copy( normalVector );
-						tmpVec.multiplyScalar( alpha );
-						reflectionVector.sub( tmpVec );
-
-					}
-
-				}
-
-				var theta = Math.max( eyeVector.dot( normalVector ), 0.0 );
-				var rf0 = reflectivity;
-				var fresnel = rf0 + ( 1.0 - rf0 ) * Math.pow( ( 1.0 - theta ), 5.0 );
-
-				var weight = fresnel;
-
-				var zColor = tmpColor[ recursionDepth ];
-
-				spawnRay( point, reflectionVector, zColor, recursionDepth + 1 );
-
-				if ( material.specular !== undefined ) {
-
-					zColor.multiply( material.specular );
-
-				}
-
-				zColor.multiplyScalar( weight );
-				outputColor.multiplyScalar( 1 - weight );
-				outputColor.add( zColor );
+				renderNext( worker );
 
 
 			}
 			}
 
 
-		};
-
-	}() );
+		}
 
 
-	var computePixelNormal = ( function () {
+		if ( ! renderering ) {
 
 
-		var tmpVec1 = new THREE.Vector3();
-		var tmpVec2 = new THREE.Vector3();
-		var tmpVec3 = new THREE.Vector3();
+			while ( pool.length > workers ) {
 
 
-		return function computePixelNormal( outputVector, point, shading, face, vertices ) {
+				pool.pop().terminate();
 
 
-			var faceNormal = face.normal;
-			var vertexNormals = face.vertexNormals;
+			}
 
 
-			if ( shading === THREE.FlatShading ) {
+		}
 
 
-				outputVector.copy( faceNormal );
+	};
 
 
-			} else if ( shading === THREE.SmoothShading ) {
+	this.setWorkers( workers );
 
 
-				// compute barycentric coordinates
+	this.setClearColor = function ( color, alpha ) {
 
 
-				var vA = vertices[ face.a ];
-				var vB = vertices[ face.b ];
-				var vC = vertices[ face.c ];
+		clearColor.set( color );
 
 
-				tmpVec3.crossVectors( tmpVec1.subVectors( vB, vA ), tmpVec2.subVectors( vC, vA ) );
-				var areaABC = faceNormal.dot( tmpVec3 );
+	};
 
 
-				tmpVec3.crossVectors( tmpVec1.subVectors( vB, point ), tmpVec2.subVectors( vC, point ) );
-				var areaPBC = faceNormal.dot( tmpVec3 );
-				var a = areaPBC / areaABC;
+	this.setPixelRatio = function () {};
 
 
-				tmpVec3.crossVectors( tmpVec1.subVectors( vC, point ), tmpVec2.subVectors( vA, point ) );
-				var areaPCA = faceNormal.dot( tmpVec3 );
-				var b = areaPCA / areaABC;
+	this.setSize = function ( width, height ) {
 
 
-				var c = 1.0 - a - b;
+		canvas.width = width;
+		canvas.height = height;
 
 
-				// compute interpolated vertex normal
+		canvasWidth = canvas.width;
+		canvasHeight = canvas.height;
 
 
-				tmpVec1.copy( vertexNormals[ 0 ] );
-				tmpVec1.multiplyScalar( a );
+		canvasWidthHalf = Math.floor( canvasWidth / 2 );
+		canvasHeightHalf = Math.floor( canvasHeight / 2 );
 
 
-				tmpVec2.copy( vertexNormals[ 1 ] );
-				tmpVec2.multiplyScalar( b );
+		context.fillStyle = 'white';
 
 
-				tmpVec3.copy( vertexNormals[ 2 ] );
-				tmpVec3.multiplyScalar( c );
+		pool.forEach( updateSettings );
 
 
-				outputVector.addVectors( tmpVec1, tmpVec2 );
-				outputVector.add( tmpVec3 );
+	};
 
 
-			}
+	this.setSize( canvas.width, canvas.height );
 
 
-		};
+	this.clear = function () {
 
 
-	}() );
+	};
 
 
-	var renderBlock = ( function () {
+	//
 
 
-		var blockSize = 64;
+	var totalBlocks, xblocks, yblocks;
 
 
-		var canvasBlock = document.createElement( 'canvas' );
-		canvasBlock.width = blockSize;
-		canvasBlock.height = blockSize;
+	function updateSettings( worker ) {
 
 
-		var contextBlock = canvasBlock.getContext( '2d', {
+		worker.postMessage( {
 
 
-			alpha: parameters.alpha === true
+			init: [ canvasWidth, canvasHeight ],
+			worker: worker.id,
+			// workers: pool.length,
+			blockSize: blockSize
 
 
 		} );
 		} );
 
 
-		var imagedata = contextBlock.getImageData( 0, 0, blockSize, blockSize );
-		var data = imagedata.data;
-
-		var pixelColor = new THREE.Color();
+	}
 
 
-		return function renderBlock( blockX, blockY ) {
+	function renderNext( worker ) {
+		if ( ! toRender.length ) {
 
 
-			var index = 0;
+			renderering = false;
+			return scope.dispatchEvent( { type: "complete" } );
 
 
-			for ( var y = 0; y < blockSize; y ++ ) {
-
-				for ( var x = 0; x < blockSize; x ++, index += 4 ) {
-
-					// spawn primary ray at pixel position
+		}
 
 
-					origin.copy( cameraPosition );
+		var current = toRender.pop();
 
 
-					direction.set( x + blockX - canvasWidthHalf, - ( y + blockY - canvasHeightHalf ), - perspective );
-					direction.applyMatrix3( cameraNormalMatrix ).normalize();
+		var blockX = ( current % xblocks ) * blockSize;
+		var blockY = ( current / xblocks | 0 ) * blockSize;
 
 
-					spawnRay( origin, direction, pixelColor, 0 );
+		worker.postMessage( {
+			render: true,
+			x: blockX,
+			y: blockY,
+			sceneId: sceneId
+		} );
 
 
-					// convert from linear to gamma
+		context.fillStyle = '#' + worker.color;
 
 
-					data[ index ]     = Math.sqrt( pixelColor.r ) * 255;
-					data[ index + 1 ] = Math.sqrt( pixelColor.g ) * 255;
-					data[ index + 2 ] = Math.sqrt( pixelColor.b ) * 255;
+		context.fillRect( blockX, blockY, blockSize, blockSize );
 
 
-				}
+	}
 
 
-			}
+	var materials = {};
 
 
-			context.putImageData( imagedata, blockX, blockY );
+	var sceneJSON, cameraJSON, reallyThen;
 
 
-			blockX += blockSize;
+	// additional properties that were not serialize automatically
 
 
-			if ( blockX >= canvasWidth ) {
+	var _annex = {
 
 
-				blockX = 0;
-				blockY += blockSize;
+		mirror: 1,
+		reflectivity: 1,
+		refractionRatio: 1,
+		glass: 1,
 
 
-				if ( blockY >= canvasHeight ) {
+	};
 
 
-					scope.dispatchEvent( { type: "complete" } );
-					return;
+	function serializeObject( o ) {
 
 
-				}
+		var mat = o.material;
 
 
-			}
+		if ( ! mat || mat.uuid in materials ) return;
 
 
-			context.fillRect( blockX, blockY, blockSize, blockSize );
+		var props = {};
+		for ( var m in _annex ) {
 
 
-			animationFrameId = requestAnimationFrame( function () {
+			if ( mat[ m ] !== undefined ) {
 
 
-				renderBlock( blockX, blockY );
+				props[ m ] = mat[ m ];
 
 
-			} );
+			}
 
 
-		};
+		}
 
 
-	}() );
+		materials[ mat.uuid ] = props;
+	}
 
 
 	this.render = function ( scene, camera ) {
 	this.render = function ( scene, camera ) {
 
 
-		if ( this.autoClear === true ) this.clear();
-
-		cancelAnimationFrame( animationFrameId );
+		renderering = true;
 
 
 		// update scene graph
 		// update scene graph
 
 
@@ -495,49 +231,56 @@ THREE.RaytracingRenderer = function ( parameters ) {
 
 
 		if ( camera.parent === null ) camera.updateMatrixWorld();
 		if ( camera.parent === null ) camera.updateMatrixWorld();
 
 
-		camera.matrixWorldInverse.getInverse( camera.matrixWorld );
-		cameraPosition.setFromMatrixPosition( camera.matrixWorld );
 
 
-		//
+		sceneJSON = scene.toJSON();
+		cameraJSON = camera.toJSON();
+		++ sceneId;
 
 
-		cameraNormalMatrix.getNormalMatrix( camera.matrixWorld );
-		origin.copy( cameraPosition );
+		scene.traverse( serializeObject );
 
 
-		perspective = 0.5 / Math.tan( THREE.Math.degToRad( camera.fov * 0.5 ) ) * canvasHeight;
+		pool.forEach( function( worker ) {
 
 
-		objects = scene.children;
+			worker.postMessage( {
+				scene: sceneJSON,
+				camera: cameraJSON,
+				annex: materials,
+				sceneId: sceneId
+			} );
+		} );
 
 
-		// collect lights and set up object matrices
+		context.clearRect( 0, 0, canvasWidth, canvasHeight );
+		reallyThen = Date.now();
 
 
-		lights.length = 0;
+		xblocks = Math.ceil( canvasWidth / blockSize );
+		yblocks = Math.ceil( canvasHeight / blockSize );
+		totalBlocks = xblocks * yblocks;
 
 
-		scene.traverse( function ( object ) {
+		toRender = [];
 
 
-			if ( object instanceof THREE.Light ) {
+		for ( var i = 0; i < totalBlocks; i ++ ) {
 
 
-				lights.push( object );
+			toRender.push( i );
 
 
-			}
+		}
 
 
-			if ( cache[ object.id ] === undefined ) {
 
 
-				cache[ object.id ] = {
-					normalMatrix: new THREE.Matrix3(),
-					inverseMatrix: new THREE.Matrix4()
-				};
+		// Randomize painting :)
 
 
-			}
+		if ( scope.randomize ) {
 
 
-			modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
+			for ( var i = 0; i < totalBlocks; i ++ ) {
 
 
-			var _object = cache[ object.id ];
+				var swap = Math.random()  * totalBlocks | 0;
+				var tmp = toRender[ swap ];
+				toRender[ swap ] = toRender[ i ];
+				toRender[ i ] = tmp;
 
 
-			_object.normalMatrix.getNormalMatrix( modelViewMatrix );
-			_object.inverseMatrix.getInverse( object.matrixWorld );
+			}
+
+		}
 
 
-		} );
 
 
-		renderBlock( 0, 0 );
+		pool.forEach( renderNext );
 
 
 	};
 	};
 
 

+ 571 - 0
examples/js/renderers/RaytracingWorker.js

@@ -0,0 +1,571 @@
+var worker;
+var BLOCK = 128;
+var startX, startY, division, completed = 0;
+
+var scene, camera, renderer, loader, sceneId;
+
+importScripts( '../../../build/three.min.js' );
+
+
+self.onmessage = function( e ) {
+
+	var data = e.data;
+	if ( ! data ) return;
+
+	if ( data.init ) {
+
+		var
+			width = data.init[ 0 ],
+			height = data.init[ 1 ];
+
+		worker = data.worker;
+		BLOCK = data.blockSize;
+
+		if ( ! renderer ) renderer = new THREE.RaytracingRendererWorker();
+		if ( ! loader ) loader = new THREE.ObjectLoader();
+
+		renderer.setSize( width, height );
+
+		// TODO fix passing maxRecursionDepth as parameter.
+		// if (data.maxRecursionDepth) maxRecursionDepth = data.maxRecursionDepth;
+
+		completed = 0;
+
+	}
+
+	if ( data.scene ) {
+
+		scene = loader.parse( data.scene );
+		camera = loader.parse( data.camera );
+
+		var meta = data.annex;
+		scene.traverse( function( o ) {
+
+			if ( o instanceof THREE.PointLight ) {
+
+				o.physicalAttenuation = true;
+
+			}
+
+			var mat = o.material;
+
+			if (!mat) return;
+
+			var material = meta[ mat.uuid ];
+			for (var m in material) {
+
+				mat[ m ] = material[ m ];
+
+			}
+
+		} );
+
+		sceneId = data.sceneId;
+	}
+
+	if ( data.render && scene && camera ) {
+
+		startX = data.x;
+		startY = data.y;
+		renderer.render( scene, camera );
+
+	}
+
+}
+
+/**
+ * DOM-less version of Raytracing Renderer
+ * @author mrdoob / http://mrdoob.com/
+ * @author alteredq / http://alteredqualia.com/
+ * @author zz95 / http://github.com/zz85
+ */
+
+THREE.RaytracingRendererWorker = function ( parameters ) {
+
+	console.log( 'THREE.RaytracingRendererWorker', THREE.REVISION );
+
+	parameters = parameters || {};
+
+	var scope = this;
+
+	var maxRecursionDepth = 3;
+
+	var canvasWidth, canvasHeight;
+	var canvasWidthHalf, canvasHeightHalf;
+	var origin = new THREE.Vector3();
+	var direction = new THREE.Vector3();
+
+	var cameraPosition = new THREE.Vector3();
+
+	var raycaster = new THREE.Raycaster( origin, direction );
+	var raycasterLight = new THREE.Raycaster();
+
+	var perspective;
+	var modelViewMatrix = new THREE.Matrix4();
+	var cameraNormalMatrix = new THREE.Matrix3();
+
+	var objects;
+	var lights = [];
+	var cache = {};
+
+	var animationFrameId = null;
+
+	this.setSize = function ( width, height ) {
+
+		canvasWidth = width;
+		canvasHeight = height;
+
+		canvasWidthHalf = Math.floor( canvasWidth / 2 );
+		canvasHeightHalf = Math.floor( canvasHeight / 2 );
+
+	};
+
+	//
+
+	var spawnRay = ( function () {
+
+		var diffuseColor = new THREE.Color();
+		var specularColor = new THREE.Color();
+		var lightColor = new THREE.Color();
+		var schlick = new THREE.Color();
+
+		var lightContribution = new THREE.Color();
+
+		var eyeVector = new THREE.Vector3();
+		var lightVector = new THREE.Vector3();
+		var normalVector = new THREE.Vector3();
+		var halfVector = new THREE.Vector3();
+
+		var localPoint = new THREE.Vector3();
+		var reflectionVector = new THREE.Vector3();
+
+		var tmpVec = new THREE.Vector3();
+
+		var tmpColor = [];
+
+		for ( var i = 0; i < maxRecursionDepth; i ++ ) {
+
+			tmpColor[ i ] = new THREE.Color();
+
+		}
+
+		return function spawnRay( rayOrigin, rayDirection, outputColor, recursionDepth ) {
+
+			var ray = raycaster.ray;
+
+			ray.origin = rayOrigin;
+			ray.direction = rayDirection;
+
+			//
+
+			var rayLight = raycasterLight.ray;
+
+			//
+
+			outputColor.setRGB( 0, 0, 0 );
+
+			//
+
+			var intersections = raycaster.intersectObjects( objects, true );
+
+			// ray didn't find anything
+			// (here should come setting of background color?)
+
+			if ( intersections.length === 0 ) {
+
+				return;
+
+			}
+
+			// ray hit
+
+			var intersection = intersections[ 0 ];
+
+			var point = intersection.point;
+			var object = intersection.object;
+			var material = object.material;
+			var face = intersection.face;
+
+			var vertices = object.geometry.vertices;
+
+			//
+
+			var _object = cache[ object.id ];
+
+			localPoint.copy( point ).applyMatrix4( _object.inverseMatrix );
+			eyeVector.subVectors( raycaster.ray.origin, point ).normalize();
+
+			// resolve pixel diffuse color
+
+			if ( material instanceof THREE.MeshLambertMaterial ||
+				 material instanceof THREE.MeshPhongMaterial ||
+				 material instanceof THREE.MeshBasicMaterial ) {
+
+				diffuseColor.copyGammaToLinear( material.color );
+
+			} else {
+
+				diffuseColor.setRGB( 1, 1, 1 );
+
+			}
+
+			if ( material.vertexColors === THREE.FaceColors ) {
+
+				diffuseColor.multiply( face.color );
+
+			}
+
+			// compute light shading
+
+			rayLight.origin.copy( point );
+
+			if ( material instanceof THREE.MeshBasicMaterial ) {
+
+				for ( var i = 0, l = lights.length; i < l; i ++ ) {
+
+					var light = lights[ i ];
+
+					lightVector.setFromMatrixPosition( light.matrixWorld );
+					lightVector.sub( point );
+
+					rayLight.direction.copy( lightVector ).normalize();
+
+					var intersections = raycasterLight.intersectObjects( objects, true );
+
+					// point in shadow
+
+					if ( intersections.length > 0 ) continue;
+
+					// point visible
+
+					outputColor.add( diffuseColor );
+
+				}
+
+			} else if ( material instanceof THREE.MeshLambertMaterial ||
+						material instanceof THREE.MeshPhongMaterial ) {
+
+				var normalComputed = false;
+
+				for ( var i = 0, l = lights.length; i < l; i ++ ) {
+
+					var light = lights[ i ];
+
+					lightColor.copyGammaToLinear( light.color );
+
+					lightVector.setFromMatrixPosition( light.matrixWorld );
+					lightVector.sub( point );
+
+					rayLight.direction.copy( lightVector ).normalize();
+
+					var intersections = raycasterLight.intersectObjects( objects, true );
+
+					// point in shadow
+
+					if ( intersections.length > 0 ) continue;
+
+					// point lit
+
+					if ( normalComputed === false ) {
+
+						// the same normal can be reused for all lights
+						// (should be possible to cache even more)
+
+						computePixelNormal( normalVector, localPoint, material.shading, face, vertices );
+						normalVector.applyMatrix3( _object.normalMatrix ).normalize();
+
+						normalComputed = true;
+
+					}
+
+					// compute attenuation
+
+					var attenuation = 1.0;
+
+					if ( light.physicalAttenuation === true ) {
+
+						attenuation = lightVector.length();
+						attenuation = 1.0 / ( attenuation * attenuation );
+
+					}
+
+					lightVector.normalize();
+
+					// compute diffuse
+
+					var dot = Math.max( normalVector.dot( lightVector ), 0 );
+					var diffuseIntensity = dot * light.intensity;
+
+					lightContribution.copy( diffuseColor );
+					lightContribution.multiply( lightColor );
+					lightContribution.multiplyScalar( diffuseIntensity * attenuation );
+
+					outputColor.add( lightContribution );
+
+					// compute specular
+
+					if ( material instanceof THREE.MeshPhongMaterial ) {
+
+						halfVector.addVectors( lightVector, eyeVector ).normalize();
+
+						var dotNormalHalf = Math.max( normalVector.dot( halfVector ), 0.0 );
+						var specularIntensity = Math.max( Math.pow( dotNormalHalf, material.shininess ), 0.0 ) * diffuseIntensity;
+
+						var specularNormalization = ( material.shininess + 2.0 ) / 8.0;
+
+						specularColor.copyGammaToLinear( material.specular );
+
+						var alpha = Math.pow( Math.max( 1.0 - lightVector.dot( halfVector ), 0.0 ), 5.0 );
+
+						schlick.r = specularColor.r + ( 1.0 - specularColor.r ) * alpha;
+						schlick.g = specularColor.g + ( 1.0 - specularColor.g ) * alpha;
+						schlick.b = specularColor.b + ( 1.0 - specularColor.b ) * alpha;
+
+						lightContribution.copy( schlick );
+
+						lightContribution.multiply( lightColor );
+						lightContribution.multiplyScalar( specularNormalization * specularIntensity * attenuation );
+						outputColor.add( lightContribution );
+
+					}
+
+				}
+
+			}
+
+			// reflection / refraction
+
+			var reflectivity = material.reflectivity;
+
+			if ( ( material.mirror || material.glass ) && reflectivity > 0 && recursionDepth < maxRecursionDepth ) {
+
+				if ( material.mirror ) {
+
+					reflectionVector.copy( rayDirection );
+					reflectionVector.reflect( normalVector );
+
+				} else if ( material.glass ) {
+
+					var eta = material.refractionRatio;
+
+					var dotNI = rayDirection.dot( normalVector );
+					var k = 1.0 - eta * eta * ( 1.0 - dotNI * dotNI );
+
+					if ( k < 0.0 ) {
+
+						reflectionVector.set( 0, 0, 0 );
+
+					} else {
+
+						reflectionVector.copy( rayDirection );
+						reflectionVector.multiplyScalar( eta );
+
+						var alpha = eta * dotNI + Math.sqrt( k );
+						tmpVec.copy( normalVector );
+						tmpVec.multiplyScalar( alpha );
+						reflectionVector.sub( tmpVec );
+
+					}
+
+				}
+
+				var theta = Math.max( eyeVector.dot( normalVector ), 0.0 );
+				var rf0 = reflectivity;
+				var fresnel = rf0 + ( 1.0 - rf0 ) * Math.pow( ( 1.0 - theta ), 5.0 );
+
+				var weight = fresnel;
+
+				var zColor = tmpColor[ recursionDepth ];
+
+				spawnRay( point, reflectionVector, zColor, recursionDepth + 1 );
+
+				if ( material.specular !== undefined ) {
+
+					zColor.multiply( material.specular );
+
+				}
+
+				zColor.multiplyScalar( weight );
+				outputColor.multiplyScalar( 1 - weight );
+				outputColor.add( zColor );
+
+			}
+
+		};
+
+	}() );
+
+	var computePixelNormal = ( function () {
+
+		var tmpVec1 = new THREE.Vector3();
+		var tmpVec2 = new THREE.Vector3();
+		var tmpVec3 = new THREE.Vector3();
+
+		return function computePixelNormal( outputVector, point, shading, face, vertices ) {
+
+			var faceNormal = face.normal;
+			var vertexNormals = face.vertexNormals;
+
+			if ( shading === THREE.FlatShading ) {
+
+				outputVector.copy( faceNormal );
+
+			} else if ( shading === THREE.SmoothShading ) {
+
+				// compute barycentric coordinates
+
+				var vA = vertices[ face.a ];
+				var vB = vertices[ face.b ];
+				var vC = vertices[ face.c ];
+
+				tmpVec3.crossVectors( tmpVec1.subVectors( vB, vA ), tmpVec2.subVectors( vC, vA ) );
+				var areaABC = faceNormal.dot( tmpVec3 );
+
+				tmpVec3.crossVectors( tmpVec1.subVectors( vB, point ), tmpVec2.subVectors( vC, point ) );
+				var areaPBC = faceNormal.dot( tmpVec3 );
+				var a = areaPBC / areaABC;
+
+				tmpVec3.crossVectors( tmpVec1.subVectors( vC, point ), tmpVec2.subVectors( vA, point ) );
+				var areaPCA = faceNormal.dot( tmpVec3 );
+				var b = areaPCA / areaABC;
+
+				var c = 1.0 - a - b;
+
+				// compute interpolated vertex normal
+
+				tmpVec1.copy( vertexNormals[ 0 ] );
+				tmpVec1.multiplyScalar( a );
+
+				tmpVec2.copy( vertexNormals[ 1 ] );
+				tmpVec2.multiplyScalar( b );
+
+				tmpVec3.copy( vertexNormals[ 2 ] );
+				tmpVec3.multiplyScalar( c );
+
+				outputVector.addVectors( tmpVec1, tmpVec2 );
+				outputVector.add( tmpVec3 );
+
+			}
+
+		};
+
+	}() );
+
+	var renderBlock = ( function () {
+
+		var blockSize = BLOCK;
+
+		var data = new Uint8ClampedArray( blockSize * blockSize * 4 );
+
+		var pixelColor = new THREE.Color();
+
+		return function renderBlock( blockX, blockY ) {
+
+			var index = 0;
+
+			for ( var y = 0; y < blockSize; y ++ ) {
+
+				for ( var x = 0; x < blockSize; x ++, index += 4 ) {
+
+					// spawn primary ray at pixel position
+
+					origin.copy( cameraPosition );
+
+					direction.set( x + blockX - canvasWidthHalf, - ( y + blockY - canvasHeightHalf ), - perspective );
+					direction.applyMatrix3( cameraNormalMatrix ).normalize();
+
+					spawnRay( origin, direction, pixelColor, 0 );
+
+					// convert from linear to gamma
+
+					data[ index ]     = Math.sqrt( pixelColor.r ) * 255;
+					data[ index + 1 ] = Math.sqrt( pixelColor.g ) * 255;
+					data[ index + 2 ] = Math.sqrt( pixelColor.b ) * 255;
+					data[ index + 3 ] = 255;
+
+				}
+
+			}
+
+			// Use transferable objects! :)
+			self.postMessage( {
+				data: data.buffer,
+				blockX: blockX,
+				blockY: blockY,
+				blockSize: blockSize,
+				sceneId: sceneId,
+				time: Date.now() - reallyThen, // time for this renderer
+			}, [ data.buffer ] );
+
+			data = new Uint8ClampedArray( blockSize * blockSize * 4 );
+
+			// OK Done!
+			completed ++;
+
+		};
+
+	}() );
+
+	this.render = function ( scene, camera ) {
+
+		reallyThen = Date.now()
+
+		cancelAnimationFrame( animationFrameId );
+
+		// update scene graph
+
+		if ( scene.autoUpdate === true ) scene.updateMatrixWorld();
+
+		// update camera matrices
+
+		if ( camera.parent === null ) camera.updateMatrixWorld();
+
+		camera.matrixWorldInverse.getInverse( camera.matrixWorld );
+		cameraPosition.setFromMatrixPosition( camera.matrixWorld );
+
+		//
+
+		cameraNormalMatrix.getNormalMatrix( camera.matrixWorld );
+		origin.copy( cameraPosition );
+
+		perspective = 0.5 / Math.tan( THREE.Math.degToRad( camera.fov * 0.5 ) ) * canvasHeight;
+
+		objects = scene.children;
+
+		// collect lights and set up object matrices
+
+		lights.length = 0;
+
+		scene.traverse( function ( object ) {
+
+			if ( object instanceof THREE.Light ) {
+
+				lights.push( object );
+
+			}
+
+			if ( cache[ object.id ] === undefined ) {
+
+				cache[ object.id ] = {
+					normalMatrix: new THREE.Matrix3(),
+					inverseMatrix: new THREE.Matrix4()
+				};
+
+			}
+
+			modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, object.matrixWorld );
+
+			var _object = cache[ object.id ];
+
+			_object.normalMatrix.getNormalMatrix( modelViewMatrix );
+			_object.inverseMatrix.getInverse( object.matrixWorld );
+
+		} );
+
+		renderBlock( startX, startY );
+
+	};
+
+};
+
+THREE.EventDispatcher.prototype.apply( THREE.RaytracingRendererWorker.prototype );

+ 86 - 30
examples/raytracing_sandbox.html

@@ -1,7 +1,7 @@
 <!DOCTYPE html>
 <!DOCTYPE html>
 <html lang="en">
 <html lang="en">
 	<head>
 	<head>
-		<title>three.js - raytracing renderer</title>
+		<title>three.js - raytracing renderer with web workers</title>
 		<meta charset="utf-8">
 		<meta charset="utf-8">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
 		<style>
 		<style>
@@ -14,36 +14,27 @@
 		</style>
 		</style>
 	</head>
 	</head>
 	<body>
 	<body>
-
 		<script src="../build/three.min.js"></script>
 		<script src="../build/three.min.js"></script>
 		<script src="js/renderers/RaytracingRenderer.js"></script>
 		<script src="js/renderers/RaytracingRenderer.js"></script>
 
 
 		<script>
 		<script>
 
 
-			var container;
+			var hash = location.hash ? location.hash.substring( 1 ) : '3';
+
+			var WORKERS = + hash || navigator.hardwareConcurrency || 3;
+
+			var container, info;
 
 
 			var camera, controls, scene, renderer;
 			var camera, controls, scene, renderer;
 
 
-			var torus, cube;
+			var torus, cube, objects = [];
 
 
 			init();
 			init();
 			render();
 			render();
 
 
-			function init() {
-
-				container = document.createElement( 'div' );
-				document.body.appendChild( container );
-
-				var info = document.createElement( 'div' );
-				info.style.position = 'absolute';
-				info.style.top = '10px';
-				info.style.width = '100%';
-				info.style.zIndex = '100';
-				info.style.textAlign = 'center';
-				info.innerHTML = '<a href="http://threejs.org" target="_blank">three.js<a/> - raytracing renderer';
-				container.appendChild( info );
+			function initScene( width, height ) {
 
 
-				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera = new THREE.PerspectiveCamera( 60, width / height, 1, 1000 );
 				camera.position.z = 600;
 				camera.position.z = 600;
 
 
 				scene = new THREE.Scene();
 				scene = new THREE.Scene();
@@ -157,51 +148,54 @@
 
 
 				sphere = new THREE.Mesh( sphereGeometry, phongMaterial );
 				sphere = new THREE.Mesh( sphereGeometry, phongMaterial );
 				sphere.scale.multiplyScalar( 0.5 );
 				sphere.scale.multiplyScalar( 0.5 );
-				sphere.position.set( -50, -250+5, -50 );
+				sphere.position.set( - 50, - 250 + 5, - 50 );
 				scene.add( sphere );
 				scene.add( sphere );
+				objects.push ( sphere );
 
 
 				sphere2 = new THREE.Mesh( sphereGeometry, mirrorMaterialSmooth );
 				sphere2 = new THREE.Mesh( sphereGeometry, mirrorMaterialSmooth );
 				sphere2.scale.multiplyScalar( 0.5 );
 				sphere2.scale.multiplyScalar( 0.5 );
-				sphere2.position.set( 175, -250+5, -150 );
+				sphere2.position.set( 175, - 250 + 5, - 150 );
 				scene.add( sphere2 );
 				scene.add( sphere2 );
+				objects.push ( sphere2 );
 
 
 				// Box
 				// Box
 
 
 				box = new THREE.Mesh( boxGeometry, mirrorMaterialFlat );
 				box = new THREE.Mesh( boxGeometry, mirrorMaterialFlat );
-				box.position.set( -175, -250+2.5, -150 );
+				box.position.set( - 175, - 250 + 2.5, - 150 );
 				box.rotation.y = 0.5;
 				box.rotation.y = 0.5;
 				scene.add( box );
 				scene.add( box );
+				objects.push ( box );
 
 
 				// Glass
 				// Glass
 
 
 				glass = new THREE.Mesh( sphereGeometry, glassMaterialSmooth );
 				glass = new THREE.Mesh( sphereGeometry, glassMaterialSmooth );
 				glass.scale.multiplyScalar( 0.5 );
 				glass.scale.multiplyScalar( 0.5 );
-				glass.position.set( 75, -250+5, -75 );
+				glass.position.set( 75, - 250 + 5, - 75 );
 				glass.rotation.y = 0.5;
 				glass.rotation.y = 0.5;
 				scene.add( glass );
 				scene.add( glass );
 
 
 				// bottom
 				// bottom
 
 
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxBottom );
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxBottom );
-				plane.position.set( 0, -300+2.5, -300 );
+				plane.position.set( 0, - 300 + 2.5, - 300 );
 				scene.add( plane );
 				scene.add( plane );
 
 
 				// top
 				// top
 
 
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBox );
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBox );
-				plane.position.set( 0, 300-2.5, -300 );
+				plane.position.set( 0, 300 - 2.5, - 300 );
 				scene.add( plane );
 				scene.add( plane );
 
 
 				// back
 				// back
 
 
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBox );
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBox );
 				plane.rotation.x = 1.57;
 				plane.rotation.x = 1.57;
-				plane.position.set( 0, 0, -300 );
+				plane.position.set( 0, 0, - 300 );
 				scene.add( plane );
 				scene.add( plane );
 
 
 				plane = new THREE.Mesh( planeGeometry, mirrorMaterialFlatDark );
 				plane = new THREE.Mesh( planeGeometry, mirrorMaterialFlatDark );
 				plane.rotation.x = 1.57;
 				plane.rotation.x = 1.57;
-				plane.position.set( 0, 0, -300+10 );
+				plane.position.set( 0, 0, - 300 + 10 );
 				plane.scale.multiplyScalar( 0.85 );
 				plane.scale.multiplyScalar( 0.85 );
 				scene.add( plane );
 				scene.add( plane );
 
 
@@ -209,14 +203,14 @@
 
 
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxLeft );
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxLeft );
 				plane.rotation.z = 1.57;
 				plane.rotation.z = 1.57;
-				plane.position.set( -300, 0, -300 );
+				plane.position.set( - 300, 0, - 300 );
 				scene.add( plane );
 				scene.add( plane );
 
 
 				// right
 				// right
 
 
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxRight );
 				plane = new THREE.Mesh( planeGeometry, phongMaterialBoxRight );
 				plane.rotation.z = 1.57;
 				plane.rotation.z = 1.57;
-				plane.position.set( 300, 0, -300 );
+				plane.position.set( 300, 0, - 300 );
 				scene.add( plane );
 				scene.add( plane );
 
 
 				// light
 				// light
@@ -224,7 +218,7 @@
 				var intensity = 70000;
 				var intensity = 70000;
 
 
 				var light = new THREE.PointLight( 0xffaa55, intensity );
 				var light = new THREE.PointLight( 0xffaa55, intensity );
-				light.position.set( -200, 100, 100 );
+				light.position.set( - 200, 100, 100 );
 				light.physicalAttenuation = true;
 				light.physicalAttenuation = true;
 				scene.add( light );
 				scene.add( light );
 
 
@@ -238,9 +232,35 @@
 				light.physicalAttenuation = true;
 				light.physicalAttenuation = true;
 				scene.add( light );
 				scene.add( light );
 
 
+			}
+
+			function init() {
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				info = document.createElement( 'div' );
+				info.style.position = 'absolute';
+				info.style.top = '10px';
+				info.style.width = '100%';
+				info.style.zIndex = '100';
+				info.style.textAlign = 'center';
+				container.appendChild( info );
+
+				updateWorkers()
+
+				//
+
+				initScene( window.innerWidth, window.innerHeight );
+
 				//
 				//
 
 
-				renderer = new THREE.RaytracingRenderer();
+				renderer = new THREE.RaytracingRenderer( {
+					workers: WORKERS,
+					workerPath: 'js/renderers/RaytracingWorker.js',
+					randomize: true,
+					blockSize: 64
+				} );
 				renderer.setClearColor( 0xf0f0f0 );
 				renderer.setClearColor( 0xf0f0f0 );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 
 
@@ -250,14 +270,50 @@
 
 
 				container.appendChild( renderer.domElement );
 				container.appendChild( renderer.domElement );
 
 
+
+				window.addEventListener( 'resize', function( e ) {
+
+					renderer.setSize( innerWidth, innerHeight );
+
+				} );
+
 			}
 			}
 
 
+			function updateWorkers( x ) {
+
+				if ( x ) {
+
+					WORKERS = Math.max( 1, WORKERS + x );
+					renderer.setWorkers( WORKERS );
+
+				}
+
+				info.innerHTML = '<a href="http://threejs.org" target="_blank">three.js<a/> - raytracing renderer (using '  + WORKERS + ' <button onclick="updateWorkers(-1)">-</button><button onclick="updateWorkers(1)">+</button> web workers)' +
+				'<br/><button onclick="rearrange()">Rearrange</button><button onclick="render()">Render</button>';
+
+
+			}
+
+			function rearrange() {
+
+				objects.forEach( function( o ) {
+
+					o.position.y += ( Math.random() - 0.5 ) * 100;
+					o.position.x += ( Math.random() - 0.5 ) * 400;
+					o.position.z += ( Math.random() - 0.5 ) * 400;
+
+				} );
+
+			}
+
+
 			function render() {
 			function render() {
 
 
 				renderer.render( scene, camera );
 				renderer.render( scene, camera );
 
 
 			}
 			}
 
 
+
 		</script>
 		</script>
 
 
 	</body>
 	</body>