diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt
index 448147b65..b2ab0bd04 100644
--- a/source/CMakeLists.txt
+++ b/source/CMakeLists.txt
@@ -31,7 +31,6 @@ else()
 	set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -funsigned-char -Wno-missing-braces -Wno-char-subscripts -Wno-unused-variable" )
 endif()
 
-option( DYN_OPENAL "Dynamically load OpenAL" ON )
 
 if( APPLE )
     option( OSX_COCOA_BACKEND "Use native Cocoa backend instead of SDL" ON )
@@ -46,7 +45,7 @@ endif()
 
 # Right now only 64 bit is supported.
 if( ${TARGET_ARCHITECTURE} MATCHES "x86_64" )
-			set( X64 64 )
+	set( X64 64 )
 endif()
 
 if( X64 OR ${TARGET_ARCHITECTURE} MATCHES "i386" )
@@ -86,7 +85,6 @@ if( WIN32 )
 		dbghelp
 		legacy_stdio_definitions )
 
-
 	if( NOT DEM_CMAKE_COMPILER_IS_GNUCXX_COMPATIBLE )
 		set( PROJECT_LIBRARIES ${PROJECT_LIBRARIES} DelayImp )
 	endif()
@@ -189,33 +187,9 @@ endif()
 
 # Decide on SSE setup
 
-# SSE only matters on 32-bit targets. We check compiler flags to know if we can do it.
-if( CMAKE_SIZEOF_VOID_P MATCHES "4" AND NOT CMAKE_OSX_ARCHITECTURES MATCHES ppc )
-		CHECK_CXX_COMPILER_FLAG( "-msse2 -mfpmath=sse" CAN_DO_MFPMATH )
-		CHECK_CXX_COMPILER_FLAG( -arch:SSE2 CAN_DO_ARCHSSE2 )
-		if( CAN_DO_MFPMATH )
-			set( SSE1_ENABLE "-msse -mfpmath=sse" )
-			set( SSE2_ENABLE "-msse2 -mfpmath=sse" )
-		elseif( CAN_DO_ARCHSSE2 )
-			set( SSE1_ENABLE -arch:SSE )
-			set( SSE2_ENABLE -arch:SSE2 )
-		endif()
-endif()
-
 if( X64 )
 	set( HAVE_MMX 1 )
 else( X64 )
-	set( SAFE_CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} )
-
-	if( DEM_CMAKE_COMPILER_IS_GNUCXX_COMPATIBLE )
-		set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mmmx")
-	endif( DEM_CMAKE_COMPILER_IS_GNUCXX_COMPATIBLE )
-
-	CHECK_CXX_SOURCE_COMPILES("#include <mmintrin.h>
-		int main(void) { __m64 v = _m_from_int(0); }"
-		HAVE_MMX)
-
-	set( CMAKE_CXX_FLAGS ${SAFE_CMAKE_CXX_FLAGS} )
 endif( X64 )
 
 CHECK_CXX_SOURCE_COMPILES("#include <ppl.h>
@@ -335,16 +309,6 @@ endif()
 
 # Check for functions that may or may not exist.
 
-CHECK_FUNCTION_EXISTS( filelength FILELENGTH_EXISTS )
-if( FILELENGTH_EXISTS )
-	add_definitions( -DHAVE_FILELENGTH=1 )
-endif()
-
-CHECK_FUNCTION_EXISTS( strupr STRUPR_EXISTS )
-if( NOT STRUPR_EXISTS )
-	add_definitions( -DNEED_STRUPR=1 )
-endif()
-
 require_stricmp()
 require_strnicmp()
 
@@ -1140,6 +1104,7 @@ set (PCH_SOURCES
 	common/models/models_voxel.cpp
 	common/models/models_ue1.cpp
 	common/models/models_obj.cpp
+	common/models/models_iqm.cpp
 	common/models/model.cpp
 	common/models/voxels.cpp
 	common/console/c_commandline.cpp
@@ -1235,6 +1200,7 @@ set (PCH_SOURCES
 	common/rendering/hwrenderer/data/hw_cvars.cpp
 	common/rendering/hwrenderer/data/hw_vrmodes.cpp
 	common/rendering/hwrenderer/data/hw_lightbuffer.cpp
+	common/rendering/hwrenderer/data/hw_bonebuffer.cpp
 	common/rendering/hwrenderer/data/hw_aabbtree.cpp
 	common/rendering/hwrenderer/data/hw_shadowmap.cpp
 	common/rendering/hwrenderer/data/hw_shaderpatcher.cpp
diff --git a/source/common/2d/v_draw.cpp b/source/common/2d/v_draw.cpp
index 941306dd4..f78446fc8 100644
--- a/source/common/2d/v_draw.cpp
+++ b/source/common/2d/v_draw.cpp
@@ -393,10 +393,14 @@ DEFINE_ACTION_FUNCTION(FCanvas, DrawShapeFill)
 
 void F2DDrawer::SetClipRect(int x, int y, int w, int h)
 {
-	clipleft = clamp(x, 0, GetWidth());
-	clipwidth = clamp(w, -1, GetWidth() - x);
-	cliptop = clamp(y, 0, GetHeight());
-	clipheight = clamp(h, -1, GetHeight() - y);
+	if (x < 0) { w += x; x = 0; }
+	if (y < 0) { h += y; y = 0; }
+	if (x >= GetWidth()) { x = GetWidth(); w = 0; }
+	if (y >= GetHeight()) { x = GetHeight(); h = 0; }
+	clipleft = x;
+	clipwidth = w;
+	cliptop = y;
+	clipheight = h;
 }
 
 DEFINE_ACTION_FUNCTION(_Screen, SetClipRect)
@@ -1564,10 +1568,10 @@ static void DrawLine(double x1, double y1, double x2, double y2, uint32_t realco
 DEFINE_ACTION_FUNCTION_NATIVE(_Screen, DrawLine, DrawLine)
 {
 	PARAM_PROLOGUE;
-	PARAM_INT(x0);
-	PARAM_INT(y0);
-	PARAM_INT(x1);
-	PARAM_INT(y1);
+	PARAM_FLOAT(x0);
+	PARAM_FLOAT(y0);
+	PARAM_FLOAT(x1);
+	PARAM_FLOAT(y1);
 	PARAM_INT(color);
 	PARAM_INT(alpha);
 	DrawLine(x0, y0, x1, y1, color, alpha);
@@ -1577,10 +1581,10 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Screen, DrawLine, DrawLine)
 DEFINE_ACTION_FUNCTION(FCanvas, DrawLine)
 {
 	PARAM_SELF_PROLOGUE(FCanvas);
-	PARAM_INT(x0);
-	PARAM_INT(y0);
-	PARAM_INT(x1);
-	PARAM_INT(y1);
+	PARAM_FLOAT(x0);
+	PARAM_FLOAT(y0);
+	PARAM_FLOAT(x1);
+	PARAM_FLOAT(y1);
 	PARAM_INT(color);
 	PARAM_INT(alpha);
 	self->Drawer.AddLine(DVector2(x0, y0), DVector2(x1, y1), nullptr, color | MAKEARGB(255, 0, 0, 0), alpha);
@@ -1597,10 +1601,10 @@ static void DrawThickLine(double x1, double y1, double x2, double y2, double thi
 DEFINE_ACTION_FUNCTION_NATIVE(_Screen, DrawThickLine, DrawThickLine)
 {
 	PARAM_PROLOGUE;
-	PARAM_INT(x0);
-	PARAM_INT(y0);
-	PARAM_INT(x1);
-	PARAM_INT(y1);
+	PARAM_FLOAT(x0);
+	PARAM_FLOAT(y0);
+	PARAM_FLOAT(x1);
+	PARAM_FLOAT(y1);
 	PARAM_FLOAT(thickness);
 	PARAM_INT(color);
 	PARAM_INT(alpha);
@@ -1611,10 +1615,10 @@ DEFINE_ACTION_FUNCTION_NATIVE(_Screen, DrawThickLine, DrawThickLine)
 DEFINE_ACTION_FUNCTION(FCanvas, DrawThickLine)
 {
 	PARAM_SELF_PROLOGUE(FCanvas);
-	PARAM_INT(x0);
-	PARAM_INT(y0);
-	PARAM_INT(x1);
-	PARAM_INT(y1);
+	PARAM_FLOAT(x0);
+	PARAM_FLOAT(y0);
+	PARAM_FLOAT(x1);
+	PARAM_FLOAT(y1);
 	PARAM_FLOAT(thickness);
 	PARAM_INT(color);
 	PARAM_INT(alpha);
diff --git a/source/common/fonts/v_font.cpp b/source/common/fonts/v_font.cpp
index 28af85202..dd3e47648 100644
--- a/source/common/fonts/v_font.cpp
+++ b/source/common/fonts/v_font.cpp
@@ -92,6 +92,7 @@ TArray<PalEntry> TranslationColors;
 
 FFont *V_GetFont(const char *name, const char *fontlumpname)
 {
+	if (name == nullptr) return nullptr;
 	if (!stricmp(name, "DBIGFONT")) name = "BigFont";
 	else if (!stricmp(name, "CONFONT")) name = "ConsoleFont";	// several mods have used the name CONFONT directly and effectively duplicated the font.
 	else if (!stricmp(name, "INDEXFON")) name = "IndexFont";	// Same here - for whatever reason some people had to use its 8 character name...
diff --git a/source/common/models/model.cpp b/source/common/models/model.cpp
index 21f5c9f41..c4b184f0c 100644
--- a/source/common/models/model.cpp
+++ b/source/common/models/model.cpp
@@ -36,6 +36,7 @@
 #include "model_md2.h"
 #include "model_md3.h"
 #include "model_kvx.h"
+#include "model_iqm.h"
 #include "i_time.h"
 #include "voxels.h"
 #include "texturemanager.h"
@@ -208,6 +209,10 @@ unsigned FindModel(const char * path, const char * modelfile)
 	{
 		model = new FMD3Model;
 	}
+	else if (!memcmp(buffer, "INTERQUAKEMODEL\0", 16))
+	{
+		model = new IQMModel;
+	}
 
 	if (model != nullptr)
 	{
diff --git a/source/common/models/model.h b/source/common/models/model.h
index edbcc4841..48e4ed34a 100644
--- a/source/common/models/model.h
+++ b/source/common/models/model.h
@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include "textureid.h"
 #include "i_modelvertexbuffer.h"
+#include "matrix.h"
 
 class FModelRenderer;
 class FGameTexture;
@@ -26,6 +27,7 @@ struct FSpriteModelFrame
 	TArray<FTextureID> skinIDs;
 	TArray<FTextureID> surfaceskinIDs;
 	TArray<int> modelframes;
+	TArray<int> animationIDs;
 	float xscale, yscale, zscale;
 	// [BB] Added zoffset, rotation parameters and flags.
 	// Added xoffset, yoffset
@@ -68,10 +70,12 @@ public:
 
 	virtual bool Load(const char * fn, int lumpnum, const char * buffer, int length) = 0;
 	virtual int FindFrame(const char * name, bool nodefault = false) = 0;
-	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) = 0;
+	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) = 0;
 	virtual void BuildVertexBuffer(FModelRenderer *renderer) = 0;
 	virtual void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) = 0;
 	virtual float getAspectFactor(float vscale) { return 1.f; }
+	virtual const TArray<VSMatrix>* AttachAnimationData() { return nullptr; };
+	virtual const TArray<VSMatrix> CalculateBones(int frame1, int frame2, double inter, const TArray<VSMatrix>& animationData) { return {}; };
 
 	void SetVertexBuffer(int type, IModelVertexBuffer *buffer) { mVBuf[type] = buffer; }
 	IModelVertexBuffer *GetVertexBuffer(int type) const { return mVBuf[type]; }
diff --git a/source/common/models/model_iqm.h b/source/common/models/model_iqm.h
new file mode 100644
index 000000000..d91157c26
--- /dev/null
+++ b/source/common/models/model_iqm.h
@@ -0,0 +1,220 @@
+#pragma once
+
+#include <stdint.h>
+#include "model.h"
+#include "vectors.h"
+#include "matrix.h"
+#include "common/rendering/i_modelvertexbuffer.h"
+
+struct IQMMesh
+{
+	FString Name;
+	FString Material;
+	uint32_t FirstVertex;
+	uint32_t NumVertices;
+	uint32_t FirstTriangle;
+	uint32_t NumTriangles;
+	FTextureID Skin;
+};
+
+enum IQMVertexArrayType
+{
+	IQM_POSITION = 0,     // float, 3
+	IQM_TEXCOORD = 1,     // float, 2
+	IQM_NORMAL = 2,       // float, 3
+	IQM_TANGENT = 3,      // float, 4
+	IQM_BLENDINDEXES = 4, // ubyte, 4
+	IQM_BLENDWEIGHTS = 5, // ubyte, 4
+	IQM_COLOR = 6,        // ubyte, 4
+	IQM_CUSTOM = 0x10
+};
+
+enum IQMVertexArrayFormat
+{
+	IQM_BYTE = 0,
+	IQM_UBYTE = 1,
+	IQM_SHORT = 2,
+	IQM_USHORT = 3,
+	IQM_INT = 4,
+	IQM_UINT = 5,
+	IQM_HALF = 6,
+	IQM_FLOAT = 7,
+	IQM_DOUBLE = 8,
+};
+
+struct IQMVertexArray
+{
+	IQMVertexArrayType Type;
+	uint32_t Flags;
+	IQMVertexArrayFormat Format;
+	uint32_t Size;
+	uint32_t Offset;
+};
+
+struct IQMTriangle
+{
+	uint32_t Vertex[3];
+};
+
+struct IQMAdjacency
+{
+	uint32_t Triangle[3];
+};
+
+struct IQMJoint
+{
+	FString Name;
+	int32_t Parent; // parent < 0 means this is a root bone
+	FVector3 Translate;
+	FVector4 Quaternion;
+	FVector3 Scale;
+};
+
+struct IQMPose
+{
+	int32_t Parent; // parent < 0 means this is a root bone
+	uint32_t ChannelMask; // mask of which 10 channels are present for this joint pose
+	float ChannelOffset[10];
+	float ChannelScale[10];
+	// channels 0..2 are translation <Tx, Ty, Tz> and channels 3..6 are quaternion rotation <Qx, Qy, Qz, Qw>
+	// rotation is in relative/parent local space
+	// channels 7..9 are scale <Sx, Sy, Sz>
+	// output = (input*scale)*rotation + translation
+};
+
+struct IQMAnim
+{
+	FString Name;
+	uint32_t FirstFrame;
+	uint32_t NumFrames;
+	float Framerate;
+	bool Loop;
+};
+
+struct IQMBounds
+{
+	float BBMins[3];
+	float BBMaxs[3];
+	float XYRadius;
+	float Radius;
+};
+
+class IQMFileReader;
+
+class IQMModel : public FModel
+{
+public:
+	IQMModel();
+	~IQMModel();
+
+	bool Load(const char* fn, int lumpnum, const char* buffer, int length) override;
+	int FindFrame(const char* name, bool nodefault) override;
+	void RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
+	void BuildVertexBuffer(FModelRenderer* renderer) override;
+	void AddSkins(uint8_t* hitlist, const FTextureID* surfaceskinids) override;
+	const TArray<VSMatrix>* AttachAnimationData() override;
+	const TArray<VSMatrix> CalculateBones(int frame1, int frame2, double inter, const TArray<VSMatrix>& animationData) override;
+
+private:
+	void LoadGeometry();
+	void UnloadGeometry();
+
+	void LoadPosition(IQMFileReader& reader, const IQMVertexArray& vertexArray);
+	void LoadTexcoord(IQMFileReader& reader, const IQMVertexArray& vertexArray);
+	void LoadNormal(IQMFileReader& reader, const IQMVertexArray& vertexArray);
+	void LoadBlendIndexes(IQMFileReader& reader, const IQMVertexArray& vertexArray);
+	void LoadBlendWeights(IQMFileReader& reader, const IQMVertexArray& vertexArray);
+
+	int mLumpNum = -1;
+	TArray<IQMMesh> Meshes;
+	TArray<IQMTriangle> Triangles;
+	TArray<IQMAdjacency> Adjacency;
+	TArray<IQMJoint> Joints;
+	TArray<IQMPose> Poses;
+	TArray<IQMAnim> Anims;
+	TArray<VSMatrix> FrameTransforms;
+	TArray<IQMBounds> Bounds;
+	TArray<IQMVertexArray> VertexArrays;
+	uint32_t NumVertices = 0;
+
+	TArray<FModelVertex> Vertices;
+
+	TArray<VSMatrix> baseframe;
+	TArray<VSMatrix> inversebaseframe;
+};
+
+struct IQMReadErrorException { };
+
+class IQMFileReader
+{
+public:
+	IQMFileReader(const void* buffer, int length) : buffer((const char*)buffer), length(length) { }
+
+	uint8_t ReadUByte()
+	{
+		uint8_t value;
+		Read(&value, sizeof(uint8_t));
+		return value;
+	}
+
+	int32_t ReadInt32()
+	{
+		int32_t value;
+		Read(&value, sizeof(int32_t));
+		value = LittleLong(value);
+		return value;
+	}
+
+	int16_t ReadInt16()
+	{
+		int16_t value;
+		Read(&value, sizeof(int16_t));
+		value = LittleShort(value);
+		return value;
+	}
+
+	uint32_t ReadUInt32()
+	{
+		return ReadInt32();
+	}
+
+	uint16_t ReadUInt16()
+	{
+		return ReadInt16();
+	}
+
+	float ReadFloat()
+	{
+		float value;
+		Read(&value, sizeof(float));
+		return value;
+	}
+
+	FString ReadName(const TArray<char>& textBuffer)
+	{
+		uint32_t nameOffset = ReadUInt32();
+		if (nameOffset >= textBuffer.Size())
+			throw IQMReadErrorException();
+		return textBuffer.Data() + nameOffset;
+	}
+
+	void Read(void* data, int size)
+	{
+		if (pos + size > length || size < 0 || size > 0x0fffffff)
+			throw IQMReadErrorException();
+		memcpy(data, buffer + pos, size);
+		pos += size;
+	}
+
+	void SeekTo(int newPos)
+	{
+		if (newPos < 0 || newPos > length)
+			throw IQMReadErrorException();
+		pos = newPos;
+	}
+
+private:
+	const char* buffer = nullptr;
+	int length = 0;
+	int pos = 0;
+};
diff --git a/source/common/models/model_kvx.h b/source/common/models/model_kvx.h
index 1491b03b4..c801e5c01 100644
--- a/source/common/models/model_kvx.h
+++ b/source/common/models/model_kvx.h
@@ -15,9 +15,9 @@ struct FVoxelVertexHash
 	// Returns the hash value for a key.
 	hash_t Hash(const FModelVertex &key) 
 	{ 
-		int ix = int(key.x);		
-		int iy = int(key.y);		
-		int iz = int(key.z);		
+		int ix = int(key.x);
+		int iy = int(key.y);
+		int iz = int(key.z);
 		return (hash_t)(ix + (iy<<9) + (iz<<18));
 	}
 
@@ -58,8 +58,8 @@ public:
 	~FVoxelModel();
 	bool Load(const char * fn, int lumpnum, const char * buffer, int length) override;
 	void Initialize();
-	virtual int FindFrame(const char * name, bool nodefault) override;
-	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) override;
+	virtual int FindFrame(const char* name, bool nodefault) override;
+	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
 	virtual void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) override;
 	FTextureID GetPaletteTexture() const { return mPalette; }
 	void BuildVertexBuffer(FModelRenderer *renderer) override;
diff --git a/source/common/models/model_md2.h b/source/common/models/model_md2.h
index b1feef645..9ead5e60b 100644
--- a/source/common/models/model_md2.h
+++ b/source/common/models/model_md2.h
@@ -112,8 +112,8 @@ public:
 	virtual ~FDMDModel();
 
 	virtual bool Load(const char * fn, int lumpnum, const char * buffer, int length) override;
-	virtual int FindFrame(const char * name, bool nodefault) override;
-	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) override;
+	virtual int FindFrame(const char* name, bool nodefault) override;
+	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
 	virtual void LoadGeometry();
 	virtual void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) override;
 
diff --git a/source/common/models/model_md3.h b/source/common/models/model_md3.h
index 3d81cb1c3..b53cd3ebb 100644
--- a/source/common/models/model_md3.h
+++ b/source/common/models/model_md3.h
@@ -66,8 +66,8 @@ public:
 	FMD3Model() = default;
 
 	virtual bool Load(const char * fn, int lumpnum, const char * buffer, int length) override;
-	virtual int FindFrame(const char * name, bool nodefault) override;
-	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) override;
+	virtual int FindFrame(const char* name, bool nodefault) override;
+	virtual void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
 	void LoadGeometry();
 	void BuildVertexBuffer(FModelRenderer *renderer);
 	virtual void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) override;
diff --git a/source/common/models/model_obj.h b/source/common/models/model_obj.h
index 55a47363c..e70679b0a 100644
--- a/source/common/models/model_obj.h
+++ b/source/common/models/model_obj.h
@@ -98,7 +98,7 @@ public:
 	~FOBJModel();
 	bool Load(const char* fn, int lumpnum, const char* buffer, int length) override;
 	int FindFrame(const char* name, bool nodefault) override;
-	void RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) override;
+	void RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
 	void BuildVertexBuffer(FModelRenderer* renderer) override;
 	void AddSkins(uint8_t* hitlist, const FTextureID* surfaceskinids) override;
 };
diff --git a/source/common/models/model_ue1.h b/source/common/models/model_ue1.h
index f2ed1952c..5123a22e9 100644
--- a/source/common/models/model_ue1.h
+++ b/source/common/models/model_ue1.h
@@ -25,8 +25,8 @@ public:
 	};
 
 	bool Load(const char * fn, int lumpnum, const char * buffer, int length) override;
-	int FindFrame(const char * name, bool nodefault) override;
-	void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids) override;
+	int FindFrame(const char* name, bool nodefault) override;
+	void RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition) override;
 	void BuildVertexBuffer(FModelRenderer *renderer) override;
 	void AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids) override;
 	void LoadGeometry();
diff --git a/source/common/models/modelrenderer.h b/source/common/models/modelrenderer.h
index 38786afe6..18c49f641 100644
--- a/source/common/models/modelrenderer.h
+++ b/source/common/models/modelrenderer.h
@@ -24,6 +24,6 @@ public:
 	virtual void SetMaterial(FGameTexture *skin, bool clampNoFilter, int translation) = 0;
 	virtual void DrawArrays(int start, int count) = 0;
 	virtual void DrawElements(int numIndices, size_t offset) = 0;
-	virtual void SetupFrame(FModel *model, unsigned int frame1, unsigned int frame2, unsigned int size) = 0;
+	virtual int SetupFrame(FModel* model, unsigned int frame1, unsigned int frame2, unsigned int size, const TArray<VSMatrix>& bones, int boneStartIndex) { return -1; };
 };
 
diff --git a/source/common/models/models_iqm.cpp b/source/common/models/models_iqm.cpp
new file mode 100644
index 000000000..314a6c600
--- /dev/null
+++ b/source/common/models/models_iqm.cpp
@@ -0,0 +1,610 @@
+
+#include "filesystem.h"
+#include "cmdlib.h"
+#include "model_iqm.h"
+#include "texturemanager.h"
+#include "modelrenderer.h"
+#include "engineerrors.h"
+
+IQMModel::IQMModel()
+{
+}
+
+IQMModel::~IQMModel()
+{
+}
+
+bool IQMModel::Load(const char* path, int lumpnum, const char* buffer, int length)
+{
+	mLumpNum = lumpnum;
+
+	try
+	{
+		IQMFileReader reader(buffer, length);
+
+		char magic[16];
+		reader.Read(magic, 16);
+		if (memcmp(magic, "INTERQUAKEMODEL\0", 16) != 0)
+			return false;
+
+		uint32_t version = reader.ReadUInt32();
+		if (version != 2)
+			return false;
+
+		uint32_t filesize = reader.ReadUInt32();
+		uint32_t flags = reader.ReadUInt32();
+		uint32_t num_text = reader.ReadUInt32();
+		uint32_t ofs_text = reader.ReadUInt32();
+		uint32_t num_meshes = reader.ReadUInt32();
+		uint32_t ofs_meshes = reader.ReadUInt32();
+		uint32_t num_vertexarrays = reader.ReadUInt32();
+		uint32_t num_vertices = reader.ReadUInt32();
+		uint32_t ofs_vertexarrays = reader.ReadUInt32();
+		uint32_t num_triangles = reader.ReadUInt32();
+		uint32_t ofs_triangles = reader.ReadUInt32();
+		uint32_t ofs_adjacency = reader.ReadUInt32();
+		uint32_t num_joints = reader.ReadUInt32();
+		uint32_t ofs_joints = reader.ReadUInt32();
+		uint32_t num_poses = reader.ReadUInt32();
+		uint32_t ofs_poses = reader.ReadUInt32();
+		uint32_t num_anims = reader.ReadUInt32();
+		uint32_t ofs_anims = reader.ReadUInt32();
+		uint32_t num_frames = reader.ReadUInt32();
+		uint32_t num_framechannels = reader.ReadUInt32();
+		uint32_t ofs_frames = reader.ReadUInt32();
+		uint32_t ofs_bounds = reader.ReadUInt32();
+		uint32_t num_comment = reader.ReadUInt32();
+		uint32_t ofs_comment = reader.ReadUInt32();
+		uint32_t num_extensions = reader.ReadUInt32();
+		uint32_t ofs_extensions = reader.ReadUInt32();
+
+		if (num_meshes <= 0)
+			I_FatalError("Invalid model: \"%s%s\", no mesh data is unsupported", path, fileSystem.GetLongName(mLumpNum).GetChars());
+
+		if (num_text == 0)
+			return false;
+
+		TArray<char> text(num_text, true);
+		reader.SeekTo(ofs_text);
+		reader.Read(text.Data(), text.Size());
+		text[text.Size() - 1] = 0;
+
+		Meshes.Resize(num_meshes);
+		Triangles.Resize(num_triangles);
+		Adjacency.Resize(num_triangles);
+		Joints.Resize(num_joints);
+		Poses.Resize(num_poses);
+		Anims.Resize(num_anims);
+		Bounds.Resize(num_frames);
+		VertexArrays.Resize(num_vertexarrays);
+		NumVertices = num_vertices;
+
+		reader.SeekTo(ofs_meshes);
+		for (IQMMesh& mesh : Meshes)
+		{
+			mesh.Name = reader.ReadName(text);
+			mesh.Material = reader.ReadName(text);
+			mesh.FirstVertex = reader.ReadUInt32();
+			mesh.NumVertices = reader.ReadUInt32();
+			mesh.FirstTriangle = reader.ReadUInt32();
+			mesh.NumTriangles = reader.ReadUInt32();
+			mesh.Skin = LoadSkin(path, mesh.Material.GetChars());
+		}
+
+		reader.SeekTo(ofs_triangles);
+		for (IQMTriangle& triangle : Triangles)
+		{
+			triangle.Vertex[0] = reader.ReadUInt32();
+			triangle.Vertex[1] = reader.ReadUInt32();
+			triangle.Vertex[2] = reader.ReadUInt32();
+		}
+
+		reader.SeekTo(ofs_adjacency);
+		for (IQMAdjacency& adj : Adjacency)
+		{
+			adj.Triangle[0] = reader.ReadUInt32();
+			adj.Triangle[1] = reader.ReadUInt32();
+			adj.Triangle[2] = reader.ReadUInt32();
+		}
+
+		reader.SeekTo(ofs_joints);
+		for (IQMJoint& joint : Joints)
+		{
+			joint.Name = reader.ReadName(text);
+			joint.Parent = reader.ReadInt32();
+			joint.Translate.X = reader.ReadFloat();
+			joint.Translate.Y = reader.ReadFloat();
+			joint.Translate.Z = reader.ReadFloat();
+			joint.Quaternion.X = reader.ReadFloat();
+			joint.Quaternion.Y = reader.ReadFloat();
+			joint.Quaternion.Z = reader.ReadFloat();
+			joint.Quaternion.W = reader.ReadFloat();
+			joint.Quaternion.MakeUnit();
+			joint.Scale.X = reader.ReadFloat();
+			joint.Scale.Y = reader.ReadFloat();
+			joint.Scale.Z = reader.ReadFloat();
+		}
+
+		reader.SeekTo(ofs_poses);
+		for (IQMPose& pose : Poses)
+		{
+			pose.Parent = reader.ReadInt32();
+			pose.ChannelMask = reader.ReadUInt32();
+			for (int i = 0; i < 10; i++) pose.ChannelOffset[i] = reader.ReadFloat();
+			for (int i = 0; i < 10; i++) pose.ChannelScale[i] = reader.ReadFloat();
+		}
+
+		reader.SeekTo(ofs_anims);
+		for (IQMAnim& anim : Anims)
+		{
+			anim.Name = reader.ReadName(text);
+			anim.FirstFrame = reader.ReadUInt32();
+			anim.NumFrames = reader.ReadUInt32();
+			anim.Framerate = reader.ReadFloat();
+			anim.Loop = !!(reader.ReadUInt32() & 1);
+		}
+
+		baseframe.Resize(num_joints);
+		inversebaseframe.Resize(num_joints);
+
+		for (uint32_t i = 0; i < num_joints; i++)
+		{
+			const IQMJoint& j = Joints[i];
+
+			VSMatrix m, invm;
+			m.loadIdentity();
+			m.translate(j.Translate.X, j.Translate.Y, j.Translate.Z);
+			m.multQuaternion(j.Quaternion);
+			m.scale(j.Scale.X, j.Scale.Y, j.Scale.Z);
+			m.inverseMatrix(invm);
+			if (j.Parent >= 0)
+			{
+				baseframe[i] = baseframe[j.Parent];
+				baseframe[i].multMatrix(m);
+				inversebaseframe[i] = invm;
+				inversebaseframe[i].multMatrix(inversebaseframe[j.Parent]);
+			}
+			else
+			{
+				baseframe[i] = m;
+				inversebaseframe[i] = invm;
+			}
+		}
+
+		// Swap YZ axis as we did that with the vertices down in LoadGeometry.
+		// This is an unfortunate side effect of the coordinate system in the gzdoom model rendering system
+		float swapYZ[16] = { 0.0f };
+		swapYZ[0 + 0 * 4] = 1.0f;
+		swapYZ[1 + 2 * 4] = 1.0f;
+		swapYZ[2 + 1 * 4] = 1.0f;
+		swapYZ[3 + 3 * 4] = 1.0f;
+
+		FrameTransforms.Resize(num_frames * num_poses);
+		reader.SeekTo(ofs_frames);
+		for (uint32_t i = 0; i < num_frames; i++)
+		{
+			for (uint32_t j = 0; j < num_poses; j++)
+			{
+				const IQMPose& p = Poses[j];
+
+				FVector3 translate;
+				translate.X = p.ChannelOffset[0]; if (p.ChannelMask & 0x01) translate.X += reader.ReadUInt16() * p.ChannelScale[0];
+				translate.Y = p.ChannelOffset[1]; if (p.ChannelMask & 0x02) translate.Y += reader.ReadUInt16() * p.ChannelScale[1];
+				translate.Z = p.ChannelOffset[2]; if (p.ChannelMask & 0x04) translate.Z += reader.ReadUInt16() * p.ChannelScale[2];
+
+				FVector4 quaternion;
+				quaternion.X = p.ChannelOffset[3]; if (p.ChannelMask & 0x08) quaternion.X += reader.ReadUInt16() * p.ChannelScale[3];
+				quaternion.Y = p.ChannelOffset[4]; if (p.ChannelMask & 0x10) quaternion.Y += reader.ReadUInt16() * p.ChannelScale[4];
+				quaternion.Z = p.ChannelOffset[5]; if (p.ChannelMask & 0x20) quaternion.Z += reader.ReadUInt16() * p.ChannelScale[5];
+				quaternion.W = p.ChannelOffset[6]; if (p.ChannelMask & 0x40) quaternion.W += reader.ReadUInt16() * p.ChannelScale[6];
+				quaternion.MakeUnit();
+
+				FVector3 scale;
+				scale.X = p.ChannelOffset[7]; if (p.ChannelMask & 0x80) scale.X += reader.ReadUInt16() * p.ChannelScale[7];
+				scale.Y = p.ChannelOffset[8]; if (p.ChannelMask & 0x100) scale.Y += reader.ReadUInt16() * p.ChannelScale[8];
+				scale.Z = p.ChannelOffset[9]; if (p.ChannelMask & 0x200) scale.Z += reader.ReadUInt16() * p.ChannelScale[9];
+
+				VSMatrix m;
+				m.loadIdentity();
+				m.translate(translate.X, translate.Y, translate.Z);
+				m.multQuaternion(quaternion);
+				m.scale(scale.X, scale.Y, scale.Z);
+
+				// Concatenate each pose with the inverse base pose to avoid doing this at animation time.
+				// If the joint has a parent, then it needs to be pre-concatenated with its parent's base pose.
+				// Thus it all negates at animation time like so: 
+				//   (parentPose * parentInverseBasePose) * (parentBasePose * childPose * childInverseBasePose) =>
+				//   parentPose * (parentInverseBasePose * parentBasePose) * childPose * childInverseBasePose =>
+				//   parentPose * childPose * childInverseBasePose
+				VSMatrix& result = FrameTransforms[i * num_poses + j];
+				if (p.Parent >= 0)
+				{
+					result = baseframe[p.Parent];
+					result.multMatrix(m);
+					result.multMatrix(inversebaseframe[j]);
+				}
+				else
+				{
+					result = m;
+					result.multMatrix(inversebaseframe[j]);
+				}
+			}
+
+			for (uint32_t j = 0; j < num_poses; j++)
+			{
+				VSMatrix m;
+				m.loadMatrix(swapYZ);
+				m.multMatrix(FrameTransforms[i * num_poses + j]);
+				m.multMatrix(swapYZ);
+				FrameTransforms[i * num_poses + j] = m;
+			}
+		}
+
+		//If a model doesn't have an animation loaded, it will crash. We don't want that!
+		if (num_frames <= 0)
+		{
+			num_frames = 1;
+			FrameTransforms.Resize(num_joints);
+
+			for (uint32_t j = 0; j < num_joints; j++)
+			{
+				FVector3 translate;
+				translate.X = Joints[j].Translate.X;
+				translate.Y = Joints[j].Translate.Y;
+				translate.Z = Joints[j].Translate.Z;
+				
+				FVector4 quaternion;
+				quaternion.X = Joints[j].Quaternion.X;
+				quaternion.Y = Joints[j].Quaternion.Y;
+				quaternion.Z = Joints[j].Quaternion.Z;
+				quaternion.W = Joints[j].Quaternion.W;
+				quaternion.MakeUnit();
+
+				FVector3 scale;
+				scale.X = Joints[j].Scale.X;
+				scale.Y = Joints[j].Scale.Y;
+				scale.Z = Joints[j].Scale.Z;
+
+				VSMatrix m;
+				m.loadIdentity();
+				m.translate(translate.X, translate.Y, translate.Z);
+				m.multQuaternion(quaternion);
+				m.scale(scale.X, scale.Y, scale.Z);
+
+				VSMatrix& result = FrameTransforms[j];
+				if (Joints[j].Parent >= 0)
+				{
+					result = baseframe[Joints[j].Parent];
+					result.multMatrix(m);
+					result.multMatrix(inversebaseframe[j]);
+				}
+				else
+				{
+					result = m;
+					result.multMatrix(inversebaseframe[j]);
+				}
+			}
+
+			for (uint32_t j = 0; j < num_joints; j++)
+			{
+				VSMatrix m;
+				m.loadMatrix(swapYZ);
+				m.multMatrix(FrameTransforms[j]);
+				m.multMatrix(swapYZ);
+				FrameTransforms[j] = m;
+			}
+		}
+
+		reader.SeekTo(ofs_bounds);
+		for (IQMBounds& bound : Bounds)
+		{
+			bound.BBMins[0] = reader.ReadFloat();
+			bound.BBMins[1] = reader.ReadFloat();
+			bound.BBMins[2] = reader.ReadFloat();
+			bound.BBMaxs[0] = reader.ReadFloat();
+			bound.BBMaxs[1] = reader.ReadFloat();
+			bound.BBMaxs[2] = reader.ReadFloat();
+			bound.XYRadius = reader.ReadFloat();
+			bound.Radius = reader.ReadFloat();
+		}
+
+		reader.SeekTo(ofs_vertexarrays);
+		for (IQMVertexArray& vertexArray : VertexArrays)
+		{
+			vertexArray.Type = (IQMVertexArrayType)reader.ReadUInt32();
+			vertexArray.Flags = reader.ReadUInt32();
+			vertexArray.Format = (IQMVertexArrayFormat)reader.ReadUInt32();
+			vertexArray.Size = reader.ReadUInt32();
+			vertexArray.Offset = reader.ReadUInt32();
+		}
+
+		return true;
+	}
+	catch (IQMReadErrorException)
+	{
+		return false;
+	}
+}
+
+void IQMModel::LoadGeometry()
+{
+	try
+	{
+		FileData lumpdata = fileSystem.ReadFile(mLumpNum);
+		IQMFileReader reader(lumpdata.GetMem(), (int)lumpdata.GetSize());
+
+		Vertices.Resize(NumVertices);
+		for (IQMVertexArray& vertexArray : VertexArrays)
+		{
+			reader.SeekTo(vertexArray.Offset);
+			if (vertexArray.Type == IQM_POSITION)
+			{
+				LoadPosition(reader, vertexArray);
+			}
+			else if (vertexArray.Type == IQM_TEXCOORD)
+			{
+				LoadTexcoord(reader, vertexArray);
+			}
+			else if (vertexArray.Type == IQM_NORMAL)
+			{
+				LoadNormal(reader, vertexArray);
+			}
+			else if (vertexArray.Type == IQM_BLENDINDEXES)
+			{
+				LoadBlendIndexes(reader, vertexArray);
+			}
+			else if (vertexArray.Type == IQM_BLENDWEIGHTS)
+			{
+				LoadBlendWeights(reader, vertexArray);
+			}
+		}
+	}
+	catch (IQMReadErrorException)
+	{
+	}
+}
+
+void IQMModel::LoadPosition(IQMFileReader& reader, const IQMVertexArray& vertexArray)
+{
+	float lu = 0.0f, lv = 0.0f, lindex = -1.0f;
+	if (vertexArray.Format == IQM_FLOAT && vertexArray.Size == 3)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			v.x = reader.ReadFloat();
+			v.z = reader.ReadFloat();
+			v.y = reader.ReadFloat();
+
+			v.lu = lu;
+			v.lv = lv;
+			v.lindex = lindex;
+		}
+	}
+	else
+	{
+		I_FatalError("Unsupported IQM_POSITION vertex format");
+	}
+}
+
+void IQMModel::LoadTexcoord(IQMFileReader& reader, const IQMVertexArray& vertexArray)
+{
+	if (vertexArray.Format == IQM_FLOAT && vertexArray.Size == 2)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			v.u = reader.ReadFloat();
+			v.v = reader.ReadFloat();
+		}
+	}
+	else
+	{
+		I_FatalError("Unsupported IQM_TEXCOORD vertex format");
+	}
+}
+
+void IQMModel::LoadNormal(IQMFileReader& reader, const IQMVertexArray& vertexArray)
+{
+	if (vertexArray.Format == IQM_FLOAT && vertexArray.Size == 3)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			float x = reader.ReadFloat();
+			float y = reader.ReadFloat();
+			float z = reader.ReadFloat();
+
+			v.SetNormal(x, z, y);
+		}
+	}
+	else
+	{
+		I_FatalError("Unsupported IQM_NORMAL vertex format");
+	}
+}
+
+void IQMModel::LoadBlendIndexes(IQMFileReader& reader, const IQMVertexArray& vertexArray)
+{
+	if (vertexArray.Format == IQM_UBYTE && vertexArray.Size == 4)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			int x = reader.ReadUByte();
+			int y = reader.ReadUByte();
+			int z = reader.ReadUByte();
+			int w = reader.ReadUByte();
+			v.SetBoneSelector(x, y, z, w);
+		}
+	}
+	else if (vertexArray.Format == IQM_INT && vertexArray.Size == 4)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			int x = reader.ReadInt32();
+			int y = reader.ReadInt32();
+			int z = reader.ReadInt32();
+			int w = reader.ReadInt32();
+			v.SetBoneSelector(x, y, z, w);
+		}
+	}
+	else
+	{
+		I_FatalError("Unsupported IQM_BLENDINDEXES vertex format");
+	}
+}
+
+void IQMModel::LoadBlendWeights(IQMFileReader& reader, const IQMVertexArray& vertexArray)
+{
+	if (vertexArray.Format == IQM_UBYTE && vertexArray.Size == 4)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			int x = reader.ReadUByte();
+			int y = reader.ReadUByte();
+			int z = reader.ReadUByte();
+			int w = reader.ReadUByte();
+			v.SetBoneWeight(x, y, z, w);
+		}
+	}
+	else if (vertexArray.Format == IQM_FLOAT && vertexArray.Size == 4)
+	{
+		for (FModelVertex& v : Vertices)
+		{
+			uint8_t x = (int)clamp(reader.ReadFloat() * 255.0f, 0.0f, 255.0f);
+			uint8_t y = (int)clamp(reader.ReadFloat() * 255.0f, 0.0f, 255.0f);
+			uint8_t z = (int)clamp(reader.ReadFloat() * 255.0f, 0.0f, 255.0f);
+			uint8_t w = (int)clamp(reader.ReadFloat() * 255.0f, 0.0f, 255.0f);
+			v.SetBoneWeight(x, y, z, w);
+		}
+	}
+	else
+	{
+		I_FatalError("Unsupported IQM_BLENDWEIGHTS vertex format");
+	}
+}
+
+void IQMModel::UnloadGeometry()
+{
+	Vertices.Reset();
+}
+
+int IQMModel::FindFrame(const char* name, bool nodefault)
+{
+	// This doesn't really mean all that much for IQM
+	for (unsigned i = 0; i < Anims.Size(); i++)
+	{
+		if (!stricmp(name, Anims[i].Name.GetChars())) return i;
+	}
+	return FErr_NotFound;
+}
+
+void IQMModel::RenderFrame(FModelRenderer* renderer, FGameTexture* skin, int frame1, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition)
+{
+	renderer->SetupFrame(this, 0, 0, NumVertices, boneData, boneStartPosition);
+
+	FGameTexture* lastSkin = nullptr;
+	for (unsigned i = 0; i < Meshes.Size(); i++)
+	{
+		FGameTexture* meshSkin = skin;
+
+		if (!meshSkin)
+		{
+			if (surfaceskinids && surfaceskinids[i].isValid())
+			{
+				meshSkin = TexMan.GetGameTexture(surfaceskinids[i], true);
+			}
+			else if (!Meshes[i].Skin.isValid())
+			{
+				continue;
+			}
+			else 
+			{
+				meshSkin = TexMan.GetGameTexture(Meshes[i].Skin, true);
+			}
+			if (!meshSkin) continue;
+		}
+
+		if (meshSkin != lastSkin)
+		{
+			renderer->SetMaterial(meshSkin, false, translation);
+			lastSkin = meshSkin;
+		}
+
+		renderer->DrawElements(Meshes[i].NumTriangles * 3, Meshes[i].FirstTriangle * 3 * sizeof(unsigned int));
+	}
+}
+
+void IQMModel::BuildVertexBuffer(FModelRenderer* renderer)
+{
+	if (!GetVertexBuffer(renderer->GetType()))
+	{
+		LoadGeometry();
+
+		auto vbuf = renderer->CreateVertexBuffer(true, true);
+		SetVertexBuffer(renderer->GetType(), vbuf);
+
+		FModelVertex* vertptr = vbuf->LockVertexBuffer(Vertices.Size());
+		memcpy(vertptr, Vertices.Data(), Vertices.Size() * sizeof(FModelVertex));
+		vbuf->UnlockVertexBuffer();
+
+		unsigned int* indxptr = vbuf->LockIndexBuffer(Triangles.Size() * 3);
+		memcpy(indxptr, Triangles.Data(), Triangles.Size() * sizeof(unsigned int) * 3);
+		vbuf->UnlockIndexBuffer();
+
+		UnloadGeometry();
+	}
+}
+
+void IQMModel::AddSkins(uint8_t* hitlist, const FTextureID* surfaceskinids)
+{
+	for (unsigned i = 0; i < Meshes.Size(); i++)
+	{
+		if (surfaceskinids && surfaceskinids[i].isValid())
+			hitlist[surfaceskinids[i].GetIndex()] |= FTextureManager::HIT_Flat;
+	}
+}
+
+const TArray<VSMatrix>* IQMModel::AttachAnimationData()
+{
+	return &FrameTransforms;
+}
+
+const TArray<VSMatrix> IQMModel::CalculateBones(int frame1, int frame2, double inter, const TArray<VSMatrix>& animationData)
+{
+	const TArray<VSMatrix>& animationFrames = &animationData ? animationData : FrameTransforms;
+
+	int numbones = Joints.Size();
+
+	frame1 = clamp(frame1, 0, ((int)animationFrames.Size() - 1) / numbones);
+	frame2 = clamp(frame2, 0, ((int)animationFrames.Size() - 1) / numbones);
+
+	int offset1 = frame1 * numbones;
+	int offset2 = frame2 * numbones;
+	float t = (float)inter;
+	float invt = 1.0f - t;
+
+	TArray<VSMatrix> bones(numbones, true);
+	for (int i = 0; i < numbones; i++)
+	{
+		const float* from = animationFrames[offset1 + i].get();
+		const float* to = animationFrames[offset2 + i].get();
+
+		// Interpolate bone between the two frames
+		float bone[16];
+		for (int j = 0; j < 16; j++)
+		{
+			bone[j] = from[j] * invt + to[j] * t;
+		}
+
+		// Apply parent bone
+		if (Joints[i].Parent >= 0)
+		{
+			bones[i] = bones[Joints[i].Parent];
+			bones[i].multMatrix(bone);
+		}
+		else
+		{
+			bones[i].loadMatrix(bone);
+		}
+	}
+
+	return bones;
+}
\ No newline at end of file
diff --git a/source/common/models/models_md2.cpp b/source/common/models/models_md2.cpp
index 81d1c3666..4f2a12d4e 100644
--- a/source/common/models/models_md2.cpp
+++ b/source/common/models/models_md2.cpp
@@ -348,7 +348,7 @@ void FDMDModel::AddSkins(uint8_t *hitlist, const FTextureID*)
 // FDMDModel::FindFrame
 //
 //===========================================================================
-int FDMDModel::FindFrame(const char * name, bool nodefault)
+int FDMDModel::FindFrame(const char* name, bool nodefault)
 {
 	for (int i=0;i<info.numFrames;i++)
 	{
@@ -363,7 +363,7 @@ int FDMDModel::FindFrame(const char * name, bool nodefault)
 //
 //===========================================================================
 
-void FDMDModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID*)
+void FDMDModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID*, const TArray<VSMatrix>& boneData, int boneStartPosition)
 {
 	if (frameno >= info.numFrames || frameno2 >= info.numFrames) return;
 
@@ -376,13 +376,11 @@ void FDMDModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int f
 
 	renderer->SetInterpolation(inter);
 	renderer->SetMaterial(skin, false, translation);
-	renderer->SetupFrame(this, frames[frameno].vindex, frames[frameno2].vindex, lodInfo[0].numTriangles * 3);
+	renderer->SetupFrame(this, frames[frameno].vindex, frames[frameno2].vindex, lodInfo[0].numTriangles * 3, {}, -1);
 	renderer->DrawArrays(0, lodInfo[0].numTriangles * 3);
 	renderer->SetInterpolation(0.f);
 }
 
-
-
 //===========================================================================
 //
 // Internal data structures of MD2 files - only used during loading
@@ -552,4 +550,3 @@ void FMD2Model::LoadGeometry()
 FMD2Model::~FMD2Model()
 {
 }
-
diff --git a/source/common/models/models_md3.cpp b/source/common/models/models_md3.cpp
index 95ac49167..512794293 100644
--- a/source/common/models/models_md3.cpp
+++ b/source/common/models/models_md3.cpp
@@ -328,7 +328,7 @@ void FMD3Model::AddSkins(uint8_t *hitlist, const FTextureID* surfaceskinids)
 //
 //===========================================================================
 
-int FMD3Model::FindFrame(const char * name, bool nodefault)
+int FMD3Model::FindFrame(const char* name, bool nodefault)
 {
 	for (unsigned i = 0; i < Frames.Size(); i++)
 	{
@@ -343,7 +343,7 @@ int FMD3Model::FindFrame(const char * name, bool nodefault)
 //
 //===========================================================================
 
-void FMD3Model::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID* surfaceskinids)
+void FMD3Model::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition)
 {
 	if ((unsigned)frameno >= Frames.Size() || (unsigned)frameno2 >= Frames.Size()) return;
 
@@ -373,7 +373,7 @@ void FMD3Model::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int f
 		}
 
 		renderer->SetMaterial(surfaceSkin, false, translation);
-		renderer->SetupFrame(this, surf->vindex + frameno * surf->numVertices, surf->vindex + frameno2 * surf->numVertices, surf->numVertices);
+		renderer->SetupFrame(this, surf->vindex + frameno * surf->numVertices, surf->vindex + frameno2 * surf->numVertices, surf->numVertices, {}, -1);
 		renderer->DrawElements(surf->numTriangles * 3, surf->iindex * sizeof(unsigned int));
 	}
 	renderer->SetInterpolation(0.f);
diff --git a/source/common/models/models_obj.cpp b/source/common/models/models_obj.cpp
index d804ffff6..65ec72915 100644
--- a/source/common/models/models_obj.cpp
+++ b/source/common/models/models_obj.cpp
@@ -615,7 +615,7 @@ FVector3 FOBJModel::CalculateNormalSmooth(unsigned int vidx, unsigned int smooth
  */
 int FOBJModel::FindFrame(const char* name, bool nodefault)
 {
-	return nodefault? FErr_Singleframe : 0; // OBJs are not animated.
+	return nodefault ? FErr_Singleframe : 0; // OBJs are not animated.
 }
 
 /**
@@ -628,7 +628,7 @@ int FOBJModel::FindFrame(const char* name, bool nodefault)
  * @param inter The amount to interpolate the two frames.
  * @param translation The translation for the skin
  */
-void FOBJModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID* surfaceskinids)
+void FOBJModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frameno, int frameno2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition)
 {
 	// Prevent the model from rendering if the frame number is < 0
 	if (frameno < 0 || frameno2 < 0) return;
@@ -657,7 +657,7 @@ void FOBJModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int f
 		}
 
 		renderer->SetMaterial(userSkin, false, translation);
-		renderer->SetupFrame(this, surf->vbStart, surf->vbStart, surf->numTris * 3);
+		renderer->SetupFrame(this, surf->vbStart, surf->vbStart, surf->numTris * 3, {}, -1);
 		renderer->DrawArrays(0, surf->numTris * 3);
 	}
 }
diff --git a/source/common/models/models_ue1.cpp b/source/common/models/models_ue1.cpp
index 3e47d87e0..416f9a391 100644
--- a/source/common/models/models_ue1.cpp
+++ b/source/common/models/models_ue1.cpp
@@ -221,7 +221,7 @@ void FUE1Model::UnloadGeometry()
 	groups.Reset();
 }
 
-int FUE1Model::FindFrame( const char *name, bool nodefault )
+int FUE1Model::FindFrame(const char* name, bool nodefault)
 {
 	// there are no named frames, but we need something here to properly interface with it. So just treat the string as an index number.
 	auto index = strtol(name, nullptr, 0);
@@ -229,7 +229,7 @@ int FUE1Model::FindFrame( const char *name, bool nodefault )
 	return index;
 }
 
-void FUE1Model::RenderFrame( FModelRenderer *renderer, FGameTexture *skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids)
+void FUE1Model::RenderFrame( FModelRenderer *renderer, FGameTexture *skin, int frame, int frame2, double inter, int translation, const FTextureID* surfaceskinids, const TArray<VSMatrix>& boneData, int boneStartPosition)
 {
 	// the moment of magic
 	if ( (frame < 0) || (frame2 < 0) || (frame >= numFrames) || (frame2 >= numFrames) ) return;
@@ -260,7 +260,7 @@ void FUE1Model::RenderFrame( FModelRenderer *renderer, FGameTexture *skin, int f
 		// TODO: Handle per-group render styles and other flags once functions for it are implemented
 		// Future note: poly renderstyles should always be enforced unless the actor itself has a style other than Normal
 		renderer->SetMaterial(sskin,false,translation);
-		renderer->SetupFrame(this, vofs+frame*fsize,vofs+frame2*fsize,vsize);
+		renderer->SetupFrame(this, vofs + frame * fsize, vofs + frame2 * fsize, vsize, {}, -1);
 		renderer->DrawArrays(0,vsize);
 		vofs += vsize;
 	}
diff --git a/source/common/models/models_voxel.cpp b/source/common/models/models_voxel.cpp
index 41f5e854a..6af34cf23 100644
--- a/source/common/models/models_voxel.cpp
+++ b/source/common/models/models_voxel.cpp
@@ -378,9 +378,9 @@ bool FVoxelModel::Load(const char * fn, int lumpnum, const char * buffer, int le
 //
 //===========================================================================
 
-int FVoxelModel::FindFrame(const char * name, bool nodefault)
+int FVoxelModel::FindFrame(const char* name, bool nodefault)
 {
-	return nodefault? FErr_Voxel : 0; // -2, not -1 because voxels are special.
+	return nodefault ? FErr_Voxel : 0; // -2, not -1 because voxels are special.
 }
 
 //===========================================================================
@@ -400,10 +400,9 @@ float FVoxelModel::getAspectFactor(float stretch)
 //
 //===========================================================================
 
-void FVoxelModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID*)
+void FVoxelModel::RenderFrame(FModelRenderer *renderer, FGameTexture * skin, int frame, int frame2, double inter, int translation, const FTextureID*, const TArray<VSMatrix>& boneData, int boneStartPosition)
 {
 	renderer->SetMaterial(skin, true, translation);
-	renderer->SetupFrame(this, 0, 0, 0);
+	renderer->SetupFrame(this, 0, 0, 0, {}, -1);
 	renderer->DrawElements(mNumIndices, 0);
 }
-
diff --git a/source/common/models/voxels.cpp b/source/common/models/voxels.cpp
index b85f2f851..d8d2b2245 100644
--- a/source/common/models/voxels.cpp
+++ b/source/common/models/voxels.cpp
@@ -314,6 +314,7 @@ FVoxelDef *R_LoadVoxelDef(int lumpnum, int spin)
 	else
 	{
 		FVoxelDef *voxdef = new FVoxelDef;
+		*voxdef = {};
 		voxdef->Voxel = vox;
 		voxdef->Scale = 1.;
 		voxdef->DroppedSpin = voxdef->PlacedSpin = spin;
diff --git a/source/common/rendering/gl/gl_buffers.cpp b/source/common/rendering/gl/gl_buffers.cpp
index 5439319a9..a576b6431 100644
--- a/source/common/rendering/gl/gl_buffers.cpp
+++ b/source/common/rendering/gl/gl_buffers.cpp
@@ -211,8 +211,10 @@ void GLBuffer::GPUWaitSync()
 
 void GLVertexBuffer::SetFormat(int numBindingPoints, int numAttributes, size_t stride, const FVertexBufferAttribute *attrs)
 {
-	static int VFmtToGLFmt[] = { GL_FLOAT, GL_FLOAT, GL_FLOAT, GL_FLOAT, GL_UNSIGNED_BYTE, GL_INT_2_10_10_10_REV };
-	static uint8_t VFmtToSize[] = {4, 3, 2, 1, 4, 4};
+	static int VFmtToGLFmt[] = { GL_FLOAT, GL_FLOAT, GL_FLOAT, GL_FLOAT, GL_UNSIGNED_BYTE, GL_INT_2_10_10_10_REV, GL_UNSIGNED_BYTE };
+	static uint8_t VFmtToSize[] = { 4, 3, 2, 1, 4, 4, 4 };
+	static bool VFmtToNormalize[] = { false, false, false, false, true, true, false };
+	static bool VFmtToIntegerType[] = { false, false, false, false, false, false, true };
 
 	mStride = stride;
 	mNumBindingPoints = numBindingPoints;
@@ -226,6 +228,8 @@ void GLVertexBuffer::SetFormat(int numBindingPoints, int numAttributes, size_t s
 			attrinf.size = VFmtToSize[attrs[i].format];
 			attrinf.offset = attrs[i].offset;
 			attrinf.bindingpoint = attrs[i].binding;
+			attrinf.normalize = VFmtToNormalize[attrs[i].format];
+			attrinf.integerType = VFmtToIntegerType[attrs[i].format];
 		}
 	}
 }
@@ -246,7 +250,10 @@ void GLVertexBuffer::Bind(int *offsets)
 		{
 			glEnableVertexAttribArray(i);
 			size_t ofs = offsets == nullptr ? attrinf.offset : attrinf.offset + mStride * offsets[attrinf.bindingpoint];
-			glVertexAttribPointer(i, attrinf.size, attrinf.format, attrinf.format != GL_FLOAT, (GLsizei)mStride, (void*)(intptr_t)ofs);
+			if (!attrinf.integerType)
+				glVertexAttribPointer(i, attrinf.size, attrinf.format, attrinf.normalize, (GLsizei)mStride, (void*)(intptr_t)ofs);
+			else
+				glVertexAttribIPointer(i, attrinf.size, attrinf.format, (GLsizei)mStride, (void*)(intptr_t)ofs);
 		}
 		i++;
 	}
diff --git a/source/common/rendering/gl/gl_buffers.h b/source/common/rendering/gl/gl_buffers.h
index d0c2b62c8..9bca819a1 100644
--- a/source/common/rendering/gl/gl_buffers.h
+++ b/source/common/rendering/gl/gl_buffers.h
@@ -46,6 +46,8 @@ class GLVertexBuffer : public IVertexBuffer, public GLBuffer
 	{
 		int bindingpoint;
 		int format;
+		bool normalize;
+		bool integerType;
 		int size;
 		int offset;
 	};
diff --git a/source/common/rendering/gl/gl_framebuffer.cpp b/source/common/rendering/gl/gl_framebuffer.cpp
index 3913a21e1..6ea31e5bd 100644
--- a/source/common/rendering/gl/gl_framebuffer.cpp
+++ b/source/common/rendering/gl/gl_framebuffer.cpp
@@ -49,6 +49,7 @@
 #include "hw_skydome.h"
 #include "hw_viewpointbuffer.h"
 #include "hw_lightbuffer.h"
+#include "hw_bonebuffer.h"
 #include "gl_shaderprogram.h"
 #include "gl_debug.h"
 #include "r_videoscale.h"
@@ -104,6 +105,7 @@ OpenGLFrameBuffer::~OpenGLFrameBuffer()
 	if (mSkyData != nullptr) delete mSkyData;
 	if (mViewpoints != nullptr) delete mViewpoints;
 	if (mLights != nullptr) delete mLights;
+	if (mBones != nullptr) delete mBones;
 	mShadowMap.Reset();
 
 	if (GLRenderer)
@@ -171,9 +173,11 @@ void OpenGLFrameBuffer::InitializeState()
 	mSkyData = new FSkyVertexBuffer;
 	mViewpoints = new HWViewpointBuffer(screen->mPipelineNbr);
 	mLights = new FLightBuffer(screen->mPipelineNbr);
+	mBones = new BoneBuffer(screen->mPipelineNbr);
 	GLRenderer = new FGLRenderer(this);
 	GLRenderer->Initialize(GetWidth(), GetHeight());
 	static_cast<GLDataBuffer*>(mLights->GetBuffer())->BindBase();
+	static_cast<GLDataBuffer*>(mBones->GetBuffer())->BindBase();
 
 	mDebug = std::make_unique<FGLDebug>();
 	mDebug->Update();
diff --git a/source/common/rendering/gl/gl_renderstate.cpp b/source/common/rendering/gl/gl_renderstate.cpp
index 688e46904..5d7fd774b 100644
--- a/source/common/rendering/gl/gl_renderstate.cpp
+++ b/source/common/rendering/gl/gl_renderstate.cpp
@@ -33,6 +33,7 @@
 #include "gl_shader.h"
 #include "gl_renderer.h"
 #include "hw_lightbuffer.h"
+#include "hw_bonebuffer.h"
 #include "gl_renderbuffers.h"
 #include "gl_hwtexture.h"
 #include "gl_buffers.h"
@@ -133,6 +134,7 @@ bool FGLRenderState::ApplyShader()
 	activeShader->muTimer.Set((double)(screen->FrameTime - firstFrame) * (double)mShaderTimer / 1000.);
 	activeShader->muAlphaThreshold.Set(mAlphaThreshold);
 	activeShader->muLightIndex.Set(-1);
+	activeShader->muBoneIndexBase.Set(-1);
 	activeShader->muClipSplit.Set(mClipSplit);
 	activeShader->muSpecularMaterial.Set(mGlossiness, mSpecularLevel);
 	activeShader->muAddColor.Set(mStreamData.uAddColor);
@@ -210,6 +212,21 @@ bool FGLRenderState::ApplyShader()
 	}
 
 	activeShader->muLightIndex.Set(index);
+
+	index = mBoneIndexBase;
+	if (!screen->mBones->GetBufferType() && index >= 0) // Uniform buffer fallback support
+	{
+		size_t start, size;
+		index = screen->mBones->GetBinding(index, &start, &size);
+
+		if (start != mLastMappedBoneIndexBase || screen->mPipelineNbr > 1) // If multiple buffers always bind
+		{
+			mLastMappedBoneIndexBase = start;
+			static_cast<GLDataBuffer*>(screen->mBones->GetBuffer())->BindRange(nullptr, start, size);
+		}
+	}
+	activeShader->muBoneIndexBase.Set(index);
+
 	return true;
 }
 
diff --git a/source/common/rendering/gl/gl_renderstate.h b/source/common/rendering/gl/gl_renderstate.h
index 9cfe3d7a2..8b9bba25c 100644
--- a/source/common/rendering/gl/gl_renderstate.h
+++ b/source/common/rendering/gl/gl_renderstate.h
@@ -66,6 +66,7 @@ class FGLRenderState final : public FRenderState
 	int lastTranslation = 0;
 	int maxBoundMaterial = -1;
 	size_t mLastMappedLightIndex = SIZE_MAX;
+	size_t mLastMappedBoneIndexBase = SIZE_MAX;
 
 	IVertexBuffer *mCurrentVertexBuffer;
 	int mCurrentVertexOffsets[2];	// one per binding point
diff --git a/source/common/rendering/gl/gl_shader.cpp b/source/common/rendering/gl/gl_shader.cpp
index 80e6a0e8a..c4e20f7ba 100644
--- a/source/common/rendering/gl/gl_shader.cpp
+++ b/source/common/rendering/gl/gl_shader.cpp
@@ -38,6 +38,7 @@
 #include "shaderuniforms.h"
 #include "hw_viewpointuniforms.h"
 #include "hw_lightbuffer.h"
+#include "hw_bonebuffer.h"
 #include "i_specialpaths.h"
 #include "printf.h"
 #include "version.h"
@@ -276,6 +277,9 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 		// dynamic lights
 		uniform int uLightIndex;
 
+		// bone animation
+		uniform int uBoneIndexBase;
+
 		// Blinn glossiness and specular level
 		uniform vec2 uSpecularMaterial;
 
@@ -297,6 +301,19 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 		};
 		#endif
 
+		// bone matrix buffers
+		#ifdef SHADER_STORAGE_BONES
+		layout(std430, binding = 7) buffer BoneBufferSSO
+		{
+			mat4 bones[];
+		};
+		#elif defined NUM_UBO_BONES
+		uniform BoneBufferUBO
+		{
+			mat4 bones[NUM_UBO_BONES];
+		};
+		#endif
+
 		// textures
 		uniform sampler2D tex;
 		uniform sampler2D ShadowMap;
@@ -369,20 +386,21 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 	FString vp_comb;
 
 	assert(screen->mLights != NULL);
+	assert(screen->mBones != NULL);
 
 	bool lightbuffertype = screen->mLights->GetBufferType();
 	unsigned int lightbuffersize = screen->mLights->GetBlockSize();
 	if (!lightbuffertype)
 	{
-		vp_comb.Format("#version 330 core\n#define NUM_UBO_LIGHTS %d\n", lightbuffersize);
+		vp_comb.Format("#version 330 core\n#define NUM_UBO_LIGHTS %d\n#define NUM_UBO_BONES %d\n", lightbuffersize, screen->mBones->GetBlockSize());
 	}
 	else
 	{
 		// This differentiation is for Intel which do not seem to expose the full extension, even if marked as required.
 		if (gl.glslversion < 4.3f)
-			vp_comb = "#version 400 core\n#extension GL_ARB_shader_storage_buffer_object : require\n#define SHADER_STORAGE_LIGHTS\n";
+			vp_comb = "#version 400 core\n#extension GL_ARB_shader_storage_buffer_object : require\n#define SHADER_STORAGE_LIGHTS\n#define SHADER_STORAGE_BONES\n";
 		else
-			vp_comb = "#version 430 core\n#define SHADER_STORAGE_LIGHTS\n";
+			vp_comb = "#version 430 core\n#define SHADER_STORAGE_LIGHTS\n#define SHADER_STORAGE_BONES\n";
 	}
 
 	if ((gl.flags & RFL_SHADER_STORAGE_BUFFER) && screen->allowSSBO())
@@ -576,6 +594,7 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 	muLightParms.Init(hShader, "uLightAttr");
 	muClipSplit.Init(hShader, "uClipSplit");
 	muLightIndex.Init(hShader, "uLightIndex");
+	muBoneIndexBase.Init(hShader, "uBoneIndexBase");
 	muFogColor.Init(hShader, "uFogColor");
 	muDynLightColor.Init(hShader, "uDynLightColor");
 	muObjectColor.Init(hShader, "uObjectColor");
@@ -610,6 +629,9 @@ bool FShader::Load(const char * name, const char * vert_prog_lump, const char *
 	{
 		int tempindex = glGetUniformBlockIndex(hShader, "LightBufferUBO");
 		if (tempindex != -1) glUniformBlockBinding(hShader, tempindex, LIGHTBUF_BINDINGPOINT);
+
+		tempindex = glGetUniformBlockIndex(hShader, "BoneBufferUBO");
+		if (tempindex != -1) glUniformBlockBinding(hShader, tempindex, BONEBUF_BINDINGPOINT);
 	}
 	int tempindex = glGetUniformBlockIndex(hShader, "ViewpointUBO");
 	if (tempindex != -1) glUniformBlockBinding(hShader, tempindex, VIEWPOINT_BINDINGPOINT);
diff --git a/source/common/rendering/gl/gl_shader.h b/source/common/rendering/gl/gl_shader.h
index 4f7debda3..6eb2b2226 100644
--- a/source/common/rendering/gl/gl_shader.h
+++ b/source/common/rendering/gl/gl_shader.h
@@ -241,6 +241,7 @@ class FShader
 	FBufferedUniform4f muLightParms;
 	FBufferedUniform2f muClipSplit;
 	FBufferedUniform1i muLightIndex;
+	FBufferedUniform1i muBoneIndexBase;
 	FBufferedUniformPE muFogColor;
 	FBufferedUniform4f muDynLightColor;
 	FBufferedUniformPE muObjectColor;
diff --git a/source/common/rendering/gles/gles_framebuffer.cpp b/source/common/rendering/gles/gles_framebuffer.cpp
index db2a69902..0d0a459c0 100644
--- a/source/common/rendering/gles/gles_framebuffer.cpp
+++ b/source/common/rendering/gles/gles_framebuffer.cpp
@@ -48,6 +48,7 @@
 #include "hw_skydome.h"
 #include "hw_viewpointbuffer.h"
 #include "hw_lightbuffer.h"
+#include "hw_bonebuffer.h"
 #include "gles_shaderprogram.h"
 #include "r_videoscale.h"
 #include "gles_buffers.h"
@@ -101,6 +102,7 @@ OpenGLFrameBuffer::~OpenGLFrameBuffer()
 	if (mSkyData != nullptr) delete mSkyData;
 	if (mViewpoints != nullptr) delete mViewpoints;
 	if (mLights != nullptr) delete mLights;
+	if (mBones != nullptr) delete mBones;
 	mShadowMap.Reset();
 
 	if (GLRenderer)
@@ -154,9 +156,11 @@ void OpenGLFrameBuffer::InitializeState()
 	mSkyData = new FSkyVertexBuffer;
 	mViewpoints = new HWViewpointBuffer(mPipelineNbr);
 	mLights = new FLightBuffer(mPipelineNbr);
+	mBones = new BoneBuffer(mPipelineNbr);
 	GLRenderer = new FGLRenderer(this);
 	GLRenderer->Initialize(GetWidth(), GetHeight());
 	static_cast<GLDataBuffer*>(mLights->GetBuffer())->BindBase();
+	static_cast<GLDataBuffer*>(mBones->GetBuffer())->BindBase();
 }
 
 //==========================================================================
diff --git a/source/common/rendering/gles/gles_renderer.h b/source/common/rendering/gles/gles_renderer.h
index 3322e06bf..b1e7d2346 100644
--- a/source/common/rendering/gles/gles_renderer.h
+++ b/source/common/rendering/gles/gles_renderer.h
@@ -17,6 +17,7 @@ class FFlatVertexBuffer;
 class FSkyVertexBuffer;
 class HWPortal;
 class FLightBuffer;
+class BoneBuffer;
 class DPSprite;
 class FGLRenderBuffers;
 class FGL2DDrawer;
diff --git a/source/common/rendering/hwrenderer/data/buffers.h b/source/common/rendering/hwrenderer/data/buffers.h
index 0a914bb9c..5e2c02728 100644
--- a/source/common/rendering/hwrenderer/data/buffers.h
+++ b/source/common/rendering/hwrenderer/data/buffers.h
@@ -25,6 +25,8 @@ enum
 	VATTR_NORMAL,
 	VATTR_NORMAL2,
 	VATTR_LIGHTMAP,	
+	VATTR_BONEWEIGHT,
+	VATTR_BONESELECTOR,
 	VATTR_MAX
 };
 
@@ -36,6 +38,7 @@ enum EVertexAttributeFormat
 	VFmt_Float,
 	VFmt_Byte4,
 	VFmt_Packed_A2R10G10B10,
+	VFmt_Byte4_UInt
 };
 
 struct FVertexBufferAttribute
diff --git a/source/common/rendering/hwrenderer/data/hw_bonebuffer.cpp b/source/common/rendering/hwrenderer/data/hw_bonebuffer.cpp
new file mode 100644
index 000000000..6335c3fc4
--- /dev/null
+++ b/source/common/rendering/hwrenderer/data/hw_bonebuffer.cpp
@@ -0,0 +1,112 @@
+// 
+//---------------------------------------------------------------------------
+//
+// Copyright(C) 2014-2016 Christoph Oelckers
+// All rights reserved.
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this program.  If not, see http://www.gnu.org/licenses/
+//
+//--------------------------------------------------------------------------
+//
+
+#include "hw_bonebuffer.h"
+#include "hw_dynlightdata.h"
+#include "shaderuniforms.h"
+
+static const int BONE_SIZE = (16*sizeof(float));
+
+BoneBuffer::BoneBuffer(int pipelineNbr) : mPipelineNbr(pipelineNbr)
+{
+	int maxNumberOfBones = 80000;
+
+	mBufferSize = maxNumberOfBones;
+	mByteSize = mBufferSize * BONE_SIZE;
+
+	// Hack alert: On Intel's GL driver SSBO's perform quite worse than UBOs.
+	// We only want to disable using SSBOs for bones but not disable the feature entirely.
+	// Note that using an uniform buffer here will limit the number of bones per model so it isn't done for NVidia and AMD.
+	if (screen->IsVulkan() || screen->IsPoly() || ((screen->hwcaps & RFL_SHADER_STORAGE_BUFFER) && screen->allowSSBO() && !strstr(screen->vendorstring, "Intel")))
+	{
+		mBufferType = true;
+		mBlockAlign = 0;
+		mBlockSize = mBufferSize;
+		mMaxUploadSize = mBlockSize;
+	}
+	else
+	{
+		mBufferType = false;
+		mBlockSize = screen->maxuniformblock / BONE_SIZE;
+		mBlockAlign = screen->uniformblockalignment < 64 ? 1 : screen->uniformblockalignment / BONE_SIZE;
+		mMaxUploadSize = (mBlockSize - mBlockAlign);
+	}
+
+	for (int n = 0; n < mPipelineNbr; n++)
+	{
+		mBufferPipeline[n] = screen->CreateDataBuffer(BONEBUF_BINDINGPOINT, mBufferType, false);
+		mBufferPipeline[n]->SetData(mByteSize, nullptr, BufferUsageType::Persistent);
+	}
+
+	Clear();
+}
+
+BoneBuffer::~BoneBuffer()
+{
+	delete mBuffer;
+}
+
+void BoneBuffer::Clear()
+{
+	mIndex = 0;
+
+	mPipelinePos++;
+	mPipelinePos %= mPipelineNbr;
+
+	mBuffer = mBufferPipeline[mPipelinePos];
+}
+
+int BoneBuffer::UploadBones(const TArray<VSMatrix>& bones)
+{
+	int totalsize = bones.Size();
+	if (totalsize > (int)mMaxUploadSize)
+	{
+		totalsize = mMaxUploadSize;
+	}
+
+	uint8_t *mBufferPointer = (uint8_t*)mBuffer->Memory();
+	assert(mBufferPointer != nullptr);
+	if (mBufferPointer == nullptr) return -1;
+	if (totalsize <= 0) return -1;	// there are no bones
+
+	unsigned int thisindex = mIndex.fetch_add(totalsize);
+
+	if (thisindex + totalsize <= mBufferSize)
+	{
+		memcpy(mBufferPointer + thisindex * BONE_SIZE, bones.Data(), totalsize * BONE_SIZE);
+		return thisindex;
+	}
+	else
+	{
+		return -1;	// Buffer is full. Since it is being used live at the point of the upload we cannot do much here but to abort.
+	}
+}
+
+int BoneBuffer::GetBinding(unsigned int index, size_t* pOffset, size_t* pSize)
+{
+	// this function will only get called if a uniform buffer is used. For a shader storage buffer we only need to bind the buffer once at the start.
+	unsigned int offset = (index / mBlockAlign) * mBlockAlign;
+
+	*pOffset = offset * BONE_SIZE;
+	*pSize = mBlockSize * BONE_SIZE;
+	return (index - offset);
+}
diff --git a/source/common/rendering/hwrenderer/data/hw_bonebuffer.h b/source/common/rendering/hwrenderer/data/hw_bonebuffer.h
new file mode 100644
index 000000000..c4ad550c9
--- /dev/null
+++ b/source/common/rendering/hwrenderer/data/hw_bonebuffer.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "tarray.h"
+#include "hwrenderer/data/buffers.h"
+#include "common/utility/matrix.h"
+#include <atomic>
+#include <mutex>
+
+class FRenderState;
+
+class BoneBuffer
+{
+	IDataBuffer *mBuffer;
+	IDataBuffer* mBufferPipeline[HW_MAX_PIPELINE_BUFFERS];
+	int mPipelineNbr;
+	int mPipelinePos = 0;
+
+	bool mBufferType;
+    std::atomic<unsigned int> mIndex;
+	unsigned int mBlockAlign;
+	unsigned int mBlockSize;
+	unsigned int mBufferSize;
+	unsigned int mByteSize;
+    unsigned int mMaxUploadSize;
+
+public:
+	BoneBuffer(int pipelineNbr = 1);
+	~BoneBuffer();
+
+	void Clear();
+	int UploadBones(const TArray<VSMatrix> &bones);
+	void Map() { mBuffer->Map(); }
+	void Unmap() { mBuffer->Unmap(); }
+	unsigned int GetBlockSize() const { return mBlockSize; }
+	bool GetBufferType() const { return mBufferType; }
+	int GetBinding(unsigned int index, size_t* pOffset, size_t* pSize);
+
+	// OpenGL needs the buffer to mess around with the binding.
+	IDataBuffer* GetBuffer() const
+	{
+		return mBuffer;
+	}
+};
diff --git a/source/common/rendering/hwrenderer/data/hw_modelvertexbuffer.cpp b/source/common/rendering/hwrenderer/data/hw_modelvertexbuffer.cpp
index da5b3be3c..9dff499f9 100644
--- a/source/common/rendering/hwrenderer/data/hw_modelvertexbuffer.cpp
+++ b/source/common/rendering/hwrenderer/data/hw_modelvertexbuffer.cpp
@@ -47,10 +47,12 @@ FModelVertexBuffer::FModelVertexBuffer(bool needindex, bool singleframe)
 		{ 0, VATTR_TEXCOORD, VFmt_Float2, (int)myoffsetof(FModelVertex, u) },
 		{ 0, VATTR_NORMAL, VFmt_Packed_A2R10G10B10, (int)myoffsetof(FModelVertex, packedNormal) },
 		{ 0, VATTR_LIGHTMAP, VFmt_Float3, (int)myoffsetof(FModelVertex, lu) },
+		{ 0, VATTR_BONESELECTOR, VFmt_Byte4_UInt, (int)myoffsetof(FModelVertex, boneselector[0])},
+		{ 0, VATTR_BONEWEIGHT, VFmt_Byte4, (int)myoffsetof(FModelVertex, boneweight[0]) },
 		{ 1, VATTR_VERTEX2, VFmt_Float3, (int)myoffsetof(FModelVertex, x) },
 		{ 1, VATTR_NORMAL2, VFmt_Packed_A2R10G10B10, (int)myoffsetof(FModelVertex, packedNormal) }
 	};
-	mVertexBuffer->SetFormat(2, 6, sizeof(FModelVertex), format);
+	mVertexBuffer->SetFormat(2, 8, sizeof(FModelVertex), format);
 }
 
 //===========================================================================
diff --git a/source/common/rendering/hwrenderer/data/hw_renderstate.h b/source/common/rendering/hwrenderer/data/hw_renderstate.h
index bd8fd4d9e..2ff77c53d 100644
--- a/source/common/rendering/hwrenderer/data/hw_renderstate.h
+++ b/source/common/rendering/hwrenderer/data/hw_renderstate.h
@@ -217,6 +217,7 @@ protected:
 	uint8_t mBrightmapEnabled : 1;
 
 	int mLightIndex;
+	int mBoneIndexBase;
 	int mSpecialEffect;
 	int mTextureMode;
 	int mTextureClamp;
@@ -278,6 +279,7 @@ public:
 		mLightParms[3] = -1.f;
 		mSpecialEffect = EFF_NONE;
 		mLightIndex = -1;
+		mBoneIndexBase = -1;
 		mStreamData.uInterpolationFactor = 0;
 		mRenderStyle = DefaultRenderStyle();
 		mMaterial.Reset();
@@ -568,6 +570,11 @@ public:
 		mLightIndex = index;
 	}
 
+	void SetBoneIndexBase(int index)
+	{
+		mBoneIndexBase = index;
+	}
+
 	void SetRenderStyle(FRenderStyle rs)
 	{
 		mRenderStyle = rs;
diff --git a/source/common/rendering/hwrenderer/data/shaderuniforms.h b/source/common/rendering/hwrenderer/data/shaderuniforms.h
index 92995fcaa..4b11a3755 100644
--- a/source/common/rendering/hwrenderer/data/shaderuniforms.h
+++ b/source/common/rendering/hwrenderer/data/shaderuniforms.h
@@ -11,7 +11,8 @@ enum
 	VIEWPOINT_BINDINGPOINT = 3,
 	LIGHTNODES_BINDINGPOINT = 4,
 	LIGHTLINES_BINDINGPOINT = 5,
-	LIGHTLIST_BINDINGPOINT = 6
+	LIGHTLIST_BINDINGPOINT = 6,
+	BONEBUF_BINDINGPOINT = 7
 };
 
 enum class UniformType
diff --git a/source/common/rendering/i_modelvertexbuffer.h b/source/common/rendering/i_modelvertexbuffer.h
index bd3df3767..f68d4e117 100644
--- a/source/common/rendering/i_modelvertexbuffer.h
+++ b/source/common/rendering/i_modelvertexbuffer.h
@@ -9,6 +9,8 @@ struct FModelVertex
 	unsigned packedNormal;	// normal vector as GL_INT_2_10_10_10_REV.
 	float lu, lv;	// lightmap texture coordinates
 	float lindex;	// lightmap texture index
+	uint8_t boneselector[4];
+	uint8_t boneweight[4];
 
 	void Set(float xx, float yy, float zz, float uu, float vv)
 	{
@@ -30,6 +32,22 @@ struct FModelVertex
 		int inw = 0;
 		packedNormal = (inw << 30) | ((inz & 1023) << 20) | ((iny & 1023) << 10) | (inx & 1023);
 	}
+
+	void SetBoneSelector(int x, int y, int z, int w)
+	{
+		boneselector[0] = x;
+		boneselector[1] = y;
+		boneselector[2] = z;
+		boneselector[3] = w;
+	}
+
+	void SetBoneWeight(int x, int y, int z, int w)
+	{
+		boneweight[0] = x;
+		boneweight[1] = y;
+		boneweight[2] = z;
+		boneweight[3] = w;
+	}
 };
 
 #define VMO ((FModelVertex*)nullptr)
diff --git a/source/common/rendering/v_video.h b/source/common/rendering/v_video.h
index aff93435e..3be892efe 100644
--- a/source/common/rendering/v_video.h
+++ b/source/common/rendering/v_video.h
@@ -59,6 +59,7 @@ struct HWDrawInfo;
 class FMaterial;
 class FGameTexture;
 class FRenderState;
+class BoneBuffer;
 
 enum EHWCaps
 {
@@ -143,6 +144,7 @@ public:
 	FFlatVertexBuffer *mVertexData = nullptr;	// Global vertex data
 	HWViewpointBuffer *mViewpoints = nullptr;	// Viewpoint render data.
 	FLightBuffer *mLights = nullptr;			// Dynamic lights
+	BoneBuffer* mBones = nullptr;				// Model bones
 	IShadowMap mShadowMap;
 
 	IntRect mScreenViewport;
diff --git a/source/common/rendering/vulkan/renderer/vk_descriptorset.cpp b/source/common/rendering/vulkan/renderer/vk_descriptorset.cpp
index 0f74124a0..ef6646580 100644
--- a/source/common/rendering/vulkan/renderer/vk_descriptorset.cpp
+++ b/source/common/rendering/vulkan/renderer/vk_descriptorset.cpp
@@ -85,6 +85,7 @@ void VkDescriptorSetManager::UpdateHWBufferSet()
 		.AddBuffer(HWBufferSet.get(), 1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, fb->GetBufferManager()->MatrixBuffer->UniformBuffer->mBuffer.get(), 0, sizeof(MatricesUBO))
 		.AddBuffer(HWBufferSet.get(), 2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, fb->GetBufferManager()->StreamBuffer->UniformBuffer->mBuffer.get(), 0, sizeof(StreamUBO))
 		.AddBuffer(HWBufferSet.get(), 3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, fb->GetBufferManager()->LightBufferSSO->mBuffer.get())
+		.AddBuffer(HWBufferSet.get(), 4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, fb->GetBufferManager()->BoneBufferSSO->mBuffer.get())
 		.Execute(fb->device);
 }
 
@@ -252,6 +253,7 @@ void VkDescriptorSetManager::CreateHWBufferSetLayout()
 		.AddBinding(1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT)
 		.AddBinding(2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 1, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT)
 		.AddBinding(3, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT)
+		.AddBinding(4, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_VERTEX_BIT)
 		.DebugName("VkDescriptorSetManager.HWBufferSetLayout")
 		.Create(fb->device);
 }
@@ -271,7 +273,7 @@ void VkDescriptorSetManager::CreateHWBufferPool()
 {
 	HWBufferDescriptorPool = DescriptorPoolBuilder()
 		.AddPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 3 * maxSets)
-		.AddPoolSize(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1 * maxSets)
+		.AddPoolSize(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 2 * maxSets)
 		.MaxSets(maxSets)
 		.DebugName("VkDescriptorSetManager.HWBufferDescriptorPool")
 		.Create(fb->device);
diff --git a/source/common/rendering/vulkan/renderer/vk_renderpass.cpp b/source/common/rendering/vulkan/renderer/vk_renderpass.cpp
index 083745a91..4a7531b74 100644
--- a/source/common/rendering/vulkan/renderer/vk_renderpass.cpp
+++ b/source/common/rendering/vulkan/renderer/vk_renderpass.cpp
@@ -230,10 +230,11 @@ std::unique_ptr<VulkanPipeline> VkRenderPassSetup::CreatePipeline(const VkPipeli
 		VK_FORMAT_R32G32_SFLOAT,
 		VK_FORMAT_R32_SFLOAT,
 		VK_FORMAT_R8G8B8A8_UNORM,
-		VK_FORMAT_A2B10G10R10_SNORM_PACK32
+		VK_FORMAT_A2B10G10R10_SNORM_PACK32,
+		VK_FORMAT_R8G8B8A8_UINT
 	};
 
-	bool inputLocations[7] = { false, false, false, false, false, false, false };
+	bool inputLocations[VATTR_MAX] = {};
 
 	for (size_t i = 0; i < vfmt.Attrs.size(); i++)
 	{
@@ -243,10 +244,10 @@ std::unique_ptr<VulkanPipeline> VkRenderPassSetup::CreatePipeline(const VkPipeli
 	}
 
 	// Vulkan requires an attribute binding for each location specified in the shader
-	for (int i = 0; i < 7; i++)
+	for (int i = 0; i < VATTR_MAX; i++)
 	{
 		if (!inputLocations[i])
-			builder.AddVertexAttribute(i, 0, VK_FORMAT_R32G32B32_SFLOAT, 0);
+			builder.AddVertexAttribute(i, 0, i != 8 ? VK_FORMAT_R32G32B32_SFLOAT : VK_FORMAT_R8G8B8A8_UINT, 0);
 	}
 
 	builder.AddDynamicState(VK_DYNAMIC_STATE_VIEWPORT);
diff --git a/source/common/rendering/vulkan/renderer/vk_renderstate.cpp b/source/common/rendering/vulkan/renderer/vk_renderstate.cpp
index d2a68d951..cb2667920 100644
--- a/source/common/rendering/vulkan/renderer/vk_renderstate.cpp
+++ b/source/common/rendering/vulkan/renderer/vk_renderstate.cpp
@@ -391,6 +391,7 @@ void VkRenderState::ApplyPushConstants()
 	}
 
 	mPushConstants.uLightIndex = mLightIndex;
+	mPushConstants.uBoneIndexBase = mBoneIndexBase;
 	mPushConstants.uDataIndex = mStreamBufferWriter.DataIndex();
 
 	auto passManager = fb->GetRenderPassManager();
diff --git a/source/common/rendering/vulkan/shaders/vk_shader.cpp b/source/common/rendering/vulkan/shaders/vk_shader.cpp
index c223cafa0..44c419dda 100644
--- a/source/common/rendering/vulkan/shaders/vk_shader.cpp
+++ b/source/common/rendering/vulkan/shaders/vk_shader.cpp
@@ -229,6 +229,12 @@ static const char *shaderBindings = R"(
 	    vec4 lights[];
 	};
 
+	// bone matrix buffers
+	layout(set = 1, binding = 4, std430) buffer BoneBufferSSO
+	{
+	    mat4 bones[];
+	};
+
 	// textures
 	layout(set = 2, binding = 0) uniform sampler2D tex;
 	layout(set = 2, binding = 1) uniform sampler2D texture2;
@@ -262,8 +268,11 @@ static const char *shaderBindings = R"(
 		// Blinn glossiness and specular level
 		vec2 uSpecularMaterial;
 
+		// bone animation
+		int uBoneIndexBase;
+
 		int uDataIndex;
-		int padding1, padding2, padding3;
+		int padding2, padding3;
 	};
 
 	// material types
diff --git a/source/common/rendering/vulkan/shaders/vk_shader.h b/source/common/rendering/vulkan/shaders/vk_shader.h
index 11682dae9..ba13726a6 100644
--- a/source/common/rendering/vulkan/shaders/vk_shader.h
+++ b/source/common/rendering/vulkan/shaders/vk_shader.h
@@ -50,6 +50,9 @@ struct PushConstants
 	// Blinn glossiness and specular level
 	FVector2 uSpecularMaterial;
 
+	// bone animation
+	int uBoneIndexBase;
+
 	int uDataIndex;
 	int padding1, padding2, padding3;
 };
diff --git a/source/common/rendering/vulkan/system/vk_buffer.cpp b/source/common/rendering/vulkan/system/vk_buffer.cpp
index db7bc4ea8..2a45cc73c 100644
--- a/source/common/rendering/vulkan/system/vk_buffer.cpp
+++ b/source/common/rendering/vulkan/system/vk_buffer.cpp
@@ -58,7 +58,7 @@ void VkBufferManager::RemoveBuffer(VkHardwareBuffer* buffer)
 	buffer->fb = nullptr;
 	Buffers.erase(buffer->it);
 
-	for (VkHardwareDataBuffer** knownbuf : { &ViewpointUBO, &LightBufferSSO, &LightNodes, &LightLines, &LightList })
+	for (VkHardwareDataBuffer** knownbuf : { &ViewpointUBO, &LightBufferSSO, &LightNodes, &LightLines, &LightList, &BoneBufferSSO })
 	{
 		if (buffer == *knownbuf) *knownbuf = nullptr;
 	}
@@ -85,6 +85,7 @@ IDataBuffer* VkBufferManager::CreateDataBuffer(int bindingpoint, bool ssbo, bool
 	case LIGHTNODES_BINDINGPOINT: LightNodes = buffer; break;
 	case LIGHTLINES_BINDINGPOINT: LightLines = buffer; break;
 	case LIGHTLIST_BINDINGPOINT: LightList = buffer; break;
+	case BONEBUF_BINDINGPOINT: BoneBufferSSO = buffer; break;
 	case POSTPROCESS_BINDINGPOINT: break;
 	default: break;
 	}
diff --git a/source/common/rendering/vulkan/system/vk_buffer.h b/source/common/rendering/vulkan/system/vk_buffer.h
index 39cc1ce15..c54d60aac 100644
--- a/source/common/rendering/vulkan/system/vk_buffer.h
+++ b/source/common/rendering/vulkan/system/vk_buffer.h
@@ -33,6 +33,7 @@ public:
 	VkHardwareDataBuffer* LightNodes = nullptr;
 	VkHardwareDataBuffer* LightLines = nullptr;
 	VkHardwareDataBuffer* LightList = nullptr;
+	VkHardwareDataBuffer* BoneBufferSSO = nullptr;
 
 	std::unique_ptr<VkStreamBuffer> MatrixBuffer;
 	std::unique_ptr<VkStreamBuffer> StreamBuffer;
diff --git a/source/common/rendering/vulkan/system/vk_framebuffer.cpp b/source/common/rendering/vulkan/system/vk_framebuffer.cpp
index b93e59784..b5925ad53 100644
--- a/source/common/rendering/vulkan/system/vk_framebuffer.cpp
+++ b/source/common/rendering/vulkan/system/vk_framebuffer.cpp
@@ -41,6 +41,7 @@
 #include "flatvertices.h"
 #include "hwrenderer/data/shaderuniforms.h"
 #include "hw_lightbuffer.h"
+#include "hw_bonebuffer.h"
 
 #include "vk_framebuffer.h"
 #include "vk_hwbuffer.h"
@@ -98,6 +99,7 @@ VulkanFrameBuffer::~VulkanFrameBuffer()
 	delete mSkyData;
 	delete mViewpoints;
 	delete mLights;
+	delete mBones;
 	mShadowMap.Reset();
 
 	if (mDescriptorSetManager)
@@ -155,6 +157,7 @@ void VulkanFrameBuffer::InitializeState()
 	mSkyData = new FSkyVertexBuffer;
 	mViewpoints = new HWViewpointBuffer;
 	mLights = new FLightBuffer();
+	mBones = new BoneBuffer();
 
 	mShaderManager.reset(new VkShaderManager(this));
 	mDescriptorSetManager->Init();
diff --git a/source/common/scripting/backend/codegen.cpp b/source/common/scripting/backend/codegen.cpp
index b274c88f9..de0408d05 100644
--- a/source/common/scripting/backend/codegen.cpp
+++ b/source/common/scripting/backend/codegen.cpp
@@ -1064,7 +1064,7 @@ FxExpression *FxFloatCast::Resolve(FCompileContext &ctx)
 ExpEmit FxFloatCast::Emit(VMFunctionBuilder *build)
 {
 	ExpEmit from = basex->Emit(build);
-	assert(!from.Konst);
+	//assert(!from.Konst);
 	assert(basex->ValueType->GetRegType() == REGT_INT);
 	from.Free(build);
 	ExpEmit to(build, REGT_FLOAT);
@@ -7506,7 +7506,7 @@ ExpEmit FxArrayElement::Emit(VMFunctionBuilder *build)
 		else
 		{
 			static int LKR_Ops[] = { OP_LK_R, OP_LKF_R, OP_LKS_R, OP_LKP_R };
-			assert(start.RegType == ValueType->GetRegType());
+			//assert(start.RegType == ValueType->GetRegType());
 			ExpEmit dest(build, start.RegType);
 			if (start.RegNum <= 255)
 			{
diff --git a/source/common/textures/gametexture.cpp b/source/common/textures/gametexture.cpp
index 8d0019081..23b98e8e3 100644
--- a/source/common/textures/gametexture.cpp
+++ b/source/common/textures/gametexture.cpp
@@ -96,8 +96,11 @@ void FGameTexture::Setup(FTexture *wrap)
 
 FGameTexture::~FGameTexture()
 {
-	FGameTexture* link = fileSystem.GetLinkedTexture(GetSourceLump());
-	if (link == this) fileSystem.SetLinkedTexture(GetSourceLump(), nullptr);
+	if (Base != nullptr)
+	{
+		FGameTexture* link = fileSystem.GetLinkedTexture(GetSourceLump());
+		if (link == this) fileSystem.SetLinkedTexture(GetSourceLump(), nullptr);
+	}
 	if (SoftwareTexture != nullptr)
 	{
 		delete SoftwareTexture;
diff --git a/source/common/utility/matrix.cpp b/source/common/utility/matrix.cpp
index 476d09b60..3d84d48e7 100644
--- a/source/common/utility/matrix.cpp
+++ b/source/common/utility/matrix.cpp
@@ -98,6 +98,21 @@ VSMatrix::multMatrix(const float *aMatrix)
 }
 #endif
 
+void VSMatrix::multQuaternion(const TVector4<FLOATTYPE>& q)
+{
+	FLOATTYPE m[16] = { FLOATTYPE(0.0) };
+	m[0 * 4 + 0] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.Y * q.Y - FLOATTYPE(2.0) * q.Z * q.Z;
+	m[1 * 4 + 0] = FLOATTYPE(2.0) * q.X * q.Y - FLOATTYPE(2.0) * q.W * q.Z;
+	m[2 * 4 + 0] = FLOATTYPE(2.0) * q.X * q.Z + FLOATTYPE(2.0) * q.W * q.Y;
+	m[0 * 4 + 1] = FLOATTYPE(2.0) * q.X * q.Y + FLOATTYPE(2.0) * q.W * q.Z;
+	m[1 * 4 + 1] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.X * q.X - FLOATTYPE(2.0) * q.Z * q.Z;
+	m[2 * 4 + 1] = FLOATTYPE(2.0) * q.Y * q.Z - FLOATTYPE(2.0) * q.W * q.X;
+	m[0 * 4 + 2] = FLOATTYPE(2.0) * q.X * q.Z - FLOATTYPE(2.0) * q.W * q.Y;
+	m[1 * 4 + 2] = FLOATTYPE(2.0) * q.Y * q.Z + FLOATTYPE(2.0) * q.W * q.X;
+	m[2 * 4 + 2] = FLOATTYPE(1.0) - FLOATTYPE(2.0) * q.X * q.X - FLOATTYPE(2.0) * q.Y * q.Y;
+	m[3 * 4 + 3] = FLOATTYPE(1.0);
+	multMatrix(m);
+}
 
 
 // gl LoadMatrix implementation
@@ -129,6 +144,29 @@ VSMatrix::translate(FLOATTYPE x, FLOATTYPE y, FLOATTYPE z)
 	mMatrix[14] = mMatrix[2] * x + mMatrix[6] * y + mMatrix[10] * z + mMatrix[14];
 }
 
+void VSMatrix::transpose()
+{
+	FLOATTYPE original[16];
+	for (int cnt = 0; cnt < 16; cnt++)
+		original[cnt] = mMatrix[cnt];
+
+	mMatrix[0] = original[0];
+	mMatrix[1] = original[4];
+	mMatrix[2] = original[8];
+	mMatrix[3] = original[12];
+	mMatrix[4] = original[1];
+	mMatrix[5] = original[5];
+	mMatrix[6] = original[9];
+	mMatrix[7] = original[13];
+	mMatrix[8] = original[2];
+	mMatrix[9] = original[6];
+	mMatrix[10] = original[10];
+	mMatrix[11] = original[14];
+	mMatrix[12] = original[3];
+	mMatrix[13] = original[7];
+	mMatrix[14] = original[11];
+	mMatrix[15] = original[15];
+}
 
 // gl Scale implementation
 void 
diff --git a/source/common/utility/matrix.h b/source/common/utility/matrix.h
index c1c7c873c..265981e1b 100644
--- a/source/common/utility/matrix.h
+++ b/source/common/utility/matrix.h
@@ -53,6 +53,7 @@ class VSMatrix {
 		{
 			multMatrix(aMatrix.mMatrix);
 		}
+		void multQuaternion(const TVector4<FLOATTYPE>& q);
 		void loadMatrix(const FLOATTYPE *aMatrix);
 #ifdef USE_DOUBLE
 		void loadMatrix(const float *aMatrix);
diff --git a/source/core/rendering/hw_models.cpp b/source/core/rendering/hw_models.cpp
index 3472d6215..81f42448a 100644
--- a/source/core/rendering/hw_models.cpp
+++ b/source/core/rendering/hw_models.cpp
@@ -109,10 +109,11 @@ void FHWModelRenderer::DrawElements(int numIndices, size_t offset)
 //
 //===========================================================================
 
-void FHWModelRenderer::SetupFrame(FModel *model, unsigned int frame1, unsigned int frame2, unsigned int size)
+int FHWModelRenderer::SetupFrame(FModel* model, unsigned int frame1, unsigned int frame2, unsigned int size, const TArray<VSMatrix>& bones, int boneStartIndex)
 {
 	auto mdbuff = static_cast<FModelVertexBuffer*>(model->GetVertexBuffer(GetType()));
 	state.SetVertexBuffer(mdbuff->vertexBuffer(), frame1, frame2);
 	if (mdbuff->indexBuffer()) state.SetIndexBuffer(mdbuff->indexBuffer());
+	return 0;
 }
 
diff --git a/source/core/rendering/hw_models.h b/source/core/rendering/hw_models.h
index b533aa428..7e41535e2 100644
--- a/source/core/rendering/hw_models.h
+++ b/source/core/rendering/hw_models.h
@@ -52,7 +52,7 @@ public:
 	void SetMaterial(FGameTexture *skin, bool clampNoFilter, int translation) override;
 	void DrawArrays(int start, int count) override;
 	void DrawElements(int numIndices, size_t offset) override;
-	void SetupFrame(FModel *model, unsigned int frame1, unsigned int frame2, unsigned int size) override;
+	int SetupFrame(FModel* model, unsigned int frame1, unsigned int frame2, unsigned int size, const TArray<VSMatrix>& bones, int boneStartIndex) override;
 
 };
 
diff --git a/source/core/rendering/scene/hw_sprites.cpp b/source/core/rendering/scene/hw_sprites.cpp
index 5f1a905dc..b2319f133 100644
--- a/source/core/rendering/scene/hw_sprites.cpp
+++ b/source/core/rendering/scene/hw_sprites.cpp
@@ -153,8 +153,9 @@ void HWSprite::DrawSprite(HWDrawInfo* di, FRenderState& state, bool translucent)
 			model->BuildVertexBuffer(&mr);
 			bool mirrored = ((Sprite->cstat & CSTAT_SPRITE_XFLIP) != 0) ^ ((Sprite->cstat & CSTAT_SPRITE_YFLIP) != 0) ^ portalState.isMirrored();
 			mr.BeginDrawModel(RenderStyle, nullptr, rotmat, mirrored);
-			mr.SetupFrame(model, 0, 0, 0);
-			model->RenderFrame(&mr, TexMan.GetGameTexture(model->GetPaletteTexture()), 0, 0, 0.f, TRANSLATION(Translation_Remap + curbasepal, palette), nullptr);
+			TArray<VSMatrix> a;
+			mr.SetupFrame(model, 0, 0, 0, a, 0);
+			model->RenderFrame(&mr, TexMan.GetGameTexture(model->GetPaletteTexture()), 0, 0, 0.f, TRANSLATION(Translation_Remap + curbasepal, palette), nullptr, a, 0);
 			mr.EndDrawModel(RenderStyle, nullptr);
 			state.SetDepthFunc(DF_Less);
 			state.SetVertexBuffer(screen->mVertexData);
diff --git a/wadsrc/static/zscript/engine/base.zs b/wadsrc/static/zscript/engine/base.zs
index 650487c2a..9000d3895 100644
--- a/wadsrc/static/zscript/engine/base.zs
+++ b/wadsrc/static/zscript/engine/base.zs
@@ -514,9 +514,9 @@ class Canvas : Object native abstract
 	native vararg void DrawShapeFill(Color col, double amount, Shape2D s, ...);
 	native vararg void DrawChar(Font font, int normalcolor, double x, double y, int character, ...);
 	native vararg void DrawText(Font font, int normalcolor, double x, double y, String text, ...);
-	native void DrawLine(int x0, int y0, int x1, int y1, Color color, int alpha = 255);
+	native void DrawLine(double x0, double y0, double x1, double y1, Color color, int alpha = 255);
 	native void DrawLineFrame(Color color, int x0, int y0, int w, int h, int thickness = 1);
-	native void DrawThickLine(int x0, int y0, int x1, int y1, double thickness, Color color, int alpha = 255);
+	native void DrawThickLine(double x0, double y0, double x1, double y1, double thickness, Color color, int alpha = 255);
 	native Vector2, Vector2 VirtualToRealCoords(Vector2 pos, Vector2 size, Vector2 vsize, bool vbottom=false, bool handleaspect=true);
 	native void SetClipRect(int x, int y, int w, int h);
 	native void ClearClipRect();
@@ -547,9 +547,9 @@ struct Screen native
 	native static vararg void DrawShapeFill(Color col, double amount, Shape2D s, ...);
 	native static vararg void DrawChar(Font font, int normalcolor, double x, double y, int character, ...);
 	native static vararg void DrawText(Font font, int normalcolor, double x, double y, String text, ...);
-	native static void DrawLine(int x0, int y0, int x1, int y1, Color color, int alpha = 255);
+	native static void DrawLine(double x0, double y0, double x1, double y1, Color color, int alpha = 255);
 	native static void DrawLineFrame(Color color, int x0, int y0, int w, int h, int thickness = 1);
-	native static void DrawThickLine(int x0, int y0, int x1, int y1, double thickness, Color color, int alpha = 255);
+	native static void DrawThickLine(double x0, double y0, double x1, double y1, double thickness, Color color, int alpha = 255);
 	native static Vector2, Vector2 VirtualToRealCoords(Vector2 pos, Vector2 size, Vector2 vsize, bool vbottom=false, bool handleaspect=true);
 	native static double GetAspectRatio();
 	native static void SetClipRect(int x, int y, int w, int h);