From 5501fa249dbe7242077039dedbe08724498a3da0 Mon Sep 17 00:00:00 2001
From: Spoike <acceptthis@users.sourceforge.net>
Date: Sat, 13 Jun 2020 00:05:47 +0000
Subject: [PATCH] makefile: attempt to fix freetype when not using makelibs
 (should make it slightly easier for people to compile with msys2 without
 needing to resort to cmake). emenu: clean up hexen2's maplist options
 slightly. emenu: modelviewer should now be slightly more friendly (click+wasd
 to move around). particles: fix up randomised s coords. csqc: try to fix
 issue with applycustomskin not refcounting properly. client: [s_]precache and
 (new) mod_precache cvars can be set to 2 to precache the resources after
 load, for faster loading at the expense of some early stutter, without
 risking later mid-game stuttering. gltf: add support for morphweights in a
 cpu-fallback path. don't expect good performance on surfaces with
 morphtargets for now. gtlf: add some support for gltf1 files. far from
 perfect. shaders: gltf1 semantics handling shaders: const correctness
 iqmtool: fix up mdl skin export. iqmtool: integrate the engine's gltf2
 loader. works with animated models, but unanimated ones suffer from
 basepose-different-from-bindpose issues. q3bsp: hopefully fixed bih traces.
 still disabled for now. qc: change default value of pr_gc_threaded to 1.
 qcext: add the '__deprecated' keyword to various symbols in fteextensions.qc,
 now that fteqcc supports it. ssqc: spit out a more readable error for
 WriteByte(MSG_CSQC,...) outside of SendEntity. ssqc: add registercommand
 builtin, for consistency with menuqc and csqc (though only one can register
 any single command). sv: report userinfo/serverinfo sizes (some clients still
 have arbitrary limits, plus its nice to see how abusive things are) sv: try
 to optimise sv_cullentities_trace a little. movechain: relink moved ents.
 csqc: add spriteframe builtin, for freecs to use instead of more ugly less
 reliable hacks. menuqc: fopen("tls://host:port", FILE_STREAM) should now open
 a tls stream. tcp:// should also work.

git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@5704 fc73d0e0-1445-4013-8a0c-d673dee63da5
---
 CMakeLists.txt                       |   89 +-
 imgtool.c                            |   86 +-
 iqm/iqm.cpp                          |  951 +++++++++++++++---
 iqm/util.h                           |   17 +-
 plugins/models/gltf.c                | 1365 ++++++++++++++++++++------
 plugins/openxr.c                     |    2 +-
 quakec/menusys/menusys/mitem_grid.qc |   11 +-
 7 files changed, 2046 insertions(+), 475 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4e76e3868..639d8f8b2 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -796,9 +796,13 @@ ELSE()
 
 	ADD_EXECUTABLE(iqmtool
 		iqm/iqm.cpp
+		plugins/models/gltf.c
+		engine/client/image.c
+		imgtool.c
 		iqm/iqm.h
 	)
-	SET_TARGET_PROPERTIES(iqmtool PROPERTIES COMPILE_DEFINITIONS "${FTE_REVISON}")
+	SET_TARGET_PROPERTIES(iqmtool PROPERTIES COMPILE_DEFINITIONS "IQMTOOL;${FTE_REVISON}")
+	TARGET_LINK_LIBRARIES(iqmtool ${CMAKE_DL_LIBS})
 	SET(INSTALLTARGS ${INSTALLTARGS} iqmtool)
 
 	ADD_EXECUTABLE(imgtool
@@ -958,8 +962,10 @@ SET(INSTALLTARGS ${INSTALLTARGS} qi)
 
 #ODE Physics library plugin
 FIND_PATH(LIBODE_INCLUDE_DIR ode/ode.h)
-FIND_LIBRARY(LIBODE_LIBRARY ode)
 IF (LIBODE_INCLUDE_DIR)
+	FIND_LIBRARY(LIBODE_LIBRARY ode)
+ENDIF()
+IF (LIBODE_LIBRARY)
 	ADD_LIBRARY(ode MODULE
 		plugins/plugin.c
 		engine/common/com_phys_ode.c
@@ -1157,41 +1163,44 @@ INSTALL(TARGETS ${INSTALLTARGS}
 	LIBRARY DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}"
 )
 
-ADD_CUSTOM_TARGET(menusys ALL
-	VERBATIM
-	COMMAND fteqcc -srcfile "${CMAKE_CURRENT_SOURCE_DIR}/quakec/menusys/menu.src" -o "${CMAKE_CURRENT_BINARY_DIR}/menu.dat"
-	BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/menu.dat" "${CMAKE_CURRENT_BINARY_DIR}/menu.lno"
-	SOURCES
-		quakec/menusys/menu.src
-		quakec/menusys/menusys/mitems.qc
-		quakec/menusys/menusys/mitems_common.qc
-		quakec/menusys/menusys/mitem_frame.qc
-		quakec/menusys/menusys/mitem_desktop.qc
-		quakec/menusys/menusys/mitem_exmenu.qc
-		quakec/menusys/menusys/mitem_edittext.qc
-		quakec/menusys/menusys/mitem_tabs.qc
-		quakec/menusys/menusys/mitem_colours.qc
-		quakec/menusys/menusys/mitem_checkbox.qc
-		quakec/menusys/menusys/mitem_slider.qc
-		quakec/menusys/menusys/mitem_combo.qc
-		quakec/menusys/menusys/mitem_bind.qc
-		quakec/menusys/menusys/mitem_spinnymodel.qc
-		quakec/menusys/menu/loadsave.qc
-		quakec/menusys/menu/newgame.qc
-		quakec/menusys/menu/options_basic.qc
-		quakec/menusys/menu/options_effects.qc
-		quakec/menusys/menu/options_keys.qc
-		quakec/menusys/menu/options.qc
-		quakec/menusys/menu/presets.qc
-		quakec/menusys/menu/servers.qc
-		quakec/menusys/menu/main.qc
-		quakec/menusys/menu/mods.qc
-		quakec/menusys/menu/cvars.qc
-		quakec/menusys/menu/updates.qc
-		quakec/menusys/menu/options_audio.qc
-		quakec/menusys/menu/options_configs.qc
-		quakec/menusys/menu/options_hud.qc
-		quakec/menusys/menu/options_particles.qc
-		quakec/menusys/menu/options_video.qc
-		quakec/menusys/menu/quit.qc
-)
+IF (1)
+	ADD_CUSTOM_TARGET(menusys ALL
+		VERBATIM
+		COMMAND fteqcc -srcfile "${CMAKE_CURRENT_SOURCE_DIR}/quakec/menusys/menu.src" -o "${CMAKE_CURRENT_BINARY_DIR}/menu.dat"
+		BYPRODUCTS "${CMAKE_CURRENT_BINARY_DIR}/menu.dat" "${CMAKE_CURRENT_BINARY_DIR}/menu.lno"
+		SOURCES
+			quakec/menusys/menu.src
+			quakec/menusys/fteextensions.qc
+			quakec/menusys/menusys/mitems.qc
+			quakec/menusys/menusys/mitems_common.qc
+			quakec/menusys/menusys/mitem_frame.qc
+			quakec/menusys/menusys/mitem_desktop.qc
+			quakec/menusys/menusys/mitem_exmenu.qc
+			quakec/menusys/menusys/mitem_edittext.qc
+			quakec/menusys/menusys/mitem_tabs.qc
+			quakec/menusys/menusys/mitem_colours.qc
+			quakec/menusys/menusys/mitem_checkbox.qc
+			quakec/menusys/menusys/mitem_slider.qc
+			quakec/menusys/menusys/mitem_combo.qc
+			quakec/menusys/menusys/mitem_bind.qc
+			quakec/menusys/menusys/mitem_spinnymodel.qc
+			quakec/menusys/menu/loadsave.qc
+			quakec/menusys/menu/newgame.qc
+			quakec/menusys/menu/options_basic.qc
+			quakec/menusys/menu/options_effects.qc
+			quakec/menusys/menu/options_keys.qc
+			quakec/menusys/menu/options.qc
+			quakec/menusys/menu/presets.qc
+			quakec/menusys/menu/servers.qc
+			quakec/menusys/menu/main.qc
+			quakec/menusys/menu/mods.qc
+			quakec/menusys/menu/cvars.qc
+			quakec/menusys/menu/updates.qc
+			quakec/menusys/menu/options_audio.qc
+			quakec/menusys/menu/options_configs.qc
+			quakec/menusys/menu/options_hud.qc
+			quakec/menusys/menu/options_particles.qc
+			quakec/menusys/menu/options_video.qc
+			quakec/menusys/menu/quit.qc
+	)
+ENDIF()
\ No newline at end of file
diff --git a/imgtool.c b/imgtool.c
index 19c11fe99..82989e7b0 100644
--- a/imgtool.c
+++ b/imgtool.c
@@ -86,6 +86,59 @@ void Z_Free(void *p)
 {
 	free(p);
 }
+#ifdef _WIN32
+// don't use these functions in MSVC8
+#if (_MSC_VER < 1400)
+int QDECL linuxlike_snprintf(char *buffer, int size, const char *format, ...)
+{
+#undef _vsnprintf
+	int ret;
+	va_list		argptr;
+
+	if (size <= 0)
+		return 0;
+	size--;
+
+	va_start (argptr, format);
+	ret = _vsnprintf (buffer,size, format,argptr);
+	va_end (argptr);
+
+	buffer[size] = '\0';
+
+	return ret;
+}
+int QDECL linuxlike_vsnprintf(char *buffer, int size, const char *format, va_list argptr)
+{
+#undef _vsnprintf
+	int ret;
+
+	if (size <= 0)
+		return 0;
+	size--;
+
+	ret = _vsnprintf (buffer,size, format,argptr);
+
+	buffer[size] = '\0';
+
+	return ret;
+}
+#elif (_MSC_VER < 1900)
+int VARGS linuxlike_snprintf_vc8(char *buffer, int size, const char *format, ...)
+{
+	int ret;
+	va_list		argptr;
+
+	va_start (argptr, format);
+	ret = vsnprintf_s (buffer,size, _TRUNCATE, format,argptr);
+	va_end (argptr);
+
+	return ret;
+}
+#endif
+#endif
+
+
+
 
 #include <sys/stat.h>
 
@@ -349,7 +402,10 @@ qbyte GetPaletteIndexNoFB(int red, int green, int blue)
 	}
 	return best;
 }
-static void ImgTool_SetupPalette(void)
+
+sh_config_t sh_config;
+viddef_t vid;
+void ImgTool_SetupPalette(void)
 {
 	int i;
 	//we ought to try to read gfx/palette.lmp, but its probably in a pak
@@ -359,7 +415,20 @@ static void ImgTool_SetupPalette(void)
 		d_8to24rgbtable[i] = (host_basepal[i*3+0]<<0)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<16);
 		d_8to24bgrtable[i] = (host_basepal[i*3+0]<<16)|(host_basepal[i*3+1]<<8)|(host_basepal[i*3+2]<<0);
 	}
+
+	sh_config.texture2d_maxsize = 1u<<31;
+	sh_config.texture3d_maxsize = 1u<<31;
+	sh_config.texture2darray_maxlayers = 1u<<31;
+	sh_config.texturecube_maxsize = 8192;
+	sh_config.texture_non_power_of_two = true;
+	sh_config.texture_non_power_of_two_pic = true;
+	sh_config.texture_allow_block_padding = true;
+	sh_config.npot_rounddown = true;	//shouldn't be relevant
+	sh_config.havecubemaps = true;	//I don't think this matters.
+
+	Image_Init();
 }
+#ifdef IMGTOOL
 static void ImgTool_FreeMips(struct pendingtextureinfo *mips)
 {
 	size_t i;
@@ -374,9 +443,6 @@ static void ImgTool_FreeMips(struct pendingtextureinfo *mips)
 	}
 }
 
-sh_config_t sh_config;
-viddef_t vid;
-
 typedef struct
 {
    unsigned int offset;				// Position of the entry in WAD
@@ -2136,18 +2202,7 @@ int main(int argc, const char **argv)
 	args.defaultext = NULL;
 	args.width = args.height = 0;
 
-	sh_config.texture2d_maxsize = 1u<<31;
-	sh_config.texture3d_maxsize = 1u<<31;
-	sh_config.texture2darray_maxlayers = 1u<<31;
-	sh_config.texturecube_maxsize = 8192;
-	sh_config.texture_non_power_of_two = true;
-	sh_config.texture_non_power_of_two_pic = true;
-	sh_config.texture_allow_block_padding = true;
-	sh_config.npot_rounddown = true;	//shouldn't be relevant
-	sh_config.havecubemaps = true;	//I don't think this matters.
-
 	ImgTool_SetupPalette();
-	Image_Init();
 
 	if (argc==1)
 		goto showhelp;
@@ -2394,3 +2449,4 @@ showhelp:
 	}
 	return EXIT_SUCCESS;
 }
+#endif
diff --git a/iqm/iqm.cpp b/iqm/iqm.cpp
index 28c69b45d..feaa543f7 100644
--- a/iqm/iqm.cpp
+++ b/iqm/iqm.cpp
@@ -1,3 +1,8 @@
+#define FTEPLUGIN
+#define GLQUAKE	//this is shit, but ensures index sizes come out the right size
+#include "../plugins/plugin.h"
+#include "com_mesh.h"
+
 #include "util.h"
 
 #define IQM_UNPACK (1u<<31)	//animations will be unpacked into individual frames-as-animations (ie: no more framegroups)
@@ -362,7 +367,7 @@ struct emesh
 	meshprop explicits;
 
 	emesh() : name(NULL), material(NULL), firsttri(0), used(false), hasexplicits(false) {}
-	emesh(const char *name, const char *material, int firsttri = 0) : name(name), material(material), firsttri(firsttri), used(false) {}
+	emesh(const char *name, const char *material, int firsttri = 0) : name(name), material(material), firsttri(firsttri), used(false), hasexplicits(false) {}
 };
 
 struct evarray
@@ -988,7 +993,8 @@ void calctangents(uint priortris, bool areaweight = true)
 {
 	uint numverts = vmap.length();
 	Vec3 *tangent = new Vec3[2*numverts], *bitangent = tangent+numverts;
-	memset(tangent, 0, 2*numverts*sizeof(Vec3));
+	for (uint i = 0; i < 2*numverts; i++)
+		tangent[i] = Vec3(0,0,0);
 	for (int i = priortris; i < triangles.length(); i++)
 	{
 		const triangle &t = triangles[i];
@@ -1245,7 +1251,12 @@ void makemeshes(const filespec &spec)
 	}
 	if(eblends.length())
 	{
-		setupvertexarray<IQM_BLENDINDEXES>(eblends, IQM_BLENDINDEXES, IQM_UBYTE, 4, priorverts);
+		if (ejoints.length() > 65535)
+			setupvertexarray<IQM_BLENDINDEXES>(eblends, IQM_BLENDINDEXES, IQM_UINT, 4, priorverts);
+		else if (ejoints.length() > 255)
+			setupvertexarray<IQM_BLENDINDEXES>(eblends, IQM_BLENDINDEXES, IQM_USHORT, 4, priorverts);
+		else
+			setupvertexarray<IQM_BLENDINDEXES>(eblends, IQM_BLENDINDEXES, IQM_UBYTE, 4, priorverts);
 		setupvertexarray<IQM_BLENDWEIGHTS>(eblends, IQM_BLENDWEIGHTS, IQM_UBYTE, 4, priorverts);
 	}
 	if(ecolors.length()) setupvertexarray<IQM_COLOR>(ecolors, IQM_COLOR, IQM_UBYTE, 4, priorverts);
@@ -1334,6 +1345,7 @@ void makerelativebasepose()
 		transform &parent = eposes[ej.parent], &child = eposes[i];
 		child.pos = (-parent.orient).transform(child.pos - parent.pos);   
 		child.orient = (-parent.orient)*child.orient;
+		child.scale = child.scale / parent.scale;
 		if(child.orient.w > 0) child.orient.flip();
 	}
 }
@@ -1372,7 +1384,7 @@ void printbones(int parent = -1, size_t ind = 1)
 	{
 		if (joints[i].parent == parent)
 		{	//show as 1-based for consistency with quake.
-			conoutf("%sbone %i:\tname=\"%s\"\tparent=%i, group=%i", prefix, i+1, &stringdata[joints[i].name], joints[i].parent+1, joints[i].group);
+			conoutf("%sbone %i:\tname=\"%s\"\tparent=%i, group=%i (%f %f %f)", prefix, i+1, &stringdata[joints[i].name], joints[i].parent+1, joints[i].group, joints[i].pos[0], joints[i].pos[1], joints[i].pos[2]);
 			printbones(i, ind+1);
 		}
 	}
@@ -3753,7 +3765,437 @@ bool loadfbx(const char *filename, const filespec &spec)
 	return true;
 }
 
+namespace fte
+{
+	static plugfsfuncs_t cppfsfuncs =
+	{
+		.OpenVFS = [](const char *filename, const char *mode, enum fs_relative relativeto)
+		{
+			stream *f = openfile(filename, "rb");
+			if (!f)
+				return (vfsfile_t*)nullptr;
+			struct cppfile_t : public vfsfile_t
+			{
+				static int ReadBytes (struct vfsfile_s *file, void *buffer, int bytestoread)
+				{
+					auto c = static_cast<cppfile_t*>(file);
+					return c->f->read(buffer, bytestoread);
+				}
+				static qboolean Seek (struct vfsfile_s *file, qofs_t pos)
+				{
+					auto c = static_cast<cppfile_t*>(file);
+					return c->f->seek(pos)?qtrue:qfalse;
+				}
+				static qofs_t Tell (struct vfsfile_s *file)
+				{
+					auto c = static_cast<cppfile_t*>(file);
+					return c->f->tell();
+				}
+				static qofs_t GetLen (struct vfsfile_s *file)
+				{
+					auto c = static_cast<cppfile_t*>(file);
+					return c->f->size();
+				}
+				static qboolean Close (struct vfsfile_s *file)
+				{
+					auto c = static_cast<cppfile_t*>(file);
+					c->f->close();
+					delete c;
+					return qtrue;
+				}
+				cppfile_t(stream *sourcefile):f(sourcefile)
+				{
+					vfsfile_t::ReadBytes = ReadBytes;
+					vfsfile_t::Seek = Seek;
+					vfsfile_t::Tell = Tell;
+					vfsfile_t::GetLen = GetLen;
+					vfsfile_t::Close = Close;
+				}
+				stream *f;
+			};
+			cppfile_t *c = new cppfile_t(f);
+			return static_cast<vfsfile_t*>(c);
+		},
+	};
+	static plugmodfuncs_t cppmodfuncs =
+	{
+		.version = MODPLUGFUNCS_VERSION,
+		.RegisterModelFormatText = [](const char *formatname, char *magictext, qboolean (QDECL *load) (struct model_s *mod, void *buffer, size_t fsize))
+		{	//called explicitly because we're lame.
+			return 0;
+		},
+		.RegisterModelFormatMagic = [](const char *formatname, unsigned int magic, qboolean (QDECL *load) (struct model_s *mod, void *buffer, size_t fsize))
+		{	//called explicitly because we're lame.
+			return 0;
+		},
+		.ZG_Malloc = [](zonegroup_t *ctx, size_t size)
+		{
+			/*leak the memory, because we're lazy*/
+			void *ret = malloc(size);
+			memset(ret, 0, size);
+			return ret;
+		},
 
+		.ConcatTransforms = [](const float in1[3][4], const float in2[3][4], float out[3][4])
+		{
+			out[0][0] = in1[0][0] * in2[0][0] + in1[0][1] * in2[1][0] +
+						in1[0][2] * in2[2][0];
+			out[0][1] = in1[0][0] * in2[0][1] + in1[0][1] * in2[1][1] +
+						in1[0][2] * in2[2][1];
+			out[0][2] = in1[0][0] * in2[0][2] + in1[0][1] * in2[1][2] +
+						in1[0][2] * in2[2][2];
+			out[0][3] = in1[0][0] * in2[0][3] + in1[0][1] * in2[1][3] +
+						in1[0][2] * in2[2][3] + in1[0][3];
+			out[1][0] = in1[1][0] * in2[0][0] + in1[1][1] * in2[1][0] +
+						in1[1][2] * in2[2][0];
+			out[1][1] = in1[1][0] * in2[0][1] + in1[1][1] * in2[1][1] +
+						in1[1][2] * in2[2][1];
+			out[1][2] = in1[1][0] * in2[0][2] + in1[1][1] * in2[1][2] +
+						in1[1][2] * in2[2][2];
+			out[1][3] = in1[1][0] * in2[0][3] + in1[1][1] * in2[1][3] +
+						in1[1][2] * in2[2][3] + in1[1][3];
+			out[2][0] = in1[2][0] * in2[0][0] + in1[2][1] * in2[1][0] +
+						in1[2][2] * in2[2][0];
+			out[2][1] = in1[2][0] * in2[0][1] + in1[2][1] * in2[1][1] +
+						in1[2][2] * in2[2][1];
+			out[2][2] = in1[2][0] * in2[0][2] + in1[2][1] * in2[1][2] +
+						in1[2][2] * in2[2][2];
+			out[2][3] = in1[2][0] * in2[0][3] + in1[2][1] * in2[1][3] +
+						in1[2][2] * in2[2][3] + in1[2][3];
+		},
+
+		.GenMatrixPosQuat4Scale = [](const vec3_t pos, const vec4_t quat, const vec3_t scale, float result[12])
+		{
+			Matrix3x4 m = Matrix3x4(Quat(quat), Vec3(pos), Vec3(scale));
+			result[0] = m.a.x;
+			result[1] = m.b.x;
+			result[2] = m.c.x;
+			result[3] = m.a.w;
+
+			result[4] = m.a.y;
+			result[5] = m.b.y;
+			result[6] = m.c.y;
+			result[7] = m.b.w;
+
+			result[8] = m.a.z;
+			result[9] = m.b.z;
+			result[10] = m.c.z;
+			result[11] = m.c.w;
+		},
+
+		.GetTexture = [](const char *identifier, const char *subpath, unsigned int flags, void *fallbackdata, void *fallbackpalette, int fallbackwidth, int fallbackheight, uploadfmt_t fallbackfmt)
+		{
+			image_t *img = (image_t*)cppmodfuncs.ZG_Malloc(NULL, sizeof(*img)+strlen(identifier)+1);
+			img->ident = (char*)(img+1);
+			strcpy(img->ident, identifier);
+			img->flags = flags;
+			return img;
+		},
+
+		.AccumulateTextureVectors = [](vecV_t *const vc, vec2_t *const tc, vec3_t *nv, vec3_t *sv, vec3_t *tv, const index_t *idx, int numidx, qboolean calcnorms)
+		{	//once per surface that shares the set of verts
+		},
+		.NormaliseTextureVectors = [](vec3_t *n, vec3_t *s, vec3_t *t, int v, qboolean calcnorms)
+		{	//once per shared set of verts.
+		},
+	};
+	static plugcorefuncs_t cppplugfuncs =
+	{
+		.GetEngineInterface = [](const char *interfacename, size_t structsize)
+		{
+			void *ret = nullptr;
+			if (!strcmp(interfacename, plugfsfuncs_name))
+				ret = &cppfsfuncs;
+			if (!strcmp(interfacename, plugmodfuncs_name))
+				ret = &cppmodfuncs;
+			return ret;
+		},
+	};
+	static plugcvarfuncs_t cppcvarfuncs =
+	{
+		.GetNVFDG = [](const char *name, const char *defaultval, unsigned int flags, const char *description, const char *groupname)
+		{	//could maybe fill with environment settings perhaps? yuck.
+			auto v = new cvar_t();
+			v->name = strdup(name);
+			v->string = strdup(defaultval);
+			v->value = atof(v->string);
+			v->ival = atoi(v->string);
+			return v;
+		},
+	};
+	extern "C"
+	{	//our plugin-style stuff has a few external dependancies not provided via pointers...
+		void ImgTool_SetupPalette(void);
+		qboolean QDECL Mod_LoadGLTFModel (struct model_s *mod, void *buffer, size_t fsize);
+		qboolean QDECL Mod_LoadGLBModel (struct model_s *mod, void *buffer, size_t fsize);
+		qboolean Plug_GLTF_Init(void);
+		plugcorefuncs_t *plugfuncs = &cppplugfuncs;
+		plugcmdfuncs_t *cmdfuncs;
+		plugcvarfuncs_t *cvarfuncs = &cppcvarfuncs;
+		void Q_strlcpy(char *d, const char *s, int n)
+		{
+			int i;
+			n--;
+			if (n < 0)
+				return;	//this could be an error
+
+			for (i=0; *s; i++)
+			{
+				if (i == n)
+					break;
+				*d++ = *s++;
+			}
+			*d='\0';
+		}
+		void Q_strlcat(char *d, const char *s, int n)
+		{
+			if (n)
+			{
+				int dlen = strlen(d);
+				int slen = strlen(s)+1;
+				if (slen > (n-1)-dlen)
+					slen = (n-1)-dlen;
+				memcpy(d+dlen, s, slen);
+				d[n - 1] = 0;
+			}
+		}
+	}
+
+	transform ftetransform(float bm[12], bool invert)
+	{
+		Matrix3x3 m(Vec3(bm[0], bm[1], bm[2]), Vec3(bm[4], bm[5], bm[6]), Vec3(bm[8], bm[9], bm[10]));
+		m.transpose();
+		Vec3 pos(bm[3], bm[7], bm[11]);
+		transform t;
+
+		Vec3 mscale(Vec3(m.a.x, m.b.x, m.c.x).magnitude(), Vec3(m.a.y, m.b.y, m.c.y).magnitude(), Vec3(m.a.z, m.b.z, m.c.z).magnitude());
+		// check determinant for sign of scaling
+		if(Matrix3x3(m).determinant() < 0) mscale = -mscale;
+		m.a /= mscale;
+		m.b /= mscale;
+		m.c /= mscale;
+		t.orient = Quat(m);
+		if(t.orient.w > 0) t.orient.flip();
+		t.scale = mscale;
+
+		if (invert)
+		{
+			// invert the translate
+			t.pos[0] = -(pos[0] * m.a[0] + pos[1] * m.a[1] + pos[2] * m.a[2]);
+			t.pos[1] = -(pos[0] * m.b[0] + pos[1] * m.b[1] + pos[2] * m.b[2]);
+			t.pos[2] = -(pos[0] * m.c[0] + pos[1] * m.c[1] + pos[2] * m.c[2]);
+		}
+		else
+			t.pos = pos;
+		return t;
+	}
+
+	bool loadfte(model_t *mod, const filespec &spec)
+	{	//import from fte's structs and convert to iqmtool's c++isms
+		if (mod->type != mod_alias)
+			return false;	//err...
+		galiasinfo_t *surf = (galiasinfo_t*)mod->meshinfo;
+		if (surf)
+		{
+			resetimporter(spec);
+
+			if (surf->baseframeofs)
+			{
+				for (int b = 0; b < surf->numbones; b++)
+				{
+					transform p(ftetransform(surf->ofsbones[b].inverse, true));
+
+					//spit out the joint info
+					ejoint &j = ejoints.add();
+					j.name = surf->ofsbones[b].name;
+					j.parent = surf->ofsbones[b].parent;
+
+					//and the base pose
+					eposes.add(p);
+				}
+				makerelativebasepose();
+
+#if 1			//import the animations
+				for (int animidx = 0; animidx < surf->numanimations; animidx++)
+				{
+					auto &anim = surf->ofsanimations[animidx];
+					int firstframe = eframes.length();
+					vector<float[12]> bonebuf;
+
+					for (int f = 0; f < anim.numposes; f++)
+					{
+						skeltype_t sk = anim.skeltype;
+						float time = f/anim.rate;
+						float *bonedata;
+						if (anim.GetRawBones)
+							bonedata = anim.GetRawBones(surf, &anim, time, bonebuf.reserve(surf->numbones)[0], surf->numbones);
+						else if (anim.boneofs)
+							bonedata = (float*)anim.boneofs;
+						else
+							bonedata = (float*)surf->baseframeofs, sk = SKEL_ABSOLUTE;	//abs...
+
+						if (sk == SKEL_RELATIVE)
+							;
+						else
+							printf("Unusable skeletal type for import - %i\n", (int)sk);
+
+						eframes.add(eposes.length());
+						for (int b = 0; b < surf->numbones; b++, bonedata += 12)
+							eposes.add(ftetransform(bonedata, false));
+					}
+
+
+					eanim &a = eanims.add();
+					if(spec.name) a.name = getnamekey(spec.name);
+					else
+					{
+						string name;
+						copystring(name, mod->name);
+						char *end = strrchr(name, '.');
+						if(end) *end = '\0';
+						a.name = getnamekey(name);
+					}
+					a.startframe = firstframe;
+					a.fps = anim.rate;
+					a.flags = anim.loop?IQM_LOOP:0;
+					a.endframe = eframes.length();
+				}
+#endif
+			}
+
+			for(; surf; surf = surf->nextsurf)
+			{
+				if (surf->shares_bones != 0)
+					continue;
+
+				if (surf->numindexes)
+				{
+					unsigned int firstvert=epositions.length();
+					for (int v = 0; v < surf->numverts; v++)
+					{
+						Vec3 pos(surf->ofs_skel_xyz[v][0], surf->ofs_skel_xyz[v][1], surf->ofs_skel_xyz[v][2]);
+						Vec3 norm;
+						if (surf->ofs_skel_norm)
+							norm = Vec3(surf->ofs_skel_norm[v][0],  surf->ofs_skel_norm[v][1],  surf->ofs_skel_norm[v][2]);
+						else
+							norm = Vec3(0,0,0);
+
+						etexcoords.add(Vec4(surf->ofs_st_array[v][0], surf->ofs_st_array[v][1], 0, 0));
+						if (surf->ofs_rgbaf)
+							ecolors.add(Vec4(surf->ofs_rgbaf[v][0], surf->ofs_rgbaf[v][1], surf->ofs_rgbaf[v][2], surf->ofs_rgbaf[v][3]));
+						else if (surf->ofs_rgbaub)
+							ecolors.add(Vec4(surf->ofs_rgbaub[v][0]/255.0, surf->ofs_rgbaub[v][1]/255.0, surf->ofs_rgbaub[v][2]/255.0, surf->ofs_rgbaub[v][3]/255.0));
+
+//						if (surf->ofs_skel_svect)
+//							etangents.add  (Vec4(surf->ofs_skel_svect[v][0], surf->ofs_skel_svect[v][1], surf->ofs_skel_svect[v][2], 0));
+//						if (surf->ofs_skel_tvect)
+//							ebitangents.add(Vec3(surf->ofs_skel_tvect[v][0], surf->ofs_skel_tvect[v][1], surf->ofs_skel_tvect[v][2]));
+
+						if (surf->shares_bones == 0 && surf->ofs_skel_weight && surf->ofs_skel_idx)
+						{
+							blendcombo b = {};
+							//Vec3 newpos(0,0,0);
+							//Vec3 newnorm(0,0,0);
+							for (size_t w = 0; w < 4; w++)
+							{
+								//newpos += bonerepositions[surf->ofs_skel_idx[v][w]].transform(pos) * surf->ofs_skel_weight[v][w];
+								//newnorm += bonerepositions[surf->ofs_skel_idx[v][w]].transform3(norm) * surf->ofs_skel_weight[v][w];
+								if (surf->ofs_skel_weight[v][w] > 0)
+									b.addweight(surf->ofs_skel_weight[v][w], surf->ofs_skel_idx[v][w]);
+							}
+							b.finalize();
+							eblends.add(b);
+							//pos = newpos;
+							//norm = newnorm;
+						}
+						epositions.add(Vec4(pos));
+						enormals.add(norm);
+					}
+
+					//iqms don't support skins/skingroups themselves.
+					//we have only surface name and texture(aka material) name.
+					//so use the diffuse texture's name where we can
+					//	a) its already processed properly so no ''path/model.gltf/sectionthatdoesntevenexistondisk' locations.
+					//	b) its more likely to show something without needing to synthesize shaders/textures.
+					//we should probably cvar this.
+					const char *materialname;
+					if (surf->numskins && surf->ofsskins[0].numframes && surf->ofsskins[0].frame[0].texnums.base && *surf->ofsskins[0].frame[0].texnums.base->ident != '$')
+						materialname = surf->ofsskins[0].frame[0].texnums.base->ident;
+					else if (surf->numskins)
+						materialname = surf->ofsskins[0].name;
+					else
+						materialname = surf->surfacename;
+
+					emesh mesh(surf->surfacename, materialname, etriangles.length());
+
+					//add in some extra surface properties.
+					mesh.hasexplicits = true;
+					mesh.explicits.contents = surf->contents;
+					mesh.explicits.surfaceflags = surf->csurface.flags;
+					mesh.explicits.body = surf->surfaceid;
+					mesh.explicits.geomset = surf->geomset;
+					mesh.explicits.geomid = surf->geomid;
+					mesh.explicits.mindist = surf->mindist;
+					mesh.explicits.maxdist = surf->maxdist;
+
+					emeshes.add(mesh);
+					for (int idx = 0; idx+2 < surf->numindexes; idx+=3)
+						etriangles.add(etriangle(surf->ofs_indexes[idx+0]+firstvert, surf->ofs_indexes[idx+1]+firstvert, surf->ofs_indexes[idx+2]+firstvert));
+				}
+			}
+			makeanims(spec);
+			if (emeshes.length())
+			{
+				smoothverts();
+				makemeshes(spec);
+			}
+			return true;
+		}
+		return false;
+	}
+	bool loadglb(const char *filename, const filespec &spec)
+	{
+		bool ret = false;
+		model_t mod={};
+		stream *f = openfile(filename, "rb");
+		Q_strlcpy(mod.name, filename, sizeof(mod.name));
+		if (f)
+		{
+			size_t sz = f->size();
+			auto filebuf = new char[sz];
+			if (sz == f->read(filebuf, sz))
+			{
+				if (Plug_GLTF_Init())
+					if (Mod_LoadGLBModel(&mod, filebuf, sz))
+						ret = loadfte(&mod, spec);
+			}
+			delete[] filebuf;
+			delete f;
+		}
+		return ret;
+	}
+	bool loadgltf(const char *filename, const filespec &spec)
+	{
+		bool ret = false;
+		model_t mod={};
+		stream *f = openfile(filename, "rb");
+		Q_strlcpy(mod.name, filename, sizeof(mod.name));
+		if (f)
+		{
+			size_t sz = f->size();
+			auto filebuf = new char[sz];
+			if (sz == f->read(filebuf, sz))
+			{
+				if (Plug_GLTF_Init())
+					if (Mod_LoadGLTFModel(&mod, filebuf, sz))
+						ret = loadfte(&mod, spec);
+			}
+			delete[] filebuf;
+			delete f;
+		}
+		return ret;
+	}
+}
 
 void genhitboxes(vector<hitbox> &hitboxes)
 {
@@ -3785,18 +4227,18 @@ void genhitboxes(vector<hitbox> &hitboxes)
 		formatstring(tmp, "hitbox%i", hitboxes[i].body);
 		m.name = newstring(tmp);
 		m.hasexplicits = true;
-		memset(&m.explicits, 0, sizeof(m.explicits));
+		m.explicits = {};
 		m.explicits.contents = 0x02000000;
 		m.explicits.surfaceflags = 0x80;
 		m.explicits.body = hitboxes[i].body;
 		m.explicits.geomset = ~0u;
 
 		//spit out some verts
+		Matrix3x4 bm(mjoints[bone]);
+		bm.invert();
 		for (int j = 0; j < 8; j++)
 		{
 			Vec3 p = Vec3((j&1)?hb.mins[0]:hb.maxs[0], (j&2)?hb.mins[1]:hb.maxs[1], (j&4)?hb.mins[2]:hb.maxs[2]);
-			Matrix3x4 bm(mjoints[bone]);
-			bm.invert();
 			p = bm.transform(p);
 			epositions.add(Vec4(p, 0));
 			enormals.add(p);
@@ -4140,7 +4582,7 @@ bool writeiqm(const char *filename)
 		}
 	}
 
-	if(stringdata.length()) hdr.ofs_text = hdr.filesize; hdr.num_text = stringdata.length(); hdr.filesize += hdr.num_text;
+	if(stringdata.length()) hdr.ofs_text = hdr.filesize, hdr.num_text = stringdata.length(), hdr.filesize += hdr.num_text;
 	hdr.num_meshes = meshes.length(); if(meshes.length()) hdr.ofs_meshes = hdr.filesize; hdr.filesize += meshes.length() * sizeof(iqmmesh);
 	uint voffset = hdr.filesize + varrays.length() * sizeof(iqmvertexarray);
 	hdr.num_vertexarrays = varrays.length(); if(varrays.length()) hdr.ofs_vertexarrays = hdr.filesize; hdr.filesize += varrays.length() * sizeof(iqmvertexarray);
@@ -4149,17 +4591,17 @@ bool writeiqm(const char *filename)
 	hdr.filesize += valign + vdata.length();
 	hdr.num_vertexes = numfverts;
 	hdr.num_triangles = triangles.length(); if(triangles.length()) hdr.ofs_triangles = hdr.filesize; hdr.filesize += triangles.length() * sizeof(iqmtriangle);
-	if(neighbors.length()) hdr.ofs_adjacency = hdr.filesize; hdr.filesize += neighbors.length() * sizeof(iqmtriangle);
+	if(neighbors.length()) hdr.ofs_adjacency = hdr.filesize, hdr.filesize += neighbors.length() * sizeof(iqmtriangle);
 	hdr.num_joints = joints.length(); if(joints.length()) hdr.ofs_joints = hdr.filesize; hdr.filesize += joints.length() * sizeof(iqmjoint);
 	hdr.num_poses = poses.length(); if(poses.length()) hdr.ofs_poses = hdr.filesize; hdr.filesize += poses.length() * sizeof(iqmpose);
 	hdr.num_anims = anims.length(); if(anims.length()) hdr.ofs_anims = hdr.filesize; hdr.filesize += anims.length() * sizeof(iqmanim);
 	hdr.num_frames = frames.length(); hdr.num_framechannels = framesize; 
-	if(animdata.length()) hdr.ofs_frames = hdr.filesize; hdr.filesize += animdata.length() * sizeof(ushort); 
-	if(bounds.length()) hdr.ofs_bounds = hdr.filesize; hdr.filesize += bounds.length() * sizeof(float[8]);
-	if(commentdata.length()) hdr.ofs_comment = hdr.filesize; hdr.num_comment = commentdata.length(); hdr.filesize += hdr.num_comment;
-	if (extensions.length()) hdr.ofs_extensions = hdr.filesize; hdr.num_extensions = extensions.length(); hdr.filesize += sizeof(iqmextension) * hdr.num_extensions;
-	if (ext_meshes_fte) {ext_meshes_fte->ofs_data = hdr.filesize; ext_meshes_fte->num_data = meshes_fte.length()*sizeof(iqmext_fte_mesh); hdr.filesize += ext_meshes_fte->num_data;}
-	if (ext_events_fte) {ext_events_fte->ofs_data = hdr.filesize; ext_events_fte->num_data = events_fte.length()*sizeof(iqmext_fte_events); hdr.filesize += ext_events_fte->num_data;}
+	if(animdata.length()) hdr.ofs_frames = hdr.filesize, hdr.filesize += animdata.length() * sizeof(ushort);
+	if(bounds.length()) hdr.ofs_bounds = hdr.filesize, hdr.filesize += bounds.length() * sizeof(float[8]);
+	if(commentdata.length()) hdr.ofs_comment = hdr.filesize, hdr.num_comment = commentdata.length(), hdr.filesize += hdr.num_comment;
+	if (extensions.length()) hdr.ofs_extensions = hdr.filesize, hdr.num_extensions = extensions.length(), hdr.filesize += sizeof(iqmextension) * hdr.num_extensions;
+	if (ext_meshes_fte) ext_meshes_fte->ofs_data = hdr.filesize, ext_meshes_fte->num_data = meshes_fte.length()*sizeof(iqmext_fte_mesh), hdr.filesize += ext_meshes_fte->num_data;
+	if (ext_events_fte) ext_events_fte->ofs_data = hdr.filesize, ext_events_fte->num_data = events_fte.length()*sizeof(iqmext_fte_events), hdr.filesize += ext_events_fte->num_data;
 
 	lilswap(&hdr.version, (sizeof(hdr) - sizeof(hdr.magic))/sizeof(uint));
 
@@ -4283,83 +4725,249 @@ bool writeiqm(const char *filename)
 }
 
 
-uchar qmdl_bestnorm(Vec3 &v)
+static uchar qmdl_bestnorm(Vec3 &v)
 {
-	//FIXME
-	return 0;
+	#define NUMVERTEXNORMALS	162
+	static	float	r_avertexnormals[NUMVERTEXNORMALS][3] = {
+	#include "anorms.h"
+	};
+	uchar best = 0;
+	float bestdot = -FLT_MAX, dot;
+	for (size_t i = 0; i < countof(r_avertexnormals); i++)
+	{
+		dot = DotProduct(v, r_avertexnormals[i]);
+		if (dot > bestdot)
+		{
+			bestdot = dot;
+			best = i;
+		}
+	}
+	return best;
 }
 struct qmdl_vertex_t
 {
 	unsigned char	v[3];
 	unsigned char	normalIndex;
 };
-template<int md16> bool writemdl(const char *filename)
+static bool writemdl(const char *filename, bool md16)
 {
 	if (meshes.length() != 1)
 	{
 		conoutf("warning: mdl output requires exactly one mesh");
-		return false;	//must have ONE mesh only.
+		if (meshes.length() < 0)
+			return false;	//must have ONE mesh only.
+		else
+			conoutf("using first...");
 	}
-	auto mesh = meshes[0];
+	auto mesh = meshes[0];	//should probably favour the mesh with the most verts or something.
 	vertexarray *texcoords = NULL;
 	vertexarray *vertcoords = NULL;
 	vertexarray *vertnorm = NULL;
+	vertexarray *vertbones = NULL;
+	vertexarray *vertweights = NULL;
 	uint skinwidth = 0;
 	uint skinheight = 0;
 	Vec3 offset={0,0,0};
 	Vec3 scale={1,1,1};
 	uint numskins = 0;
 	vector<uchar> skindata;
+	unsigned char *paletteddata;
 
 	loopv(varrays)
 	{
-		if(varrays[i].type == IQM_TEXCOORD && varrays[i].format == IQM_FLOAT && varrays[i].count == 2)
+		if(varrays[i].type == IQM_TEXCOORD && varrays[i].format == IQM_FLOAT && varrays[i].size == 2)
 			texcoords = &varrays[i];
-		if(varrays[i].type == IQM_POSITION && varrays[i].format == IQM_FLOAT && varrays[i].count == 3)
+		if(varrays[i].type == IQM_POSITION && varrays[i].format == IQM_FLOAT && varrays[i].size == 3)
 			vertcoords = &varrays[i];
-		if(varrays[i].type == IQM_NORMAL && varrays[i].format == IQM_FLOAT && varrays[i].count == 3)
+		if(varrays[i].type == IQM_NORMAL && varrays[i].format == IQM_FLOAT && varrays[i].size == 3)
 			vertnorm = &varrays[i];
-//		if(varrays[i].type == IQM_BLENDINDEXES && varrays[i].format == IQM_BYTE && varrays[i].count == 4)
-//			vertbones = &varrays[i];
-//		if(varrays[i].type == IQM_BLENDWEIGHTS && varrays[i].format == IQM_FLOAT && varrays[i].count == 4)
-//			vertweights = &varrays[i];
+		if(varrays[i].type == IQM_BLENDINDEXES && varrays[i].format == IQM_UBYTE && varrays[i].size == 4)
+			vertbones = &varrays[i];
+		if(varrays[i].type == IQM_BLENDWEIGHTS && varrays[i].format == IQM_UBYTE && varrays[i].size == 4)
+			vertweights = &varrays[i];
 	}
 	if (!texcoords)
 	{
 		conoutf("warning: mdl output requires a float texcoord array");
 		return false;	//must have some vertex coords...
 	}
+	if (!vertcoords)
+	{
+		conoutf("warning: mdl output requires a suitable vertex positions array...");
+		return false;	//must have some vertex coords...
+	}
+	if (!vertnorm)
+	{
+		conoutf("warning: mdl output requires a suitable vertex normals array...");
+		return false;	//must have some vertex coords...
+	}
 	float *tcdata = (float*)texcoords->vdata.getbuf();
 
-	skinwidth = 4;
-	skinheight = 4;
-	memset(skindata.reserve(skinwidth*skinheight), 15, skinwidth*skinheight);
+	//the actual mdl limit is really annoying to calculate.
+	if (mesh.numverts >= 1024)
+		conoutf("Writing mdl %s with %u verts exceeds regular limit of %u", filename, mesh.numverts, 1024);
+
+	//read the skin...
+	size_t filesize=0;
+	qbyte *filedata = NULL;
+	auto s = openfile(&stringdata[mesh.material], "rb");
+	if (s)
+	{
+		filesize = s->size();
+		filedata = (qbyte*)malloc(filesize);
+		s->read(filedata, filesize);
+		delete s;
+	}
+	//decode it...
+	fte::ImgTool_SetupPalette();
+	struct pendingtextureinfo *tex = NULL;
+	if (filedata)
+		tex = Image_LoadMipsFromMemory(IF_NOMIPMAP, &stringdata[mesh.material], &stringdata[mesh.material], filedata, filesize);
+	else
+		conoutf("could not open file %s", &stringdata[mesh.material]);
+	if (tex)
+	{	//okay, we have a valid image!
+#if 1
+		//downsize it to work around glquake's limitations. square textures will generally end up 256*256 instead of 512*512 due to that stupid 480 height limit
+		int newwidth = tex->mip[0].width;
+		int newheight = tex->mip[0].height;
+		auto npotup = [](unsigned val)
+		{	//convert to npot, rounding up.
+			unsigned scaled = 1;
+			while(scaled < val)
+				scaled<<=1;
+			return scaled;
+		};
+		while (newwidth*newheight > 640*480/*GL_Upload8 limit*/ || npotup(newwidth)*npotup(newheight) > 1024*512/*GL_Upload32 limit, may be higher thanks to gl_max_size or gl_picmip but really that sucks*/ || newheight > 480/*Mod_LoadAliasModel limit -- weird MAX_LBM_HEIGHT check*/)
+		{
+			newwidth >>= 1;
+			newheight >>= 1;
+		}
+		auto resized = (tex->mip[0].width == newwidth&&tex->mip[0].height == newheight)?NULL:Image_ResampleTexture(tex->encoding, tex->mip[0].data, tex->mip[0].width, tex->mip[0].height, NULL, newwidth, newheight);
+		if (resized)
+		{
+			tex->mip[0].data = resized;
+			tex->mip[0].datasize = 0; /*o.O*/
+			tex->mip[0].width = newwidth;
+			tex->mip[0].height = newheight;
+		}
+#endif
+
+		//palettize it, to match the q1 palette.
+		qboolean allowedformats[PTI_MAX] = {};
+		allowedformats[PTI_P8]=qtrue;
+		//FIXME: add hexen2's alpha stuff?
+		conoutf("Palettizing \"%s\" (%u*%u)", &stringdata[mesh.material], tex->mip[0].width, tex->mip[0].height);
+		Image_ChangeFormat(tex, allowedformats, PTI_INVALID, "foo");
+
+		skinwidth = tex->mip[0].width;
+		skinheight = tex->mip[0].height;
+	}
+	else
+	{	//texture coords are ints. if we don't have a large enough texture then we don't have much texture coord precision either, so use something reasonable.
+		skinwidth = 128;
+		skinheight = 128;
+	}
+	paletteddata = skindata.reserve(skinwidth*skinheight);
+	if (tex)
+	{
+		memcpy(paletteddata, tex->mip[0].data, skinwidth*skinheight);
+		*paletteddata^=1;	//try to work around glquake's flood fill...
+	}
+	else
+	{	//fill with some sort of grey.
+		memset(paletteddata, 8, skinwidth*skinheight);
+		paletteddata[0] = 7;	//work around flood filling...
+	}
 	skindata.advance(skinwidth*skinheight);
 	numskins++;
 
 	//we're going to need the transformed pose data, without any bone weights getting in the way.
 	vector<Vec3> vpos, vnorm;
 	Vec3 min={FLT_MAX,FLT_MAX,FLT_MAX}, max={-FLT_MAX,-FLT_MAX,-FLT_MAX};
-	loopv(anims)
+	if (!anims.length())
+	{
+		Vec3 *outv = vpos.reserve(mesh.numverts);
+		Vec3 *outn = vnorm.reserve(mesh.numverts);
+
+		auto invert = (float*)vertcoords->vdata.getbuf();
+		auto innorm = (float*)vertnorm->vdata.getbuf();
+//		auto inbones = (uchar*)vertbones->vdata.getbuf();
+//		auto inweights = (uchar*)vertweights->vdata.getbuf();
+
+		//FIXME: generate bone matricies from base pose? or just use the vertex data as-is...
+		for (uint i = mesh.firstvert; i < mesh.firstvert+mesh.numverts; i++, outv++, outn++)
+		{
+			//FIXME: generate vert's matrix
+
+			//transform each vert
+			*outv = Vec3(invert[i*3+0], invert[i*3+1], invert[i*3+2]);
+
+			//bound it to find the model's extents
+			for (uint c = 0; c < 3; c++)
+			{
+				if (min.v[c] > outv->v[c])
+					min.v[c] = outv->v[c];
+				if (max.v[c] < outv->v[c])
+					max.v[c] = outv->v[c];
+			}
+
+			*outn = Vec3(innorm[i*3+0], innorm[i*3+1], innorm[i*3+2]);
+		}
+		vpos.advance(mesh.numverts);
+		vnorm.advance(mesh.numverts);
+	}
+	else loopv(anims)
 	{
 		anim &a = anims[i];
 		Vec3 *outv = vpos.reserve(mesh.numverts*a.numframes);
 		Vec3 *outn = vnorm.reserve(mesh.numverts*a.numframes);
 
-		Vec3 *invert = (Vec3*)vertcoords->vdata.getbuf();
-		Vec3 *innorm = (Vec3*)vertnorm->vdata.getbuf();
-//		uchar *inbones = (uchar*)vertbones->vdata.getbuf();
-//		Vec4 *inweights = (Vec4*)vertweights->vdata.getbuf();
+//		Matrix3x4 bonepose[joints.length()];
+		vector<Matrix3x4> bonepose;
+		bonepose.reserve(joints.length());
 
-		for (int j = 0; j < a.numframes; j++)
+		auto invert = (float*)vertcoords->vdata.getbuf();
+		auto innorm = (float*)vertnorm->vdata.getbuf();
+		auto inbones = vertbones?(uchar*)vertbones->vdata.getbuf():NULL;
+		auto inweights = vertweights?(uchar*)vertweights->vdata.getbuf():NULL;
+
+		if (!inbones || !inweights)
+			printf("no bone indexes\n");
+
+		for (uint j = 0; j < a.numframes; j++)
 		{
-			//FIXME: generate bone matricies
-			for (int i = mesh.firstvert; i < mesh.numverts; i++, outv++, outn++)
+			//build absolute poses.
+			frame &fr = frames[a.firstframe+j];
+			for (int b = 0; b < fr.pose.length(); b++)
 			{
-				//FIXME: generate vert's matrix
+				auto &frpose = fr.pose[b];
+				bonepose[b] = Matrix3x4(Quat(frpose.tr.orient), Vec3(frpose.tr.pos), Vec3(frpose.tr.scale));
+				if (frpose.boneparent >= 0)
+					bonepose[b]	= bonepose[frpose.boneparent] * bonepose[b];
+			}
+			//done with parents... now we want to invert them
+			for (int b = 0; b < fr.pose.length(); b++)
+			{
+				Matrix3x4 invbind = Matrix3x4(mjoints[b]);
+				//invbind.invert();
+				bonepose[b] = bonepose[b] * invbind;
+			}
+			for (uint i = mesh.firstvert; i < mesh.numverts; i++, outv++, outn++)
+			{
+				//generate per-vert matrix...
+				Matrix3x4 blend;
+				blend *= 0;
+				if (inweights && inbones)
+				{
+					if (inweights[i*4+0]) blend += bonepose[inbones[i*4+0]] * (inweights[i*4+0]/255.0);
+					if (inweights[i*4+1]) blend += bonepose[inbones[i*4+1]] * (inweights[i*4+1]/255.0);
+					if (inweights[i*4+2]) blend += bonepose[inbones[i*4+2]] * (inweights[i*4+2]/255.0);
+					if (inweights[i*4+3]) blend += bonepose[inbones[i*4+3]] * (inweights[i*4+3]/255.0);
+				}
 
 				//transform each vert
-				*outv = invert[i];
+				*outv = blend.transform(Vec3(invert[i*3+0], invert[i*3+1], invert[i*3+2]));
 
 				//bound it to find the model's extents
 				for (uint c = 0; c < 3; c++)
@@ -4370,14 +4978,14 @@ template<int md16> bool writemdl(const char *filename)
 						max.v[c] = outv->v[c];
 				}
 
-				*outn = innorm[i];
+				*outn = blend.transform3(Vec3(innorm[i*3+0], innorm[i*3+1], innorm[i*3+2]));
 			}
 			vpos.advance(mesh.numverts);
 			vnorm.advance(mesh.numverts);
 		}
 	}
 
-	offset = -min;
+	offset = min;
 	scale = (max-min)/255; //ignore low order info here
 
 	stream *f = openfile(filename, "wb");
@@ -4405,27 +5013,36 @@ template<int md16> bool writemdl(const char *filename)
 
 	f->putlil((uint)mesh.numverts);
 	f->putlil((uint)mesh.numtris);
-	f->putlil((uint)anims.length());	//numanims
+	if (!anims.length())
+		f->putlil((uint)1);					//numanims
+	else
+		f->putlil((uint)anims.length());	//numanims
 
 	f->putlil((uint)0);	//synctype
 	f->putlil((uint)modelflags);	//flags
 	f->putlil(0.f);	//size
 
 	//skins
-	for (int i = 0; i < numskins; i++)
+	for (uint i = 0; i < numskins; i++)
 	{
 		f->putlil((uint)0);	//ALIAS_SKIN_SINGLE
 		f->write(skindata.getbuf()+i*skinwidth*skinheight, skinwidth*skinheight);
 	}
 	//texcoords
-	for (int i = mesh.firstvert; i < mesh.numverts; i++)
+	for (uint i = mesh.firstvert; i < mesh.firstvert+mesh.numverts; i++)
 	{
+		float s = (tcdata[i*2+0])*skinwidth;
+		float t = (tcdata[i*2+1])*skinheight;
+		s -= 0.5;	//glquake has some annoying half-texel offset thing.
+		t -= 0.5;
+		s = bound(0, s, skinwidth);
+		t = bound(0, t, skinwidth);
 		f->putlil((uint)(0?32:0));	//onseam. no verts are ever onseam for us, as we don't do that nonsense here.
-		f->putlil((int)((tcdata[i*2+0]+.5)*skinwidth));	//mdl texcoords are ints, in texels. which sucks, but what can you do...
-		f->putlil((int)((tcdata[i*2+1]+.5)*skinheight));
+		f->putlil((int)s);	//mdl texcoords are ints, in texels. which sucks, but what can you do...
+		f->putlil((int)t);
 	}
 	//tris
-	for (int i = mesh.firsttri; i < mesh.firsttri+mesh.numtris; i++)
+	for (uint i = mesh.firsttri; i < mesh.firsttri+mesh.numtris; i++)
 	{
 		f->putlil((uint)1);	//faces front. All are effectively front-facing for us. This avoids annoying tc additions.
 		f->putlil((uint)triangles[i].vert[0]);
@@ -4435,90 +5052,158 @@ template<int md16> bool writemdl(const char *filename)
 	//animations
 	vector<qmdl_vertex_t> high, low;
 	size_t voffset = 0;
-	loopv(anims)
-	{
-		anim &a = anims[i];
-		for (int j = 0; j < a.numframes; j++)
-		{
-			qmdl_vertex_t *th=high.reserve(mesh.numverts),*tl=low.reserve(mesh.numverts);
-			for (int i = mesh.firstvert; i < mesh.numverts; i++, th++, tl++)
-			{
-				int l;
-				for (uint c = 0; c < 3; c++)
-				{
-					l = (((vpos[voffset][c]-offset[c])*256) / scale[c]);
-					if (l<0)		l = 0;
-					if (l > 0xff00)	l = 0xff00;	//0xffff would exceed the bounds values, so don't use it.
-					th->v[c] = l>>8;
-					tl->v[c] = l&0xff;
-				}
-				tl->normalIndex = th->normalIndex = qmdl_bestnorm(vnorm[voffset]);
 
-				voffset++;
+	if (!anims.length())
+	{
+		qmdl_vertex_t *th=high.reserve(mesh.numverts),*tl=low.reserve(mesh.numverts);
+		for (uint i = mesh.firstvert; i < mesh.numverts; i++, th++, tl++)
+		{
+			int l;
+			for (uint c = 0; c < 3; c++)
+			{
+				l = (((vpos[voffset][c]-offset[c])*256) / scale[c]);
+				if (l<0)		l = 0;
+				if (l > 0xff00)	l = 0xff00;	//0xffff would exceed the bounds values, so don't use it.
+				th->v[c] = l>>8;
+				tl->v[c] = l&0xff;
 			}
-			high.advance(mesh.numverts);
-			low.advance(mesh.numverts);
+			tl->normalIndex = th->normalIndex = qmdl_bestnorm(vnorm[voffset]);
+
+			voffset++;
 		}
+		high.advance(mesh.numverts);
+		low.advance(mesh.numverts);
+
+		voffset = 0;
+		f->putlil((uint)0);	//single-pose type
+
+		char name[16]="base";
+		qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}};
+		for (uint k = 0; k < mesh.numverts; k++)
+		{
+			for (uint c = 0; c < 3; c++)
+			{
+				if (min.v[c] > high[voffset+k].v[c])
+					min.v[c] = high[voffset+k].v[c];
+				if (max.v[c] < high[voffset+k].v[c])
+					max.v[c] = high[voffset+k].v[c];
+			}
+		}
+		f->put(min);
+		f->put(max);
+
+		name[countof(name)-1] = 0;
+		for (uint k = 0; k < countof(name); k++)
+			f->put(name[k]);
+
+		f->write(&high[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
+		if (md16)
+			f->write(&low[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
+		voffset += mesh.numverts;
 	}
-	voffset = 0;
-	loopv(anims)
+	else
 	{
-		anim &a = anims[i];
-		if (a.numframes == 1)
-			f->putlil((uint)0);	//single-pose type
-		else
+		loopv(anims)
 		{
-			f->putlil((uint)1);	//anim type
-			f->putlil((uint)a.numframes);
-
-			qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}};
-			for (uint k = 0; k < mesh.numverts*a.numframes; k++)
+			anim &a = anims[i];
+			for (uint j = 0; j < a.numframes; j++)
 			{
-				for (uint c = 0; c < 3; c++)
+				qmdl_vertex_t *th=high.reserve(mesh.numverts),*tl=low.reserve(mesh.numverts);
+
+				for (uint i = mesh.firstvert; i < mesh.numverts; i++, th++, tl++)
 				{
-					if (min.v[c] > high[voffset+k].v[c])
-						min.v[c] = high[voffset+k].v[c];
-					if (max.v[c] < high[voffset+k].v[c])
-						max.v[c] = high[voffset+k].v[c];
+					int l;
+					for (uint c = 0; c < 3; c++)
+					{
+						l = (((vpos[voffset][c]-offset[c])*256) / scale[c]);
+						if (l<0)		l = 0;
+						if (l > 0xff00)	l = 0xff00;	//0xffff would exceed the bounds values, so don't use it.
+						th->v[c] = l>>8;
+						tl->v[c] = l&0xff;
+					}
+					tl->normalIndex = th->normalIndex = qmdl_bestnorm(vnorm[voffset]);
+
+					voffset++;
 				}
+				high.advance(mesh.numverts);
+				low.advance(mesh.numverts);
 			}
-			f->put(min);
-			f->put(max);
-			for (int j = 0; j < a.numframes; j++)
-				f->putlil(1.0f/a.fps);	//intervals. we use the same value for each
 		}
-
-		for (int j = 0; j < a.numframes; j++)
+		voffset = 0;
+		loopv(anims)
 		{
-			char name[16]={0};
-			qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}};
-			for (uint k = 0; k < mesh.numverts; k++)
+			anim &a = anims[i];
+			if (a.numframes == 1)
+				f->putlil((uint)0);	//single-pose type
+			else
 			{
-				for (uint c = 0; c < 3; c++)
+				f->putlil((uint)1);	//anim type
+				f->putlil((uint)a.numframes);
+
+				qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}};
+				for (uint k = 0; k < mesh.numverts*a.numframes; k++)
 				{
-					if (min.v[c] > high[voffset+k].v[c])
-						min.v[c] = high[voffset+k].v[c];
-					if (max.v[c] < high[voffset+k].v[c])
-						max.v[c] = high[voffset+k].v[c];
+					for (uint c = 0; c < 3; c++)
+					{
+						if (min.v[c] > high[voffset+k].v[c])
+							min.v[c] = high[voffset+k].v[c];
+						if (max.v[c] < high[voffset+k].v[c])
+							max.v[c] = high[voffset+k].v[c];
+					}
 				}
+				f->put(min);
+				f->put(max);
+				for (uint j = 0; j < a.numframes; j++)
+					f->putlil(1.0f/a.fps);	//intervals. we use the same value for each
 			}
-			f->put(min);
-			f->put(max);
 
-			strncpy(name, &stringdata[a.name], sizeof(name));
-			f->put(name);
+			for (uint j = 0; j < a.numframes; j++)
+			{
+				char name[16]={0};
+				qmdl_vertex_t min={{255,255,255}}, max={{0,0,0}};
+				for (uint k = 0; k < mesh.numverts; k++)
+				{
+					for (uint c = 0; c < 3; c++)
+					{
+						if (min.v[c] > high[voffset+k].v[c])
+							min.v[c] = high[voffset+k].v[c];
+						if (max.v[c] < high[voffset+k].v[c])
+							max.v[c] = high[voffset+k].v[c];
+					}
+				}
+				f->put(min);
+				f->put(max);
 
-			f->write(&high[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
-			if (md16)
-				f->write(&low[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
-			voffset += mesh.numverts;
+				strncpy(name, &stringdata[a.name], sizeof(name));
+				name[countof(name)-1] = 0;
+				for (uint k = 0; k < countof(name); k++)
+					f->put(name[k]);
+
+				f->write(&high[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
+				if (md16)
+					f->write(&low[voffset], sizeof(qmdl_vertex_t)*mesh.numverts);
+				voffset += mesh.numverts;
+			}
 		}
 	}
 
 	delete f;
 	return true;
 }
+static bool writeqmdl(const char *filename)
+{
+	return writemdl(filename, false);
+}
+static bool writemd16(const char *filename)
+{
+	return writemdl(filename, true);
+}
 
+static bool writemd3(const char *filename)
+{
+	fprintf(stderr, "writemd3 is not implemented yet\n");
+	return false;
+}
 
 void help(bool exitstatus = EXIT_SUCCESS)
 {
@@ -4808,15 +5493,17 @@ bool parseanimfield(const char *tok, char **line, filespec &spec, bool defaults)
 
 struct
 {
+	const char *extname;
 	bool (*write)(const char *filename);
 	const char *cmdname;
 	const char *altcmdname;
 } outputtypes[] =
 {
-	{writeiqm,		"output_iqm"},
-	{writemdl<0>,	"output_qmdl"},
-	{writemdl<1>,	"output_md16"},
-//	{writemd3,		"output_md3"},
+	{".vvm",	writeiqm,		"output_vvm"},
+	{".iqm",	writeiqm,		"output_iqm"},
+	{".mdl",	writeqmdl,		"output_qmdl"},
+	{".md16",	writemd16,		"output_md16"},
+	{".md3",	writemd3,		"output_md3"},
 };
 
 void parsecommands(char *filename, const char *outfiles[countof(outputtypes)], vector<filespec> &infiles, vector<hitbox> &hitboxes)
@@ -5036,12 +5723,24 @@ int main(int argc, char **argv)
 			const char *type = strrchr(argv[i], '.');
 			if (type && (!strcasecmp(type, ".cmd")||!strcasecmp(type, ".cfg")||!strcasecmp(type, ".txt")||!strcasecmp(type, ".qc")))	//.qc to humour halflife fanboys
 				parsecommands(argv[i], outfiles, infiles, hitboxes);
-			else if(!outfiles[0] && !outfiles[1] && !outfiles[2])
-				outfiles[0] = argv[i];	//first arg is the output name, if its not an export script thingie.
 			else
 			{
-				infiles.add(inspec).file = argv[i];
-				inspec.reset();
+				size_t j;
+				for (j = 0; j < countof(outfiles); j++)
+					if (outfiles[j])
+						break;
+				if(j == countof(outfiles))
+				{
+					for (j = countof(outfiles); j --> 0; )
+						if (type && !strcasecmp(type, outputtypes[j].extname))
+							break;
+					outfiles[j] = argv[i];	//first arg is the output name, if its not an export script thingie.
+				}
+				else
+				{
+					infiles.add(inspec).file = argv[i];
+					inspec.reset();
+				}
 			}
 		}
 	}
@@ -5049,7 +5748,17 @@ int main(int argc, char **argv)
 	size_t n;
 	for (n = 0; n < countof(outputtypes) && !outfiles[n]; n++);
 	if(n == countof(outfiles)) fatal("no output file specified");
-	if(infiles.empty()) fatal("no input files specified");
+	if(infiles.empty())
+	{
+		if (outfiles[0])
+		{
+			inspec.reset();
+			infiles.add(inspec).file = outfiles[0];
+			outfiles[0] = NULL;
+		}
+		else
+			fatal("no input files specified");
+	}
 
 	if(gscale != 1) printf("scale: %f\n", escale);
 	if(gmeshtrans != Vec3(0, 0, 0)) printf("mesh translate: %f, %f, %f\n", gmeshtrans.x, gmeshtrans.y, gmeshtrans.z);
@@ -5085,6 +5794,14 @@ int main(int argc, char **argv)
 		{
 			if(!loadobj(infile, inspec)) fatal("failed reading: %s", infile);
 		}
+		else if(!strcasecmp(type, ".glb"))
+		{
+			if(!fte::loadglb(infile, inspec)) fatal("failed reading: %s", infile);
+		}
+		else if(!strcasecmp(type, ".gltf"))
+		{
+			if(!fte::loadgltf(infile, inspec)) fatal("failed reading: %s", infile);
+		}
 		else fatal("unknown file type: %s", type);	 
 	}
 
diff --git a/iqm/util.h b/iqm/util.h
index a9196afb9..08eca8056 100644
--- a/iqm/util.h
+++ b/iqm/util.h
@@ -20,9 +20,13 @@
 #ifndef M_PI
 #define M_PI 3.1415926535897932384626433832795
 #endif
+#ifndef strcasecmp
 #define strcasecmp _stricmp
+#endif
+#ifndef strncasecmp
 #define strncasecmp _strnicmp
 #endif
+#endif
 
 typedef unsigned char uchar;
 typedef unsigned short ushort;
@@ -30,7 +34,7 @@ typedef unsigned int uint;
 typedef signed long long int llong;
 typedef unsigned long long int ullong;
 
-inline void *operator new(size_t size)
+/*inline void *operator new(size_t size)
 {
 	void *p = malloc(size);
 	if(!p) abort();
@@ -44,7 +48,9 @@ inline void *operator new[](size_t size)
 }
 inline void operator delete(void *p) { if(p) free(p); }
 inline void operator delete[](void *p) { if(p) free(p); }
-
+inline void operator delete(void *p, size_t sz) { if(p) free(p); }
+inline void operator delete[](void *p, size_t sz) { if(p) free(p); }
+*/
 inline void *operator new(size_t, void *p) { return p; }
 inline void *operator new[](size_t, void *p) { return p; }
 inline void operator delete(void *, void *) {}
@@ -77,7 +83,9 @@ static inline T min(T a, T b)
 	return a < b ? a : b;
 }
 
+#ifndef countof
 #define countof(n) (sizeof(n)/sizeof(n[0]))
+#endif
 #define clamp(a,b,c) (max(b, min(a, c)))
 
 #define loop(v,m) for(int v = 0; v<int(m); v++)
@@ -100,7 +108,6 @@ static inline T min(T a, T b)
 #pragma warning (disable: 4996) // 'strncpy' was declared deprecated
 #endif
 
-#define strcasecmp _stricmp
 #define PATHDIV '\\'
 #else
 #define __cdecl
@@ -722,6 +729,7 @@ struct Vec3
 	Vec3() {}
 	Vec3(double x, double y, double z) : x(x), y(y), z(z) {}
 	explicit Vec3(const double *v) : x(v[0]), y(v[1]), z(v[2]) {}
+	explicit Vec3(const float *v) : x(v[0]), y(v[1]), z(v[2]) {}
 	explicit Vec3(const Vec4 &v);
 
 	double &operator[](int i) { return v[i]; }
@@ -792,6 +800,7 @@ struct Vec4
 	Vec4(double x, double y, double z, double w) : x(x), y(y), z(z), w(w) {}
 	explicit Vec4(const Vec3 &p, double w = 0) : x(p.x), y(p.y), z(p.z), w(w) {}
 	explicit Vec4(const double *v) : x(v[0]), y(v[1]), z(v[2]), w(v[3]) {}
+	explicit Vec4(const float *v) : x(v[0]), y(v[1]), z(v[2]), w(v[3]) {}
 
 	double &operator[](int i)       { return v[i]; }
 	double  operator[](int i) const { return v[i]; }
@@ -853,6 +862,7 @@ struct Quat : Vec4
 {
 	Quat() {}
 	Quat(double x, double y, double z, double w) : Vec4(x, y, z, w) {}
+	Quat(const float *ptr) : Vec4(ptr) {}
 	Quat(double angle, const Vec3 &axis)
 	{
 		double s = sin(0.5*angle);
@@ -1073,6 +1083,7 @@ struct Matrix3x4
 	Matrix3x4 &operator*=(const Matrix3x4 &o) { return (*this = *this * o); }
 
 	Vec3 transform(const Vec3 &o) const { return Vec3(a.dot(o), b.dot(o), c.dot(o)); }
+	Vec3 transform3(const Vec3 &o) const { return Vec3(a.dot3(o), b.dot3(o), c.dot3(o)); }
 };
 
 void conoutf(const char *s, ...)
diff --git a/plugins/models/gltf.c b/plugins/models/gltf.c
index 31bdb394b..e73897c5a 100644
--- a/plugins/models/gltf.c
+++ b/plugins/models/gltf.c
@@ -1,10 +1,17 @@
 #if !defined(GLQUAKE) && !defined(FTEENGINE)
 #define GLQUAKE	//this is shit, but ensures index sizes come out the right size
 #endif
-#include "quakedef.h"
+#ifdef IQMTOOL
+#define FTEPLUGIN
+#endif
 #include "../plugin.h"
 #include "com_mesh.h"
 
+#ifdef IQMTOOL
+#define Con_Printf printf
+#define Con_DPrintf printf
+#endif
+
 #ifdef SKELETALMODELS
 #define GLTFMODELS
 #endif
@@ -15,32 +22,53 @@
 		texture modes (like clamp) must match on both axis (either both clamp or both wrap, no mixing)
 		mirrored-repeat not supported
 		mip-mag-mip filters must match (all linear, or all nearest)
+		gltf1: techniques are disabled by default.
+		gltf1: symbol names are linked via defines. this results in potential conflicts.
+		gltf1: static uniforms are not set.
 	animations:
-		input framerates are not well-defined. this can result in issues when converting to other formats (especially with stepping anims).
-		morph targets are not supported.
+		input framerates are not well-defined. this can result in issues when converting to other formats (especially with stepping anims). this does not necessarily affect directly-loaded animations.
 		total nodes(+joints) must be < MAX_BONES, and ideally <MAX_GPU_BONES too but it is sufficient for that to be per-mesh.
 	meshes:
 		multiple texture coord sets are not supported.
 		additional colours/weights attributes are not supported.
 		multiple meshes with the same material will not be merged.
 	scene:
-		cameras can be parsed, but are not necessarily useful as they are not exposed to the gamecode.
+		cameras are parsed, but are not necessarily useful as they are not exposed to the gamecode.
 	extensions:
-		no KHR_draco_mesh_compression
-		unknown extensions will result in warning spam for each occurence.
-		gltf1 is NOT supported, only gltf2.
+		no KHR_draco_mesh_compression, so many sample models will fail.
+		unknown extensions will result in warning spam for each occurence. :(
 */
 
 
 //'The units for all linear distances are meters.'
 //'feh: 1 metre is approx. 26.24671916 qu.'
 //if the player is 1.6m tall, and the player's model is around 48qu, then 1m=30qu, which is a slightly nicer number to work with, and 1qu is a really poorly defined unit.
-#define GLTFSCALE 30
+#define MAX_MORPHWEIGHTS 16	//we're required to support up to 8 accessors (so mandatory max changes according to supplied data, so 8 morphs with just positions, 4 with positions+normals, or 2 with positions+normals+tangents - plus the base info)
 
 #ifdef GLTFMODELS
 static plugmodfuncs_t *modfuncs;
 static plugfsfuncs_t *filefuncs;
 
+static cvar_t *mod_gltf_scale;
+static cvar_t *mod_gltf_fixbuggyanims;
+static cvar_t *mod_gltf_privatematerials;
+static cvar_t *mod_gltf_ignoretechniques;
+
+#include <stdarg.h>
+void VARGS Q_snprintfcat (char *dest, size_t size, const char *fmt, ...)
+{
+	va_list		argptr;
+
+	size_t skip = strlen(dest);
+	dest += skip;
+	size -= skip;
+
+	va_start (argptr, fmt);
+	vsnprintf(dest, size, fmt, argptr);
+	va_end (argptr);
+}
+
+
 typedef struct json_s
 {
 	const char *bodystart;
@@ -363,7 +391,7 @@ static json_t *JSON_Parse(json_t *t, const char *namestart, const char *nameend,
 					continue;
 				}
 				break;
-			} 
+			}
 
 			JSON_SkipWhite(json, jsonpos, jsonlen);
 			if (*jsonpos < jsonlen && json[*jsonpos] == ']')
@@ -475,12 +503,14 @@ static qintptr_t JSON_GetInteger(json_t *t, const char *child, int fallback)
 	}
 	return fallback;
 }
+#ifndef SERVERONLY//ffs
 static qintptr_t JSON_GetIndexedInteger(json_t *t, unsigned int idx, int fallback)
 {
 	char idxname[MAX_QPATH];
 	Q_snprintf(idxname, sizeof(idxname), "%u", idx);
 	return JSON_GetInteger(t, idxname, fallback);
 }
+#endif
 static double JSON_GetFloat(json_t *t, const char *child, double fallback)
 {
 	if (child)
@@ -556,7 +586,7 @@ static void JSON_FlagAsUsed(json_t *t, const char *child)
 }
 static void JSON_WarnIfChild(json_t *t, const char *child, int *warnlimit)
 {
-	t = JSON_FindChild(t, child); 
+	t = JSON_FindChild(t, child);
 	if (t)
 	{
 		char path[8192];
@@ -580,7 +610,7 @@ static unsigned int FromBase64(char c)
 		return 62;
 	if (c == '/')
 		return 63;
-	return 64;
+	return 64;	//'=' for no-more/padding.
 }
 //fancy parsing of content. NOTE: doesn't bother to handle escape codes, which shouldn't be present (\u for ascii chars is horribly wasteful).
 static void *JSON_MallocDataURI(json_t *t, size_t *outlen)
@@ -633,7 +663,7 @@ static void *JSON_MallocDataURI(json_t *t, size_t *outlen)
 						break;
 					*out++ = (c2<<4) | (c3>>2);
 					c4 = FromBase64(*in++);
-					if (c3 >= 64)
+					if (c4 >= 64)
 						break;
 					*out++ = (c3<<6) | (c4>>0);
 				}
@@ -676,11 +706,13 @@ typedef struct gltf_s
 	struct model_s *mod;
 	unsigned int numsurfaces;
 	json_t *r;
+	int ver;
 
 	int *bonemap;//[MAX_BONES];	//remap skinned bones. I hate that we have to do this.
 	struct gltfbone_s
 	{
 		char name[32];
+		char jointname[32];	//gltf1 only
 		int parent;
 		int camera;
 		double amatrix[16];
@@ -699,6 +731,55 @@ typedef struct gltf_s
 	struct gltf_buffer buffers[64];
 } gltf_t;
 
+static void GLTF_FlagExtras(json_t *node)
+{
+	JSON_FindChild(node, "extensions");	//warn about child extensions, but not the extensions table itself.
+	JSON_FlagAsUsed(node, "extras");	//don't warn about application-specific extras
+}
+
+static json_t *GLTF_FindJSONIDParent(struct gltf_s *gltf, json_t *parent, json_t *id, quintptr_t *idx)
+{
+	if (gltf->ver == 1)
+	{	//gltf1 uses string-based names
+		char name[64];
+		JSON_ReadBody(id, name, sizeof(name));
+		id = parent;
+		if (idx)
+			*idx = 0;
+		if (id)
+		for (id = id->child; id; id = id->sibling)
+		{
+			if (!strcmp(id->name, name))
+			{
+				id->used = true;
+				return id;
+			}
+			if (idx)
+				*idx += 1;
+		}
+		return NULL;
+	}
+	else
+	{	//gltf2 uses array indexes.
+		quintptr_t num;
+		num = id?JSON_GetInteger(id, NULL, -1):-1;
+		if (idx)
+			*idx = num;
+		return JSON_FindIndexedChild(parent, NULL, num);
+	}
+}
+static json_t *GLTF_FindJSONID(struct gltf_s *gltf, const char *restype, json_t *id, quintptr_t *idx)
+{	//if there's no scene info, treat it as "-1"
+	return GLTF_FindJSONIDParent(gltf, JSON_FindChild(gltf->r, restype), id, idx);
+}
+static json_t *GLTF_FindJSONID_First(struct gltf_s *gltf, const char *restype, json_t *id, quintptr_t *idx)
+{	//if there's no scene info, treat it as "0"
+
+	if (!id && gltf->ver > 1)
+		return JSON_FindIndexedChild(gltf->r, restype, 0);
+	return GLTF_FindJSONIDParent(gltf, JSON_FindChild(gltf->r, restype), id, idx);
+}
+
 static void GLTF_RelativePath(const char *base, const char *relative, char *out, size_t outsize)
 {
 	size_t t;
@@ -755,18 +836,30 @@ static void GLTF_RelativePath(const char *base, const char *relative, char *out,
 	*out = 0;
 }
 
-static struct gltf_buffer *GLTF_GetBufferData(gltf_t *gltf, int bufferidx)
+static struct gltf_buffer *GLTF_GetBufferData(gltf_t *gltf, json_t *bufferid)
 {
-	json_t *b = JSON_FindIndexedChild(gltf->r, "buffers", bufferidx);
+	quintptr_t bufferidx;
+	json_t *b = GLTF_FindJSONID(gltf, "buffers", bufferid, &bufferidx);
 	json_t *uri = JSON_FindChild(b, "uri");
 	size_t length = JSON_GetUInteger(b, "byteLength", 0);
 	struct gltf_buffer *out;
 
-//	JSON_WarnIfChild(b, "name");
-//	JSON_WarnIfChild(b, "extensions");
-//	JSON_WarnIfChild(b, "extras");
+	if (gltf->ver <= 1)
+	{
+		char body[64];
+		JSON_ReadBody(bufferid, body, sizeof(body));
+		if (!strcmp(body, "binary_glTF") && !gltf->buffers[0].malloced && gltf->buffers[0].data)
+			return &gltf->buffers[0];
+		else
+			bufferidx++;
 
-	if (bufferidx < 0 || bufferidx >= countof(gltf->buffers))
+		JSON_FlagAsUsed(b, "type");	//default: "arraybuffer"... yeah, not relevant to anything.
+	}
+	JSON_FlagAsUsed(b, "name");
+	GLTF_FlagExtras(b);
+
+
+	if (bufferidx >= countof(gltf->buffers))
 		return NULL;
 	out = &gltf->buffers[bufferidx];
 
@@ -809,28 +902,27 @@ struct gltf_bufferview
 	size_t length;
 	int bytestride;
 };
-static qboolean GLTF_GetBufferViewData(gltf_t *gltf, int bufferview, struct gltf_bufferview *view)
+static qboolean GLTF_GetBufferViewData(gltf_t *gltf, json_t *bufferviewid, struct gltf_bufferview *view)
 {
 	struct gltf_buffer *buf;
-	json_t *bv = JSON_FindIndexedChild(gltf->r, "bufferViews", bufferview);
+	json_t *bv = GLTF_FindJSONID(gltf, "bufferViews", bufferviewid, NULL);
 	size_t offset;
 	if (!bv)
 		return false;
 
-	buf = GLTF_GetBufferData(gltf, JSON_GetInteger(bv, "buffer", 0));
+	buf = GLTF_GetBufferData(gltf, JSON_FindChild(bv, "buffer"));
 	if (!buf)
 		return false;
 	offset = JSON_GetUInteger(bv, "byteOffset", 0);
 	view->data = (char*)buf->data + offset;
 	view->length = JSON_GetUInteger(bv, "byteLength", 0);	//required
-	view->bytestride = JSON_GetInteger(bv, "byteStride", 0);
+	view->bytestride = (gltf->ver<=1)?0:JSON_GetInteger(bv, "byteStride", 0);
 	if (offset + view->length > buf->length)
 		return false;
 
 	JSON_FlagAsUsed(bv, "target");	//required, but not useful for us.
 	JSON_FlagAsUsed(bv, "name");
-//	JSON_WarnIfChild(bv, "extensions");
-//	JSON_WarnIfChild(bv, "extras");
+	GLTF_FlagExtras(bv);
 	return true;
 }
 //accessors are basically VAs blocks that refer inside a bufferview/VBO.
@@ -842,13 +934,13 @@ struct gltf_accessor
 
 	int componentType;		//5120 BYTE, 5121 UNSIGNED_BYTE, 5122 SHORT, 5123 UNSIGNED_SHORT, 5125 UNSIGNED_INT, 5126 FLOAT
 	qboolean normalized;
-	int count;
+	size_t count;
 	int type;	//1,2,3,4 says component count, 256|(4,9,16) for square matricies...
 
 	double mins[16];
 	double maxs[16];
 };
-static qboolean GLTF_GetAccessor(gltf_t *gltf, int accessorid, struct gltf_accessor *out)
+static qboolean GLTF_GetAccessor(gltf_t *gltf, json_t *accessorid, struct gltf_accessor *out)
 {
 	struct gltf_bufferview bv;
 	json_t *a, *mins, *maxs;
@@ -856,17 +948,22 @@ static qboolean GLTF_GetAccessor(gltf_t *gltf, int accessorid, struct gltf_acces
 	int j;
 	memset(out, 0, sizeof(*out));
 
-	a = JSON_FindIndexedChild(gltf->r, "accessors", accessorid);
+	a = GLTF_FindJSONID(gltf, "accessors", accessorid, NULL);
 	if (!a)
 		return false;
 
-	if (!GLTF_GetBufferViewData(gltf, JSON_GetInteger(a, "bufferView", 0), &bv))
+	JSON_FlagAsUsed(a, "name");
+
+	if (!GLTF_GetBufferViewData(gltf, JSON_FindChild(a, "bufferView"), &bv))
 		return false;
 	offset = JSON_GetUInteger(a, "byteOffset", 0);
 	if (offset > bv.length)
 		return false;
 	out->length = bv.length - offset;
-	out->bytestride = bv.bytestride;
+	if (gltf->ver <= 1)
+		out->bytestride = JSON_GetInteger(a, "byteStride", 0);
+	else
+		out->bytestride = bv.bytestride;
 	out->componentType = JSON_GetInteger(a, "componentType", 0);
 	out->normalized = JSON_GetInteger(a, "normalized", false);
 	out->count = JSON_GetInteger(a, "count", 0);
@@ -924,24 +1021,21 @@ static qboolean GLTF_GetAccessor(gltf_t *gltf, int accessorid, struct gltf_acces
 
 //	JSON_WarnIfChild(a, "sparse");
 //	JSON_WarnIfChild(a, "name");
-//	JSON_WarnIfChild(a, "extensions");
-//	JSON_WarnIfChild(a, "extras");
+	GLTF_FlagExtras(a);
 
 	out->data = (char*)bv.data + offset;
 	return true;
 }
 
-static void GLTF_AccessorToTangents(gltf_t *gltf, vec3_t *norm, vec3_t **sdir, vec3_t **tdir, size_t outverts, struct gltf_accessor *a)
+static void GLTF_AccessorToTangents(gltf_t *gltf, const vec3_t *norm, size_t outverts, const struct gltf_accessor *a, vec3_t *sdir, vec3_t *tdir)
 {	//input MUST be a single float4
 	//output is two vec3s. wasteful perhaps.
-	vec3_t *os = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*os) * 3 * outverts);
-	vec3_t *ot = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*ot) * 3 * outverts);
+	vec3_t *os = sdir;
+	vec3_t *ot = tdir;
 	char *in = a->data;
 
 	size_t v, c;
 	float side;
-	*sdir = os;
-	*tdir = ot;
 	if ((a->type&0xff) != 4)
 		return;
 	switch(a->componentType)
@@ -1004,14 +1098,16 @@ static void GLTF_AccessorToTangents(gltf_t *gltf, vec3_t *norm, vec3_t **sdir, v
 	}
 }
 
-static void *GLTF_AccessorToDataF(gltf_t *gltf, size_t outverts, unsigned int outcomponents, struct gltf_accessor *a)
+static void *GLTF_AccessorToDataF(gltf_t *gltf, size_t outverts, unsigned int outcomponents, const struct gltf_accessor *a, void *out)
 {
-	float *ret = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*ret) * outcomponents * outverts), *o;
+	float *ret = out, *o;
 	char *in = a->data;
 
-	int c, ic = a->type&0xff;
+	size_t c, ic = a->type&0xff;
 	if (ic > outcomponents)
 		ic = outcomponents;
+	if (!ret)
+		ret = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*ret) * outcomponents * outverts);
 	o = ret;
 	switch(a->componentType)
 	{
@@ -1170,7 +1266,7 @@ static void *GLTF_AccessorToDataUB(gltf_t *gltf, size_t outverts, unsigned int o
 	unsigned char *ret = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*ret) * outcomponents * outverts), *o;
 	char *in = a->data;
 
-	int c, ic = a->type&0xff;
+	size_t c, ic = a->type&0xff;
 	if (ic > outcomponents)
 		ic = outcomponents;
 	o = ret;
@@ -1218,7 +1314,7 @@ static void *GLTF_AccessorToDataBone(gltf_t *gltf, size_t outverts, struct gltf_
 	char *in = a->data;
 
 
-	int c, ic = a->type&0xff;
+	size_t c, ic = a->type&0xff;
 	if (ic > outcomponents)
 		ic = outcomponents;
 	o = ret;
@@ -1241,6 +1337,8 @@ static void *GLTF_AccessorToDataBone(gltf_t *gltf, size_t outverts, struct gltf_
 			for (c = 0; c < ic; c++)
 			{
 				v = ((unsigned char*)in)[c];
+				if ((unsigned int)v >= MAX_BONES)
+					v = 0;
 				o[c] = gltf->bonemap[v];
 			}
 			for (; c < outcomponents; c++)
@@ -1257,7 +1355,7 @@ static void *GLTF_AccessorToDataBone(gltf_t *gltf, size_t outverts, struct gltf_
 			for (c = 0; c < ic; c++)
 			{
 				v = ((unsigned short*)in)[c];
-				if (v > MAX_BONES)
+				if (v >= MAX_BONES)
 					v = 0;
 				o[c] = gltf->bonemap[v];
 			}
@@ -1268,42 +1366,66 @@ static void *GLTF_AccessorToDataBone(gltf_t *gltf, size_t outverts, struct gltf_
 		}
 		break;
 		//the spec doesn't require these.
-//	case 5125: //UNSIGNED_INT
-/*	case 5126: //FLOAT
+	case 5125: //UNSIGNED_INT
 		while(outverts --> 0)
 		{
+			unsigned int v;
 			for (c = 0; c < ic; c++)
-				o[c] = ((float*)in)[c];
+			{
+				v = ((unsigned short*)in)[c];
+				if (v >= MAX_BONES)
+					v = 0;
+				o[c] = gltf->bonemap[v];
+			}
 			for (; c < outcomponents; c++)
-				o[c] = 0;
+				o[c] = gltf->bonemap[0];
 			o += outcomponents;
 			in += a->bytestride;
 		}
-		break;*/
+		break;
+	case 5126: //FLOAT. for bone indexes. wtf?
+		while(outverts --> 0)
+		{
+			unsigned int v;
+			for (c = 0; c < ic; c++)
+			{
+				v = ((float*)in)[c];
+				if (v >= MAX_BONES)
+					v = 0;
+				o[c] = gltf->bonemap[v];
+			}
+			for (; c < outcomponents; c++)
+				o[c] = gltf->bonemap[0];
+			o += outcomponents;
+			in += a->bytestride;
+		}
+		break;
 	}
 	return ret;
 }
-void TransformArrayD(vecV_t *data, size_t vcount, double matrix[])
+static void TransformPosArray(vecV_t *data, size_t vcount, double matrix[])
 {
 	while (vcount --> 0)
 	{
 		vec3_t t;
 		VectorCopy((*data), t);
-		(*data)[0] = DotProduct(t, (matrix+0)) + matrix[0+3];
-		(*data)[1] = DotProduct(t, (matrix+4)) + matrix[4+3];
-		(*data)[2] = DotProduct(t, (matrix+8)) + matrix[8+3];
+
+		(*data)[0] = matrix[0]*t[0] + matrix[4]*t[1] + matrix[8]*t[2] + matrix[12];
+		(*data)[1] = matrix[1]*t[0] + matrix[5]*t[1] + matrix[9]*t[2] + matrix[13];
+		(*data)[2] = matrix[2]*t[0] + matrix[6]*t[1] + matrix[10]*t[2] + matrix[14];
+		//1        = matrix[3]*t[0] + matrix[7]*t[1] + matrix[11]*t[2] + matrix[14];	//hopefully...
 		data++;
 	}
 }
-void TransformArrayA(vec3_t *data, size_t vcount, double matrix[])
+static void TransformDirArray(vec3_t *data, size_t vcount, double matrix[])
 {
 	vec3_t t;
 	float mag;
 	while (vcount --> 0)
 	{
-		t[0] = DotProduct((*data), (matrix+0));
-		t[1] = DotProduct((*data), (matrix+4));
-		t[2] = DotProduct((*data), (matrix+8));
+		t[0] = matrix[0]*(*data)[0] + matrix[4]*(*data)[1] + matrix[8]*(*data)[2];
+		t[1] = matrix[1]*(*data)[0] + matrix[5]*(*data)[1] + matrix[9]*(*data)[2];
+		t[2] = matrix[2]*(*data)[0] + matrix[6]*(*data)[1] + matrix[10]*(*data)[2];
 
 		//scaling is bad for axis.
 		mag = DotProduct(t,t);
@@ -1318,19 +1440,34 @@ void TransformArrayA(vec3_t *data, size_t vcount, double matrix[])
 	}
 }
 #ifndef SERVERONLY
-static texid_t GLTF_LoadImage(gltf_t *gltf, int imageidx, unsigned int flags)
+static texid_t GLTF_LoadImage(gltf_t *gltf, json_t *imageid, unsigned int flags)
 {
 	size_t size;
 	texid_t ret = r_nulltex;
-	json_t *image     = JSON_FindIndexedChild(gltf->r, "images", imageidx);
-	json_t *uri        = JSON_FindChild(image, "uri");
-	json_t *mimeType   = JSON_FindChild(image, "mimeType");
-	int bufferView     = JSON_GetInteger(image, "bufferView", -1);
+	json_t *image			= GLTF_FindJSONID(gltf, "images", imageid, NULL);
+	json_t *uri				= JSON_FindChild(image, "uri");
+	json_t *mimeType		= JSON_FindChild(image, "mimeType");
+	json_t *bufferViewid	= JSON_FindChild(image, "bufferView");
 	char uritext[MAX_QPATH];
 	char filename[MAX_QPATH];
 	void *mem;
 	struct gltf_bufferview view;
 
+	JSON_FlagAsUsed(image, "name");
+
+	if (gltf->ver <= 1)
+	{
+		json_t *binary_glTF	= JSON_FindChild(image, "extensions.KHR_binary_glTF");
+		if (binary_glTF)
+		{
+			bufferViewid = JSON_FindChild(binary_glTF, "bufferView");
+			mimeType = JSON_FindChild(binary_glTF, "mimeType");
+			JSON_FlagAsUsed(binary_glTF, "width");
+			JSON_FlagAsUsed(binary_glTF, "height");
+			uri = NULL;
+		}
+	}
+
 	//potentially valid mime types:
 	//image/png
 	//image/vnd-ms.dds (MSFT_texture_dds)
@@ -1353,9 +1490,9 @@ static texid_t GLTF_LoadImage(gltf_t *gltf, int imageidx, unsigned int flags)
 			ret = modfuncs->GetTexture(filename, NULL, flags, NULL, NULL, 0, 0, TF_INVALID);
 		}
 	}
-	else if (bufferView >= 0)
+	else if (bufferViewid)
 	{
-		if (GLTF_GetBufferViewData(gltf, bufferView, &view))
+		if (GLTF_GetBufferViewData(gltf, bufferViewid, &view))
 		{
 			JSON_GetPath(image, false, uritext, sizeof(uritext));
 			ret = modfuncs->GetTexture(uritext, NULL, flags, view.data, NULL, view.length, 0, TF_INVALID);
@@ -1364,16 +1501,16 @@ static texid_t GLTF_LoadImage(gltf_t *gltf, int imageidx, unsigned int flags)
 
 	return ret;
 }
-static texid_t GLTF_LoadTexture(gltf_t *gltf, int texture, unsigned int flags)
+static texid_t GLTF_LoadTexture(gltf_t *gltf, json_t *textureid, unsigned int flags)
 {
-	json_t *tex = JSON_FindIndexedChild(gltf->r, "textures", texture);
-	json_t *sampler = JSON_FindIndexedChild(gltf->r, "samplers", JSON_GetInteger(tex, "sampler", -1));
+	json_t *tex = GLTF_FindJSONID(gltf, "textures", textureid, NULL);
+	json_t *sampler = GLTF_FindJSONID(gltf, "samplers", JSON_FindChild(tex, "sampler"), NULL);
 
 	int magFilter = JSON_GetInteger(sampler, "magFilter", 0);
 	int minFilter = JSON_GetInteger(sampler, "minFilter", 0);
 	int wrapS = JSON_GetInteger(sampler, "wrapS", 10497);
 	int wrapT = JSON_GetInteger(sampler, "wrapT", 10497);
-	int source;
+	json_t *sourceid;
 
 	JSON_FlagAsUsed(sampler, "name");
 	JSON_FlagAsUsed(sampler, "extensions");
@@ -1436,18 +1573,398 @@ static texid_t GLTF_LoadTexture(gltf_t *gltf, int texture, unsigned int flags)
 
 	flags |= IF_NOREPLACE;
 
-	source = JSON_GetInteger(tex, "source", -1);
-	source = JSON_GetInteger(tex, "extensions.MSFT_texture_dds.source", source);	//load a dds instead, if one is available.
-	return GLTF_LoadImage(gltf, source, flags);
+	sourceid = JSON_FindChild(tex, "extensions.MSFT_texture_dds.source");	//load a dds instead, if one is available.
+	if (!sourceid)
+		sourceid = JSON_FindChild(tex, "source");	//fall back on the normal source
+	return GLTF_LoadImage(gltf, sourceid, flags);
 }
-static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vertexcolours)
+static char *GLTF1_LoadShader(gltf_t *gltf, json_t *shaderid)
+{	//reads a vertex or fragment shader blob
+	json_t *shader = GLTF_FindJSONID(gltf, "shaders", shaderid, NULL);
+	json_t *uri = JSON_FindChild(shader, "uri");
+	char *out = NULL;
+	json_t *bufferviewid = JSON_FindChild(shader, "extensions.KHR_binary_glTF.bufferView");
+	struct gltf_bufferview view;
+	if (bufferviewid && GLTF_GetBufferViewData(gltf, bufferviewid, &view) && view.data && view.length)
+	{
+		out = malloc(view.length+1);
+		memcpy(out, view.data, view.length);
+		out[view.length] = 0;
+	}
+	else
+	{
+		JSON_FlagAsUsed(shader, "type");	//don't care
+
+		if (uri)
+		{
+			size_t length;
+			out = JSON_MallocDataURI(uri, &length);	//try and decode data schemes...
+			if (!out)
+			{
+				//read a file from disk.
+				vfsfile_t *f;
+				char uritext[MAX_QPATH];
+				char filename[MAX_QPATH];
+				JSON_ReadBody(uri, uritext, sizeof(uritext));
+				GLTF_RelativePath(gltf->mod->name, uritext, filename, sizeof(filename));
+				f = filefuncs->OpenVFS(filename, "rb", FS_GAME);
+				if (f)
+				{
+					length = VFS_GETLEN(f);
+					length = min(length, length);
+					out = malloc(length+1);
+					out[length] = 0;
+					VFS_READ(f, out, length);
+					VFS_CLOSE(f);
+				}
+				else
+					Con_Printf(CON_WARNING"%s: Unable to read buffer file %s\n", gltf->mod->name, filename);
+			}
+		}
+	}
+
+	//if it starts with a precision modifier then just strip that out... gl doesn't like gles's precision modifiers and we tend to try to provide our own too, which doesn't help things.
+	if (out && !strncmp(out, "precision ", 10))
+	{
+		char *le = strchr(out, '\n');
+		if (le++)
+			memmove(out, le, strlen(le)+1);
+	}
+	return out;
+}
+static qboolean GLTF1_LoadMaterial(gltf_t *gltf, json_t *mat, texnums_t *texnums, char *shadertext, size_t shadertextsize)
+{
+	json_t *technique = GLTF_FindJSONID(gltf, "techniques", JSON_FindChild(mat, "technique"), NULL);
+	json_t *parameters = JSON_FindChild(technique, "parameters");
+	json_t *values = JSON_FindChild(mat, "values");
+	json_t *common = JSON_FindChild(mat, "extensions.KHR_materials_common");
+	json_t *v;
+	json_t *p;
+	char header[8192];
+	char samplers[8192];
+	char attributes[8192];
+	char uniforms[8192];
+	char *vertshader = NULL;
+	char *fragshader = NULL;
+	int type;
+	char semantic[64];
+	int sampidx = 0;
+	texid_t tex;
+
+	if (common)
+	{
+		json_t *values = JSON_FindChild(common, "values");
+//		char techniquebuf[64];
+//		const char *technique = JSON_GetString(common, "technique", techniquebuf, sizeof(techniquebuf), "");
+		qboolean doubleSided = JSON_GetInteger(common, "doubleSided", false);
+//		qboolean transparent = JSON_GetInteger(common, "transparent", false);
+//		vec4_t ambient;
+		json_t *diffusename = JSON_FindChild(values, "diffuse");
+		vec4_t diffusetint = {1,1,1,1};
+		json_t *emissionname = JSON_FindChild(values, "emission");
+		vec4_t emissiontint = {1,1,1,1};
+		json_t *specularname = JSON_FindChild(values, "emission");
+		vec4_t speculartint = {1,1,1,1};
+		float shininess = JSON_GetFloat(values, "shininess", 0);
+
+		if (emissionname)
+		{
+			if (!emissionname->child)	//a string, so a texture id
+				texnums->fullbright = GLTF_LoadTexture(gltf, emissionname, IF_NOALPHA);
+			else
+			{	//child nodes means its a vec4.
+				emissiontint[0] = JSON_GetIndexedFloat(emissionname, 0, 0);
+				emissiontint[1] = JSON_GetIndexedFloat(emissionname, 1, 0);
+				emissiontint[2] = JSON_GetIndexedFloat(emissionname, 2, 0);
+				emissiontint[3] = JSON_GetIndexedFloat(emissionname, 3, 1);
+
+				if (emissiontint[0] || emissiontint[1] || emissiontint[2])
+					texnums->fullbright = modfuncs->GetTexture("$whiteimage", NULL, IF_NOMIPMAP|IF_NOPICMIP|IF_NEAREST|IF_NOGAMMA, NULL, NULL, 0, 0, TF_INVALID);
+			}
+		}
+		if (diffusename)
+		{
+			if (!diffusename->child)	//a string, so a texture id
+				texnums->base = GLTF_LoadTexture(gltf, diffusename, 0);
+			else
+			{	//child nodes means its a vec4.
+				texnums->base = modfuncs->GetTexture("$whiteimage", NULL, IF_NOMIPMAP|IF_NOPICMIP|IF_NEAREST|IF_NOGAMMA, NULL, NULL, 0, 0, TF_INVALID);
+				diffusetint[0] = JSON_GetIndexedFloat(diffusename, 0, 0);
+				diffusetint[1] = JSON_GetIndexedFloat(diffusename, 1, 0);
+				diffusetint[2] = JSON_GetIndexedFloat(diffusename, 2, 0);
+				diffusetint[3] = JSON_GetIndexedFloat(diffusename, 3, 1);
+			}
+		}
+		if (specularname)
+		{
+			if (!specularname->child)	//a string, so a texture id
+				texnums->specular = GLTF_LoadTexture(gltf, specularname, IF_NOALPHA);
+			else
+			{	//child nodes means its a vec4.
+				texnums->specular = modfuncs->GetTexture("$whiteimage", NULL, IF_NOMIPMAP|IF_NOPICMIP|IF_NEAREST|IF_NOGAMMA, NULL, NULL, 0, 0, TF_INVALID);
+				speculartint[0] = JSON_GetIndexedFloat(specularname, 0, 0);
+				speculartint[1] = JSON_GetIndexedFloat(specularname, 1, 0);
+				speculartint[2] = JSON_GetIndexedFloat(specularname, 2, 0);
+				speculartint[3] = JSON_GetIndexedFloat(specularname, 3, 1);
+			}
+		}
+
+		Q_snprintf(shadertext, shadertextsize,
+			"{\n"
+				"%s"//cull
+				"program defaultskin#VC%s#FTE_SPECULAR_EXPONENT=%f\n"
+				"{\n"
+					"map $diffuse\n"
+					"%s"	//blend
+					"%s"	//rgbgen
+				"}\n"
+				"fte_basefactor %f %f %f %f\n"
+				"fte_specularfactor %f %f %f %f\n"
+				"fte_fullbrightfactor %f %f %f %f\n"
+			"}\n",
+			doubleSided?"cull disable\n":"",
+			"",//alphaCutoffmodifier,
+			shininess,
+			"",//(alphaMode==1)?"":(alphaMode==2)?"blendfunc blend\n":"",
+			"",//vertexcolours?"rgbgen vertex\nalphagen vertex\n":"",
+			diffusetint[0],diffusetint[1],diffusetint[2],diffusetint[3],
+			speculartint[0],speculartint[1],speculartint[2],speculartint[3],
+			emissiontint[0],emissiontint[1],emissiontint[2],emissiontint[3]);
+		return true;
+	}
+
+	//mat->values.diffuse tends to be quite common. make an executive descision...
+	texnums->base = GLTF_LoadTexture(gltf, JSON_FindChild(values, "diffuse"), 0);
+	if (mod_gltf_ignoretechniques->ival)
+	{	//mat->values.diffuse tends to be quite common
+		return false;
+	}
+	if (!technique)
+	{
+		//missing technique is supposed to result in a greyscale model
+		Q_snprintf(shadertext, shadertextsize,
+			"{\n"
+				"program defaultskin\n"
+				"diffusemap $whiteimage\n"
+				"{\n"
+					"map $diffuse\n"
+					"rgbgen const 0.5 0.5 0.5\n"
+					"alphagen const 1.0\n"
+				"}\n"
+			"}\n"
+			);
+		return true;
+	}
+
+	*header = 0;
+	*samplers = 0;
+	*attributes = 0;
+	*uniforms = 0;
+
+	//this is supposed to be glessl 100. lets try to do our best to get something compatible.
+	Q_snprintfcat(header, sizeof(header), "!!ver 100 120\n");
+	//and reduce conflicts with fte's normal symbols.
+	Q_snprintfcat(header, sizeof(header), "!!explicit\n");
+
+	v = JSON_FindChild(technique, "uniforms");
+	if (v)
+		for (v = v->child; v; v = v->sibling)
+		{
+			enum {
+				GLTF_BYTE = 5120,
+				GLTF_UNSIGNED_BYTE = 5121,
+				GLTF_SHORT = 5122,
+				GLTF_UNSIGNED_SHORT = 5123,
+				GLTF_INT = 5124,
+				GLTF_UNSIGNED_INT = 5125,
+				GLTF_FLOAT = 5126,
+				GLTF_FLOAT_VEC2 = 35664,
+				GLTF_FLOAT_VEC3 = 35665,
+				GLTF_FLOAT_VEC4 = 35666,
+				GLTF_INT_VEC2 = 35667,
+				GLTF_INT_VEC3 = 35668,
+				GLTF_INT_VEC4 = 35669,
+				GLTF_BOOL = 35670,
+				GLTF_BOOL_VEC2 = 35671,
+				GLTF_BOOL_VEC3 = 35672,
+				GLTF_BOOL_VEC4 = 35673,
+				GLTF_FLOAT_MAT2 = 35674,
+				GLTF_FLOAT_MAT3 = 35675,
+				GLTF_FLOAT_MAT4 = 35676,
+				GLTF_SAMPLER_2D = 35678,
+			};
+			v->used = true;
+			p = GLTF_FindJSONIDParent(gltf, parameters, v, NULL);
+
+			if (1 != JSON_GetInteger(p, "count", 1))
+			{
+				if (gltf->warnlimit --> 0)
+					Con_Printf(CON_WARNING"%s: Unsupported parameter->count for uniform %s\n", gltf->mod->name, v->name);
+				return false;
+			}
+			if (JSON_GetString(p, "node", semantic, sizeof(semantic), NULL))
+			{
+				if (gltf->warnlimit --> 0)
+					Con_Printf(CON_WARNING"%s: Unsupported parameter->node for uniform %s\n", gltf->mod->name, v->name);
+				return false;
+			}
+
+			if (!JSON_GetString(p, "semantic", semantic, sizeof(semantic), NULL))
+				*semantic = 0;
+			type = JSON_GetInteger(p, "type", 0);
+
+			if (!strcasecmp(semantic, "MODEL") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_model\n", v->name);
+			else if (!strcasecmp(semantic, "VIEW") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_view\n", v->name);
+			else if (!strcasecmp(semantic, "PROJECTION") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_projection\n", v->name);
+			else if (!strcasecmp(semantic, "MODELVIEW") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_modelview\n", v->name);
+			else if (!strcasecmp(semantic, "MODELVIEWPROJECTION") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_modelviewprojection\n", v->name);
+			else if (!strcasecmp(semantic, "MODELINVERSE") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invmodel\n", v->name);
+			else if (!strcasecmp(semantic, "VIEWINVERSE") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invviewprojection\n", v->name);
+			else if (!strcasecmp(semantic, "PROJECTIONINVERSE") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invprojection\n", v->name);
+			else if (!strcasecmp(semantic, "MODELVIEWINVERSE") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invmodelview\n", v->name);
+			else if (!strcasecmp(semantic, "MODELVIEWPROJECTIONINVERSE") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invmodelviewprojection\n", v->name);
+			else if (!strcasecmp(semantic, "MODELINVERSETRANSPOSE") && type == GLTF_FLOAT_MAT3)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invmodeltranspose\n", v->name);
+			else if (!strcasecmp(semantic, "MODELVIEWINVERSETRANSPOSE") && type == GLTF_FLOAT_MAT3)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_invmodelviewtranspose\n", v->name);
+//			else if (!strcasecmp(semantic, "VIEWPORT") && type == GLTF_FLOAT_VEC4)
+//				Q_snprintfcat(header, sizeof(header), "!!semantic %s UNSUPPORTED\n", v->name);
+			else if (!strcasecmp(semantic, "JOINTMATRIX") && type == GLTF_FLOAT_MAT4)
+				Q_snprintfcat(header, sizeof(header), "!!semantic %s m_bones_mat4\n", v->name);
+//			else if (!strcasecmp(semantic, "LOCAL") && type == GLTF_FLOAT_MAT4)
+//				Q_snprintfcat(header, sizeof(header), "!!semantic %s UNSUPPORTED\n", v->name);
+			else if (!strcasecmp(semantic, ""))
+			{
+				json_t *val = GLTF_FindJSONIDParent(gltf, values, v, NULL);
+				switch(type)
+				{
+				case GLTF_SAMPLER_2D:
+					Q_snprintfcat(header, sizeof(header), "!!constt %s %i\n", v->name, sampidx++);
+					tex = GLTF_LoadTexture(gltf, val, 0);
+					Q_snprintfcat(samplers, sizeof(samplers), "{\nmap %s\n}\n", tex?tex->ident:"$whiteimage");
+					break;
+
+				case GLTF_FLOAT:		Q_snprintfcat(header, sizeof(header), "!!const1f %s %f\n", v->name, JSON_GetFloat(val, NULL, 0));	break;
+				case GLTF_FLOAT_VEC2:	Q_snprintfcat(header, sizeof(header), "!!const2f %s %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0));	break;
+				case GLTF_FLOAT_VEC3:	Q_snprintfcat(header, sizeof(header), "!!const3f %s %f %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0), JSON_GetIndexedFloat(val, 2, 0));	break;
+				case GLTF_FLOAT_VEC4:	Q_snprintfcat(header, sizeof(header), "!!const4f %s %f %f %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0), JSON_GetIndexedFloat(val, 2, 0), JSON_GetIndexedFloat(val, 3, 0));	break;
+
+				case GLTF_BOOL:			//glsl's bool/bvecN types can be ininitialised as floats or ints. lets just use ints here.
+				case GLTF_BYTE:			//FIXME: it is not specified whether these are meant to be normalized or not. are they always float/vecN or int/ivecN? the spec doesn't say.
+				case GLTF_SHORT:
+				case GLTF_INT:			Q_snprintfcat(header, sizeof(header), "!!const1i %s %i\n", v->name, JSON_GetInteger(val, NULL, 0));	break;
+				case GLTF_BOOL_VEC2:
+				case GLTF_INT_VEC2:		Q_snprintfcat(header, sizeof(header), "!!const2i %s %i %i\n", v->name, JSON_GetIndexedInteger(val, 0, 0), JSON_GetIndexedInteger(val, 1, 0));	break;
+				case GLTF_BOOL_VEC3:
+				case GLTF_INT_VEC3:		Q_snprintfcat(header, sizeof(header), "!!const3i %s %i %i %i\n", v->name, JSON_GetIndexedInteger(val, 0, 0), JSON_GetIndexedInteger(val, 1, 0), JSON_GetIndexedInteger(val, 2, 0));	break;
+				case GLTF_BOOL_VEC4:
+				case GLTF_INT_VEC4:		Q_snprintfcat(header, sizeof(header), "!!const4i %s %i %i %i %i\n", v->name, JSON_GetIndexedInteger(val, 0, 0), JSON_GetIndexedInteger(val, 1, 0), JSON_GetIndexedInteger(val, 2, 0), JSON_GetIndexedInteger(val, 3, 0));	break;
+
+				case GLTF_UNSIGNED_BYTE://FIXME: it is not specified whether these are meant to be normalized or not. are they always float/vecN or int/ivecN? the spec doesn't say.
+				case GLTF_UNSIGNED_SHORT:
+				case GLTF_UNSIGNED_INT:	Q_snprintfcat(header, sizeof(header), "!!const1u %s %f\n", v->name, JSON_GetFloat(val, NULL, 0));	break;
+				//curiously no uvecs listed by the spec
+
+				case GLTF_FLOAT_MAT2:	Q_snprintfcat(header, sizeof(header), "!!const2m %s %f %f %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0), JSON_GetIndexedFloat(val, 2, 0), JSON_GetIndexedFloat(val, 3, 0));	break;
+				case GLTF_FLOAT_MAT3:	Q_snprintfcat(header, sizeof(header), "!!const3m %s %f %f %f %f %f %f %f %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0), JSON_GetIndexedFloat(val, 2, 0), JSON_GetIndexedFloat(val, 3, 0), JSON_GetIndexedFloat(val, 4, 0), JSON_GetIndexedFloat(val, 5, 0), JSON_GetIndexedFloat(val, 6, 0), JSON_GetIndexedFloat(val, 7, 0), JSON_GetIndexedFloat(val, 8, 0));	break;
+				case GLTF_FLOAT_MAT4:	Q_snprintfcat(header, sizeof(header), "!!const4m %s %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f\n", v->name, JSON_GetIndexedFloat(val, 0, 0), JSON_GetIndexedFloat(val, 1, 0), JSON_GetIndexedFloat(val, 2, 0), JSON_GetIndexedFloat(val, 3, 0), JSON_GetIndexedFloat(val, 4, 0), JSON_GetIndexedFloat(val, 5, 0), JSON_GetIndexedFloat(val, 6, 0), JSON_GetIndexedFloat(val, 7, 0), JSON_GetIndexedFloat(val, 8, 0), JSON_GetIndexedFloat(val, 9, 0), JSON_GetIndexedFloat(val, 10, 0), JSON_GetIndexedFloat(val, 11, 0), JSON_GetIndexedFloat(val, 12, 0), JSON_GetIndexedFloat(val, 13, 0), JSON_GetIndexedFloat(val, 14, 0), JSON_GetIndexedFloat(val, 15, 0));	break;
+
+				default:
+					if (gltf->warnlimit --> 0)
+						Con_Printf(CON_WARNING"%s: Unsupported constant uniform type %i for uniform %s\n", gltf->mod->name, type, v->name);
+					return false;
+				}
+			}
+			else
+			{
+				if (gltf->warnlimit --> 0)
+					Con_Printf(CON_WARNING"%s: Unknown/Unsupported semantic %s for uniform %s\n", gltf->mod->name, semantic, v->name);
+				return false;
+			}
+		}
+	v = JSON_FindChild(technique, "attributes");
+	if (v)
+		for (v = v->child; v; v = v->sibling)
+		{
+			v->used = true;
+			p = GLTF_FindJSONIDParent(gltf, parameters, v, NULL);
+			if (!JSON_GetString(p, "semantic", semantic, sizeof(semantic), NULL))
+				*semantic = 0;
+			type = JSON_GetInteger(p, "type", 0);
+
+			if (!strcasecmp(semantic, "POSITION"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_position\n", v->name);
+			else if (!strcasecmp(semantic, "NORMAL"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_normal\n", v->name);
+			else if (!strcasecmp(semantic, "TEXCOORD_0"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_texcoord\n", v->name);
+			else if (!strcasecmp(semantic, "TEXCOORD_1"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_lmcoord\n", v->name);
+			else if (!strcasecmp(semantic, "JOINT"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_bone\n", v->name);
+			else if (!strcasecmp(semantic, "WEIGHT"))
+				Q_snprintfcat(attributes, sizeof(attributes), "#define %s fte_v_weight\n", v->name);
+			else
+			{
+				if (gltf->warnlimit --> 0)
+					Con_Printf(CON_WARNING"%s: Unknown semantic %s for attribute %s\n", gltf->mod->name, semantic, v->name);
+				return false;
+			}
+		}
+
+	p = GLTF_FindJSONID(gltf, "programs", JSON_FindChild(technique, "program"), NULL);
+	vertshader = GLTF1_LoadShader(gltf, JSON_FindChild(p, "vertexShader"));
+	fragshader = GLTF1_LoadShader(gltf, JSON_FindChild(p, "fragmentShader"));
+
+	Q_snprintf(shadertext, shadertextsize,
+		"{\n"
+			"surfaceparm nodlight\n"	//o.O
+			"surfaceparm noshadows\n"	//no surprises please.
+			"glslprogram\n"
+			"{\n"
+				"%s"	//header
+				"%s"	//uniformmaps
+				"#ifdef VERTEX_SHADER\n"
+					"%s"	//attributemaps
+					"%s\n"	//vertexshader
+				"#endif\n"
+				"#ifdef FRAGMENT_SHADER\n"
+					"%s\n"	//fragmentshader
+				"#endif\n"
+			"}\n"
+			"%s"	//{map foo} {map}...
+		"}\n",
+		header,
+		uniforms,
+		attributes,
+		vertshader,
+		fragshader,
+		samplers
+		);
+	free(vertshader);
+	free(fragshader);
+	return true;
+}
+
+static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, json_t *materialid, qboolean vertexcolours)
 {
 	qboolean doubleSided;
 	int alphaMode;
 	double alphaCutoff;
-	char shader[8192];
+	char shader[65536];
 	char alphaCutoffmodifier[128];
-	json_t *mat = JSON_FindIndexedChild(gltf->r, "materials", material);
+	quintptr_t materialidx;
+	json_t *mat = GLTF_FindJSONID(gltf, "materials", materialid, &materialidx);
 	galiasskin_t *ret;
 	char tmp[64];
 	const char *t;
@@ -1481,23 +1998,49 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 	ret->skinspeed = 0.1;
 	ret->frame = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*ret->frame));
 
-	if (nam)
-		JSON_ReadBody(nam, ret->frame->shadername, sizeof(ret->frame->shadername));
-	else if (mat)
-		JSON_GetPath(mat, false, ret->frame->shadername, sizeof(ret->frame->shadername));
-	else if (material == -1)	//explicit invalid material
-		Q_snprintf(ret->frame->shadername, sizeof(ret->frame->shadername), "%s", gltf->mod->name);
-	else
-		Q_snprintf(ret->frame->shadername, sizeof(ret->frame->shadername), "%.100s/%i", gltf->mod->name, material);
+	{
+		int skip;
+		if (nam)
+			JSON_ReadBody(nam, shader, sizeof(shader));
+		else if (mat && *mat->name)
+			Q_snprintf(shader, sizeof(shader), "%s", mat->name);
+		else if (!mat)	//explicit invalid material
+			Q_snprintf(shader, sizeof(shader), "");
+		else
+			Q_snprintf(shader, sizeof(shader), "%i", (int)materialidx);
+		skip = sizeof(ret->frame->shadername)-32 - strlen(shader);
+		if (skip > 0)
+			skip = 0;
+		if (mod_gltf_privatematerials->ival && !strchr(shader, '/'))
+		{
+			Q_snprintf(ret->frame->shadername, sizeof(ret->frame->shadername), "%s", gltf->mod->name-skip);
+			Q_strncatz(ret->frame->shadername, "/", sizeof(ret->frame->shadername));
+		}
+		else
+			*ret->frame->shadername = 0;
+
+		Q_strncatz(ret->frame->shadername, shader, sizeof(ret->frame->shadername));
+	}
 
 	if (alphaMode == 1)
 		Q_snprintf(alphaCutoffmodifier, sizeof(alphaCutoffmodifier), "#ALPHATEST=>%f", alphaCutoff);
 	else
 		*alphaCutoffmodifier = 0;
 
-	if (unlit)
+	if (gltf->ver <= 1)
+	{	//fixme: break
+		if (!GLTF1_LoadMaterial(gltf, mat, &ret->frame->texnums, shader, sizeof(shader)))
+		{	//some lame placeholder/fallback.
+			Q_snprintf(shader, sizeof(shader),
+					"{\n"
+						"program defaultskin\n"
+					"}\n"
+				);
+		}
+	}
+	else if (unlit)
 	{	//if this extension was present, then we don't get ANY lighting info.
-		int albedo = JSON_GetInteger(pbrmr, "baseColorTexture.index", -1);	//.rgba
+		json_t *albedo = JSON_FindChild(pbrmr, "baseColorTexture.index");	//.rgba
 		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, albedo, 0);
 
 		Q_snprintf(shader, sizeof(shader),
@@ -1526,8 +2069,8 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 	{
 		Con_DPrintf(CON_WARNING"%s: KHR_materials_cmnBlinnPhong implemented according to draft spec\n", gltf->mod->name);
 
-		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, JSON_GetInteger(pbrsg, "diffuseTexture.index", -1), 0);
-		ret->frame->texnums.specular = GLTF_LoadTexture(gltf, JSON_GetInteger(pbrsg, "specularGlossinessTexture.index", -1), 0);
+		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, JSON_FindChild(pbrsg, "diffuseTexture.index"), 0);
+		ret->frame->texnums.specular = GLTF_LoadTexture(gltf, JSON_FindChild(pbrsg, "specularGlossinessTexture.index"), 0);
 
 		//you wouldn't normally want this, but we have separate factors so lack of a texture is technically valid.
 		if (!ret->frame->texnums.base)
@@ -1567,10 +2110,10 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 	}
 	else if (pbrsg)
 	{	//if this extension was used, then we can use rgb gloss instead of metalness stuff.
-		int occ = JSON_GetInteger(mat, "occlusionTexture.index", -1);	//.r
-		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, JSON_GetInteger(pbrsg, "diffuseTexture.index", -1), 0);
-		ret->frame->texnums.specular = GLTF_LoadTexture(gltf, JSON_GetInteger(pbrsg, "specularGlossinessTexture.index", -1), 0);
-		if (occ != -1)
+		json_t *occ = JSON_FindChild(mat, "occlusionTexture.index");	//.r
+		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, JSON_FindChild(pbrsg, "diffuseTexture.index"), 0);
+		ret->frame->texnums.specular = GLTF_LoadTexture(gltf, JSON_FindChild(pbrsg, "specularGlossinessTexture.index"), 0);
+		if (occ)
 			ret->frame->texnums.occlusion = GLTF_LoadTexture(gltf, occ, IF_NOSRGB);
 
 		Q_snprintf(shader, sizeof(shader),
@@ -1588,7 +2131,7 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 				"bemode rtlight rtlight_sg\n"
 			"}\n",
 			doubleSided?"cull disable\n":"",
-			(occ!=-1)?"#OCCLUDE":"",
+			(occ)?"#OCCLUDE":"",
 			alphaCutoffmodifier,
 			(alphaMode==1)?"":(alphaMode==2)?"blendfunc blend\n":"",
 			vertexcolours?"rgbgen vertex\nalphagen vertex\n":"",
@@ -1608,23 +2151,44 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 	else// if (pbrmr)
 	{	//this is the standard lighting model for gltf2
 		//'When not specified, all the default values of pbrMetallicRoughness apply'
-		int albedo = JSON_GetInteger(pbrmr, "baseColorTexture.index", -1);	//.rgba
-		int mrt = JSON_GetInteger(pbrmr, "metallicRoughnessTexture.index", -1);	//.r = unused, .g = roughness, .b = metalic, .a = unused
-		int occ = JSON_GetInteger(mat, "occlusionTexture.index", -1);	//.r
+		json_t *albedo = JSON_FindChild(pbrmr, "baseColorTexture.index");	//.rgba
+		json_t *mrt = JSON_FindChild(pbrmr, "metallicRoughnessTexture.index");	//.r = unused, .g = roughness, .b = metalic, .a = unused
+		json_t *occ = JSON_FindChild(mat, "occlusionTexture.index");	//.r
+		json_t *n;
+		char occname[MAX_QPATH];
+		char mrtname[MAX_QPATH];
+
+		if (JSON_GetInteger(pbrmr, "baseColorTexture.texCoord", 0) != 0)
+			if (gltf->warnlimit --> 0)
+				Con_Printf("%s: Unsupported baseColorTexture texCoord value\n", gltf->mod->name);
+		if (JSON_GetInteger(pbrmr, "metallicRoughnessTexture.texCoord", 0) != 0)
+			if (gltf->warnlimit --> 0)
+				Con_Printf("%s: Unsupported metallicRoughnessTexture texCoord value\n", gltf->mod->name);
+		if (JSON_GetInteger(mat, "occlusionTexture.texCoord", 0) != 0)
+			if (gltf->warnlimit --> 0)
+				Con_Printf("%s: Unsupported occlusionTexture texCoord value\n", gltf->mod->name);
 
 		//now work around potential lame exporters (yay dds?).
-		occ = JSON_GetInteger(mat, "extensions.MSFT_packing_occlusionRoughnessMetallic.occlusionRoughnessMetallicTexture.index", occ);
-		mrt = JSON_GetInteger(mat, "extensions.MSFT_packing_occlusionRoughnessMetallic.occlusionRoughnessMetallicTexture.index", mrt);
+		n = JSON_FindChild(mat, "extensions.MSFT_packing_occlusionRoughnessMetallic.occlusionRoughnessMetallicTexture.index");
+		if (n)
+			occ = n;
+		n = JSON_FindChild(mat, "extensions.MSFT_packing_occlusionRoughnessMetallic.occlusionRoughnessMetallicTexture.index");
+		if (n)
+			mrt = n;
 
 		//ideally we use the ORM.r for the occlusion map, but some people just love being annoying.
-		if (occ != mrt && occ != -1)
+		JSON_ReadBody(occ, occname, sizeof(occname));
+		JSON_ReadBody(mrt, mrtname, sizeof(mrtname));
+		if (strcmp(occname,mrtname) && occ)
 			ret->frame->texnums.occlusion = GLTF_LoadTexture(gltf, occ, IF_NOSRGB);
 
 		//note: extensions.MSFT_packing_normalRoughnessMetallic.normalRoughnessMetallicTexture.index gives rg=normalxy, b=roughness, .a=metalic
 		//(would still need an ao map, and probably wouldn't work well as bc3 either)
 
-		ret->frame->texnums.base     = GLTF_LoadTexture(gltf, albedo, 0);
-		ret->frame->texnums.specular = GLTF_LoadTexture(gltf, mrt, IF_NOSRGB);
+		if (albedo)
+			ret->frame->texnums.base     = GLTF_LoadTexture(gltf, albedo, 0);
+		if (mrt)
+			ret->frame->texnums.specular = GLTF_LoadTexture(gltf, mrt, IF_NOSRGB);
 
 		Q_snprintf(shader, sizeof(shader),
 			"{\n"
@@ -1641,7 +2205,7 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 				"bemode rtlight rtlight_orm\n"
 			"}\n",
 			doubleSided?"cull disable\n":"",
-			(occ==-1)?"#NOOCCLUDE":((occ!=mrt)?"#OCCLUDE":""),
+			(!occ)?"#NOOCCLUDE":(strcmp(occname,mrtname)?"#OCCLUDE":""),
 			alphaCutoffmodifier,
 			(alphaMode==1)?"":(alphaMode==2)?"blendfunc blend\n":"",
 			vertexcolours?"rgbgen vertex\nalphagen vertex\n":"",
@@ -1657,8 +2221,10 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 				JSON_GetFloat(mat, "emissiveFactor.2", 0)
 			);
 	}
-	ret->frame->texnums.bump = GLTF_LoadTexture(gltf, JSON_GetInteger(mat, "normalTexture.index", -1), IF_NOSRGB|IF_TRYBUMP);
-	ret->frame->texnums.fullbright = GLTF_LoadTexture(gltf, JSON_GetInteger(mat, "emissiveTexture.index", -1), 0);
+	if (!ret->frame->texnums.bump)
+		ret->frame->texnums.bump = GLTF_LoadTexture(gltf, JSON_FindChild(mat, "normalTexture.index"), IF_NOSRGB|IF_TRYBUMP);
+	if (!ret->frame->texnums.fullbright)
+		ret->frame->texnums.fullbright = GLTF_LoadTexture(gltf, JSON_FindChild(mat, "emissiveTexture.index"), 0);
 
 	if (!ret->frame->texnums.base)
 		ret->frame->texnums.base = modfuncs->GetTexture("$whiteimage", NULL, IF_NOMIPMAP|IF_NOPICMIP|IF_NEAREST|IF_NOGAMMA, NULL, NULL, 0, 0, TF_INVALID);
@@ -1669,22 +2235,30 @@ static galiasskin_t *GLTF_LoadMaterial(gltf_t *gltf, int material, qboolean vert
 	return ret;
 }
 #endif
-static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double pmatrix[])
+static const float *QDECL GLTF_AnimateMorphs(const galiasinfo_t *surf, const framestate_t *framestate);
+static qboolean GLTF_ProcessMesh(gltf_t *gltf, json_t *meshid, int basebone, double skinmatrix[])
 {
 	model_t *mod = gltf->mod;
-	json_t *mesh = JSON_FindIndexedChild(gltf->r, "meshes", meshidx);
+	quintptr_t meshidx;
+	json_t *mesh = GLTF_FindJSONID(gltf, "meshes", meshid, &meshidx);
 	json_t *prim;
 	json_t *meshname = JSON_FindChild(mesh, "name");
+	json_t *target = NULL;
+	float	morphweights[MAX_MORPHWEIGHTS];
+	size_t morphtargets;
 
-	JSON_WarnIfChild(mesh, "weights", &gltf->warnlimit);
-	JSON_WarnIfChild(mesh, "extensions", &gltf->warnlimit);
-//	JSON_WarnIfChild(mesh, "extras", &gltf->warnlimit);
+	target = JSON_FindChild(mesh, "weights");
+	for (morphtargets = 0; morphtargets < MAX_MORPHWEIGHTS; morphtargets++)
+		morphweights[morphtargets] = JSON_GetIndexedFloat(target, morphtargets, 0);
+	morphtargets = ~0;
+	GLTF_FlagExtras(mesh);
 
 	for(prim = JSON_FindIndexedChild(mesh, "primitives", 0); prim; prim = prim->sibling)
 	{
 		int mode  = JSON_GetInteger(prim, "mode", 4);
 		json_t *attr = JSON_FindChild(prim, "attributes");
 		struct gltf_accessor tc_0, tc_1, norm, tang, vpos, col0, idx, sidx, swgt;
+		struct gltf_accessor morph_vpos[MAX_MORPHWEIGHTS], morph_norm[MAX_MORPHWEIGHTS], morph_tang[MAX_MORPHWEIGHTS];
 		galiasinfo_t *surf;
 		size_t i, j;
 
@@ -1696,20 +2270,48 @@ static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double
 			continue;
 		}
 
-		JSON_WarnIfChild(prim, "targets", &gltf->warnlimit);	//morph targets...
-		JSON_FindChild(prim, "extensions");
-//		JSON_WarnIfChild(prim, "extensions", &gltf->warnlimit);
-//		JSON_WarnIfChild(prim, "extras", &gltf->warnlimit);
+		for (i = 0; ; i++)
+		{
+			target = JSON_FindIndexedChild(prim, "targets", i);
+			if (!target)
+				break;
+			GLTF_GetAccessor(gltf, JSON_FindChild(target, "POSITION"), &morph_vpos[i]);
+			GLTF_GetAccessor(gltf, JSON_FindChild(target, "NORMAL"), &morph_norm[i]);
+			GLTF_GetAccessor(gltf, JSON_FindChild(target, "TANGENT"), &morph_tang[i]);
+		}
+		if (i != morphtargets)
+		{
+			if (morphtargets == ~0)
+				morphtargets = i;
+			else if (gltf->warnlimit --> 0)
+				Con_Printf(CON_WARNING"morphtargets count changed between primitives\n");
+			for (; i < morphtargets; i++)
+			{
+				memset(&morph_vpos[i], 0, sizeof(morph_vpos[i]));
+				memset(&morph_norm[i], 0, sizeof(morph_norm[i]));
+				memset(&morph_tang[i], 0, sizeof(morph_tang[i]));
+			}
+		}
 
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "TEXCOORD_0",	-1), &tc_0);	//float, ubyte, ushort
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "TEXCOORD_1",	-1), &tc_1);	//float, ubyte, ushort
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "NORMAL",		-1), &norm);	//float
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "TANGENT",		-1), &tang);	//float
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "POSITION",	-1), &vpos);	//float
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "COLOR_0",		-1), &col0);	//float, ubyte, ushort
-		GLTF_GetAccessor(gltf, JSON_GetInteger(prim, "indices",		-1), &idx);
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "JOINTS_0",	-1), &sidx);	//ubyte, ushort
-		GLTF_GetAccessor(gltf, JSON_GetInteger(attr, "WEIGHTS_0",	-1), &swgt);	//float, ubyte, ushort
+		GLTF_FlagExtras(prim);
+
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "TEXCOORD_0"),	&tc_0);	//float, ubyte, ushort
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "TEXCOORD_1"),	&tc_1);	//float, ubyte, ushort
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "NORMAL"),		&norm);	//float
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "TANGENT"),		&tang);	//float
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "POSITION"),	&vpos);	//float
+		GLTF_GetAccessor(gltf, JSON_FindChild(attr, "COLOR_0"),		&col0);	//float, ubyte, ushort
+		GLTF_GetAccessor(gltf, JSON_FindChild(prim, "indices"),		&idx);
+		if (gltf->ver <= 1)
+		{
+			GLTF_GetAccessor(gltf, JSON_FindChild(attr, "JOINT"),	&sidx);	//ubyte, ushort
+			GLTF_GetAccessor(gltf, JSON_FindChild(attr, "WEIGHT"),	&swgt);	//float, ubyte, ushort
+		}
+		else
+		{	//potentially multiple, each a vec4.
+			GLTF_GetAccessor(gltf, JSON_FindChild(attr, "JOINTS_0"),	&sidx);	//ubyte, ushort
+			GLTF_GetAccessor(gltf, JSON_FindChild(attr, "WEIGHTS_0"),	&swgt);	//float, ubyte, ushort
+		}
 
 		if (JSON_GetInteger(attr, "JOINTS_1",	-1) != -1 || JSON_GetInteger(attr, "WEIGHTS_1",	-1) != -1)
 			if (gltf->warnlimit --> 0)
@@ -1718,9 +2320,9 @@ static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double
 		if (!vpos.count)
 			continue;
 
-		surf = modfuncs->ZG_Malloc(&mod->memgroup, sizeof(*surf));
+		surf = modfuncs->ZG_Malloc(&mod->memgroup, sizeof(*surf) + (morphtargets*sizeof(float)));
 
-		surf->surfaceid = surf->contents = JSON_GetInteger(prim, "extras.fte.surfaceid", meshidx);
+		surf->surfaceid = JSON_GetInteger(prim, "extras.fte.surfaceid", meshidx);
 		surf->contents = JSON_GetInteger(prim, "extras.fte.contents", FTECONTENTS_BODY);
 		surf->csurface.flags = JSON_GetInteger(prim, "extras.fte.surfaceflags", 0);
 		surf->geomset = JSON_GetInteger(prim, "extras.fte.geomset", ~0u);
@@ -1771,20 +2373,42 @@ static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double
 			surf->ofs_indexes[i+2] = t;
 		}
 
-		surf->ofs_skel_xyz		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_xyz[0]),		&vpos);
-		surf->ofs_skel_norm		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_norm[0]),	&norm);
-		GLTF_AccessorToTangents(gltf, surf->ofs_skel_norm, &surf->ofs_skel_svect, &surf->ofs_skel_tvect, surf->numverts, &tang);
-		surf->ofs_st_array		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_st_array[0]),		&tc_0);
+		surf->AnimateMorphs = GLTF_AnimateMorphs;
+		memcpy((float*)(surf+1), morphweights, sizeof(float)*morphtargets);
+		surf->nummorphs = morphtargets;
+		surf->ofs_skel_xyz = modfuncs->ZG_Malloc(&mod->memgroup, (sizeof(*surf->ofs_skel_xyz)+sizeof(*surf->ofs_skel_norm)+sizeof(*surf->ofs_skel_svect)+sizeof(*surf->ofs_skel_tvect)) * surf->numverts * (1+morphtargets));
+		surf->ofs_skel_norm = (vec3_t*)(surf->ofs_skel_xyz+surf->numverts*(1+morphtargets));
+		surf->ofs_skel_svect = (vec3_t*)(surf->ofs_skel_norm+surf->numverts*(1+morphtargets));
+		surf->ofs_skel_tvect = (vec3_t*)(surf->ofs_skel_svect+surf->numverts*(1+morphtargets));
+
+		surf->ofs_skel_xyz		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_xyz[0]),		&vpos, surf->ofs_skel_xyz);
+		surf->ofs_skel_norm		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_norm[0]),	&norm, surf->ofs_skel_norm);			//if no normals, normals should be flat (fragment shader or unwelding the verts...)
+		GLTF_AccessorToTangents(gltf, surf->ofs_skel_norm, surf->numverts, &tang, surf->ofs_skel_svect, surf->ofs_skel_tvect);
+
+		for (i = 0; i < morphtargets; i++)
+		{
+			size_t offset = (i+1) * surf->numverts;
+			GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_xyz[0]),	&morph_vpos[i], surf->ofs_skel_xyz+offset);
+			GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_norm[0]),	&morph_norm[i], surf->ofs_skel_norm+offset);			//if no normals, normals should be flat (fragment shader or unwelding the verts...)
+			GLTF_AccessorToTangents(gltf, surf->ofs_skel_norm+offset, surf->numverts,   &morph_tang[i], surf->ofs_skel_svect+offset, surf->ofs_skel_tvect+offset);
+		}
+
+		surf->ofs_st_array		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_st_array[0]),		&tc_0, NULL);
 		if (tc_1.data)
-			surf->ofs_lmst_array	= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_lmst_array[0]),	&tc_1);
+			surf->ofs_lmst_array	= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_lmst_array[0]),	&tc_1, NULL);
 		if (col0.data && col0.componentType == 5121)	//UNSIGNED_BYTE
 			surf->ofs_rgbaub	= GLTF_AccessorToDataUB(gltf, surf->numverts, countof(surf->ofs_rgbaub[0]),		&col0);
 		else if (col0.data)
-			surf->ofs_rgbaf		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_rgbaf[0]),		&col0);
+			surf->ofs_rgbaf		= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_rgbaf[0]),		&col0, NULL);
+		/*else
+		{
+			surf->ofs_rgbaub = modfuncs->ZG_Malloc(&gltf->mod->memgroup, sizeof(*surf->ofs_rgbaub) * surf->numverts);
+			memset(surf->ofs_rgbaub, 0xff, sizeof(*surf->ofs_rgbaub) * surf->numverts);
+		}*/
 		if (sidx.data && swgt.data)
 		{
 			surf->ofs_skel_idx		= GLTF_AccessorToDataBone(gltf,surf->numverts, &sidx);
-			surf->ofs_skel_weight	= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_weight[0]),	&swgt);
+			surf->ofs_skel_weight	= GLTF_AccessorToDataF(gltf, surf->numverts, countof(surf->ofs_skel_weight[0]),	&swgt, NULL);
 
 			for (i = 0; i < surf->numverts; i++)
 			{
@@ -1806,9 +2430,17 @@ static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double
 			}
 		}
 
-//		TransformArrayD(surf->ofs_skel_xyz, surf->numverts, pmatrix);
-//		TransformArrayA(surf->ofs_skel_norm, surf->numverts, pmatrix);
-//		TransformArrayA(surf->ofs_skel_svect, surf->numverts, pmatrix);
+		if (skinmatrix)
+		{
+			TransformPosArray(surf->ofs_skel_xyz, surf->numverts, skinmatrix);
+			if (norm.data)
+				TransformDirArray(surf->ofs_skel_norm, surf->numverts, skinmatrix);
+			if (tang.data)
+			{
+				TransformDirArray(surf->ofs_skel_svect, surf->numverts, skinmatrix);
+				TransformDirArray(surf->ofs_skel_tvect, surf->numverts, skinmatrix);
+			}
+		}
 
 		for (i = 0; i < surf->numverts; i++)
 		{
@@ -1824,7 +2456,7 @@ static qboolean GLTF_ProcessMesh(gltf_t *gltf, int meshidx, int basebone, double
 
 #ifndef SERVERONLY
 		surf->numskins = 1;
-		surf->ofsskins = GLTF_LoadMaterial(gltf, JSON_GetInteger(prim, "material", -1), surf->ofs_rgbaub||surf->ofs_rgbaf);
+		surf->ofsskins = GLTF_LoadMaterial(gltf, JSON_FindChild(prim, "material"), surf->ofs_rgbaub||surf->ofs_rgbaf);
 #endif
 
 		if (!tang.data)
@@ -1900,24 +2532,26 @@ static void GenMatrixPosQuat4ScaleDouble(const double pos[3], const double quat[
 	result[3*4+3] = 1;
 }
 
-static qboolean GLTF_ProcessNode(gltf_t *gltf, int nodeidx, double pmatrix[16], int parentidx, qboolean isjoint)
+static qboolean GLTF_ProcessNode(gltf_t *gltf, json_t *nodeid, double pmatrix[16], int parentidx, qboolean isjoint)
 {
+	double skinmatrix[16], *skinmatrixptr=NULL;
 	json_t *c;
 	json_t *node;
 	json_t *t;
 	json_t *skin;
-	int mesh;
-	int skinidx;
+	json_t *meshid;
+	quintptr_t nodeidx;
 	struct gltfbone_s *b;
-	if (nodeidx < 0 || nodeidx >= gltf->numbones)
+	node = GLTF_FindJSONID(gltf, "nodes", nodeid, &nodeidx);
+	if (nodeidx >= gltf->numbones)
 	{
-		Con_Printf(CON_WARNING"%s: Invalid node index %i\n", gltf->mod->name, nodeidx);
+		if (nodeidx < MAX_BONES)	//don't spam if its detected elsewhere.
+			Con_Printf(CON_WARNING"%s: Invalid node index %i\n", gltf->mod->name, (int)nodeidx);
 		return false;
 	}
-	node = JSON_FindIndexedChild(gltf->r, "nodes", nodeidx);
 	if (!node)
 	{
-		Con_Printf(CON_WARNING"%s: Invalid node index %i\n", gltf->mod->name, nodeidx);
+		Con_Printf(CON_WARNING"%s: Invalid node index %i\n", gltf->mod->name, (int)nodeidx);
 		return false;
 	}
 
@@ -1973,41 +2607,50 @@ static qboolean GLTF_ProcessNode(gltf_t *gltf, int nodeidx, double pmatrix[16],
 
 		//T * R * S
 		GenMatrixPosQuat4ScaleDouble(trans, rot, scale, b->rel.rmatrix);
-/*
-		memset(mmatrix, 0, sizeof(mmatrix));
-		mmatrix[0] = 1;
-		mmatrix[5] = 1;
-		(void)rot,(void)scale;
-		mmatrix[10] = 1;
-		mmatrix[15] = 1;
-		mmatrix[3] = trans[0];
-		mmatrix[7] = trans[1];
-		mmatrix[11] = trans[2];
-*/
 	}
 	Matrix4D_Multiply(b->rel.rmatrix, pmatrix, b->amatrix);
 
-	skinidx = JSON_GetInteger(node, "skin", -1);
-	if (skinidx >= 0)
+	skin = GLTF_FindJSONID(gltf, "skins", JSON_FindChild(node, "skin"), NULL);
+	if (skin)
 	{
-//		double identity[16];
-		int j;
+		quintptr_t j;
 		json_t *joints;
 		struct gltf_accessor inverse;
 		float *inversef;
 
-		skin = JSON_FindIndexedChild(gltf->r, "skins", skinidx);
-
-		joints = JSON_FindChild(skin, "joints");
-		GLTF_GetAccessor(gltf, JSON_GetInteger(skin, "inverseBindMatrices", -1), &inverse);
+		if (gltf->ver <= 1)
+			joints = JSON_FindChild(skin, "jointNames");
+		else
+			joints = JSON_FindChild(skin, "joints");
+		if (joints)
+			joints = joints->child;
+		GLTF_GetAccessor(gltf, JSON_FindChild(skin, "inverseBindMatrices"), &inverse);
 		inversef = inverse.data;
 		if (inverse.componentType != 5126/*FLOAT*/ || inverse.type != ((4<<8) | 4)/*mat4x4*/)
 			inverse.count = 0;
-		for (j = 0; j < MAX_BONES; j++, inversef+=inverse.bytestride/sizeof(float))
+		memset(gltf->bonemap, 0, sizeof(*gltf->bonemap)*MAX_BONES);	//avoid unexpected surprises...
+		for (j = 0; j < MAX_BONES && joints; j++, inversef+=inverse.bytestride/sizeof(float), joints=joints->sibling)
 		{
-			int b = JSON_GetIndexedInteger(joints, j, -1);
-			if (b < 0)
-				break;
+			quintptr_t b;
+			joints->used = true;
+			if (gltf->ver <= 1)
+			{	//urgh
+				char jointname[64];
+				JSON_ReadBody(joints, jointname, sizeof(jointname));	//this is matched to nodes[b].jointName rather than (textual) b, so we can't use our helpers.
+				for (b = 0; b < gltf->numbones; b++)
+				{
+					if (!strcmp(gltf->bones[b].jointname, jointname))
+						break;
+				}
+				if (b == gltf->numbones)
+					break;
+			}
+			else
+			{
+				b = JSON_GetUInteger(joints, NULL, ~0);
+				if (b >= gltf->numbones)
+					break;
+			}
 			gltf->bonemap[j] = b;
 			if (j < inverse.count)
 			{
@@ -2055,26 +2698,46 @@ static qboolean GLTF_ProcessNode(gltf_t *gltf, int nodeidx, double pmatrix[16],
 			}
 		}
 
-//		GLTF_ProcessNode(gltf, JSON_GetInteger(skin, "skeleton", -1), identity, nodeidx, true);
+//		GLTF_ProcessNode(gltf, JSON_FindChild(skin, "skeleton"), identity, nodeidx, true);
 
+		if (gltf->ver <= 1)
+		{
+			int i;
+			json_t *bdsm = JSON_FindChild(skin, "bindShapeMatrix");
+			if (bdsm)
+			{
+				skinmatrixptr = skinmatrix;
+				for (i = 0; i < 16; i++)
+					skinmatrix[i] = JSON_GetIndexedFloat(bdsm, i, ((i%5)==0)?1.0:0.0);
+			}
+		}
 		JSON_FlagAsUsed(node, "name");
 	}
-	
-	mesh = JSON_GetInteger(node, "mesh", -1);
-	if (mesh >= 0)
-		GLTF_ProcessMesh(gltf, mesh, nodeidx, b->amatrix);
+
+	if (gltf->ver <= 1)
+	{	//multiple in gltf1
+		meshid = JSON_FindChild(node, "meshes");
+		if (meshid)
+			for (meshid = meshid->child; meshid; meshid = meshid->sibling)
+				GLTF_ProcessMesh(gltf, meshid, nodeidx, skinmatrixptr);
+	}
+	else
+	{	//gltf2 moved to only one.
+		meshid = JSON_FindChild(node, "mesh");
+		if (meshid)
+			GLTF_ProcessMesh(gltf, meshid, nodeidx, skinmatrixptr);
+	}
 
 	for(c = JSON_FindIndexedChild(node, "children", 0); c; c = c->sibling)
 	{
 		c->used = true;
-		GLTF_ProcessNode(gltf, JSON_GetInteger(c, NULL, -1), b->amatrix, nodeidx, isjoint);
+		GLTF_ProcessNode(gltf, c, b->amatrix, nodeidx, isjoint);
 	}
 
 	b->camera = JSON_GetInteger(node, "camera", -1);
 
 	JSON_WarnIfChild(node, "weights", &gltf->warnlimit);	//default value for morph weight animations
-	JSON_WarnIfChild(node, "extensions", &gltf->warnlimit);
-//	JSON_WarnIfChild(node, "extras", &gltf->warnlimit);
+	GLTF_FlagExtras(node);
 
 	return true;
 }
@@ -2088,6 +2751,7 @@ struct gltf_animsampler
 	} interptype;
 	struct gltf_accessor input;	//timestamps
 	struct gltf_accessor output;	//values
+	int outputs;
 };
 static void GLTF_Animation_Persist(gltf_t *gltf, struct gltf_accessor *accessor)
 {
@@ -2096,11 +2760,11 @@ static void GLTF_Animation_Persist(gltf_t *gltf, struct gltf_accessor *accessor)
 	memcpy(newdata, accessor->data, accessor->length);
 	accessor->data = newdata;
 }
-static struct gltf_animsampler GLTF_AnimationSampler(gltf_t *gltf, json_t *samplers, int sampleridx, int elems)
+static struct gltf_animsampler GLTF_AnimationSampler(gltf_t *gltf, json_t *samplers, json_t *params, json_t *samplerid, int elems)
 {
 	int outsperinput=1;
 	struct gltf_animsampler r;
-	json_t *sampler = JSON_FindIndexedChild(samplers, NULL, sampleridx);
+	json_t *sampler = GLTF_FindJSONIDParent(gltf, samplers, samplerid, NULL);
 
 	char t[32];
 	const char *lerptype = JSON_GetString(sampler, "interpolation", t, sizeof(t), "LINEAR");
@@ -2119,10 +2783,22 @@ static struct gltf_animsampler GLTF_AnimationSampler(gltf_t *gltf, json_t *sampl
 		r.interptype = AINTERP_LINEAR;
 	}
 
-	GLTF_GetAccessor(gltf, JSON_GetInteger(sampler, "input", -1), &r.input);
-	GLTF_GetAccessor(gltf, JSON_GetInteger(sampler, "output", -1), &r.output);
+	if (gltf->ver <= 1)
+	{
+		GLTF_GetAccessor(gltf, GLTF_FindJSONIDParent(gltf, params, JSON_FindChild(sampler, "input"), NULL), &r.input);
+		GLTF_GetAccessor(gltf, GLTF_FindJSONIDParent(gltf, params, JSON_FindChild(sampler, "output"), NULL), &r.output);
+	}
+	else
+	{
+		GLTF_GetAccessor(gltf, JSON_FindChild(sampler, "input"), &r.input);
+		GLTF_GetAccessor(gltf, JSON_FindChild(sampler, "output"), &r.output);
+	}
 
-	if (!r.input.data || !r.output.data || r.input.count*outsperinput != r.output.count)
+	if (!r.input.count)
+		r.input.count = 1;
+	else
+		r.outputs = r.output.count / (r.input.count*outsperinput);
+	if (!r.input.data || !r.output.data || r.input.count*outsperinput*r.outputs != r.output.count)
 		memset(&r, 0, sizeof(r));
 	else
 	{
@@ -2237,7 +2913,7 @@ static void QuaternionSlerp_(const vec4_t p, const vec4_t q, float t, vec4_t qt)
 		}
 	}
 }
-static void LerpAnimData(const struct gltf_animsampler *samp, float time, float *result, int elems)
+static void LerpAnimData(const struct gltf_animsampler *samp, float time, float *result, int elems, qboolean slerp)
 {
 	float t0, t1;
 	float w0, w1;
@@ -2256,6 +2932,9 @@ static void LerpAnimData(const struct gltf_animsampler *samp, float time, float
 		t1 = Anim_GetTime(in, f1);
 	}
 
+	f0 *= samp->outputs;
+	f1 *= samp->outputs;
+
 	if (samp->interptype == AINTERP_CUBICSPLINE)
 	{
 		float step=t1-t0;
@@ -2282,7 +2961,7 @@ static void LerpAnimData(const struct gltf_animsampler *samp, float time, float
 			result[c] = m0*v0[c] + mb*b[c] + m1*v1[c] + ma*a[c];
 
 		//quats must be normalized.
-		if (elems == 4)
+		if (slerp)
 		{
 			float len = sqrt(DotProduct4(result,result));
 			Vector4Scale(result, 1/len, result);
@@ -2306,7 +2985,7 @@ static void LerpAnimData(const struct gltf_animsampler *samp, float time, float
 	{
 		Anim_GetVal(out, f0, v0, elems);
 		Anim_GetVal(out, f1, v1, elems);
-		if (elems == 4)
+		if (slerp)
 			QuaternionSlerp_(v0, v1, w1, result);
 		else
 		{
@@ -2317,9 +2996,9 @@ static void LerpAnimData(const struct gltf_animsampler *samp, float time, float
 	}
 }
 
-static void GLTF_RemapBone(gltf_t *gltf, int *nextidx, int b)
+static void GLTF_RemapBone(gltf_t *gltf, size_t *nextidx, size_t b)
 {	//potentially needs to walk to the root before the child. recursion sucks.
-	if (gltf->bonemap[b] >= 0)
+	if (b == -1 || gltf->bonemap[b] >= 0)
 		return;	//already got remapped
 	GLTF_RemapBone(gltf, nextidx, gltf->bones[b].parent);
 	gltf->bonemap[b] = (*nextidx)++;
@@ -2327,7 +3006,7 @@ static void GLTF_RemapBone(gltf_t *gltf, int *nextidx, int b)
 static void GLTF_RewriteBoneTree(gltf_t *gltf)
 {
 	galiasinfo_t *surf;
-	int j, n;
+	size_t j, n;
 	struct gltfbone_s *tmpbones;
 
 	for (j = 0; j < gltf->numbones; j++)
@@ -2371,9 +3050,45 @@ struct galiasanimation_gltf_s
 	float duration;
 	struct
 	{
-		struct gltf_animsampler rot,scale,trans;
+		struct gltf_animsampler rot,scale,trans,morph;
 	} bone[1];
 };
+cvar_t temp1;
+static const float *QDECL GLTF_AnimateMorphs(const galiasinfo_t *surf, const framestate_t *framestate)
+{
+	static float morphs[MAX_MORPHWEIGHTS];
+	float imorphs[MAX_MORPHWEIGHTS], *src;
+	size_t influence, m;
+	int bone = temp1.ival;
+	const struct galiasanimation_gltf_s *a;
+	const struct framestateregion_s *fg = &framestate->g[FS_REG];
+	memset(morphs, 0, sizeof(morphs[0])*surf->nummorphs);
+	for (influence = 0; influence < countof(framestate->g[FS_REG].frame); influence++)
+	{
+		m = 0;
+		if (!fg->lerpweight[influence])
+			continue;	//mneh, don't care.
+		if (surf->ofsanimations && (unsigned int)fg->frame[influence] < (unsigned int)surf->numanimations)
+		{
+			a = surf->ofsanimations[fg->frame[influence]].boneofs;
+			if (a && a->bone[bone].morph.input.count)
+			{
+				int asz = min(a->bone[bone].morph.outputs, surf->nummorphs);
+				double time = fg->frametime[influence];
+				if (surf->ofsanimations[fg->frame[influence]].loop && time >= a->duration)
+					time = time - a->duration*floor(time/a->duration);
+				LerpAnimData(&a->bone[bone].morph, time, imorphs, asz, false);
+				for (; m < surf->nummorphs && m < asz; m++)
+					morphs[m] += fg->lerpweight[influence] * imorphs[m];
+			}
+		}
+
+		src = (float*)(surf+1);	//our static morphs are stuck on the end of our surf struct.;
+		for (; m < surf->nummorphs; m++)
+			morphs[m] += fg->lerpweight[influence]*src[m];
+	}
+	return morphs;
+}
 static float *QDECL GLTF_AnimateBones(const galiasinfo_t *surf, const galiasanimation_t *anim, float time, float *bonematrix, int numbones)
 {
 	const struct galiasbone_gltf_s *defbone = surf->ctx;
@@ -2396,11 +3111,11 @@ static float *QDECL GLTF_AnimateBones(const galiasinfo_t *surf, const galiasanim
 			VectorCopy(defbone[j].trans, trans);
 
 			if (a->bone[j].rot.input.data)
-				LerpAnimData(&a->bone[j].rot, time, rot, 4);
+				LerpAnimData(&a->bone[j].rot, time, rot, 4, true);
 			if (a->bone[j].scale.input.data)
-				LerpAnimData(&a->bone[j].scale, time, scale, 3);
+				LerpAnimData(&a->bone[j].scale, time, scale, 3, false);
 			if (a->bone[j].trans.input.data)
-				LerpAnimData(&a->bone[j].trans, time, trans, 3);
+				LerpAnimData(&a->bone[j].trans, time, trans, 3, false);
 			//figure out the bone matrix...
 			modfuncs->GenMatrixPosQuat4Scale(trans, rot, scale, bonematrix);
 		}
@@ -2412,10 +3127,10 @@ static float *QDECL GLTF_AnimateBones(const galiasinfo_t *surf, const galiasanim
 		if (surf->ofsbones[j].parent < 0)
 		{	//rotate any root bones from gltf to quake's orientation.
 			float fnar[12];
-			static float toquake[12]={
-				0,0,GLTFSCALE, 0,
-				GLTFSCALE,0,0, 0,
-				0,GLTFSCALE,0, 0};
+			float toquake[12]={
+				0,0,mod_gltf_scale->value, 0,
+				mod_gltf_scale->value,0,0, 0,
+				0,mod_gltf_scale->value,0, 0};
 			memcpy(fnar, bonematrix, sizeof(fnar));
 			modfuncs->ConcatTransforms((void*)toquake, (void*)fnar, (void*)bonematrix);
 		}
@@ -2428,12 +3143,17 @@ static float *QDECL GLTF_AnimateBones(const galiasinfo_t *surf, const galiasanim
 //we do NOT supported nested nodes right now...
 static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize, void *buffer, size_t buffersize)
 {
-	static struct
+	static struct knowngltfextensions_s
 	{
 		const char *name;
 		qboolean supported;	//unsupported extensions don't really need to be listed, but they do prevent warnings from unknown-but-used extensions
 		qboolean draft;		//true when our implementation is probably buggy on account of the spec maybe changing.
-	} extensions[] =
+	} extensions_v1[] =
+	{
+		{"KHR_binary_glTF",							true,	false},
+		{"KHR_materials_common",					true,   false},
+		{NULL}
+	}, extensions_v2[] =
 	{
 		{"KHR_materials_pbrSpecularGlossiness",		true,   false},
 //		{"KHR_materials_cmnBlinnPhong",				true,   true},
@@ -2443,9 +3163,11 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 		{"KHR_mesh_quantization",					true,	true},
 		{"MSFT_texture_dds",						true,	false},
 		{"MSFT_packing_occlusionRoughnessMetallic", true,	false},
-	};
+		{NULL}
+	}, *extensions;
 	gltf_t gltf;
-	int pos=0, j, k;
+	int pos=0;
+	quintptr_t j,k;
 	json_t *scene, *n, *anim;
 	double rootmatrix[16];
 	double gltfver;
@@ -2470,7 +3192,19 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 	if (gltfver != 2.0)
 		gltfver = JSON_GetFloat(gltf.r, "asset.version", 0.0);
 	if (gltfver == 2.0)
+		gltf.ver = 2;
+	if (gltfver == 1.0)	//we load gltf1 models. we don't get the materials right but gltf1 sucks for that anyway.
+		gltf.ver = 1;
+	if (gltf.ver)
 	{
+		if (gltf.ver <= 1)
+		{
+			JSON_FlagAsUsed(gltf.r, "asset.profile");
+			JSON_FlagAsUsed(gltf.r, "asset.premultipliedAlpha");
+			extensions = extensions_v1;
+		}
+		else
+			extensions = extensions_v2;
 		JSON_FlagAsUsed(gltf.r, "asset.copyright");
 		JSON_FlagAsUsed(gltf.r, "asset.generator");
 		JSON_WarnIfChild(gltf.r, "asset.minVersion", &gltf.warnlimit);
@@ -2480,14 +3214,14 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 		{
 			char extname[256];
 			JSON_ReadBody(n, extname, sizeof(extname));
-			for (j = 0; j < countof(extensions); j++)
+			for (j = 0; extensions[j].name; j++)
 			{
 				if (!strcmp(extname, extensions[j].name))
 					break;
 			}
-			if (j==countof(extensions) || !extensions[j].supported)
+			if (!extensions[j].supported)
 			{
-				Con_Printf(CON_ERROR "%s: Required gltf2 extension \"%s\" not supported\n", mod->name, extname);
+				Con_Printf(CON_ERROR "%s: Required gltf%i extension \"%s\" not supported\n", mod->name, gltf.ver, extname);
 				goto abort;
 			}
 		}
@@ -2496,15 +3230,15 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 		{	//must be a superset of the above.
 			char extname[256];
 			JSON_ReadBody(n, extname, sizeof(extname));
-			for (j = 0; j < countof(extensions); j++)
+			for (j = 0; extensions[j].name; j++)
 			{
 				if (!strcmp(extname, extensions[j].name))
 					break;
 			}
-			if (j==countof(extensions) || !extensions[j].supported)
-				Con_Printf(CON_WARNING "%s: gltf2 extension \"%s\" not known\n", mod->name, extname);
+			if (!extensions[j].supported)
+				Con_Printf(CON_WARNING "%s: gltf%i extension \"%s\" not known\n", mod->name, gltf.ver, extname);
 			else if (extensions[j].draft)
-				Con_Printf(CON_WARNING "%s: gltf2 extension \"%s\" follows draft implementation, and may be non-standard/buggy\n", mod->name, extname);
+				Con_Printf(CON_WARNING "%s: gltf%i extension \"%s\" follows draft implementation, and may be non-standard/buggy\n", mod->name, gltf.ver, extname);
 		}
 
 		VectorClear(mod->maxs);
@@ -2513,18 +3247,22 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 		//we don't really care about cameras.
 		JSON_FlagAsUsed(gltf.r, "cameras");
 
-		scene = JSON_FindIndexedChild(gltf.r, "scenes", JSON_GetInteger(gltf.r, "scene", 0));
+		scene = GLTF_FindJSONID_First(&gltf, "scenes", JSON_FindChild(gltf.r, "scene"), NULL);
 
 		memset(&rootmatrix, 0, sizeof(rootmatrix));
 #if 1	//transform from gltf to quake. mostly only needed for the base pose.
-		rootmatrix[2] = rootmatrix[4] = rootmatrix[9] = GLTFSCALE; rootmatrix[15] = 1;
+		rootmatrix[2] = rootmatrix[4] = rootmatrix[9] = mod_gltf_scale->value; rootmatrix[15] = 1;
 #else
 		rootmatrix[0] = rootmatrix[5] = rootmatrix[10] = 1; rootmatrix[15] = 1;
 #endif
 
+		n = JSON_FindChild(gltf.r, "nodes");
 		for (j = 0; ; j++)
 		{
-			n = JSON_FindIndexedChild(gltf.r, "nodes", j);
+			if (gltf.ver <= 1)
+				n = j?n->sibling:n->child;
+			else
+				n = JSON_FindIndexedChild(gltf.r, "nodes", j);
 			if (!n)
 				break;
 			if (j == MAX_BONES)
@@ -2532,12 +3270,13 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 				Con_Printf(CON_WARNING"%s: too many nodes (max %i)\n", mod->name, MAX_BONES);
 				break;
 			}
+			JSON_ReadBody(JSON_FindChild(n, "jointName"), gltf.bones[j].jointname, sizeof(gltf.bones[j].jointname));
 			if (!JSON_ReadBody(JSON_FindChild(n, "name"), gltf.bones[j].name, sizeof(gltf.bones[j].name)))
 			{
 				if (n)
 					JSON_GetPath(n, true, gltf.bones[j].name, sizeof(gltf.bones[j].name));
 				else
-					Q_snprintf(gltf.bones[j].name, sizeof(gltf.bones[j].name), "bone%i", j);
+					Q_snprintf(gltf.bones[j].name, sizeof(gltf.bones[j].name), "bone%u", (unsigned)j);
 			}
 			gltf.bones[j].camera = -1;
 			gltf.bones[j].parent = -1;
@@ -2548,8 +3287,7 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 		gltf.numbones = j;
 
 		JSON_FlagAsUsed(scene, "name");
-		JSON_WarnIfChild(scene, "extensions", &gltf.warnlimit);
-//		JSON_WarnIfChild(scene, "extras");
+		GLTF_FlagExtras(scene);
 		for (j = 0; ; j++)
 		{
 			n = JSON_FindIndexedChild(scene, "nodes", j);
@@ -2557,7 +3295,7 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 				break;
 			n->used = true;
 //			if (!
-			GLTF_ProcessNode(&gltf, JSON_GetInteger(n, NULL, -1), rootmatrix, -1, false);
+			GLTF_ProcessNode(&gltf, n, rootmatrix, -1, false);
 //				break;
 		}
 
@@ -2582,27 +3320,39 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 			gltfbone[j] = gltf.bones[j].rel;
 		}
 
-		for(anim = JSON_FindIndexedChild(gltf.r, "animations", 0); anim; anim = anim->sibling)
-			numframegroups++;
+		anim = JSON_FindChild(gltf.r, "animations");
+		if (anim)
+			for(anim = anim->child; anim; anim = anim->sibling)
+				numframegroups++;
 		if (numframegroups)
 		{
+			struct galiasanimation_gltf_s **mergeanims = NULL;
+			if (mod_gltf_fixbuggyanims->ival)
+			{
+				mergeanims = alloca(sizeof(*mergeanims)*gltf.numbones);
+				memset(mergeanims, 0, sizeof(mergeanims)*gltf.numbones);
+			}
 			framegroups = modfuncs->ZG_Malloc(&mod->memgroup, sizeof(*framegroups)*numframegroups);
-			for (k = 0; k < numframegroups; k++)
+			anim = JSON_FindChild(gltf.r, "animations")->child;
+			for (k = 0; k < numframegroups; k++, anim = anim->sibling)
 			{
 				galiasanimation_t *fg = &framegroups[k];
-				json_t *anim = JSON_FindIndexedChild(gltf.r, "animations", k);
 				json_t *chan;
 				json_t *samps = JSON_FindChild(anim, "samplers");
-//				int f, l;
+				json_t *params = gltf.ver<=1?JSON_FindChild(anim, "parameters"):0;	//gltf1
 				float maxtime = 0;
+				unsigned maxposes = 0;
 				struct galiasanimation_gltf_s *a = modfuncs->ZG_Malloc(&mod->memgroup, sizeof(*a)+sizeof(a->bone[0])*(gltf.numbones-1));
+				anim->used = true;
 
 				if (!JSON_ReadBody(JSON_FindChild(anim, "name"), fg->name, sizeof(fg->name)))
 				{
-					if (anim)
+					if (gltf.ver <= 1)
+						Q_snprintf(fg->name, sizeof(fg->name), "%s", anim->name);
+					else if (anim)
 						JSON_GetPath(anim, true, fg->name, sizeof(fg->name));
 					else
-						Q_snprintf(fg->name, sizeof(fg->name), "anim%i", k);
+						Q_snprintf(fg->name, sizeof(fg->name), "anim%u", (unsigned int)k);
 				}
 				fg->loop = true;
 				fg->skeltype = SKEL_RELATIVE;
@@ -2610,89 +3360,89 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 				{
 					struct gltf_animsampler s;
 					json_t *targ = JSON_FindChild(chan, "target");
-					int sampler = JSON_GetInteger(chan, "sampler", -1);
-					int bone = JSON_GetInteger(targ, "node", -2);
+					json_t *samplerid = JSON_FindChild(chan, "sampler");
+					qintptr_t bone;
 					json_t *path = JSON_FindChild(targ, "path");
-					if (bone == -2)
-						continue;	//'When node isn't defined, channel should be ignored'
+					chan->used = true;
+
+					if (gltf.ver <= 1)
+						GLTF_FindJSONID(&gltf, "nodes", JSON_FindChild(targ, "id"), (quintptr_t*)&bone);
+					else
+					{
+						bone = JSON_GetInteger(targ, "node", -2);
+						if (bone == -2)
+							continue;	//'When node isn't defined, channel should be ignored'
+					}
 					if (bone < 0 || bone >= gltf.numbones)
 					{
 						if (gltf.warnlimit --> 0)
-							Con_Printf("%s: invalid node index %i\n", mod->name, bone);
+							Con_Printf("%s: invalid node index %i\n", mod->name, (int)bone);
 						continue;	//error...
 					}
 					bone = gltf.bonemap[bone];
-					s = GLTF_AnimationSampler(&gltf, samps, sampler, 4);
-					maxtime = max(maxtime, s.input.maxs[0]);
-					if (JSON_Equals(path, NULL, "rotation"))
-						a->bone[bone].rot = s;
-					else if (JSON_Equals(path, NULL, "scale"))
-						a->bone[bone].scale = s;
-					else if (JSON_Equals(path, NULL, "translation"))
-						a->bone[bone].trans = s;
-					else if (gltf.warnlimit --> 0)
-					{	//these are unsupported
-						if (JSON_Equals(path, NULL, "weights"))	//morph weights
-							Con_Printf(CON_WARNING"%s: morph animations are not supported\n", mod->name);
+					if (mergeanims)
+					{
+						if (mergeanims[bone] && mergeanims[bone] != a)
+							mergeanims = NULL;	//was some other animation... which means two animations are fighting over the same joint. which means we don't need to use this hack! yay!
 						else
-							Con_Printf("%s: undocumented animation type\n", mod->name);
+							mergeanims[bone] = a;
+					}
+					s = GLTF_AnimationSampler(&gltf, samps, params, samplerid, 4);
+					if (!s.input.maxs[0] && s.input.count)
+						s.input.maxs[0] = Anim_GetTime(&s.input, s.input.count-1);
+					maxtime = max(maxtime, s.input.maxs[0]);
+					maxposes = max(maxposes, s.input.count);
+					if (JSON_Equals(path, NULL, "rotation") && s.outputs==1)
+						a->bone[bone].rot = s;
+					else if (JSON_Equals(path, NULL, "scale") && s.outputs==1)
+						a->bone[bone].scale = s;
+					else if (JSON_Equals(path, NULL, "translation") && s.outputs==1)
+						a->bone[bone].trans = s;
+					else if (JSON_Equals(path, NULL, "weights") && s.outputs>=1)
+						a->bone[bone].morph = s;
+					else
+					{
+						if (gltf.warnlimit --> 0)
+						{
+							char buf[64];
+							JSON_ReadBody(path, buf, sizeof(buf));
+							Con_Printf("%s: unknown animation data - %s\n", mod->name, buf);
+						}
 					}
 				}
 
+				if (!maxtime)
+					maxtime = 1.0/30;	//some stuff doesn't like 0-length animations. divisions by 0 are not nice.
 				a->duration = maxtime;
 
-				//TODO: make a guess at the framerate according to sampler intervals
-				fg->rate = 60;
-				fg->numposes = max(1, maxtime*fg->rate);
+				//calc average framerate
+				fg->numposes = maxposes;
 				if (maxtime)
 					fg->rate = fg->numposes/maxtime;	//fix up the rate so we hit the exact end of the animation (so it doesn't have to be quite so exact).
+				else
+					fg->rate = 60;
 
 				fg->skeltype = SKEL_RELATIVE;
 				fg->GetRawBones = GLTF_AnimateBones;
 				fg->boneofs = a;
-#if 0
-				fg->boneofs = modfuncs->ZG_Malloc(&mod->memgroup, sizeof(*fg->boneofs)*12*gltf.numbones*fg->numposes);
+			}
 
-				for (f = 0; f < fg->numposes; f++)
-				{
-					float *bonematrix = &fg->boneofs[f*gltf.numbones*12];
-					float time = f/fg->rate;
-					for (j = 0; j < gltf.numbones; j++, bonematrix+=12)
+			if (mergeanims)
+			{	//if we got this far then no two animations referenced the same bone.
+				//this is a common issue with various sample models, I'm going to call it an exporter bug, and work around it by merging them into a single animation.
+				for (k = 1; k < numframegroups && framegroups[k].rate == framegroups[0].rate && framegroups[k].numposes == framegroups[0].numposes; k++)
+					;
+				if (k == numframegroups)
+				{	//yup, all have the same duration+posecount. lets call it a valid fix.
+					struct galiasanimation_gltf_s *merged = framegroups[0].boneofs;
+					Q_snprintf(framegroups->name, sizeof(framegroups->name), "allbones");
+					numframegroups = 1;
+					for (k = 0; k < gltf.numbones; k++)
 					{
-						float scale[3];
-						float rot[4];
-						float trans[3];
-						//eww, weird inheritance crap.
-						if (b[j].rot.input.data || b[j].scale.input.data || b[j].trans.input.data)
-						{
-							VectorCopy(gltf.bones[j].rel.scale, scale);
-							Vector4Copy(gltf.bones[j].rel.quat, rot);
-							VectorCopy(gltf.bones[j].rel.trans, trans);
-
-							if (b[j].rot.input.data)
-								LerpAnimData(&b[j].rot, time, rot, 4);
-							if (b[j].scale.input.data)
-								LerpAnimData(&b[j].scale, time, scale, 3);
-							if (b[j].trans.input.data)
-								LerpAnimData(&b[j].trans, time, trans, 3);
-							//figure out the bone matrix...
-							modfuncs->GenMatrixPosQuat4Scale(trans, rot, scale, bonematrix);
-						}
-						else
-						{	//nothing animated, use what we calculated earlier.
-							for (l = 0; l < 12; l++)
-								bonematrix[l] = gltf.bones[j].rel.rmatrix[l];
-						}
-						if (gltf.bones[j].parent < 0)
-						{	//rotate any root bones from gltf to quake's orientation.
-							float fnar[12];
-							static float toquake[12]={0,0,GLTFSCALE,0,GLTFSCALE,0,0,0,0,GLTFSCALE,0,0};
-							memcpy(fnar, bonematrix, sizeof(fnar));
-							modfuncs->ConcatTransforms((void*)toquake, (void*)fnar, (void*)bonematrix);
-						}
+						if (mergeanims[k])
+							merged->bone[k] = mergeanims[k]->bone[k];	//copy over that bone.
 					}
 				}
-#endif
 			}
 		}
 
@@ -2710,8 +3460,8 @@ static qboolean GLTF_LoadModel(struct model_s *mod, char *json, size_t jsonsize,
 			surf->geomset = ~0;	//invalid set = always visible. FIXME: set this according to scene numbers?
 			surf->geomid = 0;
 		}
-		VectorScale(mod->mins, GLTFSCALE, mod->mins);
-		VectorScale(mod->maxs, GLTFSCALE, mod->maxs);
+		VectorScale(mod->mins, mod_gltf_scale->value, mod->mins);
+		VectorScale(mod->maxs, mod_gltf_scale->value, mod->maxs);
 
 		if (!mod->meshinfo)
 			Con_Printf("%s: Doesn't contain any meshes...\n", mod->name);
@@ -2724,7 +3474,6 @@ abort:
 	free(gltf.bones);
 	free(gltf.bonemap);
 
-
 	mod->type = mod_alias;
 	return !!mod->meshinfo;
 }
@@ -2737,32 +3486,50 @@ qboolean QDECL Mod_LoadGLTFModel (struct model_s *mod, void *buffer, size_t fsiz
 qboolean QDECL Mod_LoadGLBModel (struct model_s *mod, void *buffer, size_t fsize)
 {
 	unsigned char *header = buffer;
-	unsigned int magic = header[0]|(header[1]<<8)|(header[2]<<16)|(header[3]<<24);
-	unsigned int version = header[4]|(header[5]<<8)|(header[6]<<16)|(header[7]<<24);
-	unsigned int length = header[8]|(header[9]<<8)|(header[10]<<16)|(header[11]<<24);
+	unsigned int magic = header[0]|(header[1]<<8)|(header[2]<<16)|(header[3]<<24);		//gltf
+	unsigned int version = header[4]|(header[5]<<8)|(header[6]<<16)|(header[7]<<24);	//2
+	unsigned int length = header[8]|(header[9]<<8)|(header[10]<<16)|(header[11]<<24);	//fsize
 
 	unsigned int jsonlen = header[12]|(header[13]<<8)|(header[14]<<16)|(header[15]<<24);
 	unsigned int jsontype = header[16]|(header[17]<<8)|(header[18]<<16)|(header[19]<<24);
 	char *json = (char*)(header+20);
 
-	unsigned int binlen = header[20+jsonlen]|(header[21+jsonlen]<<8)|(header[22+jsonlen]<<16)|(header[23+jsonlen]<<24);
-	unsigned int bintype = header[24+jsonlen]|(header[25+jsonlen]<<8)|(header[26+jsonlen]<<16)|(header[27+jsonlen]<<24);
-	unsigned char *bin = header+28+jsonlen;
-
 	if (fsize < 28)
 		return false;
 	if (magic != (('F'<<24)+('T'<<16)+('l'<<8)+'g'))
 		return false;
-	if (version != 2)
-		return false;
-	if (jsontype != 0x4E4F534A)	//'JSON'
-		return false;
-	if (length != 28+jsonlen+binlen)
-		return false;
-	if (bintype != 0x004E4942)	//'BIN\0'
-		return false;
-	
-	return GLTF_LoadModel(mod, json, jsonlen, bin, binlen);
+	if (fsize < length)
+		return false;	//allow padding on the end, but not truncation
+	if (version == 1)
+	{
+		unsigned int binlen = (length-20) - jsonlen;
+		unsigned char *bin = header+20+jsonlen;
+
+		if (jsonlen&3)
+			return false;	//exporter is expected to pad with spaces.
+		if (jsontype != 0)	//'JSON'
+			return false;
+		if (length != 20+jsonlen+binlen)
+			return false;
+
+		return GLTF_LoadModel(mod, json, jsonlen, bin, binlen);
+	}
+	else if (version == 2)
+	{
+		unsigned int binlen = header[20+jsonlen]|(header[21+jsonlen]<<8)|(header[22+jsonlen]<<16)|(header[23+jsonlen]<<24);
+		unsigned int bintype = header[24+jsonlen]|(header[25+jsonlen]<<8)|(header[26+jsonlen]<<16)|(header[27+jsonlen]<<24);
+		unsigned char *bin = header+28+jsonlen;
+
+		if (jsontype != 0x4E4F534A)	//'JSON'
+			return false;
+		if (length != 28+jsonlen+binlen)
+			return false;
+		if (bintype != 0x004E4942)	//'BIN\0'
+			return false;
+
+		return GLTF_LoadModel(mod, json, jsonlen, bin, binlen);
+	}
+	return false;
 }
 
 qboolean Plug_GLTF_Init(void)
@@ -2771,11 +3538,19 @@ qboolean Plug_GLTF_Init(void)
 	modfuncs = plugfuncs->GetEngineInterface(plugmodfuncs_name, sizeof(*modfuncs));
 	if (modfuncs && modfuncs->version < MODPLUGFUNCS_VERSION)
 		modfuncs = NULL;
+	mod_gltf_scale = cvarfuncs->GetNVFDG("mod_gltf_scale", "30", CVAR_RENDERERLATCH, "This defines the number of units per metre, in order to correctly load standard-scale gltf models.", "GLTF Models");
+	mod_gltf_fixbuggyanims = cvarfuncs->GetNVFDG("mod_gltf_fixbuggyanims", "1", CVAR_RENDERERLATCH, "Work around buggy exporters by merging animations that affect only a single bone.", "GLTF Models");
+#ifdef IQMTOOL
+	mod_gltf_privatematerials = cvarfuncs->GetNVFDG("mod_gltf_privatematerials", "0", CVAR_RENDERERLATCH, "Add the model path to material names, to isolate them between different models.", "GLTF Models");
+#else
+	mod_gltf_privatematerials = cvarfuncs->GetNVFDG("mod_gltf_privatematerials", "1", CVAR_RENDERERLATCH, "Add the model path to material names, to isolate them between different models.", "GLTF Models");
+#endif
+	mod_gltf_ignoretechniques = cvarfuncs->GetNVFDG("mod_gltf_ignoretechniques", "1", CVAR_RENDERERLATCH, "Ignore the gltf1 model-specific glsl. This is enabled by default because it just doesn't work very well.", "GLTF Models");
 
 	if (modfuncs && filefuncs)
 	{
-		modfuncs->RegisterModelFormatText("glTF2 models (glTF)", ".gltf", Mod_LoadGLTFModel);
-		modfuncs->RegisterModelFormatMagic("glTF2 models (glb)", (('F'<<24)+('T'<<16)+('l'<<8)+'g'), Mod_LoadGLBModel);
+		modfuncs->RegisterModelFormatText("glTF models (glTF)", ".gltf", Mod_LoadGLTFModel);
+		modfuncs->RegisterModelFormatMagic("glTF models (glb)", (('F'<<24)+('T'<<16)+('l'<<8)+'g'), Mod_LoadGLBModel);
 		return true;
 	}
 	return false;
diff --git a/plugins/openxr.c b/plugins/openxr.c
index ad3e7cac2..bd8998d4a 100644
--- a/plugins/openxr.c
+++ b/plugins/openxr.c
@@ -388,7 +388,7 @@ static qboolean XR_PreInit(vrsetup_t *qreqs)
 		}
 	}
 
-	xr.instance = NULL;
+	xr.instance = XR_NULL_HANDLE;
 
 	//create our instance
 	{
diff --git a/quakec/menusys/menusys/mitem_grid.qc b/quakec/menusys/menusys/mitem_grid.qc
index 4e9a21000..4295615fd 100644
--- a/quakec/menusys/menusys/mitem_grid.qc
+++ b/quakec/menusys/menusys/mitem_grid.qc
@@ -156,6 +156,7 @@ void(vector pos) mitem_grid::item_draw =
 
 	clientpos = pos;
 	clientsize = this.item_size;
+/*
 	if (vslider)
 	{
 		//scroll+shrink the client area to fit the slider on it.
@@ -185,10 +186,12 @@ void(vector pos) mitem_grid::item_draw =
 	if (pos_x+clientsize_x < ui.drawrectmax[0])
 		ui.drawrectmax[0] = pos_x+clientsize_x;
 	if (pos_y+clientsize_y < ui.drawrectmax[1])
-		ui.drawrectmax[1] = pos_y+clientsize[1];
-	if (ui.drawrectmax[0] > ui.drawrectmin[0] && ui.drawrectmax[1] > ui.drawrectmin[1])
+		ui.drawrectmax[1] = pos_y+clientsize[1];*/
+//	if (ui.drawrectmax[0] > ui.drawrectmin[0] && ui.drawrectmax[1] > ui.drawrectmin[1])
 	{
 		ui.setcliparea(ui.drawrectmin[0], ui.drawrectmin[1], ui.drawrectmax[0] - ui.drawrectmin[0], ui.drawrectmax[1] - ui.drawrectmin[1]);
+//ui.setcliparea(0, 0, 0, 0);
+
 		float c = max(0,floor((ui.drawrectmin[1]-clientpos_y)/item_scale));	//calculate how many we can skip
 		for (clientpos_y += item_scale * c; c < grid_numchildren; c++, clientpos_y += item_scale)
 		{
@@ -204,7 +207,7 @@ void(vector pos) mitem_grid::item_draw =
 			vslider.item_draw(pos + [clientsize[0], 0]);
 		}
 	}
-	ui.drawrectmin = omin;
-	ui.drawrectmax = omax;
+//	ui.drawrectmin = omin;
+//	ui.drawrectmax = omax;
 };
 #endif