API Docs for:
Show:

File: ../src/components/reflectionProbe.js

///@INFO: UNCOMMON
/**
* Realtime Reflective surface
* @class RealtimeReflector
* @namespace LS.Components
* @constructor
* @param {String} object to configure from
*/


function ReflectionProbe(o)
{
	this._enabled = true;
	this.texture_size = 512;
	this.high_precision = false;
	this.texture_name = "";
	this.generate_irradiance = true;
	this.generate_mipmaps = false;
	this.refresh_rate = 1; //in frames
	this.layers = 0xFF;

	this.near = 0.1;
	this.far = 1000;
	this.background_color = vec4.create();

	this._position = vec3.create();
	this._current = vec3.create();
	this._version = -1;

	this._texture = null;
	this._irradiance_texture = null;

	this._texid = ":probe_" + ReflectionProbe.last_id;
	ReflectionProbe.last_id++;

	if(o)
		this.configure(o);
}

ReflectionProbe.last_id = 0;

ReflectionProbe.icon = "mini-icon-reflector.png";

ReflectionProbe["@texture_size"] = { type:"enum", values:["viewport",64,128,256,512,1024,2048] };
ReflectionProbe["@layers"] = { type:"layers" };
ReflectionProbe["@background_color"] = { type:"color" };

Object.defineProperty( ReflectionProbe.prototype, "enabled", {
	set: function(v){ 
		if(v == this._enabled)
			return;
		this._enabled = v; 
		if(!this._enabled)
			return;
		this.onRenderReflection();
		LS.GlobalScene.requestFrame();
	},
	get: function() { return this._enabled; },
	enumerable: true
});

ReflectionProbe.prototype.onAddedToScene = function(scene)
{
	LEvent.bind( scene,"start", this.onRenderReflection, this );
	LEvent.bind( scene,"renderReflections", this.onRenderReflection, this );
	//LEvent.bind( scene,"afterCameraEnabled", this.onCameraEnabled, this );
	//LEvent.bind( LS.Renderer,"renderHelpers", this.onVisualizeProbe, this );

	this.assignCubemaps(scene);
}

ReflectionProbe.prototype.onRemovedFromScene = function(scene)
{
	LEvent.unbindAll( scene, this );
	//LEvent.unbindAll( LS.Renderer, this );
	
	if(this._texture)
		LS.ResourcesManager.unregisterResource( this._texid );
	if(this._irradiance_texture)
		LS.ResourcesManager.unregisterResource( this._texid + "_IR" );

	//TODO: USE POOL!!
	this._texture = null;
	this._irradiance_texture = null;
}

ReflectionProbe.prototype.afterSerialize = function(o)
{
	if(this._irradiance_texture)
		o.irradiance_info = ReflectionProbe.cubemapToObject( this._irradiance_texture );
}

ReflectionProbe.prototype.afterConfigure = function(o)
{
	if(o.irradiance_info)
	{
		this._irradiance_texture = ReflectionProbe.objectToCubemap( o.irradiance_info, this._irradiance_texture, this.high_precision );
		this.assignCubemaps();
	}
}

ReflectionProbe.prototype.onRenderReflection = function( e )
{
	this.updateTextures();
}

ReflectionProbe.prototype.updateTextures = function( render_settings, force )
{
	if(!this._enabled || !this._root || !this._root.scene )
		return;

	var scene = this._root.scene;

	this._root.transform.getGlobalPosition( this._current );
	//if ( vec3.distance( this._current, this._position ) < 0.1 )
	//	force = true;

	if( LS.ResourcesManager.isLoading() )
		return;

	this.refresh_rate = this.refresh_rate|0;
	if( this.refresh_rate < 1 && this._texture && !force )
		return;

	if ( this._texture && (scene._frame % this.refresh_rate) != 0 && !force )
		return;

	this.updateCubemap( this._current, render_settings );

	if(this.generate_irradiance)
		this.updateIrradiance();
}

ReflectionProbe.prototype.updateCubemap = function( position, render_settings )
{
	render_settings = render_settings || LS.Renderer.default_render_settings;

	var scene = this._root.scene;
	if(!scene)
		return;

	var texture_size = parseInt( this.texture_size );

	//add flags
	var old_layers = render_settings.layers;
	render_settings.layers = this.layers;

	LS.Renderer.clearSamplers();

	var texture_type = gl.TEXTURE_CUBE_MAP;
	var type = this.high_precision ? gl.HIGH_PRECISION_FORMAT : gl.UNSIGNED_BYTE;

	var texture = this._texture;

	if(!texture || texture.width != texture_size || texture.height != texture_size || texture.type != type || texture.texture_type != texture_type || texture.minFilter != (this.generate_mipmaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR) )
	{
		texture = new GL.Texture( texture_size, texture_size, { type: type, texture_type: texture_type, minFilter: this.generate_mipmaps ? gl.LINEAR_MIPMAP_LINEAR : gl.LINEAR });
		texture.has_mipmaps = this.generate_mipmaps;
		this._texture = texture;
	}

	if(position)
		this._position.set( position );
	else
		position = this._root.transform.getGlobalPosition( this._position );

	texture._in_current_fbo = true; //block binding this texture during rendering of the reflection

	//first render
	if( !LS.Renderer._visible_instances )
	{
		LS.Renderer.processVisibleData( scene, render_settings );
		LS.Renderer.regenerateShadowmaps( scene, render_settings );
	}

	//avoid reusing same irradiance from previous pass
	var tmp = null;
	if( LS.GlobalScene.info.textures.irradiance == this._irradiance_texture )
	{
		tmp = LS.GlobalScene.info.textures.irradiance;
		LS.GlobalScene.info.textures.irradiance = null;
	}

	//render all the scene inside the cubemap
	LS.Renderer.renderToCubemap( position, 0, texture, render_settings, this.near, this.far, this.background_color );

	if(tmp)
		LS.GlobalScene.info.textures.irradiance = tmp;

	texture._in_current_fbo = false;

	if(this.generate_mipmaps && isPowerOfTwo( texture_size ) )
	{
		texture.setParameter( gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR );
		gl.generateMipmap(texture.texture_type);
		texture.unbind();
	}

	if(this.texture_name)
		LS.ResourcesManager.registerResource( this.texture_name, texture );
	LS.ResourcesManager.registerResource( this._texid, texture );

	//add probe to LS.Renderer
	//TODO
	//HACK
	if( scene.info )
		scene.info.textures.environment = this._texid;

	//remove flags
	render_settings.layers = old_layers;
}

ReflectionProbe.prototype.updateIrradiance = function()
{
	var scene = this._root.scene;
	if(!scene)
		return;

	if(!this._texture)
		this.updateCubemap();

	if(!this._texture)
		return;

	var cubemap = this._texture;
	var type = this.high_precision ? gl.HIGH_PRECISION_FORMAT : gl.UNSIGNED_BYTE;

	//create textures for high and low
	if(!ReflectionProbe._downscale_cubemap)
	{
		ReflectionProbe._downscale_cubemap = new GL.Texture( 32, 32, { texture_type: gl.TEXTURE_CUBE_MAP, format: gl.RGB, filter: gl.LINEAR } );
		ReflectionProbe._downscale_cubemap_high = new GL.Texture( 32, 32, { type: gl.HIGH_PRECISION_FORMAT, texture_type: gl.TEXTURE_CUBE_MAP, format: gl.RGB, filter: gl.LINEAR } );
	}

	var downscale_cubemap = this.high_precision ? ReflectionProbe._downscale_cubemap_high : ReflectionProbe._downscale_cubemap;

	//downscale
	cubemap.copyTo( downscale_cubemap );
	
	//blur
	var temp_texture = GL.Texture.getTemporary( downscale_cubemap.width, downscale_cubemap.height, downscale_cubemap );

	var origin = downscale_cubemap;
	var destination = temp_texture;

	for(var i = 0; i < 8; ++i)
	{
		origin.applyBlur( i,i,1, destination );
		var tmp = origin;
		origin = destination;
		destination = tmp;
	}
	destination = origin;

	//downscale again
	var irradiance_cubemap = this._irradiance_texture;
	if(!irradiance_cubemap || irradiance_cubemap.type != type)
		irradiance_cubemap = this._irradiance_texture = new GL.Texture( 4, 4, { type: type, texture_type: gl.TEXTURE_CUBE_MAP, format: gl.RGB, filter: gl.LINEAR } );
	destination.copyTo( irradiance_cubemap );

	//blur again
	for(var i = 0; i < 4; ++i)
		irradiance_cubemap.applyBlur( i,i,1 );

	this.assignCubemaps();

	GL.Texture.releaseTemporary( temp_texture );

	return irradiance_cubemap;
}

ReflectionProbe.prototype.assignCubemaps = function( scene )
{
	if(!scene && this._root)
		scene = this._root.scene;
	if(!scene)
		scene = LS.GlobalScene;

	if(this._texture)
	{
		var tex = this._texture;
		LS.ResourcesManager.registerResource( this._texid, this._texture );
		if( scene.info )
			scene.info.textures.environment = this._texid;
	}

	if(this._irradiance_texture)
	{
		var ir_name = this._texid + "_IR";
		LS.ResourcesManager.registerResource( ir_name, this._irradiance_texture );
		if( scene.info )
			scene.info.textures.irradiance = ir_name;
	}
}


ReflectionProbe.prototype.renderProbe = function( visualize_irradiance, picking_color )
{
	if( !this._texture || !this._enabled )
		return;

	LS.Draw.push();
	LS.Draw.translate( this._position );
	LS.Draw.scale( ReflectionProbe.helper_size );

	if(!picking_color) //regular texture
	{
		var shader = GL.Shader.getCubemapShowShader();

        if(visualize_irradiance)
		    this._irradiance_texture.bind(0);
        else
		    this._texture.bind(0);
            
		LS.Draw.renderMesh( LS.Renderer._sphere_mesh, GL.TRIANGLES, shader );
        if(1) //contour
        {
            LS.Draw.setColor( LS.WHITE );
            LS.Draw.scale( 1.1 );
            gl.enable( gl.CULL_FACE );
            gl.frontFace( gl.CW );
            LS.Draw.renderMesh( LS.Renderer._sphere_mesh, GL.TRIANGLES );
            gl.frontFace( gl.CCW );
        }
	}
	else
	{
		LS.Draw.setColor( picking_color )
		LS.Draw.renderMesh( LS.Renderer._sphere_mesh, GL.TRIANGLES );
	}

	LS.Draw.pop();
}

ReflectionProbe.cubemapToObject = function( cubemap )
{
	var faces = [];
	for( var i = 0; i < 6; ++i )
	{
		var data = typedArrayToArray( cubemap.getPixels(i) );
		faces.push( data );
	}
	return {
		texture_type: cubemap.texture_type,
		size: cubemap.width,
		format: cubemap.format,
		faces: faces
	};
}

ReflectionProbe.objectToCubemap = function( data, out, high_precision )
{
	var type = high_precision ? gl.HIGH_PRECISION_FORMAT : gl.UNSIGNED_BYTE;
	if(!out)
		out = new GL.Texture( data.size, data.size, { type: type, texture_type: gl.TEXTURE_CUBE_MAP, format: GL.RGBA });
	for(var i = 0; i < data.faces.length; ++i )
	{
		var data_typed;
		if(type == gl.FLOAT)
			data_typed = new Float32Array( data.faces[i] );
		else if(type == gl.HIGH_PRECISION_FORMAT)
			data_typed = new Uint16Array( data.faces[i] );
		else //if(type == gl.UNSIGNED_BYTE)
			data_typed = new Uint8Array( data.faces[i] );
		out.setPixels( data_typed, true, i == 5, i );
	}
	return out;
}

/*
ReflectionProbe.cubemapToIrradiance = function( origin_cubemap, destination_cubemap )
{
	var iterations = Math.log(origin_cubemap.width) / Math.log(2);

	var width = origin_cubemap.width;
	var temp_textures = [];
	var origin = origin_cubemap;
	var dest = null;
	for( var i = 0; i < iterations; ++i)
	{
		width = width >> 1;
		if(width <= 8)
			break;
		var temp = GL.Texture.getTemporary( width, width, destination_cubemap );
		temp_textures.push( temp );
		origin.applyBlur(0.5,0.5,1, temp);
		origin = temp;
	}

	temp.copyTo( destination_cubemap );
	for(var i = 0; i < temp_textures.length; ++i)
		GL.Texture.releaseTemporary( temp_textures[i] );
}
*/

ReflectionProbe.visualize_helpers = true;
ReflectionProbe.visualize_irradiance = false;
ReflectionProbe.helper_size = 1;

LS.registerComponent( ReflectionProbe );



function IrradianceCache( o )
{
	this.enabled = true;
	this.corner = vec3.create();
	this.size = vec3.fromValues(10,10,10);
	this.subdivisions = new Uint8Array([4,4,4]);
	this.layers = 0xFF; //layers that can contribute to the irradiance
	this.high_precision = false;

	this.near = 0.1;
	this.far = 1000;
	this.background_color = vec4.create();

	this.mode = IrradianceCache.VERTEX_MODE;

	this._irradiance_texture = null;
	this._irradiance_cubemaps = [];

	if(o)
		this.configure(o);
}

IrradianceCache.show_probes = false;
IrradianceCache.probes_size = 1;
IrradianceCache.capture_cubemap_size = 64;
IrradianceCache.final_cubemap_size = 8;

IrradianceCache.OBJECT_MODE = 1;
IrradianceCache.VERTEX_MODE = 2;
IrradianceCache.PIXEL_MODE = 3;

IrradianceCache["@mode"] = { type:"enum", values: { "object": IrradianceCache.OBJECT_MODE, "vertex": IrradianceCache.VERTEX_MODE, "pixel": IrradianceCache.PIXEL_MODE } };
IrradianceCache["@size"] = { type:"vec3", min: 0.1, step: 0.1, precision:3 };
IrradianceCache["@subdivisions"] = { type:"vec3", min: 1, max: 100, step: 1, precision:0 };
IrradianceCache["@layers"] = { widget:"layers" };
IrradianceCache["@background_color"] = { type:"color" };

IrradianceCache.prototype.recompute = function()
{
	var subs = this.subdivisions;
	var corner = this.corner;
	var size = this.size;
	var iscale = vec3.fromValues( size[0]/subs[0], size[1]/subs[1], size[2]/subs[2] );

	//cubemap
	var type = this.high_precision ? gl.HIGH_PRECISION_FORMAT : gl.UNSIGNED_BYTE;
	var render_settings = LS.Renderer.default_render_settings;
	var old_layers = render_settings.layers;
	render_settings.layers = this.layers;
	LS.GlobalScene.info.textures.irradiance = null;

	var final_cubemap_size = IrradianceCache.final_cubemap_size;
	var texture_size = IrradianceCache.capture_cubemap_size; //default is 64
	var texture_settings = { type: type, texture_type: gl.TEXTURE_CUBE_MAP, format: gl.RGB };
	var texture = this._temp_cubemap;
	if( !texture || texture.width != texture_size || texture.height != texture_size || texture.type != texture_settings.type )
		this._temp_cubemap = texture = new GL.Texture( texture_size, texture_size, texture_settings );

	//first render
	if( !LS.Renderer._visible_instances )
	{
		var scene = this._root.scene;
		if(!scene)
			throw("cannot compute irradiance without scene");
		LS.Renderer.processVisibleData( scene, render_settings );
		LS.Renderer.regenerateShadowmaps( scene, render_settings );
	}

	//compute cache size
	var num_probes = this.subdivisions[0] * this.subdivisions[1] * this.subdivisions[2];
	this._irradiance_cubemaps.length = num_probes;

	var global_matrix = this._root.transform ? this._root.transform.getGlobalMatrixRef() : null;
   	var position = vec3.create();

	var i = 0;
	for(var x = 0; x < subs[0]; ++x)
	for(var y = 0; y < subs[1]; ++y)
	for(var z = 0; z < subs[2]; ++z)
	{
		position[0] = x * iscale[0] + corner[0];
		position[1] = y * iscale[1] + corner[1];
		position[2] = z * iscale[2] + corner[2];

		if( global_matrix )
			mat4.multiplyVec3( position, global_matrix, position );

		var cubemap = this._irradiance_cubemaps[ i ];
		if(!cubemap || cubemap.type != texture_settings.type || cubemap.width != final_cubemap_size )
			this._irradiance_cubemaps[ i ] = cubemap = new GL.Texture( final_cubemap_size, final_cubemap_size, texture_settings );

		this.captureIrradiance( position, cubemap, render_settings );

		i+=1;
	}

	//remove flags
	render_settings.layers = old_layers;
}

IrradianceCache.prototype.captureIrradiance = function( position, output_cubemap, render_settings )
{
	LS.Renderer.clearSamplers();

	//render all the scene inside the cubemap
	LS.Renderer.renderToCubemap( position, 0, this._temp_cubemap, render_settings, this.near, this.far, this.background_color );

	//downsample
	//ReflectionProbe.cubemapToIrradiance( this._temp_cubemap, output_cubemap );
	this._temp_cubemap.copyTo( output_cubemap );
}

IrradianceCache.prototype.encodeCacheInTexture = function()
{
		
}

IrradianceCache.prototype.renderEditor = function()
{
	if(!this.enabled || !IrradianceCache.show_probes)
		return;

	var shader = GL.Shader.getCubemapShowShader();
	var mesh = LS.Renderer._sphere_mesh;
	var subs = this.subdivisions;
	var corner = this.corner;
	var size = this.size;
	var iscale = vec3.fromValues( size[0]/subs[0], size[1]/subs[1], size[2]/subs[2] );

	var shader = GL.Shader.getCubemapShowShader();
	var mesh = LS.Renderer._sphere_mesh;

	var default_cubemap = IrradianceCache.default_cubemap;
	if(!default_cubemap)
		default_cubemap = IrradianceCache.default_cubemap = new GL.Texture(1,1,{ texture_type: GL.TEXTURE_CUBE_MAP, format: GL.RGB, pixel_data:[255,255,255] });

	var position = vec3.create();
	var global_matrix = this._root.transform ? this._root.transform.getGlobalMatrixRef() : null;
	   
	var i = 0;
	for(var x = 0; x < subs[0]; ++x)
	for(var y = 0; y < subs[1]; ++y)
	for(var z = 0; z < subs[2]; ++z)
	{
		position[0] = x * iscale[0] + corner[0];
		position[1] = y * iscale[1] + corner[1];
		position[2] = z * iscale[2] + corner[2];

		if(global_matrix)
			mat4.multiplyVec3( position, global_matrix, position );

		LS.Draw.push();
		LS.Draw.translate( position );
		LS.Draw.scale( IrradianceCache.probes_size );

		var texture = this._irradiance_cubemaps[ i++ ] || default_cubemap;
		texture.bind(0);

		LS.Draw.renderMesh( mesh, GL.TRIANGLES, shader );
		LS.Draw.pop();
	}

}

LS.registerComponent( IrradianceCache );


/*
var cubemapFaceNormals = [
  [ [0, 0, -1], [0, -1, 0], [1, 0, 0] ],  // posx
  [ [0, 0, 1], [0, -1, 0], [-1, 0, 0] ],  // negx

  [ [1, 0, 0], [0, 0, 1], [0, 1, 0] ],    // posy
  [ [1, 0, 0], [0, 0, -1], [0, -1, 0] ],  // negy

  [ [1, 0, 0], [0, -1, 0], [0, 0, 1] ],   // posz
  [ [-1, 0, 0], [0, -1, 0], [0, 0, -1] ]  // negz
]

// give me a cubemap, its size and number of channels
// and i'll give you spherical harmonics
function computeSH( faces, cubemapSize, ch) {
  var size = cubemapSize || 128
  var channels = ch || 4
  var cubeMapVecs = []

  // generate cube map vectors
  faces.forEach((face, index) => {
    var faceVecs = []
    for (let v = 0; v < size; v++) {
      for (let u = 0; u < size; u++) {
        var fU = (2.0 * u / (size - 1.0)) - 1.0
        var fV = (2.0 * v / (size - 1.0)) - 1.0

        var vecX = []
        vec3.scale(vecX, cubemapFaceNormals[index][0], fU)
        var vecY = []
        vec3.scale(vecY, cubemapFaceNormals[index][1], fV)
        var vecZ = cubemapFaceNormals[index][2]

        var res = []
        vec3.add(res, vecX, vecY)
        vec3.add(res, res, vecZ)
        vec3.normalize(res, res)

        faceVecs.push(res)
      }
    }
    cubeMapVecs.push(faceVecs)
  })

  // generate shperical harmonics
  let sh = [
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3),
    new Float32Array(3)
  ]
  let weightAccum = 0

  faces.forEach((face, index) => {
    var pixels = face
    var gammaCorrect = true
    if (Object.prototype.toString.call(pixels) === '[object Float32Array]') gammaCorrect = false // this is probably HDR image, already in linear space
    for (var y = 0; y < size; y++) {
      for (var x = 0; x < size; x++) {
        const texelVect = cubeMapVecs[index][y * size + x]

        const weight = texelSolidAngle(x, y, size, size)
        // forsyths weights
        const weight1 = weight * 4 / 17
        const weight2 = weight * 8 / 17
        const weight3 = weight * 15 / 17
        const weight4 = weight * 5 / 68
        const weight5 = weight * 15 / 68

        let dx = texelVect[0]
        let dy = texelVect[1]
        let dz = texelVect[2]

        for (let c = 0; c < 3; c++) {
          let value = pixels[y * size * channels + x * channels + c] / 255
          if (gammaCorrect) value = Math.pow(value, 2.2)

          // indexed by coeffiecent + color
          sh[0][c] += value * weight1
          sh[1][c] += value * weight2 * dx
          sh[2][c] += value * weight2 * dy
          sh[3][c] += value * weight2 * dz

          sh[4][c] += value * weight3 * dx * dz
          sh[5][c] += value * weight3 * dz * dy
          sh[6][c] += value * weight3 * dy * dx

          sh[7][c] += value * weight4 * (3.0 * dz * dz - 1.0)
          sh[8][c] += value * weight5 * (dx * dx - dy * dy)

          weightAccum += weight
        }
      }
    }
  })

  for (let i = 0; i < sh.length; i++) {
    sh[i][0] *= 4 * Math.PI / weightAccum
    sh[i][1] *= 4 * Math.PI / weightAccum
    sh[i][2] *= 4 * Math.PI / weightAccum
  }

  return sh
}

function texelSolidAngle (aU, aV, width, height) {
  // transform from [0..res - 1] to [- (1 - 1 / res) .. (1 - 1 / res)]
  // ( 0.5 is for texel center addressing)
  const U = (2.0 * (aU + 0.5) / width) - 1.0
  const V = (2.0 * (aV + 0.5) / height) - 1.0

  // shift from a demi texel, mean 1.0 / size  with U and V in [-1..1]
  const invResolutionW = 1.0 / width
  const invResolutionH = 1.0 / height

  // U and V are the -1..1 texture coordinate on the current face.
  // get projected area for this texel
  const x0 = U - invResolutionW
  const y0 = V - invResolutionH
  const x1 = U + invResolutionW
  const y1 = V + invResolutionH
  const angle = areaElement(x0, y0) - areaElement(x0, y1) - areaElement(x1, y0) + areaElement(x1, y1)

  return angle
}

function areaElement (x, y) {
  return Math.atan2(x * y, Math.sqrt(x * x + y * y + 1.0))
}
*/