API Docs for:
Show:

File: ../src/materials/shaderMaterial.js


/**
* ShaderMaterial allows to use your own shader from scratch
* @namespace LS
* @class ShaderMaterial
* @constructor
* @param {Object} object [optional] to configure from
*/
function ShaderMaterial( o )
{
	Material.call( this, null );

	this._shader = "";
	this._shader_version = -1;
	this._shader_flags = 0; //?
	this._shader_code = null;

	this._uniforms = {};
	this._samplers = [];
	this._properties = [];
	this._properties_by_name = {};

	this._passes = {};
	this._light_mode = 0;
	this._primitive = -1;
	this._allows_instancing = false;

	this._version = -1;
	this._shader_version = -1;

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

//assign a shader from a filename to a shadercode
Object.defineProperty( ShaderMaterial.prototype, "shader", {
	enumerable: true,
	get: function() {
		return this._shader;
	},
	set: function(v) {
		if(v)
			v = LS.ResourcesManager.cleanFullpath(v);
		if(this._shader == v)
			return;
		this._shader_code = null;
		this._shader = v;
		this.processShaderCode();
	}
});

//allows to assign a shader code that doesnt come from a resource
Object.defineProperty( ShaderMaterial.prototype, "shader_code", {
	enumerable: false,
	get: function() {
		return this._shader_code;
	},
	set: function(v) {
		this._shader = null;
		this._shader_code = v;
		this.processShaderCode();
	}
});

Object.defineProperty( ShaderMaterial.prototype, "properties", {
	enumerable: true,
	get: function() {
		return this._properties;
	},
	set: function(v) {
		if(!v)
			return;
		this._properties = v;
		this._properties_by_name = {};
		for(var i in this._properties)
		{
			var p = this._properties[i];
			this._properties_by_name[ p.name ] = p;
		}
	}
});

Object.defineProperty( ShaderMaterial.prototype, "enableLights", {
	enumerable: true,
	get: function() {
		return this._light_mode != 0;
	},
	set: function(v) {
		this._light_mode = v ? 1 : 0;
	}
});

Object.defineProperty( ShaderMaterial.prototype, "version", {
	enumerable: false,
	get: function() {
		return this._version;
	},
	set: function(v) {
		console.error("version cannot be set manually");
	}
});

ShaderMaterial.prototype.addPass = function( name, vertex_shader, fragment_shader, macros )
{
	this._passes[ name ] = {
		vertex: vertex_shader,
		fragment: fragment_shader,
		macros: macros
	};
}

//called when preparing materials before rendering the scene
ShaderMaterial.prototype.prepare = function( scene )
{
	this.fillUniforms();

	if( this.onPrepare )
		this.onPrepare( scene );
}

//called when filling uniforms from prepare
ShaderMaterial.prototype.fillUniforms = function()
{
	//gather uniforms & samplers
	var samplers = this._samplers;
	samplers.length = 0;

	this._uniforms.u_material_color = this._color;

	for(var i = 0; i < this._properties.length; ++i)
	{
		var p = this._properties[i];
		if(p.internal) //internal is a property that is not for the shader (is for internal computations)
			continue;

		if(p.is_texture)
		{
			this._uniforms[ p.uniform ] = samplers.length;
			if(p.value)
				samplers.push( p.value );
			else
				samplers.push( " " ); //force missing texture
		}
		else
			this._uniforms[ p.uniform ] = p.value;
	}
}

//assigns a value to a property
ShaderMaterial.prototype.setProperty = function(name, value)
{
	//redirect to base material
	if( Material.prototype.setProperty.call(this,name,value) )
		return true;

	if(name == "shader")
		this.shader = value;
	else if(name == "properties")
	{
		this.properties.length = 0;
		this._properties_by_name = {};
		for(var i = 0; i < value.length; ++i)
		{
			var prop = value[i];
			if(prop.is_texture && prop.value && prop.value.constructor === String)
				prop.value = { texture: prop.value };
			this.properties[i] = prop;
			this._properties_by_name[ prop.name ] = prop;
			//if(prop.is_texture)
			//	this._samplers.push( prop.value );
		}
	}
	else if( this._properties_by_name[ name ] )
	{
		var prop = this._properties_by_name[ name ];
		if( !prop.value || prop.value.constructor === String || !prop.value.length )
			prop.value = value;
		else
			prop.value.set( value );
	}
	else
		return false;
	return true;
}

//check the ShaderCode associated and applies it to this material (keeping the state of the properties)
ShaderMaterial.prototype.processShaderCode = function()
{
	if(!this._shader_code && !this._shader)
	{
		this._properties.length = 0;
		this._properties_by_name = {};
		this._passes = {};
		this._samplers.length = 0;
		return false;
	}

	//get shader code
	var shader_code = this._shader_code;
	
	if( !shader_code && this._shader )
		shader_code = LS.ResourcesManager.getResource( this.shader );

	if( !shader_code || shader_code.constructor !== LS.ShaderCode )
		return false;

	var old_properties = this._properties_by_name;
	this._properties.length = 0;
	this._properties_by_name = {};
	this._passes = {};
	this._samplers.length = 0;
	this._light_mode = 0;
	this._primitive = -1;

	//reset material properties
	this._queue = LS.RenderQueue.GEOMETRY;
	this._render_state.init();

	//clear old functions
	for(var i in this)
	{
		if(!this.hasOwnProperty(i))
			continue;
		if( this[i] && this[i].constructor === Function )
			delete this[i];
	}

	//apply init 
	if( shader_code._functions.init )
	{
		if(!LS.catch_exceptions)
			shader_code._functions.init.call( this );
		else
		{
			try
			{
				shader_code._functions.init.call( this );
			}
			catch (err)
			{
				LS.dispatchCodeError(err);
			}
		}
	}

	for(var i in shader_code._global_uniforms)
	{
		var global = shader_code._global_uniforms[i];
		if( global.disabled ) //in case this var is not found in the shader
			continue;
		this.createUniform( global.name, global.uniform, global.type, global.value, global.options );
	}

	//set version before asssignOldProperties
	this._shader_version = shader_code._version;
	this._version++;

	//restore old values
	this.assignOldProperties( old_properties );
}

//used after changing the code of the ShaderCode and wanting to reload the material keeping the old properties
ShaderMaterial.prototype.assignOldProperties = function( old_properties )
{
	//get shader code
	var shader = null;
	var shader_code = this.getShaderCode(); //no parameters because we just want the render_state and init stuff
	if( shader_code )
		shader = shader_code.getShader();

	for(var i = 0; i < this._properties.length; ++i)
	{
		var new_prop = this._properties[i];

		if(!old_properties[ new_prop.name ])
			continue;
		var old = old_properties[ new_prop.name ];
		if(old.value === undefined)
			continue;


		//validate (avoids error if we change the type of a uniform and try to reassign a value)
		if( !old.internal && shader && !new_prop.is_texture ) //textures are not validated (because they are samplers, not values)
		{
			var uniform_info = shader.uniformInfo[ new_prop.uniform ];
			if(!uniform_info)
				continue;
			if(new_prop.value !== undefined)
			{
				if( !GL.Shader.validateValue( new_prop.value, uniform_info ) )
				{
					new_prop.value = undefined;
					continue;
				}
			}
		}

		//this is to keep current values when coding the shader from the editor
		if( new_prop.value && new_prop.value.set ) //special case for typed arrays avoiding generating GC
		{
			//this is to be careful when an array changes sizes
			if( old.value && old.value.length && new_prop.value.length && old.value.length <= new_prop.value.length)
				new_prop.value.set( old.value );
			else
				new_prop.value = old.value;
		}
		else
			new_prop.value = old.value;
	}
}

//called from LS.Renderer when rendering an instance
ShaderMaterial.prototype.renderInstance = function( instance, render_settings, pass )
{
	//get shader code
	var shader_code = this.getShaderCode( instance, render_settings, pass );
	if(!shader_code || shader_code.constructor !== LS.ShaderCode )
		return true; //skip rendering

	//this is in case the shader has been modified in the editor (reapplies the shadercode to the material)
	if( shader_code._version !== this._shader_version && this.processShaderCode )
		this.processShaderCode();

	//some globals
	var renderer = LS.Renderer;
	var camera = LS.Renderer._current_camera;
	var scene = LS.Renderer._current_scene;
	var model = instance.matrix;
	var render_uniforms = LS.Renderer._render_uniforms;

	//maybe this two should be somewhere else
	render_uniforms.u_model = model; 
	render_uniforms.u_normal_model = instance.normal_matrix; 

	//compute flags: checks the ShaderBlocks attached to this instance and resolves the flags
	var block_flags = instance.computeShaderBlockFlags();
	block_flags |= LS.Renderer._global_block_flags; //apply global block_flags

	//global stuff
	this._render_state.enable();
	LS.Renderer.bindSamplers( this._samplers );
	LS.Renderer.bindSamplers( instance.samplers );
	var global_flags = LS.Renderer._global_shader_blocks_flags;

	//TODO: could this part be precomputed before rendering color pass?
	if( pass == COLOR_PASS ) //allow reflections only in color pass
	{
		global_flags |= LS.ShaderMaterial.reflection_block.flag_mask;
		if( LS.Renderer._global_textures.environment )
		{
			if( LS.Renderer._global_textures.environment.texture_type == GL.TEXTURE_2D )
				global_flags |= environment_2d_block.flag_mask;
			else
				global_flags |= environment_cubemap_block.flag_mask;
		}
		if( LS.Renderer._global_textures.irradiance )
		{
			global_flags |= irradiance_block.flag_mask;
		}
	}

	//for those cases
	if(this.onRenderInstance)
		this.onRenderInstance( instance );

	//add flags related to lights
	var lights = null;

	//ignore lights renders the object with flat illumination
	var ignore_lights = pass != COLOR_PASS || render_settings.lights_disabled || this._light_mode === Material.NO_LIGHTS;

	if( !ignore_lights )
		lights = LS.Renderer.getNearLights( instance );

	//if no lights are set or the render mode is flat
	if( !lights || lights.length == 0 || ignore_lights )
	{
		//global flags for environment and irradiance
		if( !ignore_lights )
			block_flags |= global_flags;

		//extract shader compiled
		var shader = shader_code.getShader( pass.name, block_flags );
		if(!shader)
		{
			//var shader = shader_code.getShader( "surface", block_flags );
			return false;
		}

		//assign
		shader.uniformsArray( [ scene._uniforms, camera._uniforms, render_uniforms, this._uniforms, instance.uniforms ] ); //removed, why this was in?? light ? light._uniforms : null, 

		shader.setUniform( "u_light_info", LS.ZEROS4 );
		if( ignore_lights )
			shader.setUniform( "u_ambient_light", LS.ONES );


		//render
		instance.render( shader, this._primitive != -1 ? this._primitive : undefined );
		renderer._rendercalls += 1;
	
		return true;
	}

	var base_block_flags = block_flags;

	var uniforms_array = [ scene._uniforms, camera._uniforms, render_uniforms, null, this._uniforms, instance.uniforms ];

	//render multipass with several lights
	var prev_shader = null;
	for(var i = 0; i < lights.length; ++i)
	{
		var light = lights[i];
		block_flags = light.applyShaderBlockFlags( base_block_flags, pass, render_settings );

		//global
		block_flags |= global_flags;

		//extract shader compiled
		var shader = shader_code.getShader( null, block_flags );
		if(!shader)
		{
			console.warn("material without pass: " + pass.name );
			continue;
		}

		//light texture like shadowmap and cookie
		LS.Renderer.bindSamplers( light._samplers );

		//light parameters (like index of pass or num passes)
		light._uniforms.u_light_info[2] = i;
		light._uniforms.u_light_info[3] = lights.length;
		uniforms_array[3] = light._uniforms;

		//assign
		if(prev_shader != shader)
			shader.uniformsArray( uniforms_array );
		else
			shader.uniforms( light._uniforms );
		prev_shader = shader;

		if(i == 1)
		{
			gl.depthMask( false );
			gl.depthFunc( gl.EQUAL );
			gl.enable( gl.BLEND );
			gl.blendFunc( gl.SRC_ALPHA, gl.ONE );
		}

		//render
		instance.render( shader, this._primitive != -1 ? this._primitive : undefined );
		renderer._rendercalls += 1;
	}

	//optimize this
	gl.disable( gl.BLEND );
	gl.depthMask( true );
	gl.depthFunc( gl.LESS );

	return true;
}

ShaderMaterial.prototype.renderPickingInstance = function( instance, render_settings, pass )
{
	//get shader code
	var shader_code = this.getShaderCode( instance, render_settings, pass );
	if(!shader_code || shader_code.constructor !== LS.ShaderCode )
		return;

	//some globals
	var renderer = LS.Renderer;
	var camera = LS.Renderer._current_camera;
	var scene = LS.Renderer._current_scene;
	var model = instance.matrix;
	var node = instance.node;
	var render_uniforms = LS.Renderer._render_uniforms;

	//maybe this two should be somewhere else
	render_uniforms.u_model = model; 
	render_uniforms.u_normal_model = instance.normal_matrix; 

	//compute flags
	var block_flags = instance.computeShaderBlockFlags();

	//global stuff
	this._render_state.enable();
	LS.Renderer.bindSamplers( this._samplers );
	LS.Renderer.bindSamplers( instance.samplers );

	//extract shader compiled
	var shader = shader_code.getShader( pass.name, block_flags );
	if(!shader)
	{
		shader_code = LS.ShaderMaterial.getDefaultPickingShaderCode();
		shader = shader_code.getShader( pass.name, block_flags );
		if(!shader)
			return false; //??!
	}

	//assign uniforms
	shader.uniformsArray( [ camera._uniforms, render_uniforms, this._uniforms, instance.uniforms ] );

	//set color
	var pick_color = LS.Picking.getNextPickingColor( instance.picking_node || node );
	shader.setUniform("u_material_color", pick_color );

	//render
	instance.render( shader, this._primitive != -1 ? this._primitive : undefined );
	renderer._rendercalls += 1;

	//optimize this
	gl.disable( gl.BLEND );
	gl.depthMask( true );
	gl.depthFunc( gl.LESS );

	return true;
}

//used by the editor to know which possible texture channels are available
ShaderMaterial.prototype.getTextureChannels = function()
{
	var channels = [];

	for(var i in this._properties)
	{
		var p = this._properties[i];
		if(p.is_texture)
			channels.push( p.name );
	}

	return channels;
}

/**
* Collects all the resources needed by this material (textures)
* @method getResources
* @param {Object} resources object where all the resources are stored
* @return {Texture}
*/
ShaderMaterial.prototype.getResources = function ( res )
{
	if(this.shader)
		res[ this.shader ] = LS.ShaderCode;

	for(var i in this._properties)
	{
		var p = this._properties[i];
		if(p.value && p.is_texture)
		{
			if(!p.value)
				continue;
			var name = null;
			if(p.value.texture)
				name = 	p.value.texture;
			else
				name = res[ p.value ];
			if(name && name.constructor === String)
				res[name] = GL.Texture;
		}
	}
	return res;
}


ShaderMaterial.prototype.getPropertyInfoFromPath = function( path )
{
	if( path.length < 1)
		return;

	var info = Material.prototype.getPropertyInfoFromPath.call(this,path);
	if(info)
		return info;

	var varname = path[0];

	for(var i = 0, l = this.properties.length; i < l; ++i )
	{
		var prop = this.properties[i];
		if(prop.name != varname)
			continue;

		return {
			node: this._root,
			target: this,
			name: prop.name,
			value: prop.value,
			type: prop.type
		};
	}

	return;
}

//get shader code
ShaderMaterial.prototype.getShaderCode = function( instance, render_settings, pass )
{
	var shader_code = this._shader_code || LS.ResourcesManager.getResource( this._shader );
	if(!shader_code || shader_code.constructor !== LS.ShaderCode )
		return null;

	//this is in case the shader has been modified in the editor (reapplies the shadercode to the material)
	if( shader_code._version !== this._shader_version && this.processShaderCode )
	{
		shader_code._version = this._shader_version;
		this.processShaderCode();
	}

	return shader_code;
}

/**
* Takes an input texture and applies the ShaderMaterial, the result is shown on the viewport or stored in the output_texture
* The ShaderCode must contain a "fx" method.
* Similar to the method BlitTexture in Unity
* @method applyToTexture
* @param {Texture} input_texture
* @param {Texture} output_texture [optional] where to store the result, if omitted it will be shown in the viewport
*/
ShaderMaterial.prototype.applyToTexture = function( input_texture, output_texture )
{
	if( !this.shader || !input_texture )
		return false;

	//get shader code
	var shader_code = this.getShaderCode(); //special use
	if(!shader_code)
		return false;

	//extract shader compiled
	var shader = shader_code.getShader("fx");
	if(!shader)
		return false;

	//global vars
	this.fillUniforms();
	this._uniforms.u_time = LS.GlobalScene._time;
	this._uniforms.u_viewport = gl.viewport_data;

	//bind samplers
	LS.Renderer.bindSamplers( this._samplers );

	gl.disable( gl.DEPTH_TEST );
	gl.disable( gl.CULL_FACE );

	//render
	if(!output_texture)
		input_texture.toViewport( shader, this._uniforms );
	else
		output_texture.drawTo( function(){
			input_texture.toViewport( shader, this._uniforms );
		});
}

/**
* Makes one shader variable (uniform) public so it can be assigned from the engine (or edited from the editor)
* @method createUniform
* @param {String} name the property name as it should be shown
* @param {String} uniform the uniform name in the shader
* @param {String} type the var type in case we want to edit it (use LS.TYPES)
* @param {*} value
* @param {Object} options an object containing all the possible options (used mostly for widgets)
*/
ShaderMaterial.prototype.createUniform = function( name, uniform, type, value, options )
{
	if(!name || !uniform)
		throw("parameter missing in createUniform");

	//
	type = type || "Number";
	if( type.constructor !== String )
		throw("type must be string");

	//cast to typed-array
	value = value || 0;
	if(value && value.length)
		value = new Float32Array( value );//cast them always
	else
	{
		//create a value, otherwise is null
		switch (type)
		{
			case "vec2": value = vec2.create(); break;
			case "color":
			case "vec3": value = vec3.create(); break;
			case "color4":
			case "vec4": value = vec4.create(); break;
			case "mat3": value = mat3.create(); break;
			case "mat4": value = mat4.create(); break;
			default:
		}
	}

	//define info
	var prop = { name: name, uniform: uniform, value: value, type: type, is_texture: 0 };

	//mark as texture (because this need to go to the textures container so they are binded)
	if(type.toLowerCase() == "texture" || type == "sampler2D" || type == "samplerCube" || type == "sampler")
		prop.is_texture = (type == "samplerCube") ? 2 : 1;

	if(prop.is_texture)
	{
		prop.sampler = {};
		prop.type = "sampler";
		prop.sampler_slot = this._samplers.length;
		this._samplers.push( prop.sampler );
	}

	if(options)
		for(var i in options)
			prop[i] = options[i];

	this._properties.push( prop );
	this._properties_by_name[ name ] = prop;
}

/**
* Similar to createUniform but for textures, it helps specifying sampler options
* @method createSampler
* @param {String} name the property name as it should be shown
* @param {String} uniform the uniform name in the shader
* @param {Object} options an object containing all the possible options (used mostly for widgets)
* @param {String} value default value (texture name)
*/
ShaderMaterial.prototype.createSampler = function( name, uniform, sampler_options, value  )
{
	if(!name || !uniform)
		throw("parameter missing in createSampler");

	var type = "sampler";
	if( sampler_options && sampler_options.type )
		type = sampler_options.type;

	var sampler = null;

	//do not overwrite
	if( this._properties_by_name[ name ] )
	{
		var current_prop = this._properties_by_name[ name ];
		if( current_prop.type == type && current_prop.value )
			sampler = current_prop.value;
	}

	if(!sampler)
		sampler = {
			texture: value
		};

	var prop = { name: name, uniform: uniform, value: sampler, type: type, is_texture: 1, sampler_slot: -1 };

	if(sampler_options)
	{
		if(sampler_options.filter)
		{
			sampler.magFilter = sampler_options.filter;
			sampler.minFilter = sampler_options.filter;
			delete sampler_options.filter;
		}

		if(sampler_options.wrap)
		{
			sampler.wrapS = sampler_options.wrap;
			sampler.wrapT = sampler_options.wrap;
			delete sampler_options.wrap;
		}

		for(var i in sampler_options)
			sampler[i] = sampler_options[i];
	}
	prop.sampler_slot = this._samplers.length;
	this._properties.push( prop );
	this._properties_by_name[ name ] = prop;
	this._samplers.push( prop.value );
}

/**
* Creates a property for this material, this property wont be passed to the shader but can be used from source code.
* You must used this function if you want the data to be stored when serializing or changing the ShaderCode
* @method createProperty
* @param {String} name the property name as it should be shown
* @param {*} value the default value
* @param {String} type the data type (use LS.TYPES)
* @param {Object} options an object containing all the possible options (used mostly for widgets)
*/
ShaderMaterial.prototype.createProperty = function( name, value, type, options )
{
	var prop = this._properties_by_name[ name ];
	if(prop && prop.type == type) //already exist with the same type
		return;

	prop = { name: name, type: type, internal: true, value: value };
	if(options)
		for(var i in options)
			prop[i] = options[i];

	this._properties.push( prop );
	this._properties_by_name[ name ] = prop;

	Object.defineProperty( this, name, {
		get: function() { 
			var prop = this._properties_by_name[ name ]; //fetch it because could have been overwritten
			if(prop)
				return prop.value;
		},
		set: function(v) { 
			var prop = this._properties_by_name[ name ]; //fetch it because could have been overwritten
			if(!prop)
				return;
			if(prop.value && prop.value.set) //for typed arrays
				prop.value.set( v );
			else
				prop.value = v;
		},
		enumerable: false, //must not be serialized
		configurable: true //allows to overwrite this property
	});
}

/**
* Event used to inform if one resource has changed its name
* @method onResourceRenamed
* @param {Object} resources object where all the resources are stored
* @return {Boolean} true if something was modified
*/
ShaderMaterial.prototype.onResourceRenamed = function (old_name, new_name, resource)
{
	var v = Material.prototype.onResourceRenamed.call(this, old_name, new_name, resource );
	if( this.shader == old_name)
	{
		this.shader = new_name;
		v = true;
	}

	//change texture also in shader values... (this should be automatic but it is not)
	for(var i = 0; i < this._properties.length; ++i)
	{
		var p = this._properties[i];
		if(p.internal) //internal is a property that is not for the shader (is for internal computations)
			continue;

		if( !p.is_texture || !p.value )
			continue;
		if( p.value.texture != old_name )
			continue;
		p.value.texture = new_name;
		v = true;
	}

	return v;
}

ShaderMaterial.getDefaultPickingShaderCode = function()
{
	if( ShaderMaterial.default_picking_shader_code )
		return ShaderMaterial.default_picking_shader_code;
	var sc = new LS.ShaderCode();
	sc.code = LS.ShaderCode.flat_code;
	ShaderMaterial.default_picking_shader_code = sc;
	return sc;
}

LS.registerMaterialClass( ShaderMaterial );
LS.ShaderMaterial = ShaderMaterial;