///@INFO: UNCOMMON
/**
* It applyes skinning to a RenderInstance created by another component (usually MeshRenderer)
* Is in charge of gathering the bone nodes and adding to the RenderInstance the information needed to perform the skinning
* It can do it using shader uniforms (simple way), a matrices texture (complex way), or by directly applying skinning by software (slow but well supported way)
* It also allow to limit the bone search to specific nodes.
*
* @class SkinDeformer
* @namespace LS.Components
* @constructor
*/
function SkinDeformer( o )
{
this.enabled = true;
this.search_bones_in_parent = true;
this.skeleton_root_node = null;
this.cpu_skinning = false;
this.ignore_transform = true;
this._mesh = null;
this._last_bones = null;
this._ris_skinned = [];
//this._skinning_mode = 0;
//check how many floats can we put in a uniform
if(!SkinDeformer._initialized && global.gl )
{
SkinDeformer.num_supported_uniforms = gl.getParameter( gl.MAX_VERTEX_UNIFORM_VECTORS );
SkinDeformer.num_supported_textures = gl.getParameter( gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS );
//check if GPU skinning is supported
if( SkinDeformer.num_supported_uniforms < SkinDeformer.MAX_BONES*3 && SkinDeformer.num_supported_textures == 0)
SkinDeformer.gpu_skinning_supported = false;
SkinDeformer._initialized = true;
}
if(o)
this.configure(o);
//this._deformer = new LS.Deformer();
}
SkinDeformer.icon = "mini-icon-stickman.png";
SkinDeformer.MAX_BONES = 64;
SkinDeformer.MAX_TEXTURE_BONES = 128; //do not change this, hardcoded in the shader
SkinDeformer.gpu_skinning_supported = true;
SkinDeformer.icon = "mini-icon-stickman.png";
SkinDeformer.apply_to_normals_by_software = false;
SkinDeformer["@skeleton_root_node"] = { type: LS.TYPES.SCENENODE };
SkinDeformer.prototype.onAddedToNode = function(node)
{
LEvent.bind(node, "collectRenderInstances", this.onCollectInstances, this);
}
SkinDeformer.prototype.onRemovedFromNode = function(node)
{
LEvent.unbind(node, "collectRenderInstances", this.onCollectInstances, this);
}
//returns the bone node taking into account the scoping of the component
SkinDeformer.prototype.getBoneNode = function( name )
{
var root_node = this._root;
var scene = root_node.scene;
if(!scene)
return null;
var node = null;
if( this.skeleton_root_node )
{
root_node = scene.getNode( this.skeleton_root_node );
if(root_node)
return root_node.findNodeByName( name );
}
else if(this.search_bones_in_parent)
{
return root_node.parentNode.findNode( name );
}
else
return scene.getNode( name );
return null;
}
//returns a reference to the global matrix of the bone
SkinDeformer.prototype.getBoneMatrix = function( name )
{
var node = this.getBoneNode( name );
if(!node)
return null;
node._is_bone = true;
return node.transform.getGlobalMatrixRef();
}
//checks the list of bones in mesh.bones and retrieves its matrices
SkinDeformer.prototype.getBoneMatrices = function( ref_mesh )
{
//bone matrices
var bones = this._last_bones;
//reuse bone matrices
if(!this._last_bones || this._last_bones.length != ref_mesh.bones.length )
{
bones = this._last_bones = [];
for(var i = 0; i < ref_mesh.bones.length; ++i)
bones[i] = mat4.create();
}
for(var i = 0; i < ref_mesh.bones.length; ++i)
{
var m = bones[i]; //mat4.create();
var joint = ref_mesh.bones[i];
var mat = this.getBoneMatrix( joint[0] ); //get the current matrix from the bone Node transform
if(!mat)
{
mat4.identity( m );
}
else
{
var inv = joint[1];
mat4.multiply( m, mat, inv );
if(ref_mesh.bind_matrix)
mat4.multiply( m, m, ref_mesh.bind_matrix);
}
//bones[i].push( m ); //multiply by the inv bindpose matrix
}
return bones;
}
//Adds the deforming data to the last RenderInstance
SkinDeformer.prototype.onCollectInstances = function( e, render_instances )
{
if(!render_instances.length)
return;
if(!this.root)
return;
var last_RI;
//TODO: fix this, allow multiple mesh renderers with one single deformer
//get index
var index = this.root.getIndexOfComponent(this);
var prev_comp = this.root.getComponentByIndex( index - 1);
if(prev_comp)
last_RI = prev_comp._RI;
if(!last_RI)
return;
this._ris_skinned.length = 0;
//take last one (although maybe using this._root.instances ...)
//last_RI = render_instances[ render_instances.length - 1];
if(!this.enabled)
{
//disable
this.disableSkinning( last_RI );
return;
}
//grab the RI created previously and modified
this.applySkinning( last_RI );
}
/*
SkinDeformer.prototype.setBonesToBindPose = function()
{
//bone matrices
var bones = this._last_bones;
if( !bones || !this._mesh || !this._mesh.bones )
return;
var mesh = this._mesh;
var m = mat4.create();
for(var i = 0; i < mesh.bones.length; ++i)
{
var m = bones[i]; //mat4.create();
var joint = mesh.bones[i];
var bone_node = this.getBoneNode( joint[0] );
if(!bone_node)
continue;
mat4.invert( m, joint[1] ); //invert first
if(mesh.bind_matrix)
mat4.multiply( m, m, mesh.bind_matrix);
bone_node.transform.matrix = m;
}
}
*/
//Applies skinning taking into account the options available (using uniforms, a texture or applying it by software)
SkinDeformer.prototype.applySkinning = function(RI)
{
var mesh = RI.mesh;
this._mesh = mesh;
this._ris_skinned.push( RI );
//this mesh doesnt have skinning info
if(!mesh || !mesh.getBuffer("vertices") || !mesh.getBuffer("bone_indices"))
return;
if( SkinDeformer.gpu_skinning_supported && !this.cpu_skinning )
{
//retrieve all the bones
var bones = this.getBoneMatrices( mesh );
var bones_size = bones.length * 12;
if(!bones.length)
{
console.warn("SkinDeformer.prototype.applySkinning: Bones not found");
return;
}
var u_bones = this._u_bones;
if(!u_bones || u_bones.length != bones_size)
this._u_bones = u_bones = new Float32Array( bones_size );
//pack the bones in one single array (also skip the last row, is always 0,0,0,1)
for(var i = 0; i < bones.length; i++)
{
mat4.transpose( bones[i], bones[i] );
u_bones.set( bones[i].subarray(0,12), i * 12, (i+1) * 12 );
}
//can we pass the bones as a uniform?
if( SkinDeformer.num_supported_uniforms >= bones_size )
{
//upload the bones as uniform (faster but doesnt work in all GPUs)
RI.uniforms["u_bones"] = u_bones;
RI.samplers[ LS.Renderer.BONES_TEXTURE_SLOT ] = null;
RI.addShaderBlock( LS.SkinDeformer.skinning_uniforms_block, { u_bones: u_bones } );
}
else if( SkinDeformer.num_supported_textures > 0 ) //upload the bones as a float texture (slower)
{
var texture = this._bones_texture;
if(!texture)
{
//TODO: to support more bones you could use a Nx3 texture instead of a 1x(N*3)
texture = this._bones_texture = new GL.Texture( 1, SkinDeformer.MAX_TEXTURE_BONES * 3, { format: gl.RGBA, type: gl.FLOAT, filter: gl.NEAREST} ); //3 rows of 4 values per matrix
texture._data = new Float32Array( texture.width * texture.height * 4 );
}
texture._data.set( u_bones );
texture.uploadData( texture._data, { no_flip: true } );
LS.RM.textures[":bones_" + this.uid ] = texture; //debug
RI.uniforms["u_bones"] = LS.Renderer.BONES_TEXTURE_SLOT;
RI.samplers[ LS.Renderer.BONES_TEXTURE_SLOT ] = texture; //{ texture: texture, magFilter: gl.NEAREST, minFilter: gl.NEAREST, wrap: gl.CLAMP_TO_EDGE };
RI.addShaderBlock( LS.SkinDeformer.skinning_texture_block, { u_bones: LS.Renderer.BONES_TEXTURE_SLOT } );
}
else
console.error("impossible to get here");
RI.addShaderBlock( LS.SkinDeformer.skinning_block );
}
else //cpu skinning (mega slow)
{
if(!this._skinned_mesh || this._skinned_mesh._reference != mesh)
{
this._skinned_mesh = new GL.Mesh();
this._skinned_mesh._reference = mesh;
var vertex_buffer = mesh.getBuffer("vertices");
var normal_buffer = mesh.getBuffer("normals");
//clone
for (var i in mesh.vertexBuffers)
this._skinned_mesh.vertexBuffers[i] = mesh.vertexBuffers[i];
for (var i in mesh.indexBuffers)
this._skinned_mesh.indexBuffers[i] = mesh.indexBuffers[i];
//new ones clonning old ones
this._skinned_mesh.createVertexBuffer("vertices","a_vertex", 3, new Float32Array( vertex_buffer.data ), gl.STREAM_DRAW );
if(normal_buffer)
this._skinned_mesh.createVertexBuffer("normals","a_normal", 3, new Float32Array( normal_buffer.data ), gl.STREAM_DRAW );
}
//apply cpu skinning
this.applySoftwareSkinning( mesh, this._skinned_mesh );
RI.setMesh( this._skinned_mesh, this.primitive );
//remove the flags to avoid recomputing shaders
RI.samplers[ LS.Renderer.BONES_TEXTURE_SLOT ] = null;
}
if( this.ignore_transform )
{
mat4.identity( RI.matrix );
RI.normal_matrix.set( RI.matrix );
}
else
this._root.transform.getGlobalMatrix( RI.matrix );
mat4.multiplyVec3( RI.center, RI.matrix, LS.ZEROS );
RI.use_bounding = false;
}
SkinDeformer.prototype.disableSkinning = function( RI )
{
this._mesh = null;
if( RI.samplers["u_bones"] )
delete RI.samplers["u_bones"];
RI.removeShaderBlock( LS.SkinDeformer.skinning_block );
RI.removeShaderBlock( LS.SkinDeformer.skinning_uniforms_block );
RI.removeShaderBlock( LS.SkinDeformer.skinning_texture_block );
}
SkinDeformer.prototype.getMesh = function()
{
return this._mesh;
}
//Takes every vertex and multiplyes it by its bone matrices (slow but it works everywhere)
SkinDeformer.prototype.applySoftwareSkinning = function(ref_mesh, skin_mesh)
{
var original_vertices = ref_mesh.getBuffer("vertices").data;
var original_normals = null;
if(ref_mesh.getBuffer("normals"))
original_normals = ref_mesh.getBuffer("normals").data;
var weights = ref_mesh.getBuffer("weights").data;
var bone_indices = ref_mesh.getBuffer("bone_indices").data;
var vertices_buffer = skin_mesh.getBuffer("vertices");
var vertices = vertices_buffer.data;
var normals_buffer = null;
var normals = null;
if(!SkinDeformer.zero_matrix)
SkinDeformer.zero_matrix = new Float32Array(16);
var zero_matrix = SkinDeformer.zero_matrix;
if(original_normals)
{
normals_buffer = skin_mesh.getBuffer("normals");
normals = normals_buffer.data;
}
//bone matrices
var bones = this.getBoneMatrices( ref_mesh );
if(bones.length == 0) //no bones found
return null;
//var factor = this.factor; //for debug
//apply skinning per vertex
var temp = vec3.create();
var ov_temp = vec3.create();
var temp_matrix = mat4.create();
for(var i = 0, l = vertices.length / 3; i < l; ++i)
{
var ov = original_vertices.subarray(i*3, i*3+3);
var b = bone_indices.subarray(i*4, i*4+4);
var w = weights.subarray(i*4, i*4+4);
var v = vertices.subarray(i*3, i*3+3);
var bmat = [ bones[ b[0] ], bones[ b[1] ], bones[ b[2] ], bones[ b[3] ] ];
temp_matrix.set( zero_matrix );
mat4.scaleAndAdd( temp_matrix, temp_matrix, bmat[0], w[0] );
if(w[1] > 0.0) mat4.scaleAndAdd( temp_matrix, temp_matrix, bmat[1], w[1] );
if(w[2] > 0.0) mat4.scaleAndAdd( temp_matrix, temp_matrix, bmat[2], w[2] );
if(w[3] > 0.0) mat4.scaleAndAdd( temp_matrix, temp_matrix, bmat[3], w[3] );
mat4.multiplyVec3(v, temp_matrix, original_vertices.subarray(i*3, i*3+3) );
if(normals)
{
var n = normals.subarray(i*3, i*3+3);
mat4.rotateVec3(n, temp_matrix, original_normals.subarray(i*3, i*3+3) );
}
//we could also multiply the normal but this is already superslow...
if(SkinDeformer.apply_to_normals_by_software)
{
//apply weights
v[0] = v[1] = v[2] = 0.0; //reset
mat4.multiplyVec3(v, bmat[0], ov_temp);
vec3.scale(v,v,w[0]);
for(var j = 1; j < 4; ++j)
if(w[j] > 0.0)
{
mat4.multiplyVec3( temp, bmat[j], ov_temp );
vec3.scaleAndAdd( v, v, temp, w[j] );
}
}
//if(factor != 1) vec3.lerp( v, ov, v, factor);
}
//upload
vertices_buffer.upload(gl.STREAM_DRAW);
if(normals_buffer)
normals_buffer.upload(gl.STREAM_DRAW);
}
SkinDeformer.prototype.extractSkeleton = function()
{
//TODO
}
//extracts the matrices from the bind pose and applies it to the bones
SkinDeformer.prototype.applyBindPose = function()
{
var mesh = this._mesh;
//this mesh doesnt have skinning info
if( !mesh || !mesh.bones )
return;
var imat = mat4.create();
var bone_nodes = this.getBones();
for(var i = 0; i < bone_nodes.length; ++i)
{
var node = bone_nodes[i];
node._level = node.getHierarchyLevel();
}
/*
for(var i = 0; i < bones.length; ++i)
{
var joint = bones[i];
var bone_name = joint[0];
var bind_matrix = joint[1];
var bone_node = this.getBoneNode( bone_name );
if( !bone_node || !bone_node.transform )
continue;
mat4.invert( imat, bind_matrix, bind_matrix );
bone_node.transform.fromGlobalMatrix( imat );
}
*/
}
//returns an array with all the bone nodes affecting this mesh
SkinDeformer.prototype.getBones = function()
{
var mesh = this._mesh;
if(!mesh && !mesh.bones)
return null;
var bones = [];
for(var i in mesh.bones)
{
var bone = this.getBoneNode( mesh.bones[i][0] );
if(bone)
bones.push( bone );
}
return bones;
}
LS.registerComponent( SkinDeformer );
LS.SkinDeformer = SkinDeformer;
SkinDeformer.skinning_shader_code = "\n\
//Skinning ******************* \n\
#ifndef MAX_BONES\n\
#define MAX_BONES 64\n\
#endif\n\
#ifdef USE_SKINNING_TEXTURE\n\
uniform sampler2D u_bones;\n\
#else\n\
uniform vec4 u_bones[ MAX_BONES * 3];\n\
#endif\n\
attribute vec4 a_weights;\n\
attribute vec4 a_bone_indices;\n\
\n\
void getMat(int id, inout mat4 m) {\n\
\n\
#ifdef USE_SKINNING_TEXTURE\n\
float i_max_texture_bones_offset = 1.0 / (128.0 * 3.0);\n\
m[0] = texture2D( u_bones, vec2( 0.0, (float(id*3)+0.5) * i_max_texture_bones_offset ) ); \n\
m[1] = texture2D( u_bones, vec2( 0.0, (float(id*3+1)+0.5) * i_max_texture_bones_offset ) );\n\
m[2] = texture2D( u_bones, vec2( 0.0, (float(id*3+2)+0.5) * i_max_texture_bones_offset ) );\n\
#else\n\
m[0] = u_bones[ id * 3];\n\
m[1] = u_bones[ id * 3 + 1];\n\
m[2] = u_bones[ id * 3 + 2];\n\
#endif\n\
}\n\
\n\
mat3 mat3_emu(mat4 m4) {\n\
return mat3(\n\
m4[0][0], m4[0][1], m4[0][2],\n\
m4[1][0], m4[1][1], m4[1][2],\n\
m4[2][0], m4[2][1], m4[2][2]);\n\
}\n\
\n\
void applySkinning(inout vec4 position, inout vec3 normal)\n\
{\n\
//toji version seems faster\n\
mat4 bone_matrix = mat4(0.0,0.0,0.0,0.0, 0.0,0.0,0.0,0.0, 0.0,0.0,0.0,0.0, 0.0,0.0,0.0,1.0);\n\
\n\
getMat( int(a_bone_indices.x), bone_matrix );\n\
mat4 result = a_weights.x * bone_matrix;\n\
getMat( int(a_bone_indices.y), bone_matrix);\n\
result = result + a_weights.y * bone_matrix;\n\
getMat( int(a_bone_indices.z), bone_matrix);\n\
result = result + a_weights.z * bone_matrix;\n\
getMat( int(a_bone_indices.w), bone_matrix);\n\
result = result + a_weights.w * bone_matrix;\n\
\n\
position.xyz = (position * result).xyz;\n\
normal = normal * mat3_emu(result);\n\
}\n\
";
SkinDeformer.skinning_enabled_shader_code = "\n#pragma shaderblock skinning_mode\n#define USING_SKINNING\n";
SkinDeformer.skinning_disabled_shader_code = "\nvoid applySkinning( inout vec4 position, inout vec3 normal) {}\n";
// ShaderBlocks used to inject to shader in runtime
var skinning_block = new LS.ShaderBlock("skinning");
skinning_block.addCode( GL.VERTEX_SHADER, SkinDeformer.skinning_enabled_shader_code, SkinDeformer.skinning_disabled_shader_code );
skinning_block.register();
SkinDeformer.skinning_block = skinning_block;
var skinning_uniforms_block = new LS.ShaderBlock("skinning_uniforms");
skinning_uniforms_block.defineContextMacros( { "skinning_mode": "skinning_uniforms"} );
skinning_uniforms_block.addCode( GL.VERTEX_SHADER, SkinDeformer.skinning_shader_code, SkinDeformer.skinning_disabled_shader_code );
skinning_uniforms_block.register();
SkinDeformer.skinning_uniforms_block = skinning_uniforms_block;
var skinning_texture_block = new LS.ShaderBlock("skinning_texture");
skinning_texture_block.defineContextMacros( { "skinning_mode": "skinning_texture"} );
skinning_texture_block.addCode( GL.VERTEX_SHADER, "\n#define USE_SKINNING_TEXTURE\n" + SkinDeformer.skinning_shader_code, SkinDeformer.skinning_disabled_shader_code );
skinning_texture_block.register();
SkinDeformer.skinning_texture_block = skinning_texture_block;