Index: trunk/nv/formats/md2_loader.hh
===================================================================
--- trunk/nv/formats/md2_loader.hh	(revision 189)
+++ trunk/nv/formats/md2_loader.hh	(revision 189)
@@ -0,0 +1,56 @@
+// Copyright (C) 2012-2013 ChaosForge / Kornel Kisielewicz
+// http://chaosforge.org/
+//
+// This file is part of NV Libraries.
+// For conditions of distribution and use, see copyright notice in nv.hh
+
+/**
+ * @file md2_loader.hh
+ * @author Kornel Kisielewicz
+ * @brief md2 loader
+ */
+
+#ifndef NV_MD2_LOADER_HH
+#define NV_MD2_LOADER_HH
+
+#include <unordered_map>
+#include <vector>
+#include <nv/common.hh>
+#include <nv/interface/mesh_loader.hh>
+#include <nv/formats/md3_loader.hh> // TODO : remove when common interface for keyframed mesh
+
+namespace nv 
+{
+
+	class md2_loader : public keyframed_loader
+	{
+	public:
+		md2_loader();
+		virtual ~md2_loader();
+		virtual bool load( stream& source );
+		virtual size_t get_size() { return m_size; }
+		virtual mesh* release_mesh();
+		virtual mesh* get_frame( sint32 frame );
+//		virtual const md3_tag* get_tag( const std::string& name ) const;
+//		virtual mat4 get_tag( sint32 frame, const std::string& name ) const;
+		sint32 get_max_frames() const;
+//		void load_tag_names( std::vector< std::string >& tags );
+//		void load_tags( std::vector<mat4>& t, const std::string& tag );
+		void load_positions( std::vector<vec3>& p, sint32 frame =-1 );
+		void load_normals( std::vector<vec3>& n, sint32 frame =-1 );
+		void load_texcoords( std::vector<vec2>& t );
+		void load_indicies( std::vector<uint16>& idx );
+	private:
+		void reindex();
+	private:
+		void* m_md2;
+//		std::unordered_map< std::string, md3_tag > m_tags;
+		std::size_t m_size;
+		std::vector< uint16 > m_new_indexes;
+		std::vector< uint16 > m_new_vindexes;
+		std::vector< uint16 > m_new_tindexes;
+	};
+
+}
+
+#endif // NV_MD2_LOADER_HH
Index: trunk/nv/formats/md3_loader.hh
===================================================================
--- trunk/nv/formats/md3_loader.hh	(revision 188)
+++ trunk/nv/formats/md3_loader.hh	(revision 189)
@@ -27,4 +27,15 @@
 		mat4 transform;
 	};
+
+	class keyframed_loader : public mesh_loader
+	{
+	public:
+		virtual sint32 get_max_frames() const = 0;
+		virtual void load_positions( std::vector<vec3>& p, sint32 frame =-1 ) = 0;
+		virtual void load_normals( std::vector<vec3>& n, sint32 frame =-1 ) = 0;
+		virtual void load_texcoords( std::vector<vec2>& t ) = 0;
+		virtual void load_indicies( std::vector<uint16>& idx ) = 0;
+	};
+
 
 	class md3_loader : public mesh_loader
@@ -58,5 +69,5 @@
 		typedef std::unordered_map< std::string, transforms > tag_map;
 
-		keyframed_mesh_data( md3_loader* loader );
+		keyframed_mesh_data( keyframed_loader* loader );
 		size_t get_frame_count() const { return m_frames; };
 		size_t get_index_count() const { return m_indices.size(); };
Index: trunk/src/formats/md2_loader.cc
===================================================================
--- trunk/src/formats/md2_loader.cc	(revision 189)
+++ trunk/src/formats/md2_loader.cc	(revision 189)
@@ -0,0 +1,407 @@
+// Copyright (C) 2012-2013 ChaosForge / Kornel Kisielewicz
+// http://chaosforge.org/
+//
+// This file is part of NV Libraries.
+// For conditions of distribution and use, see copyright notice in nv.hh
+
+#include "nv/formats/md2_loader.hh"
+
+#include <glm/gtc/constants.hpp>
+#include <glm/gtx/string_cast.hpp>
+#include "nv/logging.hh"
+#include <cstring>
+
+using namespace nv;
+
+// based on http://tfc.duke.free.fr/coding/md2-specs-en.html
+
+// assuming low-endian
+#define MD2_MAGIC         "IDP2"
+#define MD2_MAX_FRAMES    512
+#define MD2_MAX_SKINS     32
+#define MD2_MAX_VERTS     2048
+#define MD2_MAX_TRIANGLES 4096
+#define MD2_MAX_TEXCOORDS 2048
+#define MD2_NORMAL_COUNT  162
+
+typedef float md2_vec3_t[3];
+
+static const md2_vec3_t md2_normal_table[MD2_NORMAL_COUNT] = {
+#include "src/formats/md2_normals.inc"
+};
+
+struct md2_header_t
+{
+	char magic[4];
+	int version;
+
+	int skinwidth;
+	int skinheight;
+
+	int framesize;
+
+	int num_skins;
+	int num_vertices;
+	int num_st;
+	int num_tris;
+	int num_glcmds;
+	int num_frames;
+
+	int offset_skins;
+	int offset_st;
+	int offset_tris;
+	int offset_frames;
+	int offset_glcmds;
+	int offset_end;
+};
+
+struct md2_skin_t
+{
+	char name[64];
+};
+
+struct md2_texcoord_t
+{
+	short s;
+	short t;
+};
+
+struct md2_triangle_t
+{
+	unsigned short vertex[3];
+	unsigned short st[3];
+};
+
+struct md2_vertex_t
+{
+	unsigned char v[3];
+	unsigned char n;
+};
+
+struct md2_frame_t
+{
+	md2_vec3_t scale;
+	md2_vec3_t translate;
+	char name[16];
+	md2_vertex_t *vertices;
+};
+
+struct md2_glcmd_t
+{
+	float s;
+	float t;
+	int index;
+};
+
+struct md2_t
+{
+	md2_header_t    header;
+	md2_skin_t*     skins;
+	md2_texcoord_t* texcoords;
+	md2_triangle_t* triangles;
+	md2_frame_t*    frames;
+	int*            glcmds;
+};
+
+static bool check_md2_magic( char* magic )
+{
+	return magic[0] == 'I' && magic[1] == 'D' && magic[2] == 'P' && magic[3] == '2';
+}
+
+static void free_md2_frame( md2_frame_t* frame )
+{
+	delete[] frame->vertices;
+}
+
+static void free_md2( md2_t* md2 )
+{
+	delete[] md2->skins;
+	delete[] md2->texcoords;
+	delete[] md2->triangles;
+	delete[] md2->glcmds;
+	for ( int i = 0; i < md2->header.num_frames; ++i )
+	{
+		free_md2_frame( &(md2->frames[i]) );
+	}
+	delete[] md2->frames;
+}
+
+static bool read_md2_frame( md2_frame_t* frame, int vcount, nv::stream& source )
+{
+	frame->vertices = new md2_vertex_t[ vcount ];
+	source.read( frame->scale,     sizeof(md2_vec3_t), 1 );
+	source.read( frame->translate, sizeof(md2_vec3_t), 1 );
+	source.read( frame->name,      sizeof(char), 16 );
+	source.read( frame->vertices,  sizeof(md2_vertex_t), vcount );
+	return true;
+}
+
+static bool read_md2( md2_t* md2, nv::stream& source )
+{
+	md2->frames     = nullptr;
+	md2->skins      = nullptr;
+	md2->texcoords  = nullptr;
+	md2->triangles  = nullptr;
+	md2->glcmds     = nullptr;
+
+	source.read( &(md2->header), sizeof(md2_header_t), 1 );
+
+	if ( !check_md2_magic( md2->header.magic )       ||
+		md2->header.num_skins    > MD2_MAX_SKINS     ||
+		md2->header.num_vertices > MD2_MAX_VERTS     ||
+		md2->header.num_st       > MD2_MAX_TEXCOORDS ||
+		md2->header.num_tris     > MD2_MAX_TRIANGLES ||
+		md2->header.num_frames   > MD2_MAX_FRAMES )
+	{
+		return false;
+	}
+
+	NV_LOG( LOG_INFO, "num_skins    = " << md2->header.num_skins );
+	NV_LOG( LOG_INFO, "num_vertices = " << md2->header.num_vertices );
+	NV_LOG( LOG_INFO, "num_st       = " << md2->header.num_st );
+	NV_LOG( LOG_INFO, "num_tris     = " << md2->header.num_tris );
+	NV_LOG( LOG_INFO, "num_frames   = " << md2->header.num_frames );
+
+
+	md2->skins      = new md2_skin_t    [ md2->header.num_skins ];
+	md2->texcoords  = new md2_texcoord_t[ md2->header.num_st ];
+	md2->triangles  = new md2_triangle_t[ md2->header.num_tris ];
+	md2->glcmds     = new int           [ md2->header.num_glcmds ];
+	
+	source.seek( md2->header.offset_skins, origin::SET );
+	source.read( md2->skins, sizeof(md2_skin_t), md2->header.num_skins );
+
+	source.seek( md2->header.offset_st, origin::SET );
+	source.read( md2->texcoords, sizeof(md2_texcoord_t), md2->header.num_st );
+
+	source.seek( md2->header.offset_tris, origin::SET );
+	source.read( md2->triangles, sizeof(md2_triangle_t), md2->header.num_tris );
+
+	source.seek( md2->header.offset_glcmds, origin::SET);
+	source.read( md2->glcmds, sizeof(int), md2->header.num_glcmds );
+
+	md2->frames    = new md2_frame_t   [ md2->header.num_frames ];
+	source.seek( md2->header.offset_frames, origin::SET );
+	for ( int i = 0; i < md2->header.num_frames; ++i )
+	{
+		if (!read_md2_frame( &(md2->frames[i]), md2->header.num_vertices, source ) ) return false;
+	}
+
+	return true;
+}
+
+static inline vec3 md2_vec3( const md2_vec3_t& v )
+{
+	//	return vec3( v[0], v[1], v[2] );
+	return vec3( v[0], v[2], v[1] );
+}
+
+static inline vec3 md2_normal( char normal )
+{
+	return md2_vec3( md2_normal_table[normal] );
+}
+
+static vec3 s_md2_normal_cache[MD2_NORMAL_COUNT];
+static bool s_md2_normal_ready = false;
+
+md2_loader::md2_loader() : m_md2( nullptr ), m_size( 0 )
+{
+	if ( !s_md2_normal_ready )
+	{
+		for ( int i = 0; i < MD2_NORMAL_COUNT; ++i )
+		{
+			s_md2_normal_cache[i].x = md2_normal_table[i][0];
+//			s_md2_normal_cache[i].y = md2_normal_table[i][1];
+//			s_md2_normal_cache[i].z = md2_normal_table[i][2];
+			s_md2_normal_cache[i].y = md2_normal_table[i][2];
+			s_md2_normal_cache[i].z = md2_normal_table[i][1];
+		}
+	}
+}
+
+
+md2_loader::~md2_loader()
+{
+	if (m_md2 != nullptr)
+	{
+		free_md2( (md2_t*)(m_md2) );
+		delete (md2_t*)m_md2;
+	}
+}
+
+bool md2_loader::load( stream& source )
+{
+	m_size = 0;
+	m_md2 = (void*)(new md2_t);
+	if ( !read_md2( (md2_t*)m_md2, source ) )
+	{
+		return false;
+	}
+	reindex();
+	m_size = m_new_indexes.size();
+	return true;
+}
+
+mesh* nv::md2_loader::release_mesh()
+{
+	return get_frame( 0 );
+}
+
+mesh* nv::md2_loader::get_frame( sint32 frame )
+{
+	md2_t* md2 = (md2_t*)m_md2;
+	if ( md2 == nullptr || frame >= md2->header.num_frames ) return nullptr;
+	mesh* m = new mesh();
+
+	vertex_attribute< vec3 >*   position = m->add_attribute<vec3>("nv_position");
+	vertex_attribute< vec3 >*   normal   = m->add_attribute<vec3>("nv_normal");
+	vertex_attribute< vec2 >*   texcoord = m->add_attribute<vec2>("nv_texcoord");
+	vertex_attribute< uint16 >* indices  = m->add_indices<uint16>();
+
+	load_positions( position->get(), frame );
+	load_normals( normal->get(), frame );
+
+	load_texcoords( texcoord->get() );
+	load_indicies( indices->get() );
+
+	m_size = indices->get().size();
+	return m;
+}
+
+sint32 md2_loader::get_max_frames() const
+{
+	return ((md2_t*)m_md2)->header.num_frames;
+}
+
+void md2_loader::load_positions( std::vector<vec3>& p, sint32 frame /*=-1*/ )
+{
+	md2_t* md2 = (md2_t*)m_md2;
+	sint32 num_frames = md2->header.num_frames;
+	sint32 num_verts  = m_new_vindexes.size();
+	p.clear();
+	sint32 current_frame = ( frame == -1 ? 0 : frame );
+	sint32 frame_count   = ( frame == -1 ? num_frames : 1 );
+
+	p.reserve( num_verts * frame_count );
+
+	while ( frame_count > 0 )
+	{
+		const md2_frame_t& frame = md2->frames[current_frame];
+		NV_LOG( LOG_INFO, "FrameID = " << frame.name );
+
+		vec3 scale     = md2_vec3( frame.scale );
+		vec3 translate = md2_vec3( frame.translate );
+
+		for (sint32 i = 0; i < num_verts; ++i )
+		{
+			const md2_vertex_t& v = frame.vertices[ m_new_vindexes[ i ] ];
+			p.push_back( vec3( v.v[0], v.v[2], v.v[1] ) * scale + translate );
+		}
+		++current_frame;
+		--frame_count;
+	}
+}
+
+void md2_loader::load_normals( std::vector<vec3>& n, sint32 frame /*=-1*/ )
+{
+	md2_t* md2 = (md2_t*)m_md2;
+	sint32 num_frames = md2->header.num_frames;
+	sint32 num_verts  =	m_new_vindexes.size();
+	n.clear();
+	sint32 current_frame = ( frame == -1 ? 0 : frame );
+	sint32 frame_count   = ( frame == -1 ? num_frames : 1 );
+
+	n.reserve( num_verts * frame_count );
+
+	while ( frame_count > 0 )
+	{
+		const md2_frame_t& frame = md2->frames[current_frame];
+
+		for (sint32 i = 0; i < num_verts; ++i )
+		{
+			const md2_vertex_t& v = frame.vertices[ m_new_vindexes[ i ] ];
+			n.push_back( s_md2_normal_cache[ v.n ] );
+		}
+		++current_frame;
+		--frame_count;
+	}
+}
+
+void md2_loader::load_texcoords( std::vector<vec2>& t )
+{
+	md2_t* md2 = (md2_t*)m_md2;
+	sint32 num_verts  = m_new_vindexes.size();
+
+	t.clear();
+	t.reserve( num_verts );
+
+	vec2 scale( 1.0f / (float) md2->header.skinwidth, 1.0f / (float) md2->header.skinheight );
+
+	for (sint32 i = 0; i < num_verts; ++i )
+	{
+		const md2_texcoord_t& st = md2->texcoords[ m_new_tindexes[ i ] ];
+		t.push_back( scale * vec2( st.s, st.t ) );
+	}
+
+}
+
+void md2_loader::load_indicies( std::vector<uint16>& idx )
+{
+	idx.assign( m_new_indexes.begin(), m_new_indexes.end() );
+}
+
+void nv::md2_loader::reindex()
+{
+	md2_t* md2 = (md2_t*)m_md2;
+	uint32 num_indexes = md2->header.num_tris * 3;
+
+	uint32 stats_reuse      = 0;
+	uint32 stats_collision  = 0;
+
+	std::vector< sint32 > index_translation( md2->header.num_vertices, -1 );
+
+	m_new_indexes.clear();
+	m_new_indexes.reserve( num_indexes );
+	m_new_vindexes.reserve( num_indexes );
+	m_new_tindexes.reserve( num_indexes );
+
+	uint16 index_count = 0;
+
+	for ( int i = 0; i < md2->header.num_tris; ++i )
+	{
+		const md2_triangle_t& t = md2->triangles[i];
+		for ( int j = 0; j < 3; ++j )
+		{
+			short index  = t.vertex[j];
+			short tindex = t.st[j];
+
+			if ( index_translation[ index ] != -1 )
+			{
+				uint16 prev = index_translation[ index ];
+				if ( m_new_tindexes[ prev ] == tindex )
+				{
+					m_new_indexes.push_back( prev );
+					stats_reuse++;
+					continue;
+				}
+			}
+			
+			m_new_vindexes.push_back( index );
+			m_new_tindexes.push_back( tindex );
+			m_new_indexes.push_back( index_count );
+			if ( index_translation[ index ] == -1 )
+			{
+				index_translation[ index ] = index_count;
+			}
+			else
+			{
+				stats_collision++;
+			}
+			index_count++;
+		}
+	}
+
+	NV_LOG( LOG_INFO, "New vertex count = " << m_new_vindexes.size() );
+	NV_LOG( LOG_INFO, "Collisions       = " << stats_collision );
+	NV_LOG( LOG_INFO, "Reuse count      = " << stats_reuse );
+}
Index: trunk/src/formats/md2_normals.inc
===================================================================
--- trunk/src/formats/md2_normals.inc	(revision 189)
+++ trunk/src/formats/md2_normals.inc	(revision 189)
@@ -0,0 +1,162 @@
+{ -0.525731f,  0.000000f,  0.850651f }, 
+{ -0.442863f,  0.238856f,  0.864188f }, 
+{ -0.295242f,  0.000000f,  0.955423f }, 
+{ -0.309017f,  0.500000f,  0.809017f }, 
+{ -0.162460f,  0.262866f,  0.951056f }, 
+{  0.000000f,  0.000000f,  1.000000f }, 
+{  0.000000f,  0.850651f,  0.525731f }, 
+{ -0.147621f,  0.716567f,  0.681718f }, 
+{  0.147621f,  0.716567f,  0.681718f }, 
+{  0.000000f,  0.525731f,  0.850651f }, 
+{  0.309017f,  0.500000f,  0.809017f }, 
+{  0.525731f,  0.000000f,  0.850651f }, 
+{  0.295242f,  0.000000f,  0.955423f }, 
+{  0.442863f,  0.238856f,  0.864188f }, 
+{  0.162460f,  0.262866f,  0.951056f }, 
+{ -0.681718f,  0.147621f,  0.716567f }, 
+{ -0.809017f,  0.309017f,  0.500000f }, 
+{ -0.587785f,  0.425325f,  0.688191f }, 
+{ -0.850651f,  0.525731f,  0.000000f }, 
+{ -0.864188f,  0.442863f,  0.238856f }, 
+{ -0.716567f,  0.681718f,  0.147621f }, 
+{ -0.688191f,  0.587785f,  0.425325f }, 
+{ -0.500000f,  0.809017f,  0.309017f }, 
+{ -0.238856f,  0.864188f,  0.442863f }, 
+{ -0.425325f,  0.688191f,  0.587785f }, 
+{ -0.716567f,  0.681718f, -0.147621f }, 
+{ -0.500000f,  0.809017f, -0.309017f }, 
+{ -0.525731f,  0.850651f,  0.000000f }, 
+{  0.000000f,  0.850651f, -0.525731f }, 
+{ -0.238856f,  0.864188f, -0.442863f }, 
+{  0.000000f,  0.955423f, -0.295242f }, 
+{ -0.262866f,  0.951056f, -0.162460f }, 
+{  0.000000f,  1.000000f,  0.000000f }, 
+{  0.000000f,  0.955423f,  0.295242f }, 
+{ -0.262866f,  0.951056f,  0.162460f }, 
+{  0.238856f,  0.864188f,  0.442863f }, 
+{  0.262866f,  0.951056f,  0.162460f }, 
+{  0.500000f,  0.809017f,  0.309017f }, 
+{  0.238856f,  0.864188f, -0.442863f }, 
+{  0.262866f,  0.951056f, -0.162460f }, 
+{  0.500000f,  0.809017f, -0.309017f }, 
+{  0.850651f,  0.525731f,  0.000000f }, 
+{  0.716567f,  0.681718f,  0.147621f }, 
+{  0.716567f,  0.681718f, -0.147621f }, 
+{  0.525731f,  0.850651f,  0.000000f }, 
+{  0.425325f,  0.688191f,  0.587785f }, 
+{  0.864188f,  0.442863f,  0.238856f }, 
+{  0.688191f,  0.587785f,  0.425325f }, 
+{  0.809017f,  0.309017f,  0.500000f }, 
+{  0.681718f,  0.147621f,  0.716567f }, 
+{  0.587785f,  0.425325f,  0.688191f }, 
+{  0.955423f,  0.295242f,  0.000000f }, 
+{  1.000000f,  0.000000f,  0.000000f }, 
+{  0.951056f,  0.162460f,  0.262866f }, 
+{  0.850651f, -0.525731f,  0.000000f }, 
+{  0.955423f, -0.295242f,  0.000000f }, 
+{  0.864188f, -0.442863f,  0.238856f }, 
+{  0.951056f, -0.162460f,  0.262866f }, 
+{  0.809017f, -0.309017f,  0.500000f }, 
+{  0.681718f, -0.147621f,  0.716567f }, 
+{  0.850651f,  0.000000f,  0.525731f }, 
+{  0.864188f,  0.442863f, -0.238856f }, 
+{  0.809017f,  0.309017f, -0.500000f }, 
+{  0.951056f,  0.162460f, -0.262866f }, 
+{  0.525731f,  0.000000f, -0.850651f }, 
+{  0.681718f,  0.147621f, -0.716567f }, 
+{  0.681718f, -0.147621f, -0.716567f }, 
+{  0.850651f,  0.000000f, -0.525731f }, 
+{  0.809017f, -0.309017f, -0.500000f }, 
+{  0.864188f, -0.442863f, -0.238856f }, 
+{  0.951056f, -0.162460f, -0.262866f }, 
+{  0.147621f,  0.716567f, -0.681718f }, 
+{  0.309017f,  0.500000f, -0.809017f }, 
+{  0.425325f,  0.688191f, -0.587785f }, 
+{  0.442863f,  0.238856f, -0.864188f }, 
+{  0.587785f,  0.425325f, -0.688191f }, 
+{  0.688191f,  0.587785f, -0.425325f }, 
+{ -0.147621f,  0.716567f, -0.681718f }, 
+{ -0.309017f,  0.500000f, -0.809017f }, 
+{  0.000000f,  0.525731f, -0.850651f }, 
+{ -0.525731f,  0.000000f, -0.850651f }, 
+{ -0.442863f,  0.238856f, -0.864188f }, 
+{ -0.295242f,  0.000000f, -0.955423f }, 
+{ -0.162460f,  0.262866f, -0.951056f }, 
+{  0.000000f,  0.000000f, -1.000000f }, 
+{  0.295242f,  0.000000f, -0.955423f }, 
+{  0.162460f,  0.262866f, -0.951056f }, 
+{ -0.442863f, -0.238856f, -0.864188f }, 
+{ -0.309017f, -0.500000f, -0.809017f }, 
+{ -0.162460f, -0.262866f, -0.951056f }, 
+{  0.000000f, -0.850651f, -0.525731f }, 
+{ -0.147621f, -0.716567f, -0.681718f }, 
+{  0.147621f, -0.716567f, -0.681718f }, 
+{  0.000000f, -0.525731f, -0.850651f }, 
+{  0.309017f, -0.500000f, -0.809017f }, 
+{  0.442863f, -0.238856f, -0.864188f }, 
+{  0.162460f, -0.262866f, -0.951056f }, 
+{  0.238856f, -0.864188f, -0.442863f }, 
+{  0.500000f, -0.809017f, -0.309017f }, 
+{  0.425325f, -0.688191f, -0.587785f }, 
+{  0.716567f, -0.681718f, -0.147621f }, 
+{  0.688191f, -0.587785f, -0.425325f }, 
+{  0.587785f, -0.425325f, -0.688191f }, 
+{  0.000000f, -0.955423f, -0.295242f }, 
+{  0.000000f, -1.000000f,  0.000000f }, 
+{  0.262866f, -0.951056f, -0.162460f }, 
+{  0.000000f, -0.850651f,  0.525731f }, 
+{  0.000000f, -0.955423f,  0.295242f }, 
+{  0.238856f, -0.864188f,  0.442863f }, 
+{  0.262866f, -0.951056f,  0.162460f }, 
+{  0.500000f, -0.809017f,  0.309017f }, 
+{  0.716567f, -0.681718f,  0.147621f }, 
+{  0.525731f, -0.850651f,  0.000000f }, 
+{ -0.238856f, -0.864188f, -0.442863f }, 
+{ -0.500000f, -0.809017f, -0.309017f }, 
+{ -0.262866f, -0.951056f, -0.162460f }, 
+{ -0.850651f, -0.525731f,  0.000000f }, 
+{ -0.716567f, -0.681718f, -0.147621f }, 
+{ -0.716567f, -0.681718f,  0.147621f }, 
+{ -0.525731f, -0.850651f,  0.000000f }, 
+{ -0.500000f, -0.809017f,  0.309017f }, 
+{ -0.238856f, -0.864188f,  0.442863f }, 
+{ -0.262866f, -0.951056f,  0.162460f }, 
+{ -0.864188f, -0.442863f,  0.238856f }, 
+{ -0.809017f, -0.309017f,  0.500000f }, 
+{ -0.688191f, -0.587785f,  0.425325f }, 
+{ -0.681718f, -0.147621f,  0.716567f }, 
+{ -0.442863f, -0.238856f,  0.864188f }, 
+{ -0.587785f, -0.425325f,  0.688191f }, 
+{ -0.309017f, -0.500000f,  0.809017f }, 
+{ -0.147621f, -0.716567f,  0.681718f }, 
+{ -0.425325f, -0.688191f,  0.587785f }, 
+{ -0.162460f, -0.262866f,  0.951056f }, 
+{  0.442863f, -0.238856f,  0.864188f }, 
+{  0.162460f, -0.262866f,  0.951056f }, 
+{  0.309017f, -0.500000f,  0.809017f }, 
+{  0.147621f, -0.716567f,  0.681718f }, 
+{  0.000000f, -0.525731f,  0.850651f }, 
+{  0.425325f, -0.688191f,  0.587785f }, 
+{  0.587785f, -0.425325f,  0.688191f }, 
+{  0.688191f, -0.587785f,  0.425325f }, 
+{ -0.955423f,  0.295242f,  0.000000f }, 
+{ -0.951056f,  0.162460f,  0.262866f }, 
+{ -1.000000f,  0.000000f,  0.000000f }, 
+{ -0.850651f,  0.000000f,  0.525731f }, 
+{ -0.955423f, -0.295242f,  0.000000f }, 
+{ -0.951056f, -0.162460f,  0.262866f }, 
+{ -0.864188f,  0.442863f, -0.238856f }, 
+{ -0.951056f,  0.162460f, -0.262866f }, 
+{ -0.809017f,  0.309017f, -0.500000f }, 
+{ -0.864188f, -0.442863f, -0.238856f }, 
+{ -0.951056f, -0.162460f, -0.262866f }, 
+{ -0.809017f, -0.309017f, -0.500000f }, 
+{ -0.681718f,  0.147621f, -0.716567f }, 
+{ -0.681718f, -0.147621f, -0.716567f }, 
+{ -0.850651f,  0.000000f, -0.525731f }, 
+{ -0.688191f,  0.587785f, -0.425325f }, 
+{ -0.587785f,  0.425325f, -0.688191f }, 
+{ -0.425325f,  0.688191f, -0.587785f }, 
+{ -0.425325f, -0.688191f, -0.587785f }, 
+{ -0.587785f, -0.425325f, -0.688191f }, 
+{ -0.688191f, -0.587785f, -0.425325f }
Index: trunk/src/formats/md3_loader.cc
===================================================================
--- trunk/src/formats/md3_loader.cc	(revision 188)
+++ trunk/src/formats/md3_loader.cc	(revision 189)
@@ -492,5 +492,5 @@
 }
 
-keyframed_mesh_data::keyframed_mesh_data( md3_loader* loader )
+keyframed_mesh_data::keyframed_mesh_data( keyframed_loader* loader )
 {
 	loader->load_positions( m_positions );
@@ -500,8 +500,13 @@
 
 	std::vector< std::string > names;
-	loader->load_tag_names( names );
-	for ( auto& name : names )
-	{
-		loader->load_tags( m_tags[ name ], name );
+
+	md3_loader* md3loader = dynamic_cast< md3_loader* >( loader );
+	if ( md3loader != nullptr )
+	{
+		md3loader->load_tag_names( names );
+		for ( auto& name : names )
+		{
+			md3loader->load_tags( m_tags[ name ], name );
+		}
 	}
 
Index: trunk/src/gui/gui_renderer.cc
===================================================================
--- trunk/src/gui/gui_renderer.cc	(revision 188)
+++ trunk/src/gui/gui_renderer.cc	(revision 189)
@@ -105,5 +105,5 @@
 	white.pos = ivec2();
 	m_atlas.set_region( white, wfill );
-	delete wfill;
+	delete[] wfill;
 
 	screen_render_data* sr = new screen_render_data( w->get_device(), 1024 );
Index: trunk/tests/md2_test/md2_test.cc
===================================================================
--- trunk/tests/md2_test/md2_test.cc	(revision 189)
+++ trunk/tests/md2_test/md2_test.cc	(revision 189)
@@ -0,0 +1,249 @@
+#include <nv/common.hh>
+#include <iomanip>
+#include <nv/gfx/keyframed_mesh.hh>
+#include <nv/interface/vertex_buffer.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/program.hh>
+#include <nv/interface/texture2d.hh>
+#include <nv/interface/mesh_loader.hh>
+#include <nv/io/c_file_system.hh>
+#include <nv/formats/md2_loader.hh>
+#include <nv/profiler.hh>
+#include <nv/logging.hh>
+#include <nv/logger.hh>
+#include <nv/math.hh>
+#include <nv/time.hh>
+#include <nv/string.hh>
+#include <nv/types.hh>
+#include <nv/interface/mesh.hh>
+#include <glm/gtx/rotate_vector.hpp>
+#include <glm/gtc/matrix_access.hpp>
+#include <glm/gtx/matrix_interpolation.hpp>
+
+class mesh_part
+{
+public:
+	mesh_part( const std::string& path, nv::program* program, nv::window* window )
+	{
+		NV_PROFILE("mesh_construct");
+		nv::md2_loader* loader;
+		{
+			NV_PROFILE("loader->load");
+			nv::c_file_system fs;
+			nv::stream* mesh_file = fs.open( path.c_str() );
+			loader = new nv::md2_loader();
+			loader->load( *mesh_file );
+			delete mesh_file;
+		}
+
+		{
+			NV_PROFILE("create_mesh_data");
+			m_mesh_data = new nv::keyframed_mesh_data( loader );
+		}
+		delete loader;
+
+		{
+			NV_PROFILE("create_mesh");
+			m_mesh      = new nv::keyframed_mesh( window->get_context(), m_mesh_data, program );
+		}
+
+	}
+
+	void setup_animation( nv::uint32 start, nv::uint32 stop, nv::uint32 fps, bool loop )
+	{
+		m_mesh->setup_animation( start, stop, fps, loop );
+	}
+
+	void update( nv::uint32 ms )
+	{
+		m_mesh->update( ms );
+	}
+
+	size_t get_max_frames()
+	{
+		return m_mesh->get_max_frames();
+	}
+
+	void draw( nv::render_state& rstate, const glm::mat4& m, const glm::mat4& v, const glm::mat4& p )
+	{
+		NV_PROFILE( "mesh-draw" );
+		glm::mat4 mv = v * m;
+		m_mesh->get_program()->set_opt_uniform( "nv_m_modelview", mv );
+		m_mesh->get_program()->set_opt_uniform( "nv_m_normal", glm::transpose(glm::inverse(glm::mat3(mv))) );
+		m_mesh->get_program()->set_uniform( "matrix_mvp", p * mv );
+		m_mesh->draw( rstate );
+	}
+
+	~mesh_part()
+	{
+		delete m_mesh_data;
+		delete m_mesh;
+	}
+
+private:
+	nv::keyframed_mesh_data* m_mesh_data;
+	nv::keyframed_mesh*      m_mesh;
+};
+
+class application
+{
+public:
+	application();
+	bool initialize();
+	bool run();
+	~application();
+protected:
+	nv::device*       m_device;
+	nv::window*       m_window;
+	nv::texture2d*    m_diffuse;
+	nv::clear_state   m_clear_state;
+	nv::render_state  m_render_state;
+
+	mesh_part*   m_mesh;
+	nv::program* m_program;
+};
+
+application::application()
+{
+	NV_PROFILE( "app_construct" );
+	m_device = new nv::gl_device();
+	m_window = m_device->create_window( 800, 600 );
+
+	nv::sampler sampler( nv::sampler::LINEAR, nv::sampler::REPEAT );
+	nv::image_data* data = m_device->create_image_data( "data/manc.png" );
+	m_diffuse  = m_device->create_texture2d( data->get_size(), nv::RGB, nv::UBYTE, sampler, (void*)data->get_data() );
+	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.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( "obj.vert" ), nv::slurp( "obj.frag" ) );
+	m_mesh    = new mesh_part( "data/manc.md2", m_program, m_window );
+	return true;
+}
+
+bool application::run()
+{
+	nv::profiler::pointer()->log_report(); 
+	NV_PROFILE( "app_run" );
+	int keypress = 0;
+
+	glm::vec3 move( 0, 25.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();
+		nv::uint32 elapsed = ticks - last_ticks;
+		m_mesh->update( elapsed );
+		{
+			NV_PROFILE( "clear" );
+			m_window->get_context()->clear( m_clear_state );
+		}
+
+		glm::mat4 view;
+		glm::mat4 projection;
+		{
+			NV_PROFILE( "update_sh" );
+
+			glm::vec3 source( 100.0f, 0.0f, 0.0f );
+			glm::vec3 eye = glm::rotate( source, (ticks / 20.f), glm::vec3( 0.0,1.0,0.0 ) );
+
+			view       = glm::lookAt(eye + move, glm::vec3(0.0f, 0.0f, 0.0f) + move, glm::vec3(0.0, 1.0, 0.0));
+			projection = glm::perspective(60.0f, 1.0f*800.0f/600.0f, 0.1f, 1000.0f);
+
+			m_diffuse->bind( 0 );
+			m_program->set_opt_uniform( "nv_m_projection", projection );
+			m_program->set_uniform( "light_position", glm::vec3(120.0, 120.0, 0) );
+			m_program->set_uniform( "light_diffuse",  glm::vec4(1.0,1.0,1.0,1.0) );
+			m_program->set_uniform( "light_specular", glm::vec4(1.0,1.0,1.0,1.0) );
+			m_program->set_uniform( "diffuse", 0 );
+		}
+
+		{
+			NV_PROFILE( "draw" );
+			glm::mat4 model      = glm::mat4(1.0f);
+			m_mesh->draw( m_render_state, model, view, projection );
+		}
+
+		{
+			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;
+}
+
+application::~application()
+{
+	delete m_program;
+	delete m_mesh;
+	delete 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/tests/md2_test/md2_test.lua
===================================================================
--- trunk/tests/md2_test/md2_test.lua	(revision 189)
+++ trunk/tests/md2_test/md2_test.lua	(revision 189)
@@ -0,0 +1,8 @@
+-- md2 load test project definition
+project "md2_test"
+	kind "ConsoleApp"
+	files { "md2_test.cc" }
+	includedirs { "../../" }
+	targetname "md2_test"
+	links { "nv" }
+ 
Index: trunk/tests/md2_test/obj.frag
===================================================================
--- trunk/tests/md2_test/obj.frag	(revision 189)
+++ trunk/tests/md2_test/obj.frag	(revision 189)
@@ -0,0 +1,34 @@
+#version 120
+
+uniform sampler2D diffuse;
+uniform vec4 light_diffuse;
+uniform vec4 light_specular;
+
+varying vec2 v_texcoord;
+varying vec3 v_normal;
+varying vec3 v_light_vector;
+varying vec3 v_view_vector;
+ 
+void main(void) {
+	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( diffuse, v_texcoord ) );	
+	
+	float final_specular = specular_value * 0.2;
+	float final_diffuse  = diffuse_value * 0.5;
+	float final_ambient  = 0.3;
+
+	vec3 ambient_color   = final_ambient * diff_texel;
+	vec3 diffuse_color   = light_diffuse.xyz * final_diffuse * diff_texel;
+	vec3 specular_color  = light_specular.xyz * final_specular;
+
+	gl_FragColor = vec4( ambient_color + diffuse_color + specular_color, 1.0 );
+//	gl_FragColor = vec4( diff_texel, 1.0 );
+}
Index: trunk/tests/md2_test/obj.vert
===================================================================
--- trunk/tests/md2_test/obj.vert	(revision 189)
+++ trunk/tests/md2_test/obj.vert	(revision 189)
@@ -0,0 +1,30 @@
+#version 120
+
+attribute vec2 nv_texcoord;
+attribute vec3 nv_position;
+attribute vec3 nv_normal;
+attribute vec3 nv_next_position;
+attribute vec3 nv_next_normal;
+
+varying vec3 v_normal;
+varying vec3 v_light_vector;
+varying vec3 v_view_vector;
+varying vec2 v_texcoord;
+
+uniform mat4 matrix_mvp;
+uniform mat4 nv_m_modelview;
+uniform mat4 nv_m_projection;
+uniform mat3 nv_m_normal;
+uniform float nv_interpolate;
+uniform vec3 light_position;
+
+void main(void) {
+	vec4 vertex     = vec4( mix( nv_position, nv_next_position, nv_interpolate ), 1.0 );
+	vec3 eye_pos    = vec3( nv_m_modelview * vertex );
+	v_normal        = normalize( nv_m_normal * mix( nv_normal, nv_next_normal, nv_interpolate ) );
+	v_light_vector  = vec3( normalize( light_position - eye_pos ) );
+	v_view_vector   = vec3( normalize( -eye_pos ) );
+
+	v_texcoord      = nv_texcoord;
+	gl_Position     = matrix_mvp * vertex;
+}
Index: trunk/tests/md2_test/premake4.lua
===================================================================
--- trunk/tests/md2_test/premake4.lua	(revision 189)
+++ trunk/tests/md2_test/premake4.lua	(revision 189)
@@ -0,0 +1,24 @@
+solution "nv_md2_test"
+	configurations { "debug", "release" }
+
+  	language "C++"
+	flags { "ExtraWarnings", "NoPCH" }
+
+	configuration "debug"
+		defines { "DEBUG" }
+		flags { "Symbols", "StaticRuntime" }
+		objdir (_ACTION or "".."/debug")
+
+	configuration "release"
+		defines { "NDEBUG" }
+		flags { "Optimize", "StaticRuntime" }
+		objdir (_ACTION or "".."/release")
+
+	dofile("md2_test.lua")
+	dofile("../../nv.lua")
+
+if _ACTION == "clean" then
+	for action in premake.action.each() do
+		os.rmdir(action.trigger)
+	end
+end
