Index: trunk/legacy/md5_loader.cc
===================================================================
--- trunk/legacy/md5_loader.cc	(revision 441)
+++ trunk/legacy/md5_loader.cc	(revision 441)
@@ -0,0 +1,545 @@
+// Copyright (C) 2012-2015 ChaosForge Ltd
+// http://chaosforge.org/
+//
+// This file is part of Nova libraries. 
+// For conditions of distribution and use, see copying.txt file in root folder.
+
+#include "nv/formats/md5_loader.hh"
+
+#include "nv/core/logging.hh"
+#include "nv/stl/vector.hh"
+#include "nv/io/std_stream.hh"
+#include "nv/interface/data_channel_access.hh"
+
+#include <stdio.h>  // sscanf
+#include <stdlib.h> // atof
+
+#include <string> // TODO: remove
+
+using namespace nv;
+
+static void next_line( std::istream& stream )
+{
+	stream.ignore( 1024*1024, '\n' );
+}
+
+static inline void discard( std::istream& stream, const std::string& token )
+{
+	std::string discarded;
+	stream >> discarded;
+	assert( discarded == token );
+}
+
+static void remove_quotes( std::string& str )
+{
+	nv::size_t n;
+	while ( ( n = str.find('\"') ) != std::string::npos ) str.erase(n,1);
+}
+
+static void unit_quat_w( nv::quat& quat )
+{
+	float t = 1.0f - ( quat.x * quat.x ) - ( quat.y * quat.y ) - ( quat.z * quat.z );
+	quat.w = ( t < 0.0f ? 0.0f : -sqrtf(t) );
+}
+
+bool md5_loader::load( stream& source )
+{
+	reset();
+	std_stream sstream( &source );
+	std::string command;
+	size_t num_joints = 0;
+
+	// MESH data
+	dynamic_array< md5_weight > weights;
+	dynamic_array< md5_weight_info > weight_info;
+	size_t num_meshes = 0;
+
+	// MESH data
+	dynamic_array< md5_joint_info > joint_infos;
+	vector< transform >             base_frames;
+	size_t num_animated_components = 0;
+	size_t frame_rate = 0;
+	size_t num_frames = 0;
+
+	sstream >> command;
+	while ( !sstream.eof() )
+	{
+		if ( command == "MD5Version" )
+		{
+			sstream >> m_md5_version;
+			assert( m_md5_version == 10 );
+		}
+		else if ( command == "commandline" )
+		{
+			next_line( sstream ); 
+		}
+		else if ( command == "numJoints" )
+		{
+			sstream >> num_joints;
+			next_line( sstream ); 
+		}
+		else if ( command == "numMeshes" )
+		{
+			assert( m_type == UNKNOWN );
+			m_type = MESH;
+			sstream >> num_meshes;
+			m_meshes.resize( num_meshes );
+			num_meshes = 0;
+		}
+		else if ( command == "numFrames" )
+		{
+			assert( m_type == UNKNOWN || m_type == ANIMATION );
+			m_type = ANIMATION;
+			sstream >> num_frames;
+			next_line( sstream ); 
+		}
+		else if ( command == "frameRate" )
+		{
+			assert( m_type == UNKNOWN || m_type == ANIMATION );
+			m_type = ANIMATION;
+			sstream >> frame_rate;
+			next_line( sstream ); 
+		}
+		else if ( command == "numAnimatedComponents" )
+		{
+			assert( m_type == UNKNOWN || m_type == ANIMATION );
+			m_type = ANIMATION;
+			sstream >> num_animated_components;
+			next_line( sstream ); 
+		}
+		else if ( command == "joints" )
+		{
+			assert( m_type == MESH );
+			assert( m_nodes == nullptr );
+			m_nodes = new mesh_nodes_data( make_name( "md5_bones") );
+			discard( sstream, "{" );
+			for ( size_t i = 0; i < m_nodes->size(); ++i )
+			{
+				std::string name;
+				sint16 parent_id;
+				sstream >> name >> parent_id;
+				vec3 pos;
+				quat orient;
+				discard( sstream, "(" );
+				sstream >> pos.x >> pos.y >> pos.z;
+				discard( sstream, ")" );
+				discard( sstream, "(" );
+				sstream >> orient.x >> orient.y >> orient.z;
+				unit_quat_w( orient );
+				remove_quotes( name );
+				data_channel_set* set = data_channel_set_creator::create_set( 0 );
+				data_channel_set_creator access( set );
+				access.set_parent_id( parent_id );
+				access.set_transform( transform( pos, orient ).inverse().extract() );
+				access.set_name( make_name( name.c_str() ) );
+				next_line( sstream );
+				m_nodes->push_back( set );
+			}
+			discard( sstream, "}" );
+		}
+		else if ( command == "mesh" )
+		{
+			assert( m_type == MESH );
+			data_channel_set* mesh = data_channel_set_creator::create_set( 4 );
+			data_channel_set_creator maccess( mesh );
+
+			uint32 num_verts   = 0;
+			uint32 num_tris    = 0;
+			uint32 num_weights = 0;
+
+			discard( sstream, "{" );
+			sstream >> command;
+			while ( command != "}" ) 
+			{
+				if ( command == "shader" )
+				{
+					std::string shader;
+					sstream >> shader;
+					remove_quotes( shader );
+					maccess.set_name( make_name( shader.c_str() ) );
+					next_line( sstream );
+				}
+				else if ( command == "numverts")
+				{
+					sstream >> num_verts; 
+
+					md5_vtx_t* tdata = nullptr;
+					{
+						maccess.add_channel<md5_vtx_pnt>( num_verts );
+						tdata = maccess.add_channel<md5_vtx_t>( num_verts ).data();
+						maccess.add_channel<md5_vtx_pntiw>( num_verts );
+					}
+					weight_info.resize( num_verts );
+
+					next_line( sstream );
+					std::string line;
+					for ( uint32 i = 0; i < num_verts; ++i )
+					{
+						size_t weight_count;
+						size_t start_weight;
+						vec2 texcoord;
+
+						std::getline( sstream, line );
+						sscanf( line.c_str(), "%*s %*u ( %f %f ) %u %u", &(texcoord.x), &(texcoord.y), &(start_weight), &(weight_count) );
+						weight_info[i].start_weight = start_weight;
+						weight_info[i].weight_count = weight_count;
+						tdata[i].texcoord = texcoord;
+					}  
+				}
+				else if ( command == "numtris" )
+				{
+					sstream >> num_tris;
+
+					uint32* vtx_i = reinterpret_cast< uint32* >( maccess.add_channel< index_u32 >( num_tris * 3 ).raw_data() );
+					uint32 idx = 0;
+
+					next_line( sstream );
+					std::string line;
+					for ( uint32 i = 0; i < num_tris; ++i )
+					{
+						unsigned ti0;
+						unsigned ti1;
+						unsigned ti2;
+
+						std::getline( sstream, line );
+						sscanf( line.c_str(), "%*s %*u %u %u %u )", &(ti0), &(ti1), &(ti2));
+
+						vtx_i[idx++] = ti0;
+						vtx_i[idx++] = ti1;
+						vtx_i[idx++] = ti2;
+					}              
+
+				}
+				else if ( command == "numweights" )
+				{
+					sstream >> num_weights;
+					weights.resize( num_weights );
+					next_line( sstream );
+					std::string line;
+					for ( uint32 i = 0; i < num_weights; ++i )
+					{
+						md5_weight weight;
+
+						std::getline( sstream, line );
+						sscanf( line.c_str(), "%*s %*u %u %f ( %f %f %f )", &(weight.joint_id), &(weight.bias), &(weight.pos.x), &(weight.pos.y), &(weight.pos.z));
+ 						weights[i] = weight;
+					}
+				}
+				else
+				{
+					next_line( sstream );
+				}
+
+				sstream >> command;
+			}
+
+			prepare_mesh( m_nodes, weight_info.size(), mesh, weights.data(), weight_info.data() );
+
+			m_meshes[ num_meshes ] = mesh;
+			num_meshes++;
+		} // mesh
+		else if ( command == "hierarchy" )
+		{
+			assert( m_type == ANIMATION );
+			assert( m_nodes == nullptr );
+			m_nodes = new mesh_nodes_data( make_name( "md5_animation" ), static_cast< nv::uint16 >( frame_rate ), static_cast< float >( num_frames ), true );
+			joint_infos.resize( num_joints );
+
+			discard( sstream, "{" );
+			for ( size_t i = 0; i < m_nodes->size(); ++i )
+			{
+				std::string    name;
+				sint16 parent_id;
+				sstream >> name >> parent_id >> joint_infos[i].flags >> joint_infos[i].start_index;
+				remove_quotes( name );
+				data_channel_set* set = data_channel_set_creator::create_set( 1 );
+				data_channel_set_creator access( set );
+				access.add_channel< md5_key_t >( num_frames );
+				access.set_name( make_name( name.c_str() ) );
+				access.set_parent_id( parent_id );
+				m_nodes->push_back( set );
+				next_line( sstream );
+			}
+			discard( sstream, "}" );
+		}
+		else if ( command == "bounds" )
+		{
+			assert( m_type == ANIMATION );
+			discard( sstream, "{" );
+			next_line( sstream ); 
+			for ( size_t i = 0; i < num_frames; ++i ) 
+			{
+				//  				vec3 min;
+				//  				vec3 max;
+				//  				discard( sstream, "(" );
+				//  				sstream >> min.x >> min.y >> min.z;
+				//  				discard( sstream, ")" );
+				//  				discard( sstream, "(" );
+				//  				sstream >> max.x >> max.y >> max.z;
+				// 				m_bounds.push_back( bound );
+				next_line( sstream ); 
+			}
+
+			discard( sstream, "}" );
+			next_line( sstream ); 
+		}
+		else if ( command == "baseframe" )
+		{
+			assert( m_type == ANIMATION );
+			discard( sstream, "{" );
+			next_line( sstream ); 
+
+			for ( size_t i = 0; i < m_nodes->size(); ++i )
+			{
+				transform base_frame;
+				vec3 pos;
+				quat orient;
+				discard( sstream, "(" );
+				sstream >> pos.x >> pos.y >> pos.z;
+				discard( sstream, ")" );
+				discard( sstream, "(" );
+				sstream >> orient.x >> orient.y >> orient.z;
+				next_line( sstream ); 
+
+				base_frames.emplace_back( pos, orient );
+			}
+			discard( sstream, "}" );
+			next_line( sstream ); 
+		}
+		else if ( command == "frame" )
+		{
+			vector<float> frame;
+			uint32 frame_id;
+			sstream >> frame_id;
+			discard( sstream, "{" );
+			next_line( sstream ); 
+
+			frame.reserve( num_animated_components );
+			char buf[50];
+			for ( size_t i = 0; i < num_animated_components; ++i )
+			{
+				sstream >> buf;
+				frame.push_back( static_cast< float >( atof(buf) ) );
+			}
+
+			build_frame_skeleton( m_nodes, frame_id, joint_infos, base_frames, frame );
+
+			discard( sstream, "}" );
+			next_line( sstream ); 
+		}
+
+		sstream >> command;
+	}
+
+	return true;
+}
+
+bool md5_loader::prepare_mesh( mesh_nodes_data* nodes, uint32 vtx_count, data_channel_set* mdata, md5_weight* weights, md5_weight_info* weight_info )
+{
+	assert( m_type == MESH );
+	data_channel_access< md5_vtx_pnt >   pnt  ( const_cast< raw_data_channel* >( mdata->get_channel< md5_vtx_pnt >() ) );
+	data_channel_access< md5_vtx_pntiw > pntiw( const_cast< raw_data_channel* >( mdata->get_channel< md5_vtx_pntiw >() ) );
+	md5_vtx_pntiw* vtx_data = pntiw.data();
+	md5_vtx_pnt* vtcs = pnt.data();
+
+	for ( uint32 i = 0; i < vtx_count; ++i )
+	{
+		size_t start_weight = weight_info[i].start_weight;
+		size_t weight_count = weight_info[i].weight_count;
+		md5_vtx_pntiw& vdata = vtx_data[i];
+		md5_vtx_pnt& vtc = vtcs[i];
+
+		vtc.position = vec3(0);
+		vtc.normal   = vec3(0);
+		vtc.tangent  = vec3(0);
+
+		stable_sort( weights + start_weight, weights + start_weight + weight_count, [] ( const md5_weight& a, const md5_weight& b ) -> bool { return a.bias > b.bias; } );
+		//std::sort( weights + start_weight, weights + start_weight + weight_count, [](const md5_weight& a, const md5_weight& b) -> bool { return a.bias > b.bias; } );
+
+		if ( weight_count > 4 )
+		{
+			float sum = 0.0f;
+			for ( size_t j = 0; j < 4; ++j )
+			{
+				sum += weights[start_weight + j].bias;
+			}
+			float ratio = 1.0f / sum;
+			for ( size_t j = 0; j < 4; ++j )
+			{
+				weights[start_weight + j].bias = ratio * weights[start_weight + j].bias;
+			}
+			weight_count = 4;
+		}
+
+		for ( int j = 0; j < 4; ++j )
+		{
+			if ( j < int(weight_count) )
+			{
+				vdata.boneindex[j]  = int( weights[int(start_weight) + j].joint_id );
+				vdata.boneweight[j] = weights[int(start_weight) + j].bias;
+			}
+			else
+			{
+				vdata.boneindex[j]  = 0;
+				vdata.boneweight[j] = 0.0f;
+			}
+		}
+
+		for ( size_t j = 0; j < 4; ++j )
+		{
+			if ( j < weight_count )
+			{
+				md5_weight& weight             = weights[start_weight + j];
+				const data_channel_set*  joint = (*nodes)[weight.joint_id];
+				const transform tr = transform( joint->get_transform() ).inverse();
+				vec3 rot_pos = tr.get_orientation() * weight.pos;
+
+				vtc.position += ( tr.get_position() + rot_pos ) * weight.bias;
+			}
+		}
+	}
+
+	const uint32*    idata = reinterpret_cast< uint32* >( const_cast< uint8* >( mdata->get_channel( slot::INDEX )->raw_data() ) );
+	const md5_vtx_t* tdata = mdata->get_channel_data<md5_vtx_t>();
+
+	// Prepare normals
+	uint32 tri_count = mdata->get_channel_size( slot::INDEX ) / 3;
+	for ( unsigned int i = 0; i < tri_count; ++i )
+	{
+		uint32 ti0 = idata[ i * 3 ];
+		uint32 ti1 = idata[ i * 3 + 1 ];
+		uint32 ti2 = idata[ i * 3 + 2 ];
+ 
+		vec3 v1 = vtcs[ ti0 ].position;
+		vec3 v2 = vtcs[ ti1 ].position;
+		vec3 v3 = vtcs[ ti2 ].position;
+		vec3 xyz1 = v3 - v1;
+		vec3 xyz2 = v2 - v1;
+
+		vec3 normal = glm::cross( xyz1, xyz2 );
+
+		vtcs[ ti0 ].normal += normal;
+		vtcs[ ti1 ].normal += normal;
+		vtcs[ ti2 ].normal += normal;
+
+		const vec2& w1 = tdata[ ti0 ].texcoord;
+		const vec2& w2 = tdata[ ti1 ].texcoord;
+		const vec2& w3 = tdata[ ti2 ].texcoord;
+
+		vec2 st1 = w3 - w1;
+		vec2 st2 = w2 - w1;
+
+		float coef = 1.0f / (st1.x * st2.y - st2.x * st1.y);
+
+		vec3 tangent = (( xyz1 * st2.y ) - ( xyz2 * st1.y )) * coef;
+
+		vtcs[ ti0 ].tangent += tangent;
+		vtcs[ ti1 ].tangent += tangent;
+		vtcs[ ti2 ].tangent += tangent;
+	}
+
+	for ( size_t i = 0; i < vtx_count; ++i )
+	{
+		md5_vtx_pntiw& vdata = vtx_data[i];
+
+		vec3 normal  = glm::normalize( vtcs[i].normal );
+		vec3 tangent = glm::normalize( vtcs[i].tangent );
+		vtcs[i].normal   = normal;
+		vtcs[i].tangent  = tangent;
+
+		vdata.position = vtcs[i].position;
+		vdata.normal   = vec3(0);
+ 		vdata.tangent  = vec3(0);
+ 
+ 		for ( int j = 0; j < 4; ++j )
+ 		{
+			const data_channel_set*  joint = ( *nodes )[vdata.boneindex[j]];
+			const transform tr = transform( joint->get_transform() ).inverse();
+ 			vdata.normal  += ( normal  * tr.get_orientation() ) * vdata.boneweight[j];
+ 			vdata.tangent += ( tangent * tr.get_orientation() ) * vdata.boneweight[j];
+ 		}
+	}
+
+	return true;
+}
+
+void md5_loader::build_frame_skeleton( mesh_nodes_data* nodes, uint32 index, const array_view<md5_joint_info>& joint_infos, const array_view<transform>& base_frames, const array_view<float>& frame_data )
+{
+	assert( m_type == ANIMATION );
+	for ( unsigned int i = 0; i < joint_infos.size(); ++i )
+	{
+		unsigned int j = 0;
+
+		const md5_joint_info& jinfo = joint_infos[i];
+		const data_channel_set* joint = (*nodes)[i];
+		int parent_id         = joint->get_parent_id();
+
+		vec3 pos    = base_frames[i].get_position();
+		quat orient = base_frames[i].get_orientation();
+		if ( jinfo.flags & 1 )  pos.x    = frame_data[ jinfo.start_index + j++ ];
+		if ( jinfo.flags & 2 )  pos.y    = frame_data[ jinfo.start_index + j++ ];
+		if ( jinfo.flags & 4 )  pos.z    = frame_data[ jinfo.start_index + j++ ];
+		if ( jinfo.flags & 8 )  orient.x = frame_data[ jinfo.start_index + j++ ];
+		if ( jinfo.flags & 16 ) orient.y = frame_data[ jinfo.start_index + j++ ];
+		if ( jinfo.flags & 32 )	orient.z = frame_data[ jinfo.start_index + j++ ];
+		unit_quat_w( orient );
+
+		if ( parent_id >= 0 ) // Has a parent joint
+		{
+			const data_channel_set* pjoint = ( *nodes )[i];
+			const transform* ptv = reinterpret_cast< const transform* >( pjoint->get_channel(0)->raw_data() );
+			transform ptr;
+			if ( pjoint->get_channel(0)->size() > index ) ptr = ptv[ index ];
+			vec3 rot_pos = ptr.get_orientation() * pos;
+
+			pos    = ptr.get_position() + rot_pos;
+			orient = ptr.get_orientation() * orient;
+
+			orient = glm::normalize( orient );
+		}
+
+		reinterpret_cast< transform* >( const_cast< uint8* >( joint->get_channel(0)->raw_data() ) )[index] = transform( pos, orient );
+	}
+}
+
+data_channel_set* nv::md5_loader::release_mesh_data( size_t index )
+{
+	data_channel_set* result = m_meshes[ index ];
+	m_meshes[ index ] = nullptr;
+	return result;
+}
+
+mesh_nodes_data* nv::md5_loader::release_mesh_nodes_data( size_t )
+{
+	mesh_nodes_data* nodes = m_nodes;
+	m_nodes = nullptr; 
+	return nodes;
+}
+
+mesh_data_pack* nv::md5_loader::release_mesh_data_pack()
+{
+	uint32 size = m_meshes.size();
+	data_channel_set* meshes = data_channel_set_creator::create_set_array( size, 4 );
+	for ( uint32 i = 0; i < size; ++i )
+	{
+		meshes[i] = move( *m_meshes[i] );
+		delete m_meshes[i];
+		m_meshes[i] = nullptr;
+	}
+	return new mesh_data_pack( size, meshes, release_mesh_nodes_data() );
+}
+
+
+nv::md5_loader::~md5_loader()
+{
+	reset();
+}
+
+void nv::md5_loader::reset()
+{
+	if ( m_nodes ) delete m_nodes;
+	for ( auto m : m_meshes ) { if (m) delete m; }
+	m_meshes.resize(0);
+	m_nodes = nullptr;
+}
+
Index: trunk/legacy/md5_loader.hh
===================================================================
--- trunk/legacy/md5_loader.hh	(revision 441)
+++ trunk/legacy/md5_loader.hh	(revision 441)
@@ -0,0 +1,96 @@
+// Copyright (C) 2012-2015 ChaosForge Ltd
+// http://chaosforge.org/
+//
+// This file is part of Nova libraries. 
+// For conditions of distribution and use, see copying.txt file in root folder.
+
+/**
+ * @file md5_loader.hh
+ * @author Kornel Kisielewicz
+ * @brief md5 loader
+ */
+
+#ifndef NV_FORMATS_MD5_LOADER_HH
+#define NV_FORMATS_MD5_LOADER_HH
+
+#include <nv/common.hh>
+#include <nv/stl/array.hh>
+#include <nv/interface/mesh_loader.hh>
+
+namespace nv 
+{
+
+	struct md5_vtx_pnt
+	{
+		vec3 position;
+		vec3 normal;
+		vec3 tangent;
+	};
+
+	struct md5_key_t
+	{
+		transform tform;
+	};
+
+	struct md5_vtx_t
+	{
+		vec2 texcoord;
+	};
+
+	struct md5_vtx_pntiw
+	{
+		vec3  position;
+		vec3  normal;
+		vec3  tangent;
+		ivec4 boneindex;
+		vec4  boneweight;
+	};
+
+	class md5_loader : public mesh_loader
+	{
+	public:
+		enum file_type { UNKNOWN, MESH, ANIMATION };
+
+		explicit md5_loader( string_table* strings ) : mesh_loader( strings ), m_type( UNKNOWN ), m_nodes( nullptr ) {}
+		virtual ~md5_loader();
+		virtual bool load( stream& source );
+		virtual data_channel_set* release_mesh_data( size_t index = 0 );
+		virtual size_t get_nodes_data_count() const { return 1; }
+		virtual mesh_nodes_data* release_mesh_nodes_data( size_t = 0 );
+		virtual mesh_data_pack* release_mesh_data_pack();
+		virtual size_t get_mesh_count() const { return m_meshes.size(); }
+
+		struct md5_weight
+		{
+			size_t joint_id;
+			float  bias;
+			vec3   pos;
+		};
+
+	protected:
+		struct md5_joint_info
+		{
+			int         flags;
+			size_t      start_index;
+		};
+
+		struct md5_weight_info
+		{
+			size_t     start_weight;
+			size_t     weight_count;
+		};
+
+	protected:
+		void reset();
+		void build_frame_skeleton( mesh_nodes_data* nodes, uint32 index, const array_view<md5_joint_info>& joint_info, const array_view<transform>& base_frames, const array_view<float>& frame_data );
+		bool prepare_mesh( mesh_nodes_data* nodes, uint32 vtx_count, data_channel_set* mdata, md5_weight* weights, md5_weight_info* weight_info );
+	protected:
+		file_type m_type;
+		uint32 m_md5_version;
+		mesh_nodes_data* m_nodes;
+		dynamic_array<data_channel_set*> m_meshes;
+	};
+
+}
+
+#endif // NV_FORMATS_MD5_LOADER_HH
Index: trunk/legacy/md5_test/md5.frag
===================================================================
--- trunk/legacy/md5_test/md5.frag	(revision 441)
+++ trunk/legacy/md5_test/md5.frag	(revision 441)
@@ -0,0 +1,50 @@
+#version 120
+
+uniform sampler2D nv_t_diffuse;
+uniform sampler2D nv_t_specular;
+uniform sampler2D nv_t_normal;
+uniform vec4 light_diffuse;
+uniform vec4 light_specular;
+uniform mat4 nv_m_view_inv;
+
+varying vec2 v_texcoord;
+varying vec3 v_normal;
+varying vec3 v_light_vector;
+varying vec3 v_view_vector;
+varying vec4 v_position;
+varying mat3 v_m33_tangent;
+ 
+void main(void) {
+	vec2 texcoord         = v_texcoord;
+	//
+	vec4 encoded_normal   = texture2D( nv_t_normal, texcoord );
+	encoded_normal.y      = 1 - encoded_normal.y;
+	vec3 local_coords     = 2.0 * encoded_normal.rgb - vec3(1.0);
+	vec3 normal_direction = normalize(v_m33_tangent * local_coords);
+	vec3 view_direction   = normalize(vec3(nv_m_view_inv * vec4(0.0, 0.0, 0.0, 1.0) - v_position));
+	vec3 nnormal         = normal_direction;
+	//
+
+//	vec3 nnormal         = normalize( v_normal );
+	vec3 nlight_vector   = normalize( v_light_vector );
+	vec3 nview_vector    = normalize( v_view_vector );
+	vec3 nreflect_vector = reflect( -nlight_vector, nnormal );
+
+	float specular_value = clamp( dot( nreflect_vector, nview_vector ), 0.0, 1.0 );
+	specular_value       = pow( specular_value, 6.0 );
+	float diffuse_value  = max( dot( nlight_vector, nnormal ), 0.0 );
+
+	vec3 diff_texel      = vec3( texture2D( nv_t_diffuse, texcoord ) );	
+	vec4 spec_texel      = texture2D( nv_t_specular, texcoord );
+	
+	float specular_amount = spec_texel.x;
+	float diffuse_amount  = 1.0 - specular_amount;
+
+	float final_specular = specular_amount * specular_value;
+	float final_diffuse  = diffuse_amount  * diffuse_value;
+
+	vec3 diffuse_color   = light_diffuse.xyz  * final_diffuse * diff_texel;
+	vec3 specular_color  = light_specular.xyz * final_specular;
+
+	gl_FragColor = vec4( diffuse_color + specular_color, 1.0 );
+}
Index: trunk/legacy/md5_test/md5.vert
===================================================================
--- trunk/legacy/md5_test/md5.vert	(revision 441)
+++ trunk/legacy/md5_test/md5.vert	(revision 441)
@@ -0,0 +1,37 @@
+#version 120
+
+attribute vec2 nv_texcoord;
+attribute vec3 nv_position;
+attribute vec3 nv_normal;
+attribute vec3 nv_tangent;
+
+varying vec3 v_normal;
+varying vec3 v_light_vector;
+varying vec3 v_view_vector;
+varying vec4 v_position;
+varying vec2 v_texcoord;
+varying mat3 v_m33_tangent;
+
+uniform mat4 nv_m_model;
+uniform mat4 nv_m_modelview;
+uniform mat4 nv_m_projection;
+uniform mat3 nv_m_normal;
+uniform mat4 nv_m_mvp;
+uniform float nv_interpolate;
+uniform vec3 light_position;
+
+void main(void) {
+	vec4 vertex     = vec4( nv_position, 1.0 );
+	vec3 eye_pos    = vec3( nv_m_modelview * vertex );
+	v_normal        = normalize( nv_m_normal * nv_normal );
+	v_light_vector  = vec3( normalize( light_position - eye_pos ) );
+	v_view_vector   = vec3( normalize( -eye_pos ) );
+
+	v_m33_tangent[0] = normalize(nv_m_normal * nv_tangent);
+	v_m33_tangent[2] = v_normal;
+	v_m33_tangent[1] = normalize(cross(v_m33_tangent[2], v_m33_tangent[0]));
+
+	v_texcoord      = nv_texcoord;
+	v_position      = nv_m_model * vertex;
+	gl_Position     = nv_m_mvp * vertex;
+}
Index: trunk/legacy/md5_test/md5_test.lua
===================================================================
--- trunk/legacy/md5_test/md5_test.lua	(revision 441)
+++ trunk/legacy/md5_test/md5_test.lua	(revision 441)
@@ -0,0 +1,8 @@
+project "nv_md5_test"
+	kind "ConsoleApp"
+	files { "nv_md5_test.cc" }
+	includedirs { "../../" }
+	targetname "nv_md5_test"
+	links { "nv-core", "nv-gl", "nv-formats" }
+	targetdir "../../bin"	
+ 
Index: trunk/legacy/md5_test/nv_md5_test.cc
===================================================================
--- trunk/legacy/md5_test/nv_md5_test.cc	(revision 441)
+++ trunk/legacy/md5_test/nv_md5_test.cc	(revision 441)
@@ -0,0 +1,240 @@
+#include <nv/core/common.hh>
+#include <iomanip>
+#include <nv/gfx/keyframed_mesh.hh>
+#include <nv/gl/gl_device.hh>
+#include <nv/gfx/image.hh>
+#include <nv/interface/context.hh>
+#include <nv/interface/window.hh>
+#include <nv/interface/mesh_loader.hh>
+#include <nv/io/c_file_system.hh>
+#include <nv/formats/md5_loader.hh>
+#include <nv/core/profiler.hh>
+#include <nv/core/logging.hh>
+#include <nv/core/logger.hh>
+#include <nv/core/math.hh>
+#include <nv/core/time.hh>
+#include <nv/core/string.hh>
+#include <nv/gfx/skeletal_mesh.hh>
+#include <glm/gtx/rotate_vector.hpp>
+#include <glm/gtc/matrix_access.hpp>
+#include <glm/gtx/matrix_interpolation.hpp>
+
+class application
+{
+public:
+	application();
+	bool initialize();
+	bool run();
+	~application();
+protected:
+	void load_animation( const std::string& path );
+protected:
+
+
+	nv::device*       m_device;
+	nv::window*       m_window;
+	nv::context*      m_context;
+	nv::texture       m_diffuse;
+	nv::texture       m_specular;
+	nv::texture       m_normal;
+	nv::clear_state   m_clear_state;
+	nv::render_state  m_render_state;
+	nv::scene_state   m_scene_state;
+
+	nv::skeletal_mesh* m_mesh;
+	nv::program        m_program;
+	nv::mesh_data*       m_mesh_data;
+	nv::animation_entry* m_animation;
+
+};
+
+application::application()
+{
+	NV_PROFILE( "app_construct" );
+	m_device  = new nv::gl_device();
+	m_window  = m_device->create_window( 800, 600, false );
+	m_context = m_window->get_context();
+	m_animation = nullptr;
+
+	nv::sampler sampler( nv::sampler::LINEAR, nv::sampler::REPEAT );
+	nv::image_data* data = m_device->create_image_data( "data/qshambler_d.png" );
+	m_diffuse  = m_device->create_texture( data, sampler );
+	delete data;
+	data = m_device->create_image_data( "data/qshambler_s.png" );
+	m_specular = m_device->create_texture( data, sampler );
+	delete data;
+
+	data = m_device->create_image_data( "data/qshambler_local.png" );
+	m_normal = m_device->create_texture( data, sampler );
+	delete data;
+
+	m_clear_state.buffers = nv::clear_state::COLOR_AND_DEPTH_BUFFER;
+	m_render_state.depth_test.enabled = true;
+	m_render_state.culling.enabled    = true;
+	m_render_state.culling.order      = nv::culling::CCW;
+	m_render_state.blending.enabled   = false;
+	m_render_state.blending.src_rgb_factor   = nv::blending::SRC_ALPHA;
+	m_render_state.blending.dst_rgb_factor   = nv::blending::ONE_MINUS_SRC_ALPHA;
+	m_render_state.blending.src_alpha_factor = nv::blending::SRC_ALPHA;
+	m_render_state.blending.dst_alpha_factor = nv::blending::ONE_MINUS_SRC_ALPHA;
+}
+
+bool application::initialize()
+{
+	NV_PROFILE( "app_initialize" );
+	m_program = m_device->create_program( nv::slurp( "md5.vert" ), nv::slurp( "md5.frag" ) );
+
+	nv::md5_loader* loader = nullptr;
+	{
+		NV_PROFILE("loader->load");
+		nv::c_file_system fs;
+		nv::stream* mesh_file = fs.open( "data/qshambler.md5mesh" );
+		loader = new nv::md5_loader();
+		loader->load( *mesh_file );
+		delete mesh_file;
+	}
+
+	{
+		NV_PROFILE("create_mesh");
+		m_mesh_data = loader->release_mesh_data();
+		m_mesh = new nv::skeletal_mesh_cpu( m_context, m_mesh_data, loader->release_mesh_nodes_data() );
+		delete loader;
+	}
+
+	load_animation( "data/walk.md5anim" );
+	return true;
+}
+
+bool application::run()
+{
+	nv::profiler::pointer()->log_report(); 
+	NV_PROFILE( "app_run" );
+	int keypress = 0;
+
+	glm::vec3 move( 0, 50.f, 0 );
+
+	nv::uint32 ticks   = m_device->get_ticks();
+	nv::uint32 last_ticks;
+	nv::fps_counter_class< nv::system_us_timer > fps_counter;
+
+	//m_mesh->setup_animation( 0, m_mesh->get_max_frames(), 2, true );
+
+	while(!keypress) 
+	{
+		last_ticks = ticks;
+		ticks      = m_device->get_ticks();
+		m_mesh->update_animation( m_animation, ticks );
+		m_context->clear( m_clear_state );
+
+		{
+			NV_PROFILE( "update_sh" );
+
+			glm::vec3 source( 250.0f, 0.0f, 0.0f );
+			glm::vec3 eye = glm::rotate( source, (ticks / 20.f), glm::vec3( 0.0,1.0,0.0 ) );
+
+			m_scene_state.get_camera().set_lookat(eye + move, glm::vec3(0.0f, 0.0f, 0.0f) + move, glm::vec3(0.0, 1.0, 0.0));
+			m_scene_state.get_camera().set_perspective(60.0f, 1.0f*800.0f/600.0f, 0.1f, 1000.0f);
+
+			m_context->bind( m_diffuse,  nv::TEX_DIFFUSE );
+			m_context->bind( m_specular, nv::TEX_SPECULAR );
+			m_context->bind( m_normal,   nv::TEX_NORMAL );
+			m_device->set_opt_uniform( m_program, "light_position", glm::vec3(180.0, 180.0, 0) );
+			m_device->set_opt_uniform( m_program, "light_diffuse",  glm::vec4(0.7,0.7,0.7,1.0) );
+			m_device->set_opt_uniform( m_program, "light_specular", glm::vec4(1.0,1.0,1.0,1.0) );
+		}
+
+		{
+			NV_PROFILE( "draw" );
+			m_scene_state.set_model(nv::mat4(
+				1.f,0.f,0.f,0.f,
+				0.f,0.f,1.f,0.f,
+				0.f,1.f,0.f,0.f,
+				0.f,0.f,0.f,1.f
+			) );
+
+			m_mesh->update( m_program );
+			m_context->draw( nv::TRIANGLES, m_render_state, m_scene_state, m_program, m_mesh->get_vertex_array(), m_mesh->get_index_count() );
+		}
+
+		{
+			NV_PROFILE( "swap" );
+			m_window->swap_buffers();
+		}
+
+		{
+			NV_PROFILE( "pollevent" );
+			nv::io_event event;
+			while(m_window->poll_event(event)) 
+			{      
+				switch (event.type) 
+				{
+				case nv::EV_QUIT:
+					keypress = 1;
+					break;
+				case nv::EV_KEY:
+					if (event.key.pressed)
+					{
+						switch (event.key.code) 
+						{
+						case nv::KEY_ESCAPE : keypress = 1; break;
+						case nv::KEY_F1 : nv::profiler::pointer()->log_report(); break;
+						default: break;
+						}
+					}
+					break;
+				default: break;
+				}
+			}
+		}
+		fps_counter.tick();
+	}
+	return true;
+}
+
+void application::load_animation( const std::string& path )
+{
+	delete m_animation;
+	m_animation = nullptr;
+	NV_PROFILE("load_animation");
+	nv::c_file_system fs;
+	nv::stream* anim_file = fs.open( path.c_str() );
+
+	if ( anim_file != nullptr )
+	{
+		nv::md5_loader* loader = new nv::md5_loader();
+		loader->load( *anim_file );
+		delete anim_file;
+		m_animation = new nv::skeletal_animation_entry_cpu( "", loader->release_mesh_nodes_data(), true );
+		m_mesh->run_animation( m_animation );
+		delete loader;
+	}
+}
+
+
+application::~application()
+{
+	m_device->release( m_program );
+	delete m_mesh;
+	m_device->release( m_diffuse );
+	delete m_window;
+	delete m_device;
+}
+
+
+int main(int, char* [])
+{
+	nv::logger log(nv::LOG_TRACE);
+	log.add_sink( new nv::log_file_sink("log.txt"), nv::LOG_TRACE );
+	log.add_sink( new nv::log_console_sink(), nv::LOG_TRACE );
+	
+	NV_LOG( nv::LOG_NOTICE, "Logging started" );
+	application app;
+	if ( app.initialize() )
+	{
+		app.run();
+	}
+	NV_LOG( nv::LOG_NOTICE, "Logging stopped" );
+
+	return 0;
+}
+
Index: trunk/legacy/md5_test/premake4.lua
===================================================================
--- trunk/legacy/md5_test/premake4.lua	(revision 441)
+++ trunk/legacy/md5_test/premake4.lua	(revision 441)
@@ -0,0 +1,18 @@
+solution "nv_md5_test"
+	configurations { "debug", "release" }
+
+  	language "C++"
+	flags { "ExtraWarnings", "NoPCH" }
+
+	configuration "debug"
+		defines { "DEBUG" }
+		flags { "Symbols", "StaticRuntime" }
+		objdir ("../../".._ACTION.."/debug")
+
+	configuration "release"
+		defines { "NDEBUG" }
+		flags { "Optimize", "StaticRuntime" }
+		objdir ("../../".._ACTION.."/release")
+
+	dofile("md5_test.lua")
+	dofile("../../nv.lua")
