Skip to content

Conversation

@WestLangley
Copy link
Collaborator

The PR adds a method to set a probe from two colors, as we do a HemisphereLight.

I always thought HemisphereLight was a hack, but it appears the math was actually correct after all. :-)

This PR does not support tilting the axis of the light, like we can with HemisphereLight.

@WestLangley
Copy link
Collaborator Author

@bhouston Remember that factor of PI? It's coming back to haunt us.

When we are using non-physically-based units, the calculated irradiance is scaled up by a factor of PI. Consequently, the scene is rendered brighter than one would otherwise expect.

@mrdoob mrdoob added this to the r104 milestone Apr 17, 2019
@mrdoob mrdoob merged commit ce31c17 into mrdoob:dev Apr 18, 2019
@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

Thanks!

@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

Maybe it'd be simpler code if these were fromAmbientLight() and fromHemisphereLight()?

@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

This is how it would look like:

	fromAmbientLight: function ( light ) {

		var color = light.color;
		// without extra factor of PI in the shader, would be 2 / Math.sqrt( Math.PI );
		var intensity = light.intensity * 2 * Math.sqrt( Math.PI );

		this.sh.zero();
		this.sh.coefficients[ 0 ].set( color.r, color.g, color.b ).multiplyScalar( intensity );

	},

	fromHemisphereLight: function ( light ) {

		// up-direction hardwired

		var color1 = light.color;
		var color2 = light.groundColor;

		var sky = new Vector3( color1.r, color1.g, color1.b );
		var ground = new Vector3( color2.r, color2.g, color2.b );
		var intensity = light.intensity;

		// without extra factor of PI in the shader, should = 1 / Math.sqrt( Math.PI );
		var c0 = Math.sqrt( Math.PI );
		var c1 = c0 * Math.sqrt( 0.75 );

		this.sh.zero();
		this.sh.coefficients[ 0 ].copy( sky ).add( ground ).multiplyScalar( c0 );
		this.sh.coefficients[ 1 ].copy( sky ).sub( ground ).multiplyScalar( c1 );

	},

This is assuming that we remove color and intensity from LightProbe.

While rewriting this I noticed that we're not taking intensity into account in fromHemisphereProbe? Also, I'm aware that fromHemisphereProbe is currently ignoring the direction.

@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

I'm also thinking that these methods could sit in examples for now:

THREE.LightProbeGenerator = {

	fromAmbientLight: function ( light ) {

		var color = light.color;
		// without extra factor of PI in the shader, would be 2 / Math.sqrt( Math.PI );
		var intensity = light.intensity * 2 * Math.sqrt( Math.PI );

		var sh = new THREE.SphericalHarmonics3();
		sh.coefficients[ 0 ].set( color.r, color.g, color.b ).multiplyScalar( intensity );

		return new THREE.LightProbe( sh );

	},

	fromHemisphereLight: function ( light ) {

		// up-direction hardwired

		// up-direction hardwired

		var color1 = light.color;
		var color2 = light.groundColor;

		var sky = new Vector3( color1.r, color1.g, color1.b );
		var ground = new Vector3( color2.r, color2.g, color2.b );
		var intensity = light.intensity;

		// without extra factor of PI in the shader, should = 1 / Math.sqrt( Math.PI );
		var c0 = Math.sqrt( Math.PI );
		var c1 = c0 * Math.sqrt( 0.75 );

		var sh = new THREE.SphericalHarmonics3();
		sh.coefficients[ 0 ].copy( sky ).add( ground ).multiplyScalar( c0 );
		sh.coefficients[ 1 ].copy( sky ).sub( ground ).multiplyScalar( c1 );

		return new THREE.LightProbe( sh );

	},

	// https://www.ppsloan.org/publications/StupidSH36.pdf
	fromCubeTexture: function ( cubeTexture ) {

		var norm, lengthSq, weight, totalWeight = 0;

		var coord = new Vector3();

		var dir = new Vector3();

		var color = new Color();

		var shBasis = [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ];

		var sh = new THREE.SphericalHarmonics3();
		var shCoefficients = sh.coefficients;

		for ( var faceIndex = 0; faceIndex < 6; faceIndex ++ ) {

			var image = cubeTexture.image[ faceIndex ];

			var width = image.width;
			var height = image.height;

			var canvas = document.createElement( 'canvas' );

			canvas.width = width;
			canvas.height = height;

			var context = canvas.getContext( '2d' );

			context.drawImage( image, 0, 0, width, height );

			var imageData = context.getImageData( 0, 0, width, height );

			var data = imageData.data;

			var imageWidth = imageData.width; // assumed to be square

			var pixelSize = 2 / imageWidth;

			for ( var i = 0, il = data.length; i < il; i += 4 ) { // RGBA assumed

				// pixel color
				color.setRGB( data[ i ] / 255, data[ i + 1 ] / 255, data[ i + 2 ] / 255 );

				// convert to linear color space
				color.copySRGBToLinear( color );

				// pixel coordinate on unit cube

				var pixelIndex = i / 4;

				var col = - 1 + ( pixelIndex % imageWidth + 0.5 ) * pixelSize;

				var row = 1 - ( Math.floor( pixelIndex / imageWidth ) + 0.5 ) * pixelSize;

				switch ( faceIndex ) {

					case 0: coord.set( - 1, row, - col ); break;

					case 1: coord.set( 1, row, col ); break;

					case 2: coord.set( - col, 1, - row ); break;

					case 3: coord.set( - col, - 1, row ); break;

					case 4: coord.set( - col, row, 1 ); break;

					case 5: coord.set( col, row, - 1 ); break;

				}

				// weight assigned to this pixel

				lengthSq = coord.lengthSq();

				weight = 4 / ( Math.sqrt( lengthSq ) * lengthSq );

				totalWeight += weight;

				// direction vector to this pixel
				dir.copy( coord ).normalize();

				// evaluate SH basis functions in direction dir
				SphericalHarmonics3.getBasisAt( dir, shBasis );

				// accummuulate
				for ( var j = 0; j < 9; j ++ ) {

					shCoefficients[ j ].x += shBasis[ j ] * color.r * weight;
					shCoefficients[ j ].y += shBasis[ j ] * color.g * weight;
					shCoefficients[ j ].z += shBasis[ j ] * color.b * weight;

				}

			}

		}

		// normalize
		norm = ( 4 * Math.PI ) / totalWeight;

		for ( var j = 0; j < 9; j ++ ) {

			shCoefficients[ j ].x *= norm;
			shCoefficients[ j ].y *= norm;
			shCoefficients[ j ].z *= norm;

		}

		return new THREE.LightProbe( sh );

	}

};

This is how LightProbe would look:

import { _Math } from '../math/Math.js';
import { Vector3 } from '../math/Vector3.js';
import { Color } from '../math/Color.js';
import { SphericalHarmonics3 } from '../math/SphericalHarmonics3.js';
import { Light } from './Light.js';

/**
 * @author WestLangley / http://github.com/WestLangley
 */

// A LightProbe is a source of indirect-diffuse light

function LightProbe( sh ) {

	Object3D.call( this );

	this.sh = ( sh !== undefined ) ? sh : new SphericalHarmonics3();

}

LightProbe.prototype = Object.assign( Object.create( Object3D.prototype ), {

	constructor: LightProbe,

	isLightProbe: true,

	copy: function ( source ) {

		Light.prototype.copy.call( this, source );

		this.sh.copy( source.sh );

		return this;

	},

	toJSON: function ( meta ) {

		var data = Light.prototype.toJSON.call( this, meta );

		//data.sh = this.sh.toArray(); // todo

		return data;

	}

} );

export { LightProbe };

What do you think?

@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

And now I see why intensity is nice to have. Let me try again:

THREE.LightProbeGenerator = {

	fromAmbientLight: function ( light ) {

		var color = light.color;
		var intensity = light.intensity;

		var sh = new THREE.SphericalHarmonics3();

		// without extra factor of PI in the shader, would be 2 / Math.sqrt( Math.PI );
		sh.coefficients[ 0 ].set( color.r, color.g, color.b ).multiplyScalar(  2 * Math.sqrt( Math.PI ) );

		return new THREE.LightProbe( sh, intensity );

	},

	fromHemisphereLight: function ( light ) {

		// up-direction hardwired

		// up-direction hardwired

		var color1 = light.color;
		var color2 = light.groundColor;

		var sky = new Vector3( color1.r, color1.g, color1.b );
		var ground = new Vector3( color2.r, color2.g, color2.b );
		var intensity = light.intensity;

		// without extra factor of PI in the shader, should = 1 / Math.sqrt( Math.PI );
		var c0 = Math.sqrt( Math.PI );
		var c1 = c0 * Math.sqrt( 0.75 );

		var sh = new THREE.SphericalHarmonics3();
		sh.coefficients[ 0 ].copy( sky ).add( ground ).multiplyScalar( c0 );
		sh.coefficients[ 1 ].copy( sky ).sub( ground ).multiplyScalar( c1 );

		return new THREE.LightProbe( sh, intensity );

	},

	// https://www.ppsloan.org/publications/StupidSH36.pdf
	fromCubeTexture: function ( cubeTexture ) {

		var norm, lengthSq, weight, totalWeight = 0;

		var coord = new Vector3();

		var dir = new Vector3();

		var color = new Color();

		var shBasis = [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ];

		var sh = new THREE.SphericalHarmonics3();
		var shCoefficients = sh.coefficients;

		for ( var faceIndex = 0; faceIndex < 6; faceIndex ++ ) {

			var image = cubeTexture.image[ faceIndex ];

			var width = image.width;
			var height = image.height;

			var canvas = document.createElement( 'canvas' );

			canvas.width = width;
			canvas.height = height;

			var context = canvas.getContext( '2d' );

			context.drawImage( image, 0, 0, width, height );

			var imageData = context.getImageData( 0, 0, width, height );

			var data = imageData.data;

			var imageWidth = imageData.width; // assumed to be square

			var pixelSize = 2 / imageWidth;

			for ( var i = 0, il = data.length; i < il; i += 4 ) { // RGBA assumed

				// pixel color
				color.setRGB( data[ i ] / 255, data[ i + 1 ] / 255, data[ i + 2 ] / 255 );

				// convert to linear color space
				color.copySRGBToLinear( color );

				// pixel coordinate on unit cube

				var pixelIndex = i / 4;

				var col = - 1 + ( pixelIndex % imageWidth + 0.5 ) * pixelSize;

				var row = 1 - ( Math.floor( pixelIndex / imageWidth ) + 0.5 ) * pixelSize;

				switch ( faceIndex ) {

					case 0: coord.set( - 1, row, - col ); break;

					case 1: coord.set( 1, row, col ); break;

					case 2: coord.set( - col, 1, - row ); break;

					case 3: coord.set( - col, - 1, row ); break;

					case 4: coord.set( - col, row, 1 ); break;

					case 5: coord.set( col, row, - 1 ); break;

				}

				// weight assigned to this pixel

				lengthSq = coord.lengthSq();

				weight = 4 / ( Math.sqrt( lengthSq ) * lengthSq );

				totalWeight += weight;

				// direction vector to this pixel
				dir.copy( coord ).normalize();

				// evaluate SH basis functions in direction dir
				SphericalHarmonics3.getBasisAt( dir, shBasis );

				// accummuulate
				for ( var j = 0; j < 9; j ++ ) {

					shCoefficients[ j ].x += shBasis[ j ] * color.r * weight;
					shCoefficients[ j ].y += shBasis[ j ] * color.g * weight;
					shCoefficients[ j ].z += shBasis[ j ] * color.b * weight;

				}

			}

		}

		// normalize
		norm = ( 4 * Math.PI ) / totalWeight;

		for ( var j = 0; j < 9; j ++ ) {

			shCoefficients[ j ].x *= norm;
			shCoefficients[ j ].y *= norm;
			shCoefficients[ j ].z *= norm;

		}

		return new THREE.LightProbe( sh );

	}

};

LightProbe:

import { _Math } from '../math/Math.js';
import { Vector3 } from '../math/Vector3.js';
import { Color } from '../math/Color.js';
import { SphericalHarmonics3 } from '../math/SphericalHarmonics3.js';
import { Light } from './Light.js';

/**
 * @author WestLangley / http://github.com/WestLangley
 */

// A LightProbe is a source of indirect-diffuse light

function LightProbe( sh, intensity ) {

	Object3D.call( this );

	this.sh = ( sh !== undefined ) ? sh : new SphericalHarmonics3();
	this.intensity = ( intensity !== undefined ) ? intensity : 1.0;

}

LightProbe.prototype = Object.assign( Object.create( Object3D.prototype ), {

	constructor: LightProbe,

	isLightProbe: true,

	copy: function ( source ) {

		Object3D.prototype.copy.call( this, source );

		this.sh.copy( source.sh );
		this.intensity = source.intensity;

		return this;

	},

	toJSON: function ( meta ) {

		var data = Light.prototype.toJSON.call( this, meta );

		//data.sh = this.sh.toArray(); // todo

		return data;

	}

} );

export { LightProbe };

I have ended up pretty much with your original design 😅

@WestLangley WestLangley deleted the dev-light_probe_hemi branch April 18, 2019 15:34
@WestLangley
Copy link
Collaborator Author

@mrdoob Thanks! I am going to fix the factor-of-pi thing.

The purpose of this PR was to show how to set the SH coefficients to model a hemisphere light. Whether we will be doing that in practice is still up for debate.

@mrdoob
Copy link
Owner

mrdoob commented Apr 18, 2019

Sounds good. Are you okay with me refactoring the code as I proposed? I'm hoping to release r104 next week and I want to avoid increasing the filesize too much.

@WestLangley
Copy link
Collaborator Author

OK. Give it a go...

@WestLangley
Copy link
Collaborator Author

@mrdoob Can you make your desired changes now?

@mrdoob
Copy link
Owner

mrdoob commented Apr 20, 2019

Yes, sorry. I got distracted this week. Going to do the changes today.

@mrdoob
Copy link
Owner

mrdoob commented Apr 20, 2019

Done!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants