-
-
Notifications
You must be signed in to change notification settings - Fork 36.1k
LightProbe: set from hemisphere light #16271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@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. |
|
Thanks! |
|
Maybe it'd be simpler code if these were |
|
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 While rewriting this I noticed that we're not taking |
|
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 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? |
|
And now I see why 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 😅 |
|
@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. |
|
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. |
|
OK. Give it a go... |
|
@mrdoob Can you make your desired changes now? |
|
Yes, sorry. I got distracted this week. Going to do the changes today. |
|
Done! |
The PR adds a method to set a probe from two colors, as we do a
HemisphereLight.I always thought
HemisphereLightwas 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.