Бранимир Караџић 6 роки тому
батько
коміт
e8422851c5
27 змінених файлів з 13383 додано та 1341 видалено
  1. 3 2
      3rdparty/meshoptimizer/.travis.yml
  2. 4 3
      3rdparty/meshoptimizer/CMakeLists.txt
  3. 9 9
      3rdparty/meshoptimizer/Makefile
  4. 57 9
      3rdparty/meshoptimizer/README.md
  5. 3316 0
      3rdparty/meshoptimizer/demo/GLTFLoader.js
  6. 32 25
      3rdparty/meshoptimizer/demo/index.html
  7. 83 55
      3rdparty/meshoptimizer/demo/main.cpp
  8. BIN
      3rdparty/meshoptimizer/demo/pirate.glb
  9. BIN
      3rdparty/meshoptimizer/demo/pirate.optmesh
  10. 0 0
      3rdparty/meshoptimizer/js/meshopt_decoder.js
  11. 1 1
      3rdparty/meshoptimizer/src/indexcodec.cpp
  12. 19 17
      3rdparty/meshoptimizer/src/meshoptimizer.h
  13. 1 1
      3rdparty/meshoptimizer/src/overdrawanalyzer.cpp
  14. 22 20
      3rdparty/meshoptimizer/src/simplifier.cpp
  15. 40 15
      3rdparty/meshoptimizer/src/stripifier.cpp
  16. 2 2
      3rdparty/meshoptimizer/src/vcacheoptimizer.cpp
  17. 0 179
      3rdparty/meshoptimizer/tools/OptMeshLoader.js
  18. 4733 0
      3rdparty/meshoptimizer/tools/cgltf.h
  19. 1397 0
      3rdparty/meshoptimizer/tools/fast_obj.h
  20. 3493 0
      3rdparty/meshoptimizer/tools/gltfpack.cpp
  21. 52 64
      3rdparty/meshoptimizer/tools/lodviewer.cpp
  22. 0 255
      3rdparty/meshoptimizer/tools/meshencoder.cpp
  23. 9 0
      3rdparty/meshoptimizer/tools/meshloader.cpp
  24. 0 383
      3rdparty/meshoptimizer/tools/objparser.cpp
  25. 0 42
      3rdparty/meshoptimizer/tools/objparser.h
  26. 26 12
      3rdparty/meshoptimizer/tools/vcachetester.cpp
  27. 84 247
      3rdparty/meshoptimizer/tools/vcachetuner.cpp

+ 3 - 2
3rdparty/meshoptimizer/.travis.yml

@@ -22,7 +22,8 @@ script:
   - if [[ "$TRAVIS_COMPILER" == "clang" ]]; then make config=sanitize test; fi
   - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make config=debug test; fi
   - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make config=release test; fi
-  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake -G "$TARGET" -DBUILD_DEMO=ON; fi
+  - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make config=release gltfpack; fi
+  - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake -G "$TARGET" -DBUILD_DEMO=ON -DBUILD_TOOLS=ON; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake --build . -- -property:Configuration=Debug -verbosity:minimal; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then ./Debug/demo.exe demo/pirate.obj; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake --build . -- -property:Configuration=Release -verbosity:minimal; fi
@@ -34,4 +35,4 @@ after_script:
     find . -type f -name '*.gcno' -exec gcov -p {} +;
     sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/    -\1\2/" *.gcov;
     bash <(curl -s https://codecov.io/bash) -f 'src#*.gcov' -X search;
-    fi
+    fi

+ 4 - 3
3rdparty/meshoptimizer/CMakeLists.txt

@@ -6,6 +6,7 @@ option(BUILD_TOOLS "Build tools" OFF)
 
 set(SOURCES
     src/meshoptimizer.h
+    src/allocator.cpp
     src/clusterizer.cpp
     src/indexcodec.cpp
     src/indexgenerator.cpp
@@ -30,11 +31,11 @@ else()
 endif()
 
 if(BUILD_DEMO)
-    add_executable(demo demo/main.cpp demo/miniz.cpp demo/tests.cpp tools/objparser.cpp)
+    add_executable(demo demo/main.cpp demo/miniz.cpp demo/tests.cpp tools/meshloader.cpp)
     target_link_libraries(demo meshoptimizer)
 endif()
 
 if(BUILD_TOOLS)
-    add_executable(meshencoder tools/meshencoder.cpp tools/objparser.cpp)
-    target_link_libraries(meshencoder meshoptimizer)
+    add_executable(gltfpack tools/gltfpack.cpp tools/meshloader.cpp)
+    target_link_libraries(gltfpack meshoptimizer)
 endif()

+ 9 - 9
3rdparty/meshoptimizer/Makefile

@@ -9,13 +9,13 @@ BUILD=build/$(config)
 LIBRARY_SOURCES=$(wildcard src/*.cpp)
 LIBRARY_OBJECTS=$(LIBRARY_SOURCES:%=$(BUILD)/%.o)
 
-DEMO_SOURCES=$(wildcard demo/*.c demo/*.cpp) tools/objparser.cpp
+DEMO_SOURCES=$(wildcard demo/*.c demo/*.cpp) tools/meshloader.cpp
 DEMO_OBJECTS=$(DEMO_SOURCES:%=$(BUILD)/%.o)
 
-ENCODER_SOURCES=tools/meshencoder.cpp tools/objparser.cpp
-ENCODER_OBJECTS=$(ENCODER_SOURCES:%=$(BUILD)/%.o)
+GLTFPACK_SOURCES=tools/gltfpack.cpp tools/meshloader.cpp
+GLTFPACK_OBJECTS=$(GLTFPACK_SOURCES:%=$(BUILD)/%.o)
 
-OBJECTS=$(LIBRARY_OBJECTS) $(DEMO_OBJECTS) $(ENCODER_OBJECTS)
+OBJECTS=$(LIBRARY_OBJECTS) $(DEMO_OBJECTS) $(GLTFPACK_OBJECTS)
 
 LIBRARY=$(BUILD)/libmeshoptimizer.a
 EXECUTABLE=$(BUILD)/meshoptimizer
@@ -65,15 +65,15 @@ dev: $(EXECUTABLE)
 	$(EXECUTABLE) -d $(files)
 
 format:
-	clang-format -i $(LIBRARY_SOURCES) $(DEMO_SOURCES)
+	clang-format -i $(LIBRARY_SOURCES) $(DEMO_SOURCES) $(GLTFPACK_SOURCES)
 
-meshencoder: $(ENCODER_OBJECTS) $(LIBRARY)
+gltfpack: $(GLTFPACK_OBJECTS) $(LIBRARY)
 	$(CXX) $^ $(LDFLAGS) -o $@
 
-js/decoder.js: src/vertexcodec.cpp src/indexcodec.cpp
+js/meshopt_decoder.js: src/vertexcodec.cpp src/indexcodec.cpp
 	@mkdir -p build
-	emcc $(filter %.cpp,$^) -O3 -DNDEBUG -s EXPORTED_FUNCTIONS='["_meshopt_decodeVertexBuffer", "_meshopt_decodeIndexBuffer"]' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_STACK=32768 -s TOTAL_MEMORY=65536 -o build/decoder.wasm
-	sed -i "s#\(var wasm = \)\".*\";#\\1\"$$(cat build/decoder.wasm | base64 -w 0)\";#" $@
+	emcc $(filter %.cpp,$^) -O3 -DNDEBUG -s EXPORTED_FUNCTIONS='["_meshopt_decodeVertexBuffer", "_meshopt_decodeIndexBuffer"]' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_STACK=32768 -s TOTAL_MEMORY=65536 -o build/meshopt_decoder.wasm
+	sed -i "s#\(var wasm = \)\".*\";#\\1\"$$(cat build/meshopt_decoder.wasm | base64 -w 0)\";#" $@
 
 $(EXECUTABLE): $(DEMO_OBJECTS) $(LIBRARY)
 	$(CXX) $^ $(LDFLAGS) -o $@

+ 57 - 9
3rdparty/meshoptimizer/README.md

@@ -11,10 +11,10 @@ The library provides a C and C++ interface for all algorithms; you can use it fr
 meshoptimizer is hosted on GitHub; you can download the latest release using git:
 
 ```
-git clone -b v0.11 https://github.com/zeux/meshoptimizer.git
+git clone -b v0.12 https://github.com/zeux/meshoptimizer.git
 ```
 
-Alternatively you can [download the .zip archive from GitHub](https://github.com/zeux/meshoptimizer/archive/v0.11.zip).
+Alternatively you can [download the .zip archive from GitHub](https://github.com/zeux/meshoptimizer/archive/v0.12.zip).
 
 ## Building
 
@@ -146,7 +146,7 @@ Note that vertex encoding assumes that vertex buffer was optimized for vertex fe
 
 Decoding functions are heavily optimized and can directly target write-combined memory; you can expect both decoders to run at 1-3 GB/s on modern desktop CPUs. Compression ratios depend on the data; vertex data compression ratio is typically around 2-4x (compared to already quantized data), index data compression ratio is around 5-6x (compared to raw 16-bit index data). General purpose lossless compressors can further improve on these results.
 
-Due to a very high decoding performance and compatibility with general purpose lossless compressors, the compression is a good fit for the use on the web. To that end, meshoptimizer provides both vertex and index decoders compiled into WebAssembly and wrapped into a module with JavaScript-friendly interface, `js/decoder.js`, that you can use to decode meshes that were encoded offline:
+Due to a very high decoding performance and compatibility with general purpose lossless compressors, the compression is a good fit for the use on the web. To that end, meshoptimizer provides both vertex and index decoders compiled into WebAssembly and wrapped into a module with JavaScript-friendly interface, `js/meshopt_decoder.js`, that you can use to decode meshes that were encoded offline:
 
 ```js
 // ready is a Promise that is resolved when (asynchronous) WebAssembly compilation finishes
@@ -157,7 +157,7 @@ MeshoptDecoder.decodeVertexBuffer(vertexBuffer, vertexCount, vertexSize, vertexD
 MeshoptDecoder.decodeIndexBuffer(indexBuffer, indexCount, indexSize, indexData);
 ```
 
-A THREE.js mesh loader is provided as an example in `tools/OptMeshLoader.js`; it loads meshes encoded using `tools/meshencoder.cpp`. [Usage example](https://zeuxcg.org/meshoptimizer/demo/) is available, with source in `demo/index.html`.
+[Usage example](https://meshoptimizer.org/demo/) is available, with source in `demo/index.html`; this example uses .GLB files encoded using `gltfpack`.
 
 ## Triangle strip conversion
 
@@ -170,10 +170,12 @@ This library provides an algorithm for converting a vertex cache optimized trian
 
 ```c++
 std::vector<unsigned int> strip(meshopt_stripifyBound(index_count));
-size_t strip_size = meshopt_stripify(&strip[0], indices, index_count, vertex_count);
+unsigned int restart_index = ~0u;
+size_t strip_size = meshopt_stripify(&strip[0], indices, index_count, vertex_count, restart_index);
 ```
 
-Typically you should expect triangle strips to have ~50-60% of indices compared to triangle lists (~1.5-1.8 indices per triangle) and have ~5% worse ACMR. Note that triangle strips require restart index support for rendering; using degenerate triangles to connect strips is not supported.
+Typically you should expect triangle strips to have ~50-60% of indices compared to triangle lists (~1.5-1.8 indices per triangle) and have ~5% worse ACMR.
+Note that triangle strips can be stitched with or without restart index support. Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance.
 
 ## Deinterleaved geometry
 
@@ -208,7 +210,7 @@ This library provides two simplification algorithms that reduce the number of tr
 
 The first simplification algorithm, `meshopt_simplify`, follows the topology of the original mesh in an attempt to preserve attribute seams, borders and overall appearance. For meshes with inconsistent topology or many seams, such as faceted meshes, it can result in simplifier getting "stuck" and not being able to simplify the mesh fully; it's recommended to preprocess the index buffer with `meshopt_generateShadowIndexBuffer` to discard any vertex attributes that aren't critical and can be rebuilt later such as normals.
 
-```
+```c++
 float threshold = 0.2f;
 size_t target_index_count = size_t(index_count * threshold);
 float target_error = 1e-2f;
@@ -221,7 +223,7 @@ Target error is an approximate measure of the deviation from the original mesh u
 
 The second simplification algorithm, `meshopt_simplifySloppy`, doesn't follow the topology of the original mesh. This means that it doesn't preserve attribute seams or borders, but it can collapse internal details that are too small to matter better because it can merge mesh features that are topologically disjoint but spatially close.
 
-```
+```c++
 float threshold = 0.2f;
 size_t target_index_count = size_t(index_count * threshold);
 
@@ -253,12 +255,58 @@ Many algorithms allocate temporary memory to store intermediate results or accel
 meshopt_setAllocator(malloc, free);
 ```
 
-> Note that currently the library expects the allocation function to either throw in case of out-of-memory (in which case the exception will propagate to the caller) or abort, so technically the use of `malloc` above isn't safe.
+> Note that the library expects the allocation function to either throw in case of out-of-memory (in which case the exception will propagate to the caller) or abort, so technically the use of `malloc` above isn't safe. If you want to handle out-of-memory errors without using C++ exceptions, you can use `setjmp`/`longjmp` instead.
 
 Vertex and index decoders (`meshopt_decodeVertexBuffer` and `meshopt_decodeIndexBuffer`) do not allocate memory and work completely within the buffer space provided via arguments.
 
 All functions have bounded stack usage that does not exceed 32 KB for any algorithms.
 
+## gltfpack
+
+meshoptimizer provides many algorithms that can be integrated into a content pipeline or a rendering engine to improve performance. Often integration requires some conscious choices for optimal results - should we optimize for overdraw or not? what should the vertex format be? do we use triangle lists or strips? However, in some cases optimality is not a requirement.
+
+For engines that want a relatively simple way to load meshes, and would like the meshes to perform reasonably well on target hardware and be reasonably fast to load, meshoptimizer provides a command-line tool, `gltfpack`. `gltfpack` can take an `.obj` or `.gltf` file as an input, and produce a `.gltf` or `.glb` file that is optimized for rendering performance and download size.
+
+To build gltfpack on Linux/macOS, you can use make:
+
+```
+make config=release gltfpack
+```
+
+On Windows (and other platforms), you can use CMake:
+
+```
+cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_TOOLS=ON
+cmake --build . --config Release --target gltfpack
+```
+
+You can then run the resulting command-line binary like this (run it without arguments for a list of options):
+
+```
+gltfpack -i scene.gltf -o scene.glb
+```
+
+gltfpack substantially changes the glTF data by optimizing the meshes for vertex fetch and transform cache, quantizing the geometry to reduce the memory consumption and size, merging meshes to reduce the draw call count, quantizing and resampling animations to reduce animation size and simplify playback, and pruning the node tree by removing or collapsing redundant nodes.
+
+gltfpack can produce two types of output files:
+
+- By default gltfpack outputs regular `.glb`/`.gltf` files that have been optimized for GPU consumption using various cache optimizers and quantization. These files can be loaded by standard GLTF loaders present in frameworks such as [three.js](https://threejs.org/) (r107+) and [Babylon.js](https://www.babylonjs.com/) (4.1+).
+- When using `-c` option, gltfpack outputs compressed `.glb`/`.gltf` files that use meshoptimizer codecs to reduce the download size further. Loading these files requires extending GLTF loaders with custom decompression support; `demo/GLTFLoader.js` contains a custom version of three.js loader that can be used to load them.
+
+> Note: files produced by gltfpack use `MESHOPT_quantized_geometry` and `MESHOPT_compression` pseudo-extensions; both of these have *not* been standardized yet but eventually will be. glTF validator doesn't recognize these extensions and produces a large number of validation errors because of this.
+
+When using compressed files, `js/meshopt_decoder.js` needs to be loaded to provide the WebAssembly decoder module like this:
+
+```js
+<script src="js/meshopt_decoder.js"></script>
+
+...
+
+var loader = new THREE.GLTFLoader();
+loader.setMeshoptDecoder(MeshoptDecoder);
+loader.load('pirate.glb', function (gltf) { scene.add(gltf.scene); });
+```
+
 ## License
 
 This library is available to anybody free of charge, under the terms of MIT License (see LICENSE.md).

+ 3316 - 0
3rdparty/meshoptimizer/demo/GLTFLoader.js

@@ -0,0 +1,3316 @@
+/**
+ * @author Rich Tibbett / https://github.com/richtr
+ * @author mrdoob / http://mrdoob.com/
+ * @author Tony Parisi / http://www.tonyparisi.com/
+ * @author Takahiro / https://github.com/takahirox
+ * @author Don McCurdy / https://www.donmccurdy.com
+ */
+
+THREE.GLTFLoader = ( function () {
+
+	function GLTFLoader( manager ) {
+
+		this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;
+		this.dracoLoader = null;
+		this.meshoptDecoder = null;
+
+	}
+
+	GLTFLoader.prototype = {
+
+		constructor: GLTFLoader,
+
+		crossOrigin: 'anonymous',
+
+		load: function ( url, onLoad, onProgress, onError ) {
+
+			var scope = this;
+
+			var resourcePath;
+
+			if ( this.resourcePath !== undefined ) {
+
+				resourcePath = this.resourcePath;
+
+			} else if ( this.path !== undefined ) {
+
+				resourcePath = this.path;
+
+			} else {
+
+				resourcePath = THREE.LoaderUtils.extractUrlBase( url );
+
+			}
+
+			// Tells the LoadingManager to track an extra item, which resolves after
+			// the model is fully loaded. This means the count of items loaded will
+			// be incorrect, but ensures manager.onLoad() does not fire early.
+			scope.manager.itemStart( url );
+
+			var _onError = function ( e ) {
+
+				if ( onError ) {
+
+					onError( e );
+
+				} else {
+
+					console.error( e );
+
+				}
+
+				scope.manager.itemError( url );
+				scope.manager.itemEnd( url );
+
+			};
+
+			var loader = new THREE.FileLoader( scope.manager );
+
+			loader.setPath( this.path );
+			loader.setResponseType( 'arraybuffer' );
+
+			loader.load( url, function ( data ) {
+
+				try {
+
+					scope.parse( data, resourcePath, function ( gltf ) {
+
+						onLoad( gltf );
+
+						scope.manager.itemEnd( url );
+
+					}, _onError );
+
+				} catch ( e ) {
+
+					_onError( e );
+
+				}
+
+			}, onProgress, _onError );
+
+		},
+
+		setCrossOrigin: function ( value ) {
+
+			this.crossOrigin = value;
+			return this;
+
+		},
+
+		setPath: function ( value ) {
+
+			this.path = value;
+			return this;
+
+		},
+
+		setResourcePath: function ( value ) {
+
+			this.resourcePath = value;
+			return this;
+
+		},
+
+		setDRACOLoader: function ( dracoLoader ) {
+
+			this.dracoLoader = dracoLoader;
+			return this;
+
+		},
+
+		setMeshoptDecoder: function ( decoder ) {
+
+			this.meshoptDecoder = decoder;
+			return this;
+
+		},
+
+		parse: function ( data, path, onLoad, onError ) {
+
+			var content;
+			var extensions = {};
+
+			if ( typeof data === 'string' ) {
+
+				content = data;
+
+			} else {
+
+				var magic = THREE.LoaderUtils.decodeText( new Uint8Array( data, 0, 4 ) );
+
+				if ( magic === BINARY_EXTENSION_HEADER_MAGIC ) {
+
+					try {
+
+						extensions[ EXTENSIONS.KHR_BINARY_GLTF ] = new GLTFBinaryExtension( data );
+
+					} catch ( error ) {
+
+						if ( onError ) onError( error );
+						return;
+
+					}
+
+					content = extensions[ EXTENSIONS.KHR_BINARY_GLTF ].content;
+
+				} else {
+
+					content = THREE.LoaderUtils.decodeText( new Uint8Array( data ) );
+
+				}
+
+			}
+
+			var json = JSON.parse( content );
+
+			if ( json.asset === undefined || json.asset.version[ 0 ] < 2 ) {
+
+				if ( onError ) onError( new Error( 'THREE.GLTFLoader: Unsupported asset. glTF versions >=2.0 are supported. Use LegacyGLTFLoader instead.' ) );
+				return;
+
+			}
+
+			if ( json.extensionsUsed ) {
+
+				for ( var i = 0; i < json.extensionsUsed.length; ++ i ) {
+
+					var extensionName = json.extensionsUsed[ i ];
+					var extensionsRequired = json.extensionsRequired || [];
+
+					switch ( extensionName ) {
+
+						case EXTENSIONS.KHR_LIGHTS_PUNCTUAL:
+							extensions[ extensionName ] = new GLTFLightsExtension( json );
+							break;
+
+						case EXTENSIONS.KHR_MATERIALS_UNLIT:
+							extensions[ extensionName ] = new GLTFMaterialsUnlitExtension( json );
+							break;
+
+						case EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS:
+							extensions[ extensionName ] = new GLTFMaterialsPbrSpecularGlossinessExtension( json );
+							break;
+
+						case EXTENSIONS.KHR_DRACO_MESH_COMPRESSION:
+							extensions[ extensionName ] = new GLTFDracoMeshCompressionExtension( json, this.dracoLoader );
+							break;
+
+						case EXTENSIONS.MSFT_TEXTURE_DDS:
+							extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] = new GLTFTextureDDSExtension();
+							break;
+
+						case EXTENSIONS.KHR_TEXTURE_TRANSFORM:
+							extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] = new GLTFTextureTransformExtension( json );
+							break;
+
+						case EXTENSIONS.MESHOPT_COMPRESSION:
+							extensions[ extensionName ] = new GLTFMeshoptCompressionExtension( json, this.meshoptDecoder );
+							break;
+
+						default:
+
+							if ( extensionsRequired.indexOf( extensionName ) >= 0 ) {
+
+								console.warn( 'THREE.GLTFLoader: Unknown extension "' + extensionName + '".' );
+
+							}
+
+					}
+
+				}
+
+			}
+
+			var parser = new GLTFParser( json, extensions, {
+
+				path: path || this.resourcePath || '',
+				crossOrigin: this.crossOrigin,
+				manager: this.manager
+
+			} );
+
+			parser.parse( onLoad, onError );
+
+		}
+
+	};
+
+	/* GLTFREGISTRY */
+
+	function GLTFRegistry() {
+
+		var objects = {};
+
+		return	{
+
+			get: function ( key ) {
+
+				return objects[ key ];
+
+			},
+
+			add: function ( key, object ) {
+
+				objects[ key ] = object;
+
+			},
+
+			remove: function ( key ) {
+
+				delete objects[ key ];
+
+			},
+
+			removeAll: function () {
+
+				objects = {};
+
+			}
+
+		};
+
+	}
+
+	/*********************************/
+	/********** EXTENSIONS ***********/
+	/*********************************/
+
+	var EXTENSIONS = {
+		KHR_BINARY_GLTF: 'KHR_binary_glTF',
+		KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
+		KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
+		KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
+		KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
+		KHR_TEXTURE_TRANSFORM: 'KHR_texture_transform',
+		MSFT_TEXTURE_DDS: 'MSFT_texture_dds',
+		MESHOPT_COMPRESSION: 'MESHOPT_compression',
+	};
+
+	/**
+	 * DDS Texture Extension
+	 *
+	 * Specification:
+	 * https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_texture_dds
+	 *
+	 */
+	function GLTFTextureDDSExtension() {
+
+		if ( ! THREE.DDSLoader ) {
+
+			throw new Error( 'THREE.GLTFLoader: Attempting to load .dds texture without importing THREE.DDSLoader' );
+
+		}
+
+		this.name = EXTENSIONS.MSFT_TEXTURE_DDS;
+		this.ddsLoader = new THREE.DDSLoader();
+
+	}
+
+	/**
+	 * Lights Extension
+	 *
+	 * Specification: PENDING
+	 */
+	function GLTFLightsExtension( json ) {
+
+		this.name = EXTENSIONS.KHR_LIGHTS_PUNCTUAL;
+
+		var extension = ( json.extensions && json.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ] ) || {};
+		this.lightDefs = extension.lights || [];
+
+	}
+
+	GLTFLightsExtension.prototype.loadLight = function ( lightIndex ) {
+
+		var lightDef = this.lightDefs[ lightIndex ];
+		var lightNode;
+
+		var color = new THREE.Color( 0xffffff );
+		if ( lightDef.color !== undefined ) color.fromArray( lightDef.color );
+
+		var range = lightDef.range !== undefined ? lightDef.range : 0;
+
+		switch ( lightDef.type ) {
+
+			case 'directional':
+				lightNode = new THREE.DirectionalLight( color );
+				lightNode.target.position.set( 0, 0, - 1 );
+				lightNode.add( lightNode.target );
+				break;
+
+			case 'point':
+				lightNode = new THREE.PointLight( color );
+				lightNode.distance = range;
+				break;
+
+			case 'spot':
+				lightNode = new THREE.SpotLight( color );
+				lightNode.distance = range;
+				// Handle spotlight properties.
+				lightDef.spot = lightDef.spot || {};
+				lightDef.spot.innerConeAngle = lightDef.spot.innerConeAngle !== undefined ? lightDef.spot.innerConeAngle : 0;
+				lightDef.spot.outerConeAngle = lightDef.spot.outerConeAngle !== undefined ? lightDef.spot.outerConeAngle : Math.PI / 4.0;
+				lightNode.angle = lightDef.spot.outerConeAngle;
+				lightNode.penumbra = 1.0 - lightDef.spot.innerConeAngle / lightDef.spot.outerConeAngle;
+				lightNode.target.position.set( 0, 0, - 1 );
+				lightNode.add( lightNode.target );
+				break;
+
+			default:
+				throw new Error( 'THREE.GLTFLoader: Unexpected light type, "' + lightDef.type + '".' );
+
+		}
+
+		// Some lights (e.g. spot) default to a position other than the origin. Reset the position
+		// here, because node-level parsing will only override position if explicitly specified.
+		lightNode.position.set( 0, 0, 0 );
+
+		lightNode.decay = 2;
+
+		if ( lightDef.intensity !== undefined ) lightNode.intensity = lightDef.intensity;
+
+		lightNode.name = lightDef.name || ( 'light_' + lightIndex );
+
+		return Promise.resolve( lightNode );
+
+	};
+
+	/**
+	 * Unlit Materials Extension (pending)
+	 *
+	 * PR: https://github.com/KhronosGroup/glTF/pull/1163
+	 */
+	function GLTFMaterialsUnlitExtension() {
+
+		this.name = EXTENSIONS.KHR_MATERIALS_UNLIT;
+
+	}
+
+	GLTFMaterialsUnlitExtension.prototype.getMaterialType = function () {
+
+		return THREE.MeshBasicMaterial;
+
+	};
+
+	GLTFMaterialsUnlitExtension.prototype.extendParams = function ( materialParams, materialDef, parser ) {
+
+		var pending = [];
+
+		materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
+		materialParams.opacity = 1.0;
+
+		var metallicRoughness = materialDef.pbrMetallicRoughness;
+
+		if ( metallicRoughness ) {
+
+			if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {
+
+				var array = metallicRoughness.baseColorFactor;
+
+				materialParams.color.fromArray( array );
+				materialParams.opacity = array[ 3 ];
+
+			}
+
+			if ( metallicRoughness.baseColorTexture !== undefined ) {
+
+				pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) );
+
+			}
+
+		}
+
+		return Promise.all( pending );
+
+	};
+
+	/* BINARY EXTENSION */
+
+	var BINARY_EXTENSION_BUFFER_NAME = 'binary_glTF';
+	var BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
+	var BINARY_EXTENSION_HEADER_LENGTH = 12;
+	var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };
+
+	function GLTFBinaryExtension( data ) {
+
+		this.name = EXTENSIONS.KHR_BINARY_GLTF;
+		this.content = null;
+		this.body = null;
+
+		var headerView = new DataView( data, 0, BINARY_EXTENSION_HEADER_LENGTH );
+
+		this.header = {
+			magic: THREE.LoaderUtils.decodeText( new Uint8Array( data.slice( 0, 4 ) ) ),
+			version: headerView.getUint32( 4, true ),
+			length: headerView.getUint32( 8, true )
+		};
+
+		if ( this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC ) {
+
+			throw new Error( 'THREE.GLTFLoader: Unsupported glTF-Binary header.' );
+
+		} else if ( this.header.version < 2.0 ) {
+
+			throw new Error( 'THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.' );
+
+		}
+
+		var chunkView = new DataView( data, BINARY_EXTENSION_HEADER_LENGTH );
+		var chunkIndex = 0;
+
+		while ( chunkIndex < chunkView.byteLength ) {
+
+			var chunkLength = chunkView.getUint32( chunkIndex, true );
+			chunkIndex += 4;
+
+			var chunkType = chunkView.getUint32( chunkIndex, true );
+			chunkIndex += 4;
+
+			if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON ) {
+
+				var contentArray = new Uint8Array( data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength );
+				this.content = THREE.LoaderUtils.decodeText( contentArray );
+
+			} else if ( chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN ) {
+
+				var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
+				this.body = data.slice( byteOffset, byteOffset + chunkLength );
+
+			}
+
+			// Clients must ignore chunks with unknown types.
+
+			chunkIndex += chunkLength;
+
+		}
+
+		if ( this.content === null ) {
+
+			throw new Error( 'THREE.GLTFLoader: JSON content not found.' );
+
+		}
+
+	}
+
+	/**
+	 * DRACO Mesh Compression Extension
+	 *
+	 * Specification: https://github.com/KhronosGroup/glTF/pull/874
+	 */
+	function GLTFDracoMeshCompressionExtension( json, dracoLoader ) {
+
+		if ( ! dracoLoader ) {
+
+			throw new Error( 'THREE.GLTFLoader: No DRACOLoader instance provided.' );
+
+		}
+
+		this.name = EXTENSIONS.KHR_DRACO_MESH_COMPRESSION;
+		this.json = json;
+		this.dracoLoader = dracoLoader;
+
+	}
+
+	GLTFDracoMeshCompressionExtension.prototype.decodePrimitive = function ( primitive, parser ) {
+
+		var json = this.json;
+		var dracoLoader = this.dracoLoader;
+		var bufferViewIndex = primitive.extensions[ this.name ].bufferView;
+		var gltfAttributeMap = primitive.extensions[ this.name ].attributes;
+		var threeAttributeMap = {};
+		var attributeNormalizedMap = {};
+		var attributeTypeMap = {};
+
+		for ( var attributeName in gltfAttributeMap ) {
+
+			var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();
+
+			threeAttributeMap[ threeAttributeName ] = gltfAttributeMap[ attributeName ];
+
+		}
+
+		for ( attributeName in primitive.attributes ) {
+
+			var threeAttributeName = ATTRIBUTES[ attributeName ] || attributeName.toLowerCase();
+
+			if ( gltfAttributeMap[ attributeName ] !== undefined ) {
+
+				var accessorDef = json.accessors[ primitive.attributes[ attributeName ] ];
+				var componentType = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];
+
+				attributeTypeMap[ threeAttributeName ] = componentType;
+				attributeNormalizedMap[ threeAttributeName ] = accessorDef.normalized === true;
+
+			}
+
+		}
+
+		return parser.getDependency( 'bufferView', bufferViewIndex ).then( function ( bufferView ) {
+
+			return new Promise( function ( resolve ) {
+
+				dracoLoader.decodeDracoFile( bufferView, function ( geometry ) {
+
+					for ( var attributeName in geometry.attributes ) {
+
+						var attribute = geometry.attributes[ attributeName ];
+						var normalized = attributeNormalizedMap[ attributeName ];
+
+						if ( normalized !== undefined ) attribute.normalized = normalized;
+
+					}
+
+					resolve( geometry );
+
+				}, threeAttributeMap, attributeTypeMap );
+
+			} );
+
+		} );
+
+	};
+
+	/**
+	 * Texture Transform Extension
+	 *
+	 * Specification:
+	 */
+	function GLTFTextureTransformExtension() {
+
+		this.name = EXTENSIONS.KHR_TEXTURE_TRANSFORM;
+
+	}
+
+	GLTFTextureTransformExtension.prototype.extendTexture = function ( texture, transform ) {
+
+		texture = texture.clone();
+
+		if ( transform.offset !== undefined ) {
+
+			texture.offset.fromArray( transform.offset );
+
+		}
+
+		if ( transform.rotation !== undefined ) {
+
+			texture.rotation = transform.rotation;
+
+		}
+
+		if ( transform.scale !== undefined ) {
+
+			texture.repeat.fromArray( transform.scale );
+
+		}
+
+		if ( transform.texCoord !== undefined ) {
+
+			console.warn( 'THREE.GLTFLoader: Custom UV sets in "' + this.name + '" extension not yet supported.' );
+
+		}
+
+		texture.needsUpdate = true;
+
+		return texture;
+
+	};
+
+	/**
+	 * Specular-Glossiness Extension
+	 *
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Khronos/KHR_materials_pbrSpecularGlossiness
+	 */
+	function GLTFMaterialsPbrSpecularGlossinessExtension() {
+
+		return {
+
+			name: EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS,
+
+			specularGlossinessParams: [
+				'color',
+				'map',
+				'lightMap',
+				'lightMapIntensity',
+				'aoMap',
+				'aoMapIntensity',
+				'emissive',
+				'emissiveIntensity',
+				'emissiveMap',
+				'bumpMap',
+				'bumpScale',
+				'normalMap',
+				'displacementMap',
+				'displacementScale',
+				'displacementBias',
+				'specularMap',
+				'specular',
+				'glossinessMap',
+				'glossiness',
+				'alphaMap',
+				'envMap',
+				'envMapIntensity',
+				'refractionRatio',
+			],
+
+			getMaterialType: function () {
+
+				return THREE.ShaderMaterial;
+
+			},
+
+			extendParams: function ( materialParams, materialDef, parser ) {
+
+				var pbrSpecularGlossiness = materialDef.extensions[ this.name ];
+
+				var shader = THREE.ShaderLib[ 'standard' ];
+
+				var uniforms = THREE.UniformsUtils.clone( shader.uniforms );
+
+				var specularMapParsFragmentChunk = [
+					'#ifdef USE_SPECULARMAP',
+					'	uniform sampler2D specularMap;',
+					'#endif'
+				].join( '\n' );
+
+				var glossinessMapParsFragmentChunk = [
+					'#ifdef USE_GLOSSINESSMAP',
+					'	uniform sampler2D glossinessMap;',
+					'#endif'
+				].join( '\n' );
+
+				var specularMapFragmentChunk = [
+					'vec3 specularFactor = specular;',
+					'#ifdef USE_SPECULARMAP',
+					'	vec4 texelSpecular = texture2D( specularMap, vUv );',
+					'	texelSpecular = sRGBToLinear( texelSpecular );',
+					'	// reads channel RGB, compatible with a glTF Specular-Glossiness (RGBA) texture',
+					'	specularFactor *= texelSpecular.rgb;',
+					'#endif'
+				].join( '\n' );
+
+				var glossinessMapFragmentChunk = [
+					'float glossinessFactor = glossiness;',
+					'#ifdef USE_GLOSSINESSMAP',
+					'	vec4 texelGlossiness = texture2D( glossinessMap, vUv );',
+					'	// reads channel A, compatible with a glTF Specular-Glossiness (RGBA) texture',
+					'	glossinessFactor *= texelGlossiness.a;',
+					'#endif'
+				].join( '\n' );
+
+				var lightPhysicalFragmentChunk = [
+					'PhysicalMaterial material;',
+					'material.diffuseColor = diffuseColor.rgb;',
+					'material.specularRoughness = clamp( 1.0 - glossinessFactor, 0.04, 1.0 );',
+					'material.specularColor = specularFactor.rgb;',
+				].join( '\n' );
+
+				var fragmentShader = shader.fragmentShader
+					.replace( 'uniform float roughness;', 'uniform vec3 specular;' )
+					.replace( 'uniform float metalness;', 'uniform float glossiness;' )
+					.replace( '#include <roughnessmap_pars_fragment>', specularMapParsFragmentChunk )
+					.replace( '#include <metalnessmap_pars_fragment>', glossinessMapParsFragmentChunk )
+					.replace( '#include <roughnessmap_fragment>', specularMapFragmentChunk )
+					.replace( '#include <metalnessmap_fragment>', glossinessMapFragmentChunk )
+					.replace( '#include <lights_physical_fragment>', lightPhysicalFragmentChunk );
+
+				delete uniforms.roughness;
+				delete uniforms.metalness;
+				delete uniforms.roughnessMap;
+				delete uniforms.metalnessMap;
+
+				uniforms.specular = { value: new THREE.Color().setHex( 0x111111 ) };
+				uniforms.glossiness = { value: 0.5 };
+				uniforms.specularMap = { value: null };
+				uniforms.glossinessMap = { value: null };
+
+				materialParams.vertexShader = shader.vertexShader;
+				materialParams.fragmentShader = fragmentShader;
+				materialParams.uniforms = uniforms;
+				materialParams.defines = { 'STANDARD': '' };
+
+				materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
+				materialParams.opacity = 1.0;
+
+				var pending = [];
+
+				if ( Array.isArray( pbrSpecularGlossiness.diffuseFactor ) ) {
+
+					var array = pbrSpecularGlossiness.diffuseFactor;
+
+					materialParams.color.fromArray( array );
+					materialParams.opacity = array[ 3 ];
+
+				}
+
+				if ( pbrSpecularGlossiness.diffuseTexture !== undefined ) {
+
+					pending.push( parser.assignTexture( materialParams, 'map', pbrSpecularGlossiness.diffuseTexture ) );
+
+				}
+
+				materialParams.emissive = new THREE.Color( 0.0, 0.0, 0.0 );
+				materialParams.glossiness = pbrSpecularGlossiness.glossinessFactor !== undefined ? pbrSpecularGlossiness.glossinessFactor : 1.0;
+				materialParams.specular = new THREE.Color( 1.0, 1.0, 1.0 );
+
+				if ( Array.isArray( pbrSpecularGlossiness.specularFactor ) ) {
+
+					materialParams.specular.fromArray( pbrSpecularGlossiness.specularFactor );
+
+				}
+
+				if ( pbrSpecularGlossiness.specularGlossinessTexture !== undefined ) {
+
+					var specGlossMapDef = pbrSpecularGlossiness.specularGlossinessTexture;
+					pending.push( parser.assignTexture( materialParams, 'glossinessMap', specGlossMapDef ) );
+					pending.push( parser.assignTexture( materialParams, 'specularMap', specGlossMapDef ) );
+
+				}
+
+				return Promise.all( pending );
+
+			},
+
+			createMaterial: function ( params ) {
+
+				// setup material properties based on MeshStandardMaterial for Specular-Glossiness
+
+				var material = new THREE.ShaderMaterial( {
+					defines: params.defines,
+					vertexShader: params.vertexShader,
+					fragmentShader: params.fragmentShader,
+					uniforms: params.uniforms,
+					fog: true,
+					lights: true,
+					opacity: params.opacity,
+					transparent: params.transparent
+				} );
+
+				material.isGLTFSpecularGlossinessMaterial = true;
+
+				material.color = params.color;
+
+				material.map = params.map === undefined ? null : params.map;
+
+				material.lightMap = null;
+				material.lightMapIntensity = 1.0;
+
+				material.aoMap = params.aoMap === undefined ? null : params.aoMap;
+				material.aoMapIntensity = 1.0;
+
+				material.emissive = params.emissive;
+				material.emissiveIntensity = 1.0;
+				material.emissiveMap = params.emissiveMap === undefined ? null : params.emissiveMap;
+
+				material.bumpMap = params.bumpMap === undefined ? null : params.bumpMap;
+				material.bumpScale = 1;
+
+				material.normalMap = params.normalMap === undefined ? null : params.normalMap;
+
+				if ( params.normalScale ) material.normalScale = params.normalScale;
+
+				material.displacementMap = null;
+				material.displacementScale = 1;
+				material.displacementBias = 0;
+
+				material.specularMap = params.specularMap === undefined ? null : params.specularMap;
+				material.specular = params.specular;
+
+				material.glossinessMap = params.glossinessMap === undefined ? null : params.glossinessMap;
+				material.glossiness = params.glossiness;
+
+				material.alphaMap = null;
+
+				material.envMap = params.envMap === undefined ? null : params.envMap;
+				material.envMapIntensity = 1.0;
+
+				material.refractionRatio = 0.98;
+
+				material.extensions.derivatives = true;
+
+				return material;
+
+			},
+
+			/**
+			 * Clones a GLTFSpecularGlossinessMaterial instance. The ShaderMaterial.copy() method can
+			 * copy only properties it knows about or inherits, and misses many properties that would
+			 * normally be defined by MeshStandardMaterial.
+			 *
+			 * This method allows GLTFSpecularGlossinessMaterials to be cloned in the process of
+			 * loading a glTF model, but cloning later (e.g. by the user) would require these changes
+			 * AND also updating `.onBeforeRender` on the parent mesh.
+			 *
+			 * @param  {THREE.ShaderMaterial} source
+			 * @return {THREE.ShaderMaterial}
+			 */
+			cloneMaterial: function ( source ) {
+
+				var target = source.clone();
+
+				target.isGLTFSpecularGlossinessMaterial = true;
+
+				var params = this.specularGlossinessParams;
+
+				for ( var i = 0, il = params.length; i < il; i ++ ) {
+
+					var value = source[ params[ i ] ];
+					target[ params[ i ] ] = ( value && value.isColor ) ? value.clone() : value;
+
+				}
+
+				return target;
+
+			},
+
+			// Here's based on refreshUniformsCommon() and refreshUniformsStandard() in WebGLRenderer.
+			refreshUniforms: function ( renderer, scene, camera, geometry, material, group ) {
+
+				if ( material.isGLTFSpecularGlossinessMaterial !== true ) {
+
+					return;
+
+				}
+
+				var uniforms = material.uniforms;
+				var defines = material.defines;
+
+				uniforms.opacity.value = material.opacity;
+
+				uniforms.diffuse.value.copy( material.color );
+				uniforms.emissive.value.copy( material.emissive ).multiplyScalar( material.emissiveIntensity );
+
+				uniforms.map.value = material.map;
+				uniforms.specularMap.value = material.specularMap;
+				uniforms.alphaMap.value = material.alphaMap;
+
+				uniforms.lightMap.value = material.lightMap;
+				uniforms.lightMapIntensity.value = material.lightMapIntensity;
+
+				uniforms.aoMap.value = material.aoMap;
+				uniforms.aoMapIntensity.value = material.aoMapIntensity;
+
+				// uv repeat and offset setting priorities
+				// 1. color map
+				// 2. specular map
+				// 3. normal map
+				// 4. bump map
+				// 5. alpha map
+				// 6. emissive map
+
+				var uvScaleMap;
+
+				if ( material.map ) {
+
+					uvScaleMap = material.map;
+
+				} else if ( material.specularMap ) {
+
+					uvScaleMap = material.specularMap;
+
+				} else if ( material.displacementMap ) {
+
+					uvScaleMap = material.displacementMap;
+
+				} else if ( material.normalMap ) {
+
+					uvScaleMap = material.normalMap;
+
+				} else if ( material.bumpMap ) {
+
+					uvScaleMap = material.bumpMap;
+
+				} else if ( material.glossinessMap ) {
+
+					uvScaleMap = material.glossinessMap;
+
+				} else if ( material.alphaMap ) {
+
+					uvScaleMap = material.alphaMap;
+
+				} else if ( material.emissiveMap ) {
+
+					uvScaleMap = material.emissiveMap;
+
+				}
+
+				if ( uvScaleMap !== undefined ) {
+
+					// backwards compatibility
+					if ( uvScaleMap.isWebGLRenderTarget ) {
+
+						uvScaleMap = uvScaleMap.texture;
+
+					}
+
+					if ( uvScaleMap.matrixAutoUpdate === true ) {
+
+						uvScaleMap.updateMatrix();
+
+					}
+
+					uniforms.uvTransform.value.copy( uvScaleMap.matrix );
+
+				}
+
+				if ( material.envMap ) {
+
+					uniforms.envMap.value = material.envMap;
+					uniforms.envMapIntensity.value = material.envMapIntensity;
+
+					// don't flip CubeTexture envMaps, flip everything else:
+					//  WebGLRenderTargetCube will be flipped for backwards compatibility
+					//  WebGLRenderTargetCube.texture will be flipped because it's a Texture and NOT a CubeTexture
+					// this check must be handled differently, or removed entirely, if WebGLRenderTargetCube uses a CubeTexture in the future
+					uniforms.flipEnvMap.value = material.envMap.isCubeTexture ? - 1 : 1;
+
+					uniforms.reflectivity.value = material.reflectivity;
+					uniforms.refractionRatio.value = material.refractionRatio;
+
+					uniforms.maxMipLevel.value = renderer.properties.get( material.envMap ).__maxMipLevel;
+
+				}
+
+				uniforms.specular.value.copy( material.specular );
+				uniforms.glossiness.value = material.glossiness;
+
+				uniforms.glossinessMap.value = material.glossinessMap;
+
+				uniforms.emissiveMap.value = material.emissiveMap;
+				uniforms.bumpMap.value = material.bumpMap;
+				uniforms.normalMap.value = material.normalMap;
+
+				uniforms.displacementMap.value = material.displacementMap;
+				uniforms.displacementScale.value = material.displacementScale;
+				uniforms.displacementBias.value = material.displacementBias;
+
+				if ( uniforms.glossinessMap.value !== null && defines.USE_GLOSSINESSMAP === undefined ) {
+
+					defines.USE_GLOSSINESSMAP = '';
+					// set USE_ROUGHNESSMAP to enable vUv
+					defines.USE_ROUGHNESSMAP = '';
+
+				}
+
+				if ( uniforms.glossinessMap.value === null && defines.USE_GLOSSINESSMAP !== undefined ) {
+
+					delete defines.USE_GLOSSINESSMAP;
+					delete defines.USE_ROUGHNESSMAP;
+
+				}
+
+			}
+
+		};
+
+	}
+
+	/**
+	 * meshoptimizer Compression Extension
+	 */
+	function GLTFMeshoptCompressionExtension( json, meshoptDecoder ) {
+
+		if ( ! meshoptDecoder ) {
+
+			throw new Error( 'THREE.GLTFLoader: No MeshoptDecoder instance provided.' );
+
+		}
+
+		this.name = EXTENSIONS.MESHOPT_COMPRESSION;
+		this.json = json;
+		this.meshoptDecoder = meshoptDecoder;
+
+	}
+
+	GLTFMeshoptCompressionExtension.prototype.decodeBufferView = function ( bufferViewDef, buffer ) {
+
+		var decoder = this.meshoptDecoder;
+
+		return decoder.ready.then( function () {
+
+			var extensionDef = bufferViewDef.extensions[ EXTENSIONS.MESHOPT_COMPRESSION ];
+
+			var byteOffset = bufferViewDef.byteOffset || 0;
+			var byteLength = bufferViewDef.byteLength || 0;
+
+			var count = extensionDef.count;
+			var stride = extensionDef.byteStride;
+
+			var result = new ArrayBuffer(count * stride);
+			var source = new Uint8Array(buffer, byteOffset, byteLength);
+
+			switch ( extensionDef.mode ) {
+
+				case 0:
+					decoder.decodeVertexBuffer(new Uint8Array(result), count, stride, source);
+					break;
+
+				case 1:
+					decoder.decodeIndexBuffer(new Uint8Array(result), count, stride, source);
+					break;
+
+				default:
+					throw new Error( 'THREE.GLTFLoader: Unrecognized meshopt compression mode.' );
+
+			}
+
+			return result;
+
+		} );
+
+	}
+
+	/*********************************/
+	/********** INTERPOLATION ********/
+	/*********************************/
+
+	// Spline Interpolation
+	// Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#appendix-c-spline-interpolation
+	function GLTFCubicSplineInterpolant( parameterPositions, sampleValues, sampleSize, resultBuffer ) {
+
+		THREE.Interpolant.call( this, parameterPositions, sampleValues, sampleSize, resultBuffer );
+
+	}
+
+	GLTFCubicSplineInterpolant.prototype = Object.create( THREE.Interpolant.prototype );
+	GLTFCubicSplineInterpolant.prototype.constructor = GLTFCubicSplineInterpolant;
+
+	GLTFCubicSplineInterpolant.prototype.copySampleValue_ = function ( index ) {
+
+		// Copies a sample value to the result buffer. See description of glTF
+		// CUBICSPLINE values layout in interpolate_() function below.
+
+		var result = this.resultBuffer,
+			values = this.sampleValues,
+			valueSize = this.valueSize,
+			offset = index * valueSize * 3 + valueSize;
+
+		for ( var i = 0; i !== valueSize; i ++ ) {
+
+			result[ i ] = values[ offset + i ];
+
+		}
+
+		return result;
+
+	};
+
+	GLTFCubicSplineInterpolant.prototype.beforeStart_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;
+
+	GLTFCubicSplineInterpolant.prototype.afterEnd_ = GLTFCubicSplineInterpolant.prototype.copySampleValue_;
+
+	GLTFCubicSplineInterpolant.prototype.interpolate_ = function ( i1, t0, t, t1 ) {
+
+		var result = this.resultBuffer;
+		var values = this.sampleValues;
+		var stride = this.valueSize;
+
+		var stride2 = stride * 2;
+		var stride3 = stride * 3;
+
+		var td = t1 - t0;
+
+		var p = ( t - t0 ) / td;
+		var pp = p * p;
+		var ppp = pp * p;
+
+		var offset1 = i1 * stride3;
+		var offset0 = offset1 - stride3;
+
+		var s2 = - 2 * ppp + 3 * pp;
+		var s3 = ppp - pp;
+		var s0 = 1 - s2;
+		var s1 = s3 - pp + p;
+
+		// Layout of keyframe output values for CUBICSPLINE animations:
+		//   [ inTangent_1, splineVertex_1, outTangent_1, inTangent_2, splineVertex_2, ... ]
+		for ( var i = 0; i !== stride; i ++ ) {
+
+			var p0 = values[ offset0 + i + stride ]; // splineVertex_k
+			var m0 = values[ offset0 + i + stride2 ] * td; // outTangent_k * (t_k+1 - t_k)
+			var p1 = values[ offset1 + i + stride ]; // splineVertex_k+1
+			var m1 = values[ offset1 + i ] * td; // inTangent_k+1 * (t_k+1 - t_k)
+
+			result[ i ] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;
+
+		}
+
+		return result;
+
+	};
+
+	/*********************************/
+	/********** INTERNALS ************/
+	/*********************************/
+
+	/* CONSTANTS */
+
+	var WEBGL_CONSTANTS = {
+		FLOAT: 5126,
+		//FLOAT_MAT2: 35674,
+		FLOAT_MAT3: 35675,
+		FLOAT_MAT4: 35676,
+		FLOAT_VEC2: 35664,
+		FLOAT_VEC3: 35665,
+		FLOAT_VEC4: 35666,
+		LINEAR: 9729,
+		REPEAT: 10497,
+		SAMPLER_2D: 35678,
+		POINTS: 0,
+		LINES: 1,
+		LINE_LOOP: 2,
+		LINE_STRIP: 3,
+		TRIANGLES: 4,
+		TRIANGLE_STRIP: 5,
+		TRIANGLE_FAN: 6,
+		UNSIGNED_BYTE: 5121,
+		UNSIGNED_SHORT: 5123
+	};
+
+	var WEBGL_TYPE = {
+		5126: Number,
+		//35674: THREE.Matrix2,
+		35675: THREE.Matrix3,
+		35676: THREE.Matrix4,
+		35664: THREE.Vector2,
+		35665: THREE.Vector3,
+		35666: THREE.Vector4,
+		35678: THREE.Texture
+	};
+
+	var WEBGL_COMPONENT_TYPES = {
+		5120: Int8Array,
+		5121: Uint8Array,
+		5122: Int16Array,
+		5123: Uint16Array,
+		5125: Uint32Array,
+		5126: Float32Array
+	};
+
+	var WEBGL_FILTERS = {
+		9728: THREE.NearestFilter,
+		9729: THREE.LinearFilter,
+		9984: THREE.NearestMipMapNearestFilter,
+		9985: THREE.LinearMipMapNearestFilter,
+		9986: THREE.NearestMipMapLinearFilter,
+		9987: THREE.LinearMipMapLinearFilter
+	};
+
+	var WEBGL_WRAPPINGS = {
+		33071: THREE.ClampToEdgeWrapping,
+		33648: THREE.MirroredRepeatWrapping,
+		10497: THREE.RepeatWrapping
+	};
+
+	var WEBGL_SIDES = {
+		1028: THREE.BackSide, // Culling front
+		1029: THREE.FrontSide // Culling back
+		//1032: THREE.NoSide   // Culling front and back, what to do?
+	};
+
+	var WEBGL_DEPTH_FUNCS = {
+		512: THREE.NeverDepth,
+		513: THREE.LessDepth,
+		514: THREE.EqualDepth,
+		515: THREE.LessEqualDepth,
+		516: THREE.GreaterEqualDepth,
+		517: THREE.NotEqualDepth,
+		518: THREE.GreaterEqualDepth,
+		519: THREE.AlwaysDepth
+	};
+
+	var WEBGL_BLEND_EQUATIONS = {
+		32774: THREE.AddEquation,
+		32778: THREE.SubtractEquation,
+		32779: THREE.ReverseSubtractEquation
+	};
+
+	var WEBGL_BLEND_FUNCS = {
+		0: THREE.ZeroFactor,
+		1: THREE.OneFactor,
+		768: THREE.SrcColorFactor,
+		769: THREE.OneMinusSrcColorFactor,
+		770: THREE.SrcAlphaFactor,
+		771: THREE.OneMinusSrcAlphaFactor,
+		772: THREE.DstAlphaFactor,
+		773: THREE.OneMinusDstAlphaFactor,
+		774: THREE.DstColorFactor,
+		775: THREE.OneMinusDstColorFactor,
+		776: THREE.SrcAlphaSaturateFactor
+		// The followings are not supported by Three.js yet
+		//32769: CONSTANT_COLOR,
+		//32770: ONE_MINUS_CONSTANT_COLOR,
+		//32771: CONSTANT_ALPHA,
+		//32772: ONE_MINUS_CONSTANT_COLOR
+	};
+
+	var WEBGL_TYPE_SIZES = {
+		'SCALAR': 1,
+		'VEC2': 2,
+		'VEC3': 3,
+		'VEC4': 4,
+		'MAT2': 4,
+		'MAT3': 9,
+		'MAT4': 16
+	};
+
+	var ATTRIBUTES = {
+		POSITION: 'position',
+		NORMAL: 'normal',
+		TANGENT: 'tangent',
+		TEXCOORD_0: 'uv',
+		TEXCOORD_1: 'uv2',
+		COLOR_0: 'color',
+		WEIGHTS_0: 'skinWeight',
+		JOINTS_0: 'skinIndex',
+	};
+
+	var PATH_PROPERTIES = {
+		scale: 'scale',
+		translation: 'position',
+		rotation: 'quaternion',
+		weights: 'morphTargetInfluences'
+	};
+
+	var INTERPOLATION = {
+		CUBICSPLINE: undefined, // We use a custom interpolant (GLTFCubicSplineInterpolation) for CUBICSPLINE tracks. Each
+		                        // keyframe track will be initialized with a default interpolation type, then modified.
+		LINEAR: THREE.InterpolateLinear,
+		STEP: THREE.InterpolateDiscrete
+	};
+
+	var STATES_ENABLES = {
+		2884: 'CULL_FACE',
+		2929: 'DEPTH_TEST',
+		3042: 'BLEND',
+		3089: 'SCISSOR_TEST',
+		32823: 'POLYGON_OFFSET_FILL',
+		32926: 'SAMPLE_ALPHA_TO_COVERAGE'
+	};
+
+	var ALPHA_MODES = {
+		OPAQUE: 'OPAQUE',
+		MASK: 'MASK',
+		BLEND: 'BLEND'
+	};
+
+	var MIME_TYPE_FORMATS = {
+		'image/png': THREE.RGBAFormat,
+		'image/jpeg': THREE.RGBFormat
+	};
+
+	/* UTILITY FUNCTIONS */
+
+	function resolveURL( url, path ) {
+
+		// Invalid URL
+		if ( typeof url !== 'string' || url === '' ) return '';
+
+		// Absolute URL http://,https://,//
+		if ( /^(https?:)?\/\//i.test( url ) ) return url;
+
+		// Data URI
+		if ( /^data:.*,.*$/i.test( url ) ) return url;
+
+		// Blob URL
+		if ( /^blob:.*$/i.test( url ) ) return url;
+
+		// Relative URL
+		return path + url;
+
+	}
+
+	var defaultMaterial;
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
+	 */
+	function createDefaultMaterial() {
+
+		defaultMaterial = defaultMaterial || new THREE.MeshStandardMaterial( {
+			color: 0xFFFFFF,
+			emissive: 0x000000,
+			metalness: 1,
+			roughness: 1,
+			transparent: false,
+			depthTest: true,
+			side: THREE.FrontSide
+		} );
+
+		return defaultMaterial;
+
+	}
+
+	function addUnknownExtensionsToUserData( knownExtensions, object, objectDef ) {
+
+		// Add unknown glTF extensions to an object's userData.
+
+		for ( var name in objectDef.extensions ) {
+
+			if ( knownExtensions[ name ] === undefined ) {
+
+				object.userData.gltfExtensions = object.userData.gltfExtensions || {};
+				object.userData.gltfExtensions[ name ] = objectDef.extensions[ name ];
+
+			}
+
+		}
+
+	}
+
+	/**
+	 * @param {THREE.Object3D|THREE.Material|THREE.BufferGeometry} object
+	 * @param {GLTF.definition} gltfDef
+	 */
+	function assignExtrasToUserData( object, gltfDef ) {
+
+		if ( gltfDef.extras !== undefined ) {
+
+			if ( typeof gltfDef.extras === 'object' ) {
+
+				Object.assign( object.userData, gltfDef.extras );
+
+			} else {
+
+				console.warn( 'THREE.GLTFLoader: Ignoring primitive type .extras, ' + gltfDef.extras );
+
+			}
+
+		}
+
+	}
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
+	 *
+	 * @param {THREE.BufferGeometry} geometry
+	 * @param {Array<GLTF.Target>} targets
+	 * @param {GLTFParser} parser
+	 * @return {Promise<THREE.BufferGeometry>}
+	 */
+	function addMorphTargets( geometry, targets, parser ) {
+
+		var hasMorphPosition = false;
+		var hasMorphNormal = false;
+
+		for ( var i = 0, il = targets.length; i < il; i ++ ) {
+
+			var target = targets[ i ];
+
+			if ( target.POSITION !== undefined ) hasMorphPosition = true;
+			if ( target.NORMAL !== undefined ) hasMorphNormal = true;
+
+			if ( hasMorphPosition && hasMorphNormal ) break;
+
+		}
+
+		if ( ! hasMorphPosition && ! hasMorphNormal ) return Promise.resolve( geometry );
+
+		var pendingPositionAccessors = [];
+		var pendingNormalAccessors = [];
+
+		for ( var i = 0, il = targets.length; i < il; i ++ ) {
+
+			var target = targets[ i ];
+
+			if ( hasMorphPosition ) {
+
+				var pendingAccessor = target.POSITION !== undefined
+					? parser.getDependency( 'accessor', target.POSITION )
+					: geometry.attributes.position;
+
+				pendingPositionAccessors.push( pendingAccessor );
+
+			}
+
+			if ( hasMorphNormal ) {
+
+				var pendingAccessor = target.NORMAL !== undefined
+					? parser.getDependency( 'accessor', target.NORMAL )
+					: geometry.attributes.normal;
+
+				pendingNormalAccessors.push( pendingAccessor );
+
+			}
+
+		}
+
+		return Promise.all( [
+			Promise.all( pendingPositionAccessors ),
+			Promise.all( pendingNormalAccessors )
+		] ).then( function ( accessors ) {
+
+			var morphPositions = accessors[ 0 ];
+			var morphNormals = accessors[ 1 ];
+
+			// Clone morph target accessors before modifying them.
+
+			for ( var i = 0, il = morphPositions.length; i < il; i ++ ) {
+
+				if ( geometry.attributes.position === morphPositions[ i ] ) continue;
+
+				morphPositions[ i ] = cloneBufferAttribute( morphPositions[ i ] );
+
+			}
+
+			for ( var i = 0, il = morphNormals.length; i < il; i ++ ) {
+
+				if ( geometry.attributes.normal === morphNormals[ i ] ) continue;
+
+				morphNormals[ i ] = cloneBufferAttribute( morphNormals[ i ] );
+
+			}
+
+			for ( var i = 0, il = targets.length; i < il; i ++ ) {
+
+				var target = targets[ i ];
+				var attributeName = 'morphTarget' + i;
+
+				if ( hasMorphPosition ) {
+
+					// Three.js morph position is absolute value. The formula is
+					//   basePosition
+					//     + weight0 * ( morphPosition0 - basePosition )
+					//     + weight1 * ( morphPosition1 - basePosition )
+					//     ...
+					// while the glTF one is relative
+					//   basePosition
+					//     + weight0 * glTFmorphPosition0
+					//     + weight1 * glTFmorphPosition1
+					//     ...
+					// then we need to convert from relative to absolute here.
+
+					if ( target.POSITION !== undefined ) {
+
+						var positionAttribute = morphPositions[ i ];
+						positionAttribute.name = attributeName;
+
+						var position = geometry.attributes.position;
+
+						for ( var j = 0, jl = positionAttribute.count; j < jl; j ++ ) {
+
+							positionAttribute.setXYZ(
+								j,
+								positionAttribute.getX( j ) + position.getX( j ),
+								positionAttribute.getY( j ) + position.getY( j ),
+								positionAttribute.getZ( j ) + position.getZ( j )
+							);
+
+						}
+
+					}
+
+				}
+
+				if ( hasMorphNormal ) {
+
+					// see target.POSITION's comment
+
+					if ( target.NORMAL !== undefined ) {
+
+						var normalAttribute = morphNormals[ i ];
+						normalAttribute.name = attributeName;
+
+						var normal = geometry.attributes.normal;
+
+						for ( var j = 0, jl = normalAttribute.count; j < jl; j ++ ) {
+
+							normalAttribute.setXYZ(
+								j,
+								normalAttribute.getX( j ) + normal.getX( j ),
+								normalAttribute.getY( j ) + normal.getY( j ),
+								normalAttribute.getZ( j ) + normal.getZ( j )
+							);
+
+						}
+
+					}
+
+				}
+
+			}
+
+			if ( hasMorphPosition ) geometry.morphAttributes.position = morphPositions;
+			if ( hasMorphNormal ) geometry.morphAttributes.normal = morphNormals;
+
+			return geometry;
+
+		} );
+
+	}
+
+	/**
+	 * @param {THREE.Mesh} mesh
+	 * @param {GLTF.Mesh} meshDef
+	 */
+	function updateMorphTargets( mesh, meshDef ) {
+
+		mesh.updateMorphTargets();
+
+		if ( meshDef.weights !== undefined ) {
+
+			for ( var i = 0, il = meshDef.weights.length; i < il; i ++ ) {
+
+				mesh.morphTargetInfluences[ i ] = meshDef.weights[ i ];
+
+			}
+
+		}
+
+		// .extras has user-defined data, so check that .extras.targetNames is an array.
+		if ( meshDef.extras && Array.isArray( meshDef.extras.targetNames ) ) {
+
+			var targetNames = meshDef.extras.targetNames;
+
+			if ( mesh.morphTargetInfluences.length === targetNames.length ) {
+
+				mesh.morphTargetDictionary = {};
+
+				for ( var i = 0, il = targetNames.length; i < il; i ++ ) {
+
+					mesh.morphTargetDictionary[ targetNames[ i ] ] = i;
+
+				}
+
+			} else {
+
+				console.warn( 'THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.' );
+
+			}
+
+		}
+
+	}
+	function isObjectEqual( a, b ) {
+
+		if ( Object.keys( a ).length !== Object.keys( b ).length ) return false;
+
+		for ( var key in a ) {
+
+			if ( a[ key ] !== b[ key ] ) return false;
+
+		}
+
+		return true;
+
+	}
+
+	function createPrimitiveKey( primitiveDef ) {
+
+		var dracoExtension = primitiveDef.extensions && primitiveDef.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ];
+		var geometryKey;
+
+		if ( dracoExtension ) {
+
+			geometryKey = 'draco:' + dracoExtension.bufferView
+				+ ':' + dracoExtension.indices
+				+ ':' + createAttributesKey( dracoExtension.attributes );
+
+		} else {
+
+			geometryKey = primitiveDef.indices + ':' + createAttributesKey( primitiveDef.attributes ) + ':' + primitiveDef.mode;
+
+		}
+
+		return geometryKey;
+
+	}
+
+	function createAttributesKey( attributes ) {
+
+		var attributesKey = '';
+
+		var keys = Object.keys( attributes ).sort();
+
+		for ( var i = 0, il = keys.length; i < il; i ++ ) {
+
+			attributesKey += keys[ i ] + ':' + attributes[ keys[ i ] ] + ';';
+
+		}
+
+		return attributesKey;
+
+	}
+
+	function cloneBufferAttribute( attribute ) {
+
+		if ( attribute.isInterleavedBufferAttribute ) {
+
+			var count = attribute.count;
+			var itemSize = attribute.itemSize;
+			var array = attribute.array.slice( 0, count * itemSize );
+
+			for ( var i = 0, j = 0; i < count; ++ i ) {
+
+				array[ j ++ ] = attribute.getX( i );
+				if ( itemSize >= 2 ) array[ j ++ ] = attribute.getY( i );
+				if ( itemSize >= 3 ) array[ j ++ ] = attribute.getZ( i );
+				if ( itemSize >= 4 ) array[ j ++ ] = attribute.getW( i );
+
+			}
+
+			return new THREE.BufferAttribute( array, itemSize, attribute.normalized );
+
+		}
+
+		return attribute.clone();
+
+	}
+
+	/* GLTF PARSER */
+
+	function GLTFParser( json, extensions, options ) {
+
+		this.json = json || {};
+		this.extensions = extensions || {};
+		this.options = options || {};
+
+		// loader object cache
+		this.cache = new GLTFRegistry();
+
+		// BufferGeometry caching
+		this.primitiveCache = {};
+
+		this.textureLoader = new THREE.TextureLoader( this.options.manager );
+		this.textureLoader.setCrossOrigin( this.options.crossOrigin );
+
+		this.fileLoader = new THREE.FileLoader( this.options.manager );
+		this.fileLoader.setResponseType( 'arraybuffer' );
+
+	}
+
+	GLTFParser.prototype.parse = function ( onLoad, onError ) {
+
+		var parser = this;
+		var json = this.json;
+		var extensions = this.extensions;
+
+		// Clear the loader cache
+		this.cache.removeAll();
+
+		// Mark the special nodes/meshes in json for efficient parse
+		this.markDefs();
+
+		Promise.all( [
+
+			this.getDependencies( 'scene' ),
+			this.getDependencies( 'animation' ),
+			this.getDependencies( 'camera' ),
+
+		] ).then( function ( dependencies ) {
+
+			var result = {
+				scene: dependencies[ 0 ][ json.scene || 0 ],
+				scenes: dependencies[ 0 ],
+				animations: dependencies[ 1 ],
+				cameras: dependencies[ 2 ],
+				asset: json.asset,
+				parser: parser,
+				userData: {}
+			};
+
+			addUnknownExtensionsToUserData( extensions, result, json );
+
+			onLoad( result );
+
+		} ).catch( onError );
+
+	};
+
+	/**
+	 * Marks the special nodes/meshes in json for efficient parse.
+	 */
+	GLTFParser.prototype.markDefs = function () {
+
+		var nodeDefs = this.json.nodes || [];
+		var skinDefs = this.json.skins || [];
+		var meshDefs = this.json.meshes || [];
+
+		var meshReferences = {};
+		var meshUses = {};
+
+		// Nothing in the node definition indicates whether it is a Bone or an
+		// Object3D. Use the skins' joint references to mark bones.
+		for ( var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex ++ ) {
+
+			var joints = skinDefs[ skinIndex ].joints;
+
+			for ( var i = 0, il = joints.length; i < il; i ++ ) {
+
+				nodeDefs[ joints[ i ] ].isBone = true;
+
+			}
+
+		}
+
+		// Meshes can (and should) be reused by multiple nodes in a glTF asset. To
+		// avoid having more than one THREE.Mesh with the same name, count
+		// references and rename instances below.
+		//
+		// Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
+		for ( var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex ++ ) {
+
+			var nodeDef = nodeDefs[ nodeIndex ];
+
+			if ( nodeDef.mesh !== undefined ) {
+
+				if ( meshReferences[ nodeDef.mesh ] === undefined ) {
+
+					meshReferences[ nodeDef.mesh ] = meshUses[ nodeDef.mesh ] = 0;
+
+				}
+
+				meshReferences[ nodeDef.mesh ] ++;
+
+				// Nothing in the mesh definition indicates whether it is
+				// a SkinnedMesh or Mesh. Use the node's mesh reference
+				// to mark SkinnedMesh if node has skin.
+				if ( nodeDef.skin !== undefined ) {
+
+					meshDefs[ nodeDef.mesh ].isSkinnedMesh = true;
+
+				}
+
+			}
+
+		}
+
+		this.json.meshReferences = meshReferences;
+		this.json.meshUses = meshUses;
+
+	};
+
+	/**
+	 * Requests the specified dependency asynchronously, with caching.
+	 * @param {string} type
+	 * @param {number} index
+	 * @return {Promise<THREE.Object3D|THREE.Material|THREE.Texture|THREE.AnimationClip|ArrayBuffer|Object>}
+	 */
+	GLTFParser.prototype.getDependency = function ( type, index ) {
+
+		var cacheKey = type + ':' + index;
+		var dependency = this.cache.get( cacheKey );
+
+		if ( ! dependency ) {
+
+			switch ( type ) {
+
+				case 'scene':
+					dependency = this.loadScene( index );
+					break;
+
+				case 'node':
+					dependency = this.loadNode( index );
+					break;
+
+				case 'mesh':
+					dependency = this.loadMesh( index );
+					break;
+
+				case 'accessor':
+					dependency = this.loadAccessor( index );
+					break;
+
+				case 'bufferView':
+					dependency = this.loadBufferView( index );
+					break;
+
+				case 'buffer':
+					dependency = this.loadBuffer( index );
+					break;
+
+				case 'material':
+					dependency = this.loadMaterial( index );
+					break;
+
+				case 'texture':
+					dependency = this.loadTexture( index );
+					break;
+
+				case 'skin':
+					dependency = this.loadSkin( index );
+					break;
+
+				case 'animation':
+					dependency = this.loadAnimation( index );
+					break;
+
+				case 'camera':
+					dependency = this.loadCamera( index );
+					break;
+
+				case 'light':
+					dependency = this.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].loadLight( index );
+					break;
+
+				default:
+					throw new Error( 'Unknown type: ' + type );
+
+			}
+
+			this.cache.add( cacheKey, dependency );
+
+		}
+
+		return dependency;
+
+	};
+
+	/**
+	 * Requests all dependencies of the specified type asynchronously, with caching.
+	 * @param {string} type
+	 * @return {Promise<Array<Object>>}
+	 */
+	GLTFParser.prototype.getDependencies = function ( type ) {
+
+		var dependencies = this.cache.get( type );
+
+		if ( ! dependencies ) {
+
+			var parser = this;
+			var defs = this.json[ type + ( type === 'mesh' ? 'es' : 's' ) ] || [];
+
+			dependencies = Promise.all( defs.map( function ( def, index ) {
+
+				return parser.getDependency( type, index );
+
+			} ) );
+
+			this.cache.add( type, dependencies );
+
+		}
+
+		return dependencies;
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
+	 * @param {number} bufferIndex
+	 * @return {Promise<ArrayBuffer>}
+	 */
+	GLTFParser.prototype.loadBuffer = function ( bufferIndex ) {
+
+		var bufferDef = this.json.buffers[ bufferIndex ];
+		var loader = this.fileLoader;
+
+		if ( bufferDef.type && bufferDef.type !== 'arraybuffer' ) {
+
+			throw new Error( 'THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.' );
+
+		}
+
+		// If present, GLB container is required to be the first buffer.
+		if ( bufferDef.uri === undefined && bufferIndex === 0 ) {
+
+			return Promise.resolve( this.extensions[ EXTENSIONS.KHR_BINARY_GLTF ].body );
+
+		}
+
+		var options = this.options;
+
+		return new Promise( function ( resolve, reject ) {
+
+			loader.load( resolveURL( bufferDef.uri, options.path ), resolve, undefined, function () {
+
+				reject( new Error( 'THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".' ) );
+
+			} );
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
+	 * @param {number} bufferViewIndex
+	 * @return {Promise<ArrayBuffer>}
+	 */
+	GLTFParser.prototype.loadBufferView = function ( bufferViewIndex ) {
+
+		var bufferViewDef = this.json.bufferViews[ bufferViewIndex ];
+
+		if ( bufferViewDef.extensions && bufferViewDef.extensions[ EXTENSIONS.MESHOPT_COMPRESSION ] ) {
+
+			var extension = this.extensions[ EXTENSIONS.MESHOPT_COMPRESSION ];
+
+			return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) {
+
+				return extension.decodeBufferView( bufferViewDef, buffer );
+
+			} );
+
+		} else {
+
+			return this.getDependency( 'buffer', bufferViewDef.buffer ).then( function ( buffer ) {
+
+				var byteLength = bufferViewDef.byteLength || 0;
+				var byteOffset = bufferViewDef.byteOffset || 0;
+				return buffer.slice( byteOffset, byteOffset + byteLength );
+
+			} );
+
+		}
+
+	}
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
+	 * @param {number} accessorIndex
+	 * @return {Promise<THREE.BufferAttribute|THREE.InterleavedBufferAttribute>}
+	 */
+	GLTFParser.prototype.loadAccessor = function ( accessorIndex ) {
+
+		var parser = this;
+		var json = this.json;
+
+		var accessorDef = this.json.accessors[ accessorIndex ];
+
+		if ( accessorDef.bufferView === undefined && accessorDef.sparse === undefined ) {
+
+			// Ignore empty accessors, which may be used to declare runtime
+			// information about attributes coming from another source (e.g. Draco
+			// compression extension).
+			return Promise.resolve( null );
+
+		}
+
+		var pendingBufferViews = [];
+
+		if ( accessorDef.bufferView !== undefined ) {
+
+			pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.bufferView ) );
+
+		} else {
+
+			pendingBufferViews.push( null );
+
+		}
+
+		if ( accessorDef.sparse !== undefined ) {
+
+			pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.indices.bufferView ) );
+			pendingBufferViews.push( this.getDependency( 'bufferView', accessorDef.sparse.values.bufferView ) );
+
+		}
+
+		return Promise.all( pendingBufferViews ).then( function ( bufferViews ) {
+
+			var bufferView = bufferViews[ 0 ];
+
+			var itemSize = WEBGL_TYPE_SIZES[ accessorDef.type ];
+			var TypedArray = WEBGL_COMPONENT_TYPES[ accessorDef.componentType ];
+
+			// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
+			var elementBytes = TypedArray.BYTES_PER_ELEMENT;
+			var itemBytes = elementBytes * itemSize;
+			var byteOffset = accessorDef.byteOffset || 0;
+			var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[ accessorDef.bufferView ].byteStride : undefined;
+			var normalized = accessorDef.normalized === true;
+			var array, bufferAttribute;
+
+			// The buffer is not interleaved if the stride is the item size in bytes.
+			if ( byteStride && byteStride !== itemBytes ) {
+
+				// Each "slice" of the buffer, as defined by 'count' elements of 'byteStride' bytes, gets its own InterleavedBuffer
+				// This makes sure that IBA.count reflects accessor.count properly
+				var ibSlice = Math.floor( byteOffset / byteStride );
+				var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType + ':' + ibSlice + ':' + accessorDef.count;
+				var ib = parser.cache.get( ibCacheKey );
+
+				if ( ! ib ) {
+
+					array = new TypedArray( bufferView, ibSlice * byteStride, accessorDef.count * byteStride / elementBytes );
+
+					// Integer parameters to IB/IBA are in array elements, not bytes.
+					ib = new THREE.InterleavedBuffer( array, byteStride / elementBytes );
+
+					parser.cache.add( ibCacheKey, ib );
+
+				}
+
+				bufferAttribute = new THREE.InterleavedBufferAttribute( ib, itemSize, (byteOffset % byteStride) / elementBytes, normalized );
+
+			} else {
+
+				if ( bufferView === null ) {
+
+					array = new TypedArray( accessorDef.count * itemSize );
+
+				} else {
+
+					array = new TypedArray( bufferView, byteOffset, accessorDef.count * itemSize );
+
+				}
+
+				bufferAttribute = new THREE.BufferAttribute( array, itemSize, normalized );
+
+			}
+
+			// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
+			if ( accessorDef.sparse !== undefined ) {
+
+				var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
+				var TypedArrayIndices = WEBGL_COMPONENT_TYPES[ accessorDef.sparse.indices.componentType ];
+
+				var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
+				var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;
+
+				var sparseIndices = new TypedArrayIndices( bufferViews[ 1 ], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices );
+				var sparseValues = new TypedArray( bufferViews[ 2 ], byteOffsetValues, accessorDef.sparse.count * itemSize );
+
+				if ( bufferView !== null ) {
+
+					// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
+					bufferAttribute.setArray( bufferAttribute.array.slice() );
+
+				}
+
+				for ( var i = 0, il = sparseIndices.length; i < il; i ++ ) {
+
+					var index = sparseIndices[ i ];
+
+					bufferAttribute.setX( index, sparseValues[ i * itemSize ] );
+					if ( itemSize >= 2 ) bufferAttribute.setY( index, sparseValues[ i * itemSize + 1 ] );
+					if ( itemSize >= 3 ) bufferAttribute.setZ( index, sparseValues[ i * itemSize + 2 ] );
+					if ( itemSize >= 4 ) bufferAttribute.setW( index, sparseValues[ i * itemSize + 3 ] );
+					if ( itemSize >= 5 ) throw new Error( 'THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.' );
+
+				}
+
+			}
+
+			return bufferAttribute;
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
+	 * @param {number} textureIndex
+	 * @return {Promise<THREE.Texture>}
+	 */
+	GLTFParser.prototype.loadTexture = function ( textureIndex ) {
+
+		var parser = this;
+		var json = this.json;
+		var options = this.options;
+		var textureLoader = this.textureLoader;
+
+		var URL = window.URL || window.webkitURL;
+
+		var textureDef = json.textures[ textureIndex ];
+
+		var textureExtensions = textureDef.extensions || {};
+
+		var source;
+
+		if ( textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ] ) {
+
+			source = json.images[ textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].source ];
+
+		} else {
+
+			source = json.images[ textureDef.source ];
+
+		}
+
+		var sourceURI = source.uri;
+		var isObjectURL = false;
+
+		if ( source.bufferView !== undefined ) {
+
+			// Load binary image data from bufferView, if provided.
+
+			sourceURI = parser.getDependency( 'bufferView', source.bufferView ).then( function ( bufferView ) {
+
+				isObjectURL = true;
+				var blob = new Blob( [ bufferView ], { type: source.mimeType } );
+				sourceURI = URL.createObjectURL( blob );
+				return sourceURI;
+
+			} );
+
+		}
+
+		return Promise.resolve( sourceURI ).then( function ( sourceURI ) {
+
+			// Load Texture resource.
+
+			var loader = THREE.Loader.Handlers.get( sourceURI );
+
+			if ( ! loader ) {
+
+				loader = textureExtensions[ EXTENSIONS.MSFT_TEXTURE_DDS ]
+					? parser.extensions[ EXTENSIONS.MSFT_TEXTURE_DDS ].ddsLoader
+					: textureLoader;
+
+			}
+
+			return new Promise( function ( resolve, reject ) {
+
+				loader.load( resolveURL( sourceURI, options.path ), resolve, undefined, reject );
+
+			} );
+
+		} ).then( function ( texture ) {
+
+			// Clean up resources and configure Texture.
+
+			if ( isObjectURL === true ) {
+
+				URL.revokeObjectURL( sourceURI );
+
+			}
+
+			texture.flipY = false;
+
+			if ( textureDef.name !== undefined ) texture.name = textureDef.name;
+
+			// Ignore unknown mime types, like DDS files.
+			if ( source.mimeType in MIME_TYPE_FORMATS ) {
+
+				texture.format = MIME_TYPE_FORMATS[ source.mimeType ];
+
+			}
+
+			var samplers = json.samplers || {};
+			var sampler = samplers[ textureDef.sampler ] || {};
+
+			texture.magFilter = WEBGL_FILTERS[ sampler.magFilter ] || THREE.LinearFilter;
+			texture.minFilter = WEBGL_FILTERS[ sampler.minFilter ] || THREE.LinearMipMapLinearFilter;
+			texture.wrapS = WEBGL_WRAPPINGS[ sampler.wrapS ] || THREE.RepeatWrapping;
+			texture.wrapT = WEBGL_WRAPPINGS[ sampler.wrapT ] || THREE.RepeatWrapping;
+
+			return texture;
+
+		} );
+
+	};
+
+	/**
+	 * Asynchronously assigns a texture to the given material parameters.
+	 * @param {Object} materialParams
+	 * @param {string} mapName
+	 * @param {Object} mapDef
+	 * @return {Promise}
+	 */
+	GLTFParser.prototype.assignTexture = function ( materialParams, mapName, mapDef ) {
+
+		var parser = this;
+
+		return this.getDependency( 'texture', mapDef.index ).then( function ( texture ) {
+
+			if ( ! texture.isCompressedTexture ) {
+
+				switch ( mapName ) {
+
+					case 'aoMap':
+					case 'emissiveMap':
+					case 'metalnessMap':
+					case 'normalMap':
+					case 'roughnessMap':
+						texture.format = THREE.RGBFormat;
+						break;
+
+				}
+
+			}
+
+			if ( parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] ) {
+
+				var transform = mapDef.extensions !== undefined ? mapDef.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ] : undefined;
+
+				if ( transform ) {
+
+					texture = parser.extensions[ EXTENSIONS.KHR_TEXTURE_TRANSFORM ].extendTexture( texture, transform );
+
+				}
+
+			}
+
+			materialParams[ mapName ] = texture;
+
+		} );
+
+	};
+
+	/**
+	 * Assigns final material to a Mesh, Line, or Points instance. The instance
+	 * already has a material (generated from the glTF material options alone)
+	 * but reuse of the same glTF material may require multiple threejs materials
+	 * to accomodate different primitive types, defines, etc. New materials will
+	 * be created if necessary, and reused from a cache.
+	 * @param  {THREE.Object3D} mesh Mesh, Line, or Points instance.
+	 */
+	GLTFParser.prototype.assignFinalMaterial = function ( mesh ) {
+
+		var geometry = mesh.geometry;
+		var material = mesh.material;
+		var extensions = this.extensions;
+
+		var useVertexTangents = geometry.attributes.tangent !== undefined;
+		var useVertexColors = geometry.attributes.color !== undefined;
+		var useFlatShading = geometry.attributes.normal === undefined;
+		var useSkinning = mesh.isSkinnedMesh === true;
+		var useMorphTargets = Object.keys( geometry.morphAttributes ).length > 0;
+		var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined;
+
+		if ( mesh.isPoints ) {
+
+			var cacheKey = 'PointsMaterial:' + material.uuid;
+
+			var pointsMaterial = this.cache.get( cacheKey );
+
+			if ( ! pointsMaterial ) {
+
+				pointsMaterial = new THREE.PointsMaterial();
+				THREE.Material.prototype.copy.call( pointsMaterial, material );
+				pointsMaterial.color.copy( material.color );
+				pointsMaterial.map = material.map;
+				pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
+
+				this.cache.add( cacheKey, pointsMaterial );
+
+			}
+
+			material = pointsMaterial;
+
+		} else if ( mesh.isLine ) {
+
+			var cacheKey = 'LineBasicMaterial:' + material.uuid;
+
+			var lineMaterial = this.cache.get( cacheKey );
+
+			if ( ! lineMaterial ) {
+
+				lineMaterial = new THREE.LineBasicMaterial();
+				THREE.Material.prototype.copy.call( lineMaterial, material );
+				lineMaterial.color.copy( material.color );
+				lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet
+
+				this.cache.add( cacheKey, lineMaterial );
+
+			}
+
+			material = lineMaterial;
+
+		}
+
+		// Clone the material if it will be modified
+		if ( useVertexTangents || useVertexColors || useFlatShading || useSkinning || useMorphTargets ) {
+
+			var cacheKey = 'ClonedMaterial:' + material.uuid + ':';
+
+			if ( material.isGLTFSpecularGlossinessMaterial ) cacheKey += 'specular-glossiness:';
+			if ( useSkinning ) cacheKey += 'skinning:';
+			if ( useVertexTangents ) cacheKey += 'vertex-tangents:';
+			if ( useVertexColors ) cacheKey += 'vertex-colors:';
+			if ( useFlatShading ) cacheKey += 'flat-shading:';
+			if ( useMorphTargets ) cacheKey += 'morph-targets:';
+			if ( useMorphNormals ) cacheKey += 'morph-normals:';
+
+			var cachedMaterial = this.cache.get( cacheKey );
+
+			if ( ! cachedMaterial ) {
+
+				cachedMaterial = material.isGLTFSpecularGlossinessMaterial
+					? extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].cloneMaterial( material )
+					: material.clone();
+
+				if ( useSkinning ) cachedMaterial.skinning = true;
+				if ( useVertexTangents ) cachedMaterial.vertexTangents = true;
+				if ( useVertexColors ) cachedMaterial.vertexColors = THREE.VertexColors;
+				if ( useFlatShading ) cachedMaterial.flatShading = true;
+				if ( useMorphTargets ) cachedMaterial.morphTargets = true;
+				if ( useMorphNormals ) cachedMaterial.morphNormals = true;
+
+				this.cache.add( cacheKey, cachedMaterial );
+
+			}
+
+			material = cachedMaterial;
+
+		}
+
+		// workarounds for mesh and geometry
+
+		if ( material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined ) {
+
+			console.log( 'THREE.GLTFLoader: Duplicating UVs to support aoMap.' );
+			geometry.addAttribute( 'uv2', new THREE.BufferAttribute( geometry.attributes.uv.array, 2 ) );
+
+		}
+
+		if ( material.isGLTFSpecularGlossinessMaterial ) {
+
+			// for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update
+			mesh.onBeforeRender = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].refreshUniforms;
+
+		}
+
+		mesh.material = material;
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
+	 * @param {number} materialIndex
+	 * @return {Promise<THREE.Material>}
+	 */
+	GLTFParser.prototype.loadMaterial = function ( materialIndex ) {
+
+		var parser = this;
+		var json = this.json;
+		var extensions = this.extensions;
+		var materialDef = json.materials[ materialIndex ];
+
+		var materialType;
+		var materialParams = {};
+		var materialExtensions = materialDef.extensions || {};
+
+		var pending = [];
+
+		if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ] ) {
+
+			var sgExtension = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ];
+			materialType = sgExtension.getMaterialType();
+			pending.push( sgExtension.extendParams( materialParams, materialDef, parser ) );
+
+		} else if ( materialExtensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ] ) {
+
+			var kmuExtension = extensions[ EXTENSIONS.KHR_MATERIALS_UNLIT ];
+			materialType = kmuExtension.getMaterialType();
+			pending.push( kmuExtension.extendParams( materialParams, materialDef, parser ) );
+
+		} else {
+
+			// Specification:
+			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material
+
+			materialType = THREE.MeshStandardMaterial;
+
+			var metallicRoughness = materialDef.pbrMetallicRoughness || {};
+
+			materialParams.color = new THREE.Color( 1.0, 1.0, 1.0 );
+			materialParams.opacity = 1.0;
+
+			if ( Array.isArray( metallicRoughness.baseColorFactor ) ) {
+
+				var array = metallicRoughness.baseColorFactor;
+
+				materialParams.color.fromArray( array );
+				materialParams.opacity = array[ 3 ];
+
+			}
+
+			if ( metallicRoughness.baseColorTexture !== undefined ) {
+
+				pending.push( parser.assignTexture( materialParams, 'map', metallicRoughness.baseColorTexture ) );
+
+			}
+
+			materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
+			materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;
+
+			if ( metallicRoughness.metallicRoughnessTexture !== undefined ) {
+
+				pending.push( parser.assignTexture( materialParams, 'metalnessMap', metallicRoughness.metallicRoughnessTexture ) );
+				pending.push( parser.assignTexture( materialParams, 'roughnessMap', metallicRoughness.metallicRoughnessTexture ) );
+
+			}
+
+		}
+
+		if ( materialDef.doubleSided === true ) {
+
+			materialParams.side = THREE.DoubleSide;
+
+		}
+
+		var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;
+
+		if ( alphaMode === ALPHA_MODES.BLEND ) {
+
+			materialParams.transparent = true;
+
+		} else {
+
+			materialParams.transparent = false;
+
+			if ( alphaMode === ALPHA_MODES.MASK ) {
+
+				materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;
+
+			}
+
+		}
+
+		if ( materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) {
+
+			pending.push( parser.assignTexture( materialParams, 'normalMap', materialDef.normalTexture ) );
+
+			materialParams.normalScale = new THREE.Vector2( 1, 1 );
+
+			if ( materialDef.normalTexture.scale !== undefined ) {
+
+				materialParams.normalScale.set( materialDef.normalTexture.scale, materialDef.normalTexture.scale );
+
+			}
+
+		}
+
+		if ( materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) {
+
+			pending.push( parser.assignTexture( materialParams, 'aoMap', materialDef.occlusionTexture ) );
+
+			if ( materialDef.occlusionTexture.strength !== undefined ) {
+
+				materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;
+
+			}
+
+		}
+
+		if ( materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial ) {
+
+			materialParams.emissive = new THREE.Color().fromArray( materialDef.emissiveFactor );
+
+		}
+
+		if ( materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial ) {
+
+			pending.push( parser.assignTexture( materialParams, 'emissiveMap', materialDef.emissiveTexture ) );
+
+		}
+
+		return Promise.all( pending ).then( function () {
+
+			var material;
+
+			if ( materialType === THREE.ShaderMaterial ) {
+
+				material = extensions[ EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS ].createMaterial( materialParams );
+
+			} else {
+
+				material = new materialType( materialParams );
+
+			}
+
+			if ( materialDef.name !== undefined ) material.name = materialDef.name;
+
+			// baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding.
+			if ( material.map ) material.map.encoding = THREE.sRGBEncoding;
+			if ( material.emissiveMap ) material.emissiveMap.encoding = THREE.sRGBEncoding;
+			if ( material.specularMap ) material.specularMap.encoding = THREE.sRGBEncoding;
+
+			assignExtrasToUserData( material, materialDef );
+
+			if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef );
+
+			return material;
+
+		} );
+
+	};
+
+	/**
+	 * @param {THREE.BufferGeometry} geometry
+	 * @param {GLTF.Primitive} primitiveDef
+	 * @param {GLTFParser} parser
+	 * @return {Promise<THREE.BufferGeometry>}
+	 */
+	function addPrimitiveAttributes( geometry, primitiveDef, parser ) {
+
+		var attributes = primitiveDef.attributes;
+
+		var pending = [];
+
+		function assignAttributeAccessor( accessorIndex, attributeName ) {
+
+			return parser.getDependency( 'accessor', accessorIndex )
+				.then( function ( accessor ) {
+
+					geometry.addAttribute( attributeName, accessor );
+
+				} );
+
+		}
+
+		for ( var gltfAttributeName in attributes ) {
+
+			var threeAttributeName = ATTRIBUTES[ gltfAttributeName ] || gltfAttributeName.toLowerCase();
+
+			// Skip attributes already provided by e.g. Draco extension.
+			if ( threeAttributeName in geometry.attributes ) continue;
+
+			pending.push( assignAttributeAccessor( attributes[ gltfAttributeName ], threeAttributeName ) );
+
+		}
+
+		if ( primitiveDef.indices !== undefined && ! geometry.index ) {
+
+			var accessor = parser.getDependency( 'accessor', primitiveDef.indices ).then( function ( accessor ) {
+
+				geometry.setIndex( accessor );
+
+			} );
+
+			pending.push( accessor );
+
+		}
+
+		assignExtrasToUserData( geometry, primitiveDef );
+
+		return Promise.all( pending ).then( function () {
+
+			return primitiveDef.targets !== undefined
+				? addMorphTargets( geometry, primitiveDef.targets, parser )
+				: geometry;
+
+		} );
+
+	}
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
+	 *
+	 * Creates BufferGeometries from primitives.
+	 *
+	 * @param {Array<GLTF.Primitive>} primitives
+	 * @return {Promise<Array<THREE.BufferGeometry>>}
+	 */
+	GLTFParser.prototype.loadGeometries = function ( primitives ) {
+
+		var parser = this;
+		var extensions = this.extensions;
+		var cache = this.primitiveCache;
+
+		function createDracoPrimitive( primitive ) {
+
+			return extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ]
+				.decodePrimitive( primitive, parser )
+				.then( function ( geometry ) {
+
+					return addPrimitiveAttributes( geometry, primitive, parser );
+
+				} );
+
+		}
+
+		var pending = [];
+
+		for ( var i = 0, il = primitives.length; i < il; i ++ ) {
+
+			var primitive = primitives[ i ];
+			var cacheKey = createPrimitiveKey( primitive );
+
+			// See if we've already created this geometry
+			var cached = cache[ cacheKey ];
+
+			if ( cached ) {
+
+				// Use the cached geometry if it exists
+				pending.push( cached.promise );
+
+			} else {
+
+				var geometryPromise;
+
+				if ( primitive.extensions && primitive.extensions[ EXTENSIONS.KHR_DRACO_MESH_COMPRESSION ] ) {
+
+					// Use DRACO geometry if available
+					geometryPromise = createDracoPrimitive( primitive );
+
+				} else {
+
+					// Otherwise create a new geometry
+					geometryPromise = addPrimitiveAttributes( new THREE.BufferGeometry(), primitive, parser );
+
+				}
+
+				// Cache this geometry
+				cache[ cacheKey ] = { primitive: primitive, promise: geometryPromise };
+
+				pending.push( geometryPromise );
+
+			}
+
+		}
+
+		return Promise.all( pending );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
+	 * @param {number} meshIndex
+	 * @return {Promise<THREE.Group|THREE.Mesh|THREE.SkinnedMesh>}
+	 */
+	GLTFParser.prototype.loadMesh = function ( meshIndex ) {
+
+		var parser = this;
+		var json = this.json;
+		var extensions = this.extensions;
+
+		var meshDef = json.meshes[ meshIndex ];
+		var primitives = meshDef.primitives;
+
+		var pending = [];
+
+		for ( var i = 0, il = primitives.length; i < il; i ++ ) {
+
+			var material = primitives[ i ].material === undefined
+				? createDefaultMaterial()
+				: this.getDependency( 'material', primitives[ i ].material );
+
+			pending.push( material );
+
+		}
+
+		return Promise.all( pending ).then( function ( originalMaterials ) {
+
+			return parser.loadGeometries( primitives ).then( function ( geometries ) {
+
+				var meshes = [];
+
+				for ( var i = 0, il = geometries.length; i < il; i ++ ) {
+
+					var geometry = geometries[ i ];
+					var primitive = primitives[ i ];
+
+					// 1. create Mesh
+
+					var mesh;
+
+					var material = originalMaterials[ i ];
+
+					if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
+						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
+						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
+						primitive.mode === undefined ) {
+
+						// .isSkinnedMesh isn't in glTF spec. See .markDefs()
+						mesh = meshDef.isSkinnedMesh === true
+							? new THREE.SkinnedMesh( geometry, material )
+							: new THREE.Mesh( geometry, material );
+
+						if ( mesh.isSkinnedMesh === true && !mesh.geometry.attributes.skinWeight.normalized ) {
+
+							// we normalize floating point skin weight array to fix malformed assets (see #15319)
+							// it's important to skip this for non-float32 data since normalizeSkinWeights assumes non-normalized inputs
+							mesh.normalizeSkinWeights();
+
+						}
+
+						if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ) {
+
+							mesh.drawMode = THREE.TriangleStripDrawMode;
+
+						} else if ( primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ) {
+
+							mesh.drawMode = THREE.TriangleFanDrawMode;
+
+						}
+
+					} else if ( primitive.mode === WEBGL_CONSTANTS.LINES ) {
+
+						mesh = new THREE.LineSegments( geometry, material );
+
+					} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_STRIP ) {
+
+						mesh = new THREE.Line( geometry, material );
+
+					} else if ( primitive.mode === WEBGL_CONSTANTS.LINE_LOOP ) {
+
+						mesh = new THREE.LineLoop( geometry, material );
+
+					} else if ( primitive.mode === WEBGL_CONSTANTS.POINTS ) {
+
+						mesh = new THREE.Points( geometry, material );
+
+					} else {
+
+						throw new Error( 'THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode );
+
+					}
+
+					if ( Object.keys( mesh.geometry.morphAttributes ).length > 0 ) {
+
+						updateMorphTargets( mesh, meshDef );
+
+					}
+
+					mesh.name = meshDef.name || ( 'mesh_' + meshIndex );
+
+					if ( geometries.length > 1 ) mesh.name += '_' + i;
+
+					assignExtrasToUserData( mesh, meshDef );
+
+					parser.assignFinalMaterial( mesh );
+
+					meshes.push( mesh );
+
+				}
+
+				if ( meshes.length === 1 ) {
+
+					return meshes[ 0 ];
+
+				}
+
+				var group = new THREE.Group();
+
+				for ( var i = 0, il = meshes.length; i < il; i ++ ) {
+
+					group.add( meshes[ i ] );
+
+				}
+
+				return group;
+
+			} );
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
+	 * @param {number} cameraIndex
+	 * @return {Promise<THREE.Camera>}
+	 */
+	GLTFParser.prototype.loadCamera = function ( cameraIndex ) {
+
+		var camera;
+		var cameraDef = this.json.cameras[ cameraIndex ];
+		var params = cameraDef[ cameraDef.type ];
+
+		if ( ! params ) {
+
+			console.warn( 'THREE.GLTFLoader: Missing camera parameters.' );
+			return;
+
+		}
+
+		if ( cameraDef.type === 'perspective' ) {
+
+			camera = new THREE.PerspectiveCamera( THREE.Math.radToDeg( params.yfov ), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6 );
+
+		} else if ( cameraDef.type === 'orthographic' ) {
+
+			camera = new THREE.OrthographicCamera( params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar );
+
+		}
+
+		if ( cameraDef.name !== undefined ) camera.name = cameraDef.name;
+
+		assignExtrasToUserData( camera, cameraDef );
+
+		return Promise.resolve( camera );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
+	 * @param {number} skinIndex
+	 * @return {Promise<Object>}
+	 */
+	GLTFParser.prototype.loadSkin = function ( skinIndex ) {
+
+		var skinDef = this.json.skins[ skinIndex ];
+
+		var skinEntry = { joints: skinDef.joints };
+
+		if ( skinDef.inverseBindMatrices === undefined ) {
+
+			return Promise.resolve( skinEntry );
+
+		}
+
+		return this.getDependency( 'accessor', skinDef.inverseBindMatrices ).then( function ( accessor ) {
+
+			skinEntry.inverseBindMatrices = accessor;
+
+			return skinEntry;
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
+	 * @param {number} animationIndex
+	 * @return {Promise<THREE.AnimationClip>}
+	 */
+	GLTFParser.prototype.loadAnimation = function ( animationIndex ) {
+
+		var json = this.json;
+
+		var animationDef = json.animations[ animationIndex ];
+
+		var pendingNodes = [];
+		var pendingInputAccessors = [];
+		var pendingOutputAccessors = [];
+		var pendingSamplers = [];
+		var pendingTargets = [];
+
+		for ( var i = 0, il = animationDef.channels.length; i < il; i ++ ) {
+
+			var channel = animationDef.channels[ i ];
+			var sampler = animationDef.samplers[ channel.sampler ];
+			var target = channel.target;
+			var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
+			var input = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.input ] : sampler.input;
+			var output = animationDef.parameters !== undefined ? animationDef.parameters[ sampler.output ] : sampler.output;
+
+			pendingNodes.push( this.getDependency( 'node', name ) );
+			pendingInputAccessors.push( this.getDependency( 'accessor', input ) );
+			pendingOutputAccessors.push( this.getDependency( 'accessor', output ) );
+			pendingSamplers.push( sampler );
+			pendingTargets.push( target );
+
+		}
+
+		return Promise.all( [
+
+			Promise.all( pendingNodes ),
+			Promise.all( pendingInputAccessors ),
+			Promise.all( pendingOutputAccessors ),
+			Promise.all( pendingSamplers ),
+			Promise.all( pendingTargets )
+
+		] ).then( function ( dependencies ) {
+
+			var nodes = dependencies[ 0 ];
+			var inputAccessors = dependencies[ 1 ];
+			var outputAccessors = dependencies[ 2 ];
+			var samplers = dependencies[ 3 ];
+			var targets = dependencies[ 4 ];
+
+			var tracks = [];
+
+			for ( var i = 0, il = nodes.length; i < il; i ++ ) {
+
+				var node = nodes[ i ];
+				var inputAccessor = inputAccessors[ i ];
+				var outputAccessor = outputAccessors[ i ];
+				var sampler = samplers[ i ];
+				var target = targets[ i ];
+
+				if ( node === undefined ) continue;
+
+				node.updateMatrix();
+				node.matrixAutoUpdate = true;
+
+				var TypedKeyframeTrack;
+
+				switch ( PATH_PROPERTIES[ target.path ] ) {
+
+					case PATH_PROPERTIES.weights:
+
+						TypedKeyframeTrack = THREE.NumberKeyframeTrack;
+						break;
+
+					case PATH_PROPERTIES.rotation:
+
+						TypedKeyframeTrack = THREE.QuaternionKeyframeTrack;
+						break;
+
+					case PATH_PROPERTIES.position:
+					case PATH_PROPERTIES.scale:
+					default:
+
+						TypedKeyframeTrack = THREE.VectorKeyframeTrack;
+						break;
+
+				}
+
+				var targetName = node.name ? node.name : node.uuid;
+
+				var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[ sampler.interpolation ] : THREE.InterpolateLinear;
+
+				var targetNames = [];
+
+				if ( PATH_PROPERTIES[ target.path ] === PATH_PROPERTIES.weights ) {
+
+					// Node may be a THREE.Group (glTF mesh with several primitives) or a THREE.Mesh.
+					node.traverse( function ( object ) {
+
+						if ( object.isMesh === true && object.morphTargetInfluences ) {
+
+							targetNames.push( object.name ? object.name : object.uuid );
+
+						}
+
+					} );
+
+				} else {
+
+					targetNames.push( targetName );
+
+				}
+
+				var outputArray = outputAccessor.array;
+
+				if ( outputAccessor.normalized ) {
+
+					var scale;
+
+					if ( outputArray.constructor === Int8Array ) {
+
+						scale = 1 / 127;
+
+					} else if ( outputArray.constructor === Uint8Array ) {
+
+						scale = 1 / 255;
+
+					} else if ( outputArray.constructor == Int16Array ) {
+
+						scale = 1 / 32767;
+
+					} else if ( outputArray.constructor === Uint16Array ) {
+
+						scale = 1 / 65535;
+
+					} else {
+
+						throw new Error( 'THREE.GLTFLoader: Unsupported output accessor component type.' );
+
+					}
+
+					var scaled = new Float32Array( outputArray.length );
+
+					for ( var j = 0, jl = outputArray.length; j < jl; j ++ ) {
+
+						scaled[j] = outputArray[j] * scale;
+
+					}
+
+					outputArray = scaled;
+
+				}
+
+				for ( var j = 0, jl = targetNames.length; j < jl; j ++ ) {
+
+					var track = new TypedKeyframeTrack(
+						targetNames[ j ] + '.' + PATH_PROPERTIES[ target.path ],
+						inputAccessor.array,
+						outputArray,
+						interpolation
+					);
+
+					// Override interpolation with custom factory method.
+					if ( sampler.interpolation === 'CUBICSPLINE' ) {
+
+						track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline( result ) {
+
+							// A CUBICSPLINE keyframe in glTF has three output values for each input value,
+							// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
+							// must be divided by three to get the interpolant's sampleSize argument.
+
+							return new GLTFCubicSplineInterpolant( this.times, this.values, this.getValueSize() / 3, result );
+
+						};
+
+						// Mark as CUBICSPLINE. `track.getInterpolation()` doesn't support custom interpolants.
+						track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;
+
+					}
+
+					tracks.push( track );
+
+				}
+
+			}
+
+			var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex;
+
+			return new THREE.AnimationClip( name, undefined, tracks );
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
+	 * @param {number} nodeIndex
+	 * @return {Promise<THREE.Object3D>}
+	 */
+	GLTFParser.prototype.loadNode = function ( nodeIndex ) {
+
+		var json = this.json;
+		var extensions = this.extensions;
+		var parser = this;
+
+		var meshReferences = json.meshReferences;
+		var meshUses = json.meshUses;
+
+		var nodeDef = json.nodes[ nodeIndex ];
+
+		return ( function () {
+
+			// .isBone isn't in glTF spec. See .markDefs
+			if ( nodeDef.isBone === true ) {
+
+				return Promise.resolve( new THREE.Bone() );
+
+			} else if ( nodeDef.mesh !== undefined ) {
+
+				return parser.getDependency( 'mesh', nodeDef.mesh ).then( function ( mesh ) {
+
+					var node;
+
+					if ( meshReferences[ nodeDef.mesh ] > 1 ) {
+
+						var instanceNum = meshUses[ nodeDef.mesh ] ++;
+
+						node = mesh.clone();
+						node.name += '_instance_' + instanceNum;
+
+						// onBeforeRender copy for Specular-Glossiness
+						node.onBeforeRender = mesh.onBeforeRender;
+
+						for ( var i = 0, il = node.children.length; i < il; i ++ ) {
+
+							node.children[ i ].name += '_instance_' + instanceNum;
+							node.children[ i ].onBeforeRender = mesh.children[ i ].onBeforeRender;
+
+						}
+
+					} else {
+
+						node = mesh;
+
+					}
+
+					// if weights are provided on the node, override weights on the mesh.
+					if ( nodeDef.weights !== undefined ) {
+
+						node.traverse( function ( o ) {
+
+							if ( ! o.isMesh ) return;
+
+							for ( var i = 0, il = nodeDef.weights.length; i < il; i ++ ) {
+
+								o.morphTargetInfluences[ i ] = nodeDef.weights[ i ];
+
+							}
+
+						} );
+
+					}
+
+					return node;
+
+				} );
+
+			} else if ( nodeDef.camera !== undefined ) {
+
+				return parser.getDependency( 'camera', nodeDef.camera );
+
+			} else if ( nodeDef.extensions
+				&& nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ]
+				&& nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light !== undefined ) {
+
+				return parser.getDependency( 'light', nodeDef.extensions[ EXTENSIONS.KHR_LIGHTS_PUNCTUAL ].light );
+
+			} else {
+
+				return Promise.resolve( new THREE.Object3D() );
+
+			}
+
+		}() ).then( function ( node ) {
+
+			if ( nodeDef.name !== undefined ) {
+
+				node.userData.name = nodeDef.name;
+				node.name = THREE.PropertyBinding.sanitizeNodeName( nodeDef.name );
+
+			}
+
+			assignExtrasToUserData( node, nodeDef );
+
+			if ( nodeDef.extensions ) addUnknownExtensionsToUserData( extensions, node, nodeDef );
+
+			if ( nodeDef.matrix !== undefined ) {
+
+				var matrix = new THREE.Matrix4();
+				matrix.fromArray( nodeDef.matrix );
+				node.applyMatrix( matrix );
+
+			} else {
+
+				if ( nodeDef.translation !== undefined ) {
+
+					node.position.fromArray( nodeDef.translation );
+
+				}
+
+				if ( nodeDef.rotation !== undefined ) {
+
+					node.quaternion.fromArray( nodeDef.rotation );
+
+				}
+
+				if ( nodeDef.scale !== undefined ) {
+
+					node.scale.fromArray( nodeDef.scale );
+
+				}
+
+			}
+
+			return node;
+
+		} );
+
+	};
+
+	/**
+	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
+	 * @param {number} sceneIndex
+	 * @return {Promise<THREE.Scene>}
+	 */
+	GLTFParser.prototype.loadScene = function () {
+
+		// scene node hierachy builder
+
+		function buildNodeHierachy( nodeId, parentObject, json, parser ) {
+
+			var nodeDef = json.nodes[ nodeId ];
+
+			return parser.getDependency( 'node', nodeId ).then( function ( node ) {
+
+				if ( nodeDef.skin === undefined ) return node;
+
+				// build skeleton here as well
+
+				var skinEntry;
+
+				return parser.getDependency( 'skin', nodeDef.skin ).then( function ( skin ) {
+
+					skinEntry = skin;
+
+					var pendingJoints = [];
+
+					for ( var i = 0, il = skinEntry.joints.length; i < il; i ++ ) {
+
+						pendingJoints.push( parser.getDependency( 'node', skinEntry.joints[ i ] ) );
+
+					}
+
+					return Promise.all( pendingJoints );
+
+				} ).then( function ( jointNodes ) {
+
+					var meshes = node.isGroup === true ? node.children : [ node ];
+
+					for ( var i = 0, il = meshes.length; i < il; i ++ ) {
+
+						var mesh = meshes[ i ];
+
+						var bones = [];
+						var boneInverses = [];
+
+						for ( var j = 0, jl = jointNodes.length; j < jl; j ++ ) {
+
+							var jointNode = jointNodes[ j ];
+
+							if ( jointNode ) {
+
+								bones.push( jointNode );
+
+								var mat = new THREE.Matrix4();
+
+								if ( skinEntry.inverseBindMatrices !== undefined ) {
+
+									mat.fromArray( skinEntry.inverseBindMatrices.array, j * 16 );
+
+								}
+
+								boneInverses.push( mat );
+
+							} else {
+
+								console.warn( 'THREE.GLTFLoader: Joint "%s" could not be found.', skinEntry.joints[ j ] );
+
+							}
+
+						}
+
+						mesh.bind( new THREE.Skeleton( bones, boneInverses ), mesh.matrixWorld );
+
+					}
+
+					return node;
+
+				} );
+
+			} ).then( function ( node ) {
+
+				// build node hierachy
+
+				parentObject.add( node );
+
+				var pending = [];
+
+				if ( nodeDef.children ) {
+
+					var children = nodeDef.children;
+
+					for ( var i = 0, il = children.length; i < il; i ++ ) {
+
+						var child = children[ i ];
+						pending.push( buildNodeHierachy( child, node, json, parser ) );
+
+					}
+
+				}
+
+				return Promise.all( pending );
+
+			} );
+
+		}
+
+		return function loadScene( sceneIndex ) {
+
+			var json = this.json;
+			var extensions = this.extensions;
+			var sceneDef = this.json.scenes[ sceneIndex ];
+			var parser = this;
+
+			var scene = new THREE.Scene();
+			if ( sceneDef.name !== undefined ) scene.name = sceneDef.name;
+
+			assignExtrasToUserData( scene, sceneDef );
+
+			if ( sceneDef.extensions ) addUnknownExtensionsToUserData( extensions, scene, sceneDef );
+
+			var nodeIds = sceneDef.nodes || [];
+
+			var pending = [];
+
+			for ( var i = 0, il = nodeIds.length; i < il; i ++ ) {
+
+				pending.push( buildNodeHierachy( nodeIds[ i ], scene, json, parser ) );
+
+			}
+
+			return Promise.all( pending ).then( function () {
+
+				return scene;
+
+			} );
+
+		};
+
+	}();
+
+	return GLTFLoader;
+
+} )();

+ 32 - 25
3rdparty/meshoptimizer/demo/index.html

@@ -30,30 +30,19 @@
 		<a href="https://github.com/zeux/meshoptimizer" target="_blank" rel="noopener">meshoptimizer</a>
 		</div>
 
-		<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js"></script>
+		<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/105/three.min.js"></script>
 
-		<script src="../js/decoder.js"></script>
-		<script src="../tools/OptMeshLoader.js"></script>
+		<script src="../js/meshopt_decoder.js"></script>
+		<script src="GLTFLoader.js"></script>
 
 		<script>
 			var container;
 
-			var camera, scene, renderer;
+			var camera, scene, renderer, mixer, clock;
 
 			var windowHalfX = window.innerWidth / 2;
 			var windowHalfY = window.innerHeight / 2;
 
-			var timers = {};
-
-			console.time = function(label) {
-				timers[label] = performance.now();
-			};
-
-			console.timeEnd = function(label) {
-				var time = performance.now() - timers[label];
-				document.getElementById('info').append(label + " took " + time.toFixed(2) + " ms");
-			};
-
 			init();
 			animate();
 
@@ -67,8 +56,9 @@
 				camera.position.z = 3.0;
 
 				scene = new THREE.Scene();
+				scene.background = new THREE.Color(0x300a24);
 
-				var ambientLight = new THREE.AmbientLight(0xcccccc, 0.2);
+				var ambientLight = new THREE.AmbientLight(0xcccccc, 0.3);
 				scene.add(ambientLight);
 
 				var pointLight = new THREE.PointLight(0xffffff, 0.8);
@@ -77,16 +67,27 @@
 				scene.add(camera);
 
 				var onProgress = function (xhr) {};
-				var onError = function () {};
+				var onError = function (e) {
+					console.log(e);
+				};
+
+				var loader = new THREE.GLTFLoader()
+				loader.setMeshoptDecoder(MeshoptDecoder)
+				loader.load('pirate.glb', function (gltf) {
+					var bbox = new THREE.Box3().setFromObject(gltf.scene);
+					var scale = 2 / (bbox.max.y - bbox.min.y);
+
+					gltf.scene.scale.set(scale, scale, scale);
+					gltf.scene.position.set(0, 0, 0);
 
-				new THREE.OptMeshLoader()
-					.setDecoder(MeshoptDecoder)
-					.setMaterials(null) // materials can be fetched using MTLLoader
-					.setPath('./')
-					.load('pirate.optmesh', function (object)
-					{
-						scene.add(object);
-					}, onProgress, onError);
+					scene.add(gltf.scene);
+
+					mixer = new THREE.AnimationMixer(gltf.scene);
+
+					if (gltf.animations.length) {
+						mixer.clipAction(gltf.animations[gltf.animations.length - 1]).play();
+					}
+				}, onProgress, onError);
 
 				renderer = new THREE.WebGLRenderer();
 				renderer.setPixelRatio(window.devicePixelRatio);
@@ -94,6 +95,8 @@
 				container.appendChild(renderer.domElement);
 
 				window.addEventListener('resize', onWindowResize, false);
+
+				clock = new THREE.Clock();
 			}
 
 			function onWindowResize()
@@ -109,6 +112,10 @@
 
 			function animate()
 			{
+				if (mixer) {
+					mixer.update(clock.getDelta());
+				}
+
 				requestAnimationFrame(animate);
 				render();
 			}

+ 83 - 55
3rdparty/meshoptimizer/demo/main.cpp

@@ -8,7 +8,7 @@
 
 #include <vector>
 
-#include "../tools/objparser.h"
+#include "../tools/fast_obj.h"
 #include "miniz.h"
 
 // This file uses assert() to verify algorithm correctness
@@ -66,47 +66,58 @@ union Triangle {
 
 Mesh parseObj(const char* path, double& reindex)
 {
-	ObjFile file;
-
-	if (!objParseFile(file, path))
+	fastObjMesh* obj = fast_obj_read(path);
+	if (!obj)
 	{
 		printf("Error loading %s: file not found\n", path);
 		return Mesh();
 	}
 
-	if (!objValidate(file))
-	{
-		printf("Error loading %s: invalid file data\n", path);
-		return Mesh();
-	}
+	size_t total_indices = 0;
 
-	size_t total_indices = file.f_size / 3;
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+		total_indices += 3 * (obj->face_vertices[i] - 2);
 
 	std::vector<Vertex> vertices(total_indices);
 
-	for (size_t i = 0; i < total_indices; ++i)
-	{
-		int vi = file.f[i * 3 + 0];
-		int vti = file.f[i * 3 + 1];
-		int vni = file.f[i * 3 + 2];
-
-		Vertex v =
-		    {
-		        file.v[vi * 3 + 0],
-		        file.v[vi * 3 + 1],
-		        file.v[vi * 3 + 2],
+	size_t vertex_offset = 0;
+	size_t index_offset = 0;
 
-		        vni >= 0 ? file.vn[vni * 3 + 0] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 1] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 2] : 0,
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+	{
+		for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
+		{
+			fastObjIndex gi = obj->indices[index_offset + j];
+
+			Vertex v =
+			    {
+			        obj->positions[gi.p * 3 + 0],
+			        obj->positions[gi.p * 3 + 1],
+			        obj->positions[gi.p * 3 + 2],
+			        obj->normals[gi.n * 3 + 0],
+			        obj->normals[gi.n * 3 + 1],
+			        obj->normals[gi.n * 3 + 2],
+			        obj->texcoords[gi.t * 2 + 0],
+			        obj->texcoords[gi.t * 2 + 1],
+			    };
+
+			// triangulate polygon on the fly; offset-3 is always the first polygon vertex
+			if (j >= 3)
+			{
+				vertices[vertex_offset + 0] = vertices[vertex_offset - 3];
+				vertices[vertex_offset + 1] = vertices[vertex_offset - 1];
+				vertex_offset += 2;
+			}
 
-		        vti >= 0 ? file.vt[vti * 3 + 0] : 0,
-		        vti >= 0 ? file.vt[vti * 3 + 1] : 0,
-		    };
+			vertices[vertex_offset] = v;
+			vertex_offset++;
+		}
 
-		vertices[i] = v;
+		index_offset += obj->face_vertices[i];
 	}
 
+	fast_obj_destroy(obj);
+
 	reindex = timestamp();
 
 	Mesh result;
@@ -621,16 +632,18 @@ void encodeVertex(const Mesh& mesh, const char* pvn)
 	       (double(result.size() * sizeof(PV)) / (1 << 30)) / (end - middle));
 }
 
-void stripify(const Mesh& mesh)
+void stripify(const Mesh& mesh, bool use_restart)
 {
+	unsigned int restart_index = use_restart ? ~0u : 0;
+
 	// note: input mesh is assumed to be optimized for vertex cache and vertex fetch
 	double start = timestamp();
 	std::vector<unsigned int> strip(meshopt_stripifyBound(mesh.indices.size()));
-	strip.resize(meshopt_stripify(&strip[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size()));
+	strip.resize(meshopt_stripify(&strip[0], &mesh.indices[0], mesh.indices.size(), mesh.vertices.size(), restart_index));
 	double end = timestamp();
 
 	Mesh copy = mesh;
-	copy.indices.resize(meshopt_unstripify(&copy.indices[0], &strip[0], strip.size()));
+	copy.indices.resize(meshopt_unstripify(&copy.indices[0], &strip[0], strip.size(), restart_index));
 	assert(copy.indices.size() <= meshopt_unstripifyBound(strip.size()));
 
 	assert(isMeshValid(copy));
@@ -641,7 +654,8 @@ void stripify(const Mesh& mesh)
 	meshopt_VertexCacheStatistics vcs_amd = meshopt_analyzeVertexCache(&copy.indices[0], mesh.indices.size(), mesh.vertices.size(), 14, 64, 128);
 	meshopt_VertexCacheStatistics vcs_intel = meshopt_analyzeVertexCache(&copy.indices[0], mesh.indices.size(), mesh.vertices.size(), 128, 0, 0);
 
-	printf("Stripify : ACMR %f ATVR %f (NV %f AMD %f Intel %f); %d strip indices (%.1f%%) in %.2f msec\n",
+	printf("Stripify%c: ACMR %f ATVR %f (NV %f AMD %f Intel %f); %d strip indices (%.1f%%) in %.2f msec\n",
+	       use_restart ? 'R' : ' ',
 	       vcs.acmr, vcs.atvr, vcs_nv.atvr, vcs_amd.atvr, vcs_intel.atvr,
 	       int(strip.size()), double(strip.size()) / double(mesh.indices.size()) * 100,
 	       (end - start) * 1000);
@@ -779,43 +793,54 @@ void processDeinterleaved(const char* path)
 	// Most algorithms in the library work out of the box with deinterleaved geometry, but some require slightly special treatment;
 	// this code runs a simplified version of complete opt. pipeline using deinterleaved geo. There's no compression performed but you
 	// can trivially run it by quantizing all elements and running meshopt_encodeVertexBuffer once for each vertex stream.
-	ObjFile file;
-	if (!objParseFile(file, path) || !objValidate(file))
+	fastObjMesh* obj = fast_obj_read(path);
+	if (!obj)
 	{
-		printf("Error loading %s: file not found or invalid file data\n", path);
+		printf("Error loading %s: file not found\n", path);
 		return;
 	}
 
-	size_t total_indices = file.f_size / 3;
+	size_t total_indices = 0;
+
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+		total_indices += 3 * (obj->face_vertices[i] - 2);
 
 	std::vector<float> unindexed_pos(total_indices * 3);
 	std::vector<float> unindexed_nrm(total_indices * 3);
 	std::vector<float> unindexed_uv(total_indices * 2);
 
-	for (size_t i = 0; i < total_indices; ++i)
+	size_t vertex_offset = 0;
+	size_t index_offset = 0;
+
+	for (unsigned int i = 0; i < obj->face_count; ++i)
 	{
-		int vi = file.f[i * 3 + 0];
-		int vti = file.f[i * 3 + 1];
-		int vni = file.f[i * 3 + 2];
+		for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
+		{
+			fastObjIndex gi = obj->indices[index_offset + j];
 
-		unindexed_pos[i * 3 + 0] = file.v[vi * 3 + 0];
-		unindexed_pos[i * 3 + 1] = file.v[vi * 3 + 1];
-		unindexed_pos[i * 3 + 2] = file.v[vi * 3 + 2];
+			// triangulate polygon on the fly; offset-3 is always the first polygon vertex
+			if (j >= 3)
+			{
+				memcpy(&unindexed_pos[(vertex_offset + 0) * 3], &unindexed_pos[(vertex_offset - 3) * 3], 3 * sizeof(float));
+				memcpy(&unindexed_nrm[(vertex_offset + 0) * 3], &unindexed_nrm[(vertex_offset - 3) * 3], 3 * sizeof(float));
+				memcpy(&unindexed_uv[(vertex_offset + 0) * 2], &unindexed_uv[(vertex_offset - 3) * 2], 2 * sizeof(float));
+				memcpy(&unindexed_pos[(vertex_offset + 1) * 3], &unindexed_pos[(vertex_offset - 1) * 3], 3 * sizeof(float));
+				memcpy(&unindexed_nrm[(vertex_offset + 1) * 3], &unindexed_nrm[(vertex_offset - 1) * 3], 3 * sizeof(float));
+				memcpy(&unindexed_uv[(vertex_offset + 1) * 2], &unindexed_uv[(vertex_offset - 1) * 2], 2 * sizeof(float));
+				vertex_offset += 2;
+			}
 
-		if (vni >= 0)
-		{
-			unindexed_nrm[i * 3 + 0] = file.vn[vni * 3 + 0];
-			unindexed_nrm[i * 3 + 1] = file.vn[vni * 3 + 1];
-			unindexed_nrm[i * 3 + 2] = file.vn[vni * 3 + 2];
+			memcpy(&unindexed_pos[vertex_offset * 3], &obj->positions[gi.p * 3], 3 * sizeof(float));
+			memcpy(&unindexed_nrm[vertex_offset * 3], &obj->normals[gi.n * 3], 3 * sizeof(float));
+			memcpy(&unindexed_uv[vertex_offset * 2], &obj->texcoords[gi.t * 2], 2 * sizeof(float));
+			vertex_offset++;
 		}
 
-		if (vti >= 0)
-		{
-			unindexed_uv[i * 2 + 0] = file.vt[vti * 3 + 0];
-			unindexed_uv[i * 2 + 1] = file.vt[vti * 3 + 1];
-		}
+		index_offset += obj->face_vertices[i];
 	}
 
+	fast_obj_destroy(obj);
+
 	double start = timestamp();
 
 	meshopt_Stream streams[] = {
@@ -884,7 +909,9 @@ void process(const char* path)
 	meshopt_optimizeVertexCache(&copy.indices[0], &copy.indices[0], copy.indices.size(), copy.vertices.size());
 	meshopt_optimizeVertexFetch(&copy.vertices[0], &copy.indices[0], copy.indices.size(), &copy.vertices[0], copy.vertices.size(), sizeof(Vertex));
 
-	stripify(copy);
+	stripify(copy, false);
+	stripify(copy, true);
+
 	meshlets(copy);
 	shadow(copy);
 
@@ -907,7 +934,8 @@ void processDev(const char* path)
 	if (!loadMesh(mesh, path))
 		return;
 
-	simplify(mesh, 0.01f);
+	simplifySloppy(mesh, 0.5f);
+	simplifySloppy(mesh, 0.1f);
 	simplifySloppy(mesh, 0.01f);
 }
 

BIN
3rdparty/meshoptimizer/demo/pirate.glb


BIN
3rdparty/meshoptimizer/demo/pirate.optmesh


+ 0 - 0
3rdparty/meshoptimizer/js/decoder.js → 3rdparty/meshoptimizer/js/meshopt_decoder.js


+ 1 - 1
3rdparty/meshoptimizer/src/indexcodec.cpp

@@ -162,7 +162,7 @@ static void writeTriangle(void* destination, size_t offset, size_t index_size, u
 	}
 	else
 #ifdef __EMSCRIPTEN__
-	if (index_size == 4) // work around Edge (ChakraCore) bug - without this compiler assumes index_size==2
+	    if (index_size == 4) // work around Edge (ChakraCore) bug - without this compiler assumes index_size==2
 #endif
 	{
 		static_cast<unsigned int*>(destination)[offset + 0] = a;

+ 19 - 17
3rdparty/meshoptimizer/src/meshoptimizer.h

@@ -12,7 +12,7 @@
 #include <stddef.h>
 
 /* Version macro; major * 1000 + minor * 10 + patch */
-#define MESHOPTIMIZER_VERSION 110
+#define MESHOPTIMIZER_VERSION 120
 
 /* If no API is defined, assume default */
 #ifndef MESHOPTIMIZER_API
@@ -49,7 +49,7 @@ struct meshopt_Stream
 MESHOPTIMIZER_API size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size);
 
 /**
- * Experimental: Generates a vertex remap table from multiple vertex streams and an optional index buffer and returns number of unique vertices
+ * Generates a vertex remap table from multiple vertex streams and an optional index buffer and returns number of unique vertices
  * As a result, all vertices that are binary equivalent map to the same (new) location, with no gaps in the resulting sequence.
  * Resulting remap table maps old vertices to new vertices and can be used in meshopt_remapVertexBuffer/meshopt_remapIndexBuffer.
  * To remap vertex buffers, you will need to call meshopt_remapVertexBuffer for each vertex stream.
@@ -57,7 +57,7 @@ MESHOPTIMIZER_API size_t meshopt_generateVertexRemap(unsigned int* destination,
  * destination must contain enough space for the resulting remap table (vertex_count elements)
  * indices can be NULL if the input is unindexed
  */
-MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
+MESHOPTIMIZER_API size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
 
 /**
  * Generates vertex buffer from the source vertex buffer and remap table generated by meshopt_generateVertexRemap
@@ -76,22 +76,22 @@ MESHOPTIMIZER_API void meshopt_remapVertexBuffer(void* destination, const void*
 MESHOPTIMIZER_API void meshopt_remapIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const unsigned int* remap);
 
 /**
- * Experimental: Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary
+ * Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary
  * All vertices that are binary equivalent (wrt first vertex_size bytes) map to the first vertex in the original vertex buffer.
  * This makes it possible to use the index buffer for Z pre-pass or shadowmap rendering, while using the original index buffer for regular rendering.
  *
  * destination must contain enough space for the resulting index buffer (index_count elements)
  */
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_generateShadowIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride);
+MESHOPTIMIZER_API void meshopt_generateShadowIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride);
 
 /**
- * Experimental: Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary
+ * Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary
  * All vertices that are binary equivalent (wrt specified streams) map to the first vertex in the original vertex buffer.
  * This makes it possible to use the index buffer for Z pre-pass or shadowmap rendering, while using the original index buffer for regular rendering.
  *
  * destination must contain enough space for the resulting index buffer (index_count elements)
  */
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
+MESHOPTIMIZER_API void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count);
 
 /**
  * Vertex transform cache optimizer
@@ -217,13 +217,15 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destinati
 
 /**
  * Mesh stripifier
- * Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index
+ * Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index or degenerate triangles
  * Returns the number of indices in the resulting strip, with destination containing new index data
  * For maximum efficiency the index buffer being converted has to be optimized for vertex cache first.
+ * Using restart indices can result in ~10% smaller index buffers, but on some GPUs restart indices may result in decreased performance.
  *
  * destination must contain enough space for the target index buffer, worst case can be computed with meshopt_stripifyBound
+ * restart_index should be 0xffff or 0xffffffff depending on index size, or 0 to use degenerate triangles
  */
-MESHOPTIMIZER_API size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count);
+MESHOPTIMIZER_API size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index);
 MESHOPTIMIZER_API size_t meshopt_stripifyBound(size_t index_count);
 
 /**
@@ -233,7 +235,7 @@ MESHOPTIMIZER_API size_t meshopt_stripifyBound(size_t index_count);
  *
  * destination must contain enough space for the target index buffer, worst case can be computed with meshopt_unstripifyBound
  */
-MESHOPTIMIZER_API size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count);
+MESHOPTIMIZER_API size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index);
 MESHOPTIMIZER_API size_t meshopt_unstripifyBound(size_t index_count);
 
 struct meshopt_VertexCacheStatistics
@@ -341,12 +343,12 @@ MESHOPTIMIZER_EXPERIMENTAL struct meshopt_Bounds meshopt_computeClusterBounds(co
 MESHOPTIMIZER_EXPERIMENTAL struct meshopt_Bounds meshopt_computeMeshletBounds(const struct meshopt_Meshlet* meshlet, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride);
 
 /**
- * Experimental: Set allocation callbacks
+ * Set allocation callbacks
  * These callbacks will be used instead of the default operator new/operator delete for all temporary allocations in the library.
  * Note that all algorithms only allocate memory for temporary use.
  * allocate/deallocate are always called in a stack-like order - last pointer to be allocated is deallocated first.
  */
-MESHOPTIMIZER_EXPERIMENTAL void meshopt_setAllocator(void* (*allocate)(size_t), void (*deallocate)(void*));
+MESHOPTIMIZER_API void meshopt_setAllocator(void* (*allocate)(size_t), void (*deallocate)(void*));
 
 #ifdef __cplusplus
 } /* extern "C" */
@@ -562,21 +564,21 @@ inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t in
 }
 
 template <typename T>
-inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_count, size_t vertex_count)
+inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_count, size_t vertex_count, T restart_index)
 {
 	meshopt_IndexAdapter<T> in(0, indices, index_count);
-	meshopt_IndexAdapter<T> out(destination, 0, (index_count / 3) * 4);
+	meshopt_IndexAdapter<T> out(destination, 0, (index_count / 3) * 5);
 
-	return meshopt_stripify(out.data, in.data, index_count, vertex_count);
+	return meshopt_stripify(out.data, in.data, index_count, vertex_count, unsigned(restart_index));
 }
 
 template <typename T>
-inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_count)
+inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_count, T restart_index)
 {
 	meshopt_IndexAdapter<T> in(0, indices, index_count);
 	meshopt_IndexAdapter<T> out(destination, 0, (index_count - 2) * 3);
 
-	return meshopt_unstripify(out.data, in.data, index_count);
+	return meshopt_unstripify(out.data, in.data, index_count, unsigned(restart_index));
 }
 
 template <typename T>

+ 1 - 1
3rdparty/meshoptimizer/src/overdrawanalyzer.cpp

@@ -227,4 +227,4 @@ meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const unsigned int* indices,
 	result.overdraw = result.pixels_covered ? float(result.pixels_shaded) / float(result.pixels_covered) : 0.f;
 
 	return result;
-}
+}

+ 22 - 20
3rdparty/meshoptimizer/src/simplifier.cpp

@@ -19,6 +19,7 @@
 // Michael Garland. Quadric-based polygonal surface simplification. 1999
 // Peter Lindstrom. Out-of-Core Simplification of Large Polygonal Models. 2000
 // Matthias Teschner, Bruno Heidelberger, Matthias Mueller, Danat Pomeranets, Markus Gross. Optimized Spatial Hashing for Collision Detection of Deformable Objects. 2003
+// Peter Van Sandt, Yannis Chronis, Jignesh M. Patel. Efficiently Searching In-Memory Sorted Arrays: Revenge of the Interpolation Search? 2019
 namespace meshopt
 {
 
@@ -1080,6 +1081,14 @@ static size_t filterTriangles(unsigned int* destination, unsigned int* tritable,
 	return result * 3;
 }
 
+static float interpolate(float y, float x0, float y0, float x1, float y1, float x2, float y2)
+{
+	// three point interpolation from "revenge of interpolation search" paper
+	float num = (y1 - y) * (x1 - x2) * (x1 - x0) * (y2 - y0);
+	float den = (y2 - y) * (x1 - x2) * (y0 - y1) + (y0 - y) * (x1 - x0) * (y1 - y2);
+	return x1 + num / den;
+}
+
 } // namespace meshopt
 
 #if TRACE
@@ -1265,38 +1274,25 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind
 
 	unsigned int* vertex_ids = allocator.allocate<unsigned int>(vertex_count);
 
+	const int kInterpolationPasses = 5;
+
 	// invariant: # of triangles in min_grid <= target_count
 	int min_grid = 0;
 	int max_grid = 1025;
 	size_t min_triangles = 0;
 	size_t max_triangles = index_count / 3;
 
-	const int kInterpolationPasses = 5;
+	// instead of starting in the middle, let's guess as to what the answer might be! triangle count usually grows as a square of grid size...
+	int next_grid_size = int(sqrtf(float(target_cell_count)) + 0.5f);
 
 	for (int pass = 0; pass < 10 + kInterpolationPasses; ++pass)
 	{
 		assert(min_triangles < target_index_count / 3);
 		assert(max_grid - min_grid > 1);
 
-		int grid_size = 0;
-
-		if (pass == 0)
-		{
-			// instead of starting in the middle, let's guess as to what the answer might be! triangle count usually grows as a square of grid size...
-			grid_size = int(sqrtf(float(target_cell_count)) + 0.5f);
-			grid_size = (grid_size <= min_grid) ? min_grid + 1 : (grid_size >= max_grid) ? max_grid - 1 : grid_size;
-		}
-		else if (pass <= kInterpolationPasses)
-		{
-			float k = (float(target_index_count / 3) - float(min_triangles)) / (float(max_triangles) - float(min_triangles));
-			grid_size = int(float(min_grid) * (1 - k) + float(max_grid) * k + 0.5f);
-			grid_size = (grid_size <= min_grid) ? min_grid + 1 : (grid_size >= max_grid) ? max_grid - 1 : grid_size;
-		}
-		else
-		{
-			grid_size = (min_grid + max_grid) / 2;
-			assert(grid_size > min_grid && grid_size < max_grid);
-		}
+		// we clamp the prediction of the grid size to make sure that the search converges
+		int grid_size = next_grid_size;
+		grid_size = (grid_size <= min_grid) ? min_grid + 1 : (grid_size >= max_grid) ? max_grid - 1 : grid_size;
 
 		computeVertexIds(vertex_ids, vertex_positions, vertex_count, grid_size);
 		size_t triangles = countTriangles(vertex_ids, indices, index_count);
@@ -1308,6 +1304,8 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind
 		       (triangles <= target_index_count / 3) ? "under" : "over");
 #endif
 
+		float tip = interpolate(float(target_index_count / 3), float(min_grid), float(min_triangles), float(grid_size), float(triangles), float(max_grid), float(max_triangles));
+
 		if (triangles <= target_index_count / 3)
 		{
 			min_grid = grid_size;
@@ -1321,6 +1319,10 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind
 
 		if (triangles == target_index_count / 3 || max_grid - min_grid <= 1)
 			break;
+
+		// we start by using interpolation search - it usually converges faster
+		// however, interpolation search has a worst case of O(N) so we switch to binary search after a few iterations which converges in O(logN)
+		next_grid_size = (pass < kInterpolationPasses) ? int(tip + 0.5f) : (min_grid + max_grid) / 2;
 	}
 
 	if (min_triangles == 0)

+ 40 - 15
3rdparty/meshoptimizer/src/stripifier.cpp

@@ -48,7 +48,7 @@ static int findStripNext(const unsigned int buffer[][3], unsigned int buffer_siz
 
 } // namespace meshopt
 
-size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count)
+size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index)
 {
 	assert(destination != indices);
 	assert(index_count % 3 == 0);
@@ -197,18 +197,42 @@ size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices,
 				next = ec;
 			}
 
-			// emit the new strip; we use restart indices
-			if (strip_size)
-				destination[strip_size++] = ~0u;
+			if (restart_index)
+			{
+				if (strip_size)
+					destination[strip_size++] = restart_index;
 
-			destination[strip_size++] = a;
-			destination[strip_size++] = b;
-			destination[strip_size++] = c;
+				destination[strip_size++] = a;
+				destination[strip_size++] = b;
+				destination[strip_size++] = c;
 
-			// new strip always starts with the same edge winding
-			strip[0] = b;
-			strip[1] = c;
-			parity = 1;
+				// new strip always starts with the same edge winding
+				strip[0] = b;
+				strip[1] = c;
+				parity = 1;
+			}
+			else
+			{
+				if (strip_size)
+				{
+					// connect last strip using degenerate triangles
+					destination[strip_size++] = strip[1];
+					destination[strip_size++] = a;
+				}
+
+				// note that we may need to flip the emitted triangle based on parity
+				// we always end up with outgoing edge "cb" in the end
+				unsigned int e0 = parity ? c : b;
+				unsigned int e1 = parity ? b : c;
+
+				destination[strip_size++] = a;
+				destination[strip_size++] = e0;
+				destination[strip_size++] = e1;
+
+				strip[0] = e0;
+				strip[1] = e1;
+				parity ^= 1;
+			}
 		}
 	}
 
@@ -219,11 +243,12 @@ size_t meshopt_stripifyBound(size_t index_count)
 {
 	assert(index_count % 3 == 0);
 
-	// worst case is 1 restart index and 3 indices per triangle
-	return (index_count / 3) * 4;
+	// worst case without restarts is 2 degenerate indices and 3 indices per triangle
+	// worst case with restarts is 1 restart index and 3 indices per triangle
+	return (index_count / 3) * 5;
 }
 
-size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count)
+size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index)
 {
 	assert(destination != indices);
 
@@ -232,7 +257,7 @@ size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices
 
 	for (size_t i = 0; i < index_count; ++i)
 	{
-		if (indices[i] == ~0u)
+		if (restart_index && indices[i] == restart_index)
 		{
 			start = i + 1;
 		}

+ 2 - 2
3rdparty/meshoptimizer/src/vcacheoptimizer.cpp

@@ -15,11 +15,11 @@ const size_t kValenceMax = 8;
 
 static const float kVertexScoreTableCache[1 + kCacheSizeMax] = {
     0.f,
-    0.792f, 0.767f, 0.764f, 0.956f, 0.827f, 0.751f, 0.820f, 0.864f, 0.738f, 0.788f, 0.642f, 0.646f, 0.165f, 0.654f, 0.545f, 0.284f};
+    0.779f, 0.791f, 0.789f, 0.981f, 0.843f, 0.726f, 0.847f, 0.882f, 0.867f, 0.799f, 0.642f, 0.613f, 0.600f, 0.568f, 0.372f, 0.234f};
 
 static const float kVertexScoreTableLive[1 + kValenceMax] = {
     0.f,
-    0.994f, 0.721f, 0.479f, 0.423f, 0.174f, 0.080f, 0.249f, 0.056f};
+    0.995f, 0.713f, 0.450f, 0.404f, 0.059f, 0.005f, 0.147f, 0.006f};
 
 struct TriangleAdjacency
 {

+ 0 - 179
3rdparty/meshoptimizer/tools/OptMeshLoader.js

@@ -1,179 +0,0 @@
-THREE.OptMeshLoader = (function ()
-{
-	function OptMeshLoader(manager)
-	{
-		this.manager = (manager !== undefined) ? manager : THREE.DefaultLoadingManager;
-		this.materials = Promise.resolve(null);
-	}
-
-	OptMeshLoader.prototype =
-	{
-		constructor: OptMeshLoader,
-
-		load: function (url, onLoad, onProgress, onError)
-		{
-			var scope = this;
-
-			var loader = new THREE.FileLoader(scope.manager);
-			loader.setResponseType('arraybuffer');
-			loader.setPath(this.path);
-			loader.load(url, function (data)
-			{
-				scope.decoder.ready.then(function ()
-				{
-					scope.materials.then(function (materials)
-					{
-						onLoad(scope.parse(data, materials));
-					});
-				});
-			}, onProgress, onError);
-		},
-
-		setDecoder: function (value)
-		{
-			this.decoder = value;
-			return this;
-
-		},
-
-		setPath: function (value)
-		{
-			this.path = value;
-			return this;
-		},
-
-		setMaterials: function (materials)
-		{
-			this.materials = Promise.resolve(materials);
-			return this;
-		},
-
-		setMaterialLib: function (lib)
-		{
-			var scope = this;
-
-			this.materials = new Promise(function (resolve, reject)
-			{
-				var loader = new THREE.MTLLoader();
-				loader.setPath(scope.path);
-				loader.load(lib, function (materials) { materials.preload(); resolve(materials); }, null, reject);
-			});
-
-			return this;
-		},
-
-		parse: function (data, materials)
-		{
-			console.time('OptMeshLoader');
-
-			var array = new Uint8Array(data);
-			var view = new DataView(data);
-
-			var endian = true;
-			var magic = view.getUint32(0, endian);
-			var objectCount = view.getUint32(4, endian);
-			var vertexCount = view.getUint32(8, endian);
-			var indexCount = view.getUint32(12, endian);
-			var vertexDataSize = view.getUint32(16, endian);
-			var indexDataSize = view.getUint32(20, endian);
-			var posOffsetX = view.getFloat32(24, endian);
-			var posOffsetY = view.getFloat32(28, endian);
-			var posOffsetZ = view.getFloat32(32, endian);
-			var posScale = view.getFloat32(36, endian);
-			var uvOffsetX = view.getFloat32(40, endian);
-			var uvOffsetY = view.getFloat32(44, endian);
-			var uvScaleX = view.getFloat32(48, endian);
-			var uvScaleY = view.getFloat32(52, endian);
-
-			if (magic != 0x4D54504F)
-				throw new Error("Malformed mesh file: unrecognized header");
-
-			var objectOffset = 64;
-			var objectDataOffset = objectOffset + 16 * objectCount;
-
-			var objectDataSize = 0;
-
-			for (var i = 0; i < objectCount; ++i)
-				objectDataSize += view.getUint32(objectOffset + 16 * i + 8, endian);
-
-			var vertexDataOffset = objectDataOffset + objectDataSize;
-			var indexDataOffset = vertexDataOffset + vertexDataSize;
-
-			var endOffset = indexDataOffset + indexDataSize;
-
-			if (endOffset != data.byteLength)
-				throw new Error("Malformed mesh file: unexpected input size");
-
-			var vertexSize = 16;
-			var indexSize = 4;
-
-			var vertexBuffer = new ArrayBuffer(vertexCount * vertexSize);
-			var vertexBufferU8 = new Uint8Array(vertexBuffer);
-			this.decoder.decodeVertexBuffer(vertexBufferU8, vertexCount, vertexSize, array.subarray(vertexDataOffset, vertexDataOffset + vertexDataSize));
-
-			var indexBuffer = new ArrayBuffer(indexCount * indexSize);
-			var indexBufferU8 = new Uint8Array(indexBuffer);
-			this.decoder.decodeIndexBuffer(indexBufferU8, indexCount, indexSize, array.subarray(indexDataOffset, indexDataOffset + indexDataSize));
-
-			var geometry = new THREE.BufferGeometry();
-
-			geometry.addAttribute('position', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Uint16Array(vertexBuffer), 8), 3, 0, false));
-			geometry.addAttribute('normal', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Int8Array(vertexBuffer), 16), 3, 8, true));
-			geometry.addAttribute('uv', new THREE.InterleavedBufferAttribute(new THREE.InterleavedBuffer(new Uint16Array(vertexBuffer), 8), 2, 6, false));
-			geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indexBuffer), 1, false));
-
-			var objectDataOffsetAcc = objectDataOffset;
-
-			var objectMaterials = [];
-			var objectMaterialsLookup = {};
-
-			for (var i = 0; i < objectCount; i++)
-			{
-				var objectIndexOffset = view.getUint32(objectOffset + 16 * i + 0, endian);
-				var objectIndexCount = view.getUint32(objectOffset + 16 * i + 4, endian);
-				var objectMaterialLength = view.getUint32(objectOffset + 16 * i + 8, endian);
-
-				var objectMaterialName = String.fromCharCode.apply(null, array.subarray(objectDataOffsetAcc, objectDataOffsetAcc + objectMaterialLength));
-				var objectMaterialIndex = objectMaterialsLookup[objectMaterialName];
-
-				if (objectMaterialIndex == undefined)
-				{
-					var objectMaterial = null;
-
-					if (materials !== null)
-						objectMaterial = materials.create(objectMaterialName);
-
-					if (!objectMaterial)
-						objectMaterial = new THREE.MeshPhongMaterial();
-
-					if (objectMaterial.map)
-					{
-						objectMaterial.map.offset.set(uvOffsetX, uvOffsetY);
-						objectMaterial.map.repeat.set(uvScaleX, uvScaleY);
-					}
-
-					objectMaterialIndex = objectMaterials.length;
-					objectMaterialsLookup[objectMaterialName] = objectMaterialIndex;
-					objectMaterials.push(objectMaterial);
-				}
-
-				geometry.addGroup(objectIndexOffset, objectIndexCount, objectMaterialIndex);
-
-				objectDataOffsetAcc += objectMaterialLength;
-			}
-
-			var mesh = new THREE.Mesh(geometry, objectMaterials);
-			mesh.position.set(posOffsetX, posOffsetY, posOffsetZ);
-			mesh.scale.set(posScale, posScale, posScale);
-
-			var container = new THREE.Group();
-			container.add(mesh);
-
-			console.timeEnd('OptMeshLoader');
-
-			return container;
-		}
-	};
-
-	return OptMeshLoader;
-})();

+ 4733 - 0
3rdparty/meshoptimizer/tools/cgltf.h

@@ -0,0 +1,4733 @@
+/**
+ * cgltf - a single-file glTF 2.0 parser written in C99.
+ *
+ * Version: 1.2
+ *
+ * Website: https://github.com/jkuhlmann/cgltf
+ *
+ * Distributed under the MIT License, see notice at the end of this file.
+ *
+ * Building:
+ * Include this file where you need the struct and function
+ * declarations. Have exactly one source file where you define
+ * `CGLTF_IMPLEMENTATION` before including this file to get the
+ * function definitions.
+ *
+ * Reference:
+ * `cgltf_result cgltf_parse(const cgltf_options*, const void*,
+ * cgltf_size, cgltf_data**)` parses both glTF and GLB data. If
+ * this function returns `cgltf_result_success`, you have to call
+ * `cgltf_free()` on the created `cgltf_data*` variable.
+ * Note that contents of external files for buffers and images are not
+ * automatically loaded. You'll need to read these files yourself using
+ * URIs in the `cgltf_data` structure.
+ *
+ * `cgltf_options` is the struct passed to `cgltf_parse()` to control
+ * parts of the parsing process. You can use it to force the file type
+ * and provide memory allocation callbacks. Should be zero-initialized
+ * to trigger default behavior.
+ *
+ * `cgltf_data` is the struct allocated and filled by `cgltf_parse()`.
+ * It generally mirrors the glTF format as described by the spec (see
+ * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0).
+ *
+ * `void cgltf_free(cgltf_data*)` frees the allocated `cgltf_data`
+ * variable.
+ *
+ * `cgltf_result cgltf_load_buffers(const cgltf_options*, cgltf_data*,
+ * const char* gltf_path)` can be optionally called to open and read buffer
+ * files using the `FILE*` APIs. The `gltf_path` argument is the path to
+ * the original glTF file, which allows the parser to resolve the path to
+ * buffer files.
+ *
+ * `cgltf_result cgltf_load_buffer_base64(const cgltf_options* options,
+ * cgltf_size size, const char* base64, void** out_data)` decodes
+ * base64-encoded data content. Used internally by `cgltf_load_buffers()`
+ * and may be useful if you're not dealing with normal files.
+ *
+ * `cgltf_result cgltf_parse_file(const cgltf_options* options, const
+ * char* path, cgltf_data** out_data)` can be used to open the given
+ * file using `FILE*` APIs and parse the data using `cgltf_parse()`.
+ *
+ * `cgltf_result cgltf_validate(cgltf_data*)` can be used to do additional
+ * checks to make sure the parsed glTF data is valid.
+ *
+ * `cgltf_node_transform_local` converts the translation / rotation / scale properties of a node
+ * into a mat4.
+ *
+ * `cgltf_node_transform_world` calls `cgltf_node_transform_local` on every ancestor in order
+ * to compute the root-to-node transformation.
+ *
+ * `cgltf_accessor_read_float` reads a certain element from an accessor and converts it to
+ * floating point, assuming that `cgltf_load_buffers` has already been called. The passed-in element
+ * size is the number of floats in the output buffer, which should be in the range [1, 16]. Returns
+ * false if the passed-in element_size is too small, or if the accessor is sparse.
+ *
+ * `cgltf_accessor_read_index` is similar to its floating-point counterpart, but it returns size_t
+ * and only works with single-component data types.
+ *
+ * `cgltf_result cgltf_copy_extras_json(const cgltf_data*, const cgltf_extras*,
+ * char* dest, cgltf_size* dest_size)` allows to retrieve the "extras" data that
+ * can be attached to many glTF objects (which can be arbitrary JSON data). The
+ * `cgltf_extras` struct stores the offsets of the start and end of the extras JSON data
+ * as it appears in the complete glTF JSON data. This function copies the extras data
+ * into the provided buffer. If `dest` is NULL, the length of the data is written into
+ * `dest_size`. You can then parse this data using your own JSON parser
+ * or, if you've included the cgltf implementation using the integrated JSMN JSON parser.
+ */
+#ifndef CGLTF_H_INCLUDED__
+#define CGLTF_H_INCLUDED__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef size_t cgltf_size;
+typedef float cgltf_float;
+typedef int cgltf_int;
+typedef int cgltf_bool;
+
+typedef enum cgltf_file_type
+{
+	cgltf_file_type_invalid,
+	cgltf_file_type_gltf,
+	cgltf_file_type_glb,
+} cgltf_file_type;
+
+typedef struct cgltf_options
+{
+	cgltf_file_type type; /* invalid == auto detect */
+	cgltf_size json_token_count; /* 0 == auto */
+	void* (*memory_alloc)(void* user, cgltf_size size);
+	void (*memory_free) (void* user, void* ptr);
+	void* memory_user_data;
+} cgltf_options;
+
+typedef enum cgltf_result
+{
+	cgltf_result_success,
+	cgltf_result_data_too_short,
+	cgltf_result_unknown_format,
+	cgltf_result_invalid_json,
+	cgltf_result_invalid_gltf,
+	cgltf_result_invalid_options,
+	cgltf_result_file_not_found,
+	cgltf_result_io_error,
+	cgltf_result_out_of_memory,
+} cgltf_result;
+
+typedef enum cgltf_buffer_view_type
+{
+	cgltf_buffer_view_type_invalid,
+	cgltf_buffer_view_type_indices,
+	cgltf_buffer_view_type_vertices,
+} cgltf_buffer_view_type;
+
+typedef enum cgltf_attribute_type
+{
+	cgltf_attribute_type_invalid,
+	cgltf_attribute_type_position,
+	cgltf_attribute_type_normal,
+	cgltf_attribute_type_tangent,
+	cgltf_attribute_type_texcoord,
+	cgltf_attribute_type_color,
+	cgltf_attribute_type_joints,
+	cgltf_attribute_type_weights,
+} cgltf_attribute_type;
+
+typedef enum cgltf_component_type
+{
+	cgltf_component_type_invalid,
+	cgltf_component_type_r_8, /* BYTE */
+	cgltf_component_type_r_8u, /* UNSIGNED_BYTE */
+	cgltf_component_type_r_16, /* SHORT */
+	cgltf_component_type_r_16u, /* UNSIGNED_SHORT */
+	cgltf_component_type_r_32u, /* UNSIGNED_INT */
+	cgltf_component_type_r_32f, /* FLOAT */
+} cgltf_component_type;
+
+typedef enum cgltf_type
+{
+	cgltf_type_invalid,
+	cgltf_type_scalar,
+	cgltf_type_vec2,
+	cgltf_type_vec3,
+	cgltf_type_vec4,
+	cgltf_type_mat2,
+	cgltf_type_mat3,
+	cgltf_type_mat4,
+} cgltf_type;
+
+typedef enum cgltf_primitive_type
+{
+	cgltf_primitive_type_points,
+	cgltf_primitive_type_lines,
+	cgltf_primitive_type_line_loop,
+	cgltf_primitive_type_line_strip,
+	cgltf_primitive_type_triangles,
+	cgltf_primitive_type_triangle_strip,
+	cgltf_primitive_type_triangle_fan,
+} cgltf_primitive_type;
+
+typedef enum cgltf_alpha_mode
+{
+	cgltf_alpha_mode_opaque,
+	cgltf_alpha_mode_mask,
+	cgltf_alpha_mode_blend,
+} cgltf_alpha_mode;
+
+typedef enum cgltf_animation_path_type {
+	cgltf_animation_path_type_invalid,
+	cgltf_animation_path_type_translation,
+	cgltf_animation_path_type_rotation,
+	cgltf_animation_path_type_scale,
+	cgltf_animation_path_type_weights,
+} cgltf_animation_path_type;
+
+typedef enum cgltf_interpolation_type {
+	cgltf_interpolation_type_linear,
+	cgltf_interpolation_type_step,
+	cgltf_interpolation_type_cubic_spline,
+} cgltf_interpolation_type;
+
+typedef enum cgltf_camera_type {
+	cgltf_camera_type_invalid,
+	cgltf_camera_type_perspective,
+	cgltf_camera_type_orthographic,
+} cgltf_camera_type;
+
+typedef enum cgltf_light_type {
+	cgltf_light_type_invalid,
+	cgltf_light_type_directional,
+	cgltf_light_type_point,
+	cgltf_light_type_spot,
+} cgltf_light_type;
+
+typedef struct cgltf_extras {
+	cgltf_size start_offset;
+	cgltf_size end_offset;
+} cgltf_extras;
+
+typedef struct cgltf_buffer
+{
+	cgltf_size size;
+	char* uri;
+	void* data; /* loaded by cgltf_load_buffers */
+	cgltf_extras extras;
+} cgltf_buffer;
+
+typedef struct cgltf_buffer_view
+{
+	cgltf_buffer* buffer;
+	cgltf_size offset;
+	cgltf_size size;
+	cgltf_size stride; /* 0 == automatically determined by accessor */
+	cgltf_buffer_view_type type;
+	cgltf_extras extras;
+} cgltf_buffer_view;
+
+typedef struct cgltf_accessor_sparse
+{
+	cgltf_size count;
+	cgltf_buffer_view* indices_buffer_view;
+	cgltf_size indices_byte_offset;
+	cgltf_component_type indices_component_type;
+	cgltf_buffer_view* values_buffer_view;
+	cgltf_size values_byte_offset;
+	cgltf_extras extras;
+	cgltf_extras indices_extras;
+	cgltf_extras values_extras;
+} cgltf_accessor_sparse;
+
+typedef struct cgltf_accessor
+{
+	cgltf_component_type component_type;
+	cgltf_bool normalized;
+	cgltf_type type;
+	cgltf_size offset;
+	cgltf_size count;
+	cgltf_size stride;
+	cgltf_buffer_view* buffer_view;
+	cgltf_bool has_min;
+	cgltf_float min[16];
+	cgltf_bool has_max;
+	cgltf_float max[16];
+	cgltf_bool is_sparse;
+	cgltf_accessor_sparse sparse;
+	cgltf_extras extras;
+} cgltf_accessor;
+
+typedef struct cgltf_attribute
+{
+	char* name;
+	cgltf_attribute_type type;
+	cgltf_int index;
+	cgltf_accessor* data;
+} cgltf_attribute;
+
+typedef struct cgltf_image 
+{
+	char* name;
+	char* uri;
+	cgltf_buffer_view* buffer_view;
+	char* mime_type;
+	cgltf_extras extras;
+} cgltf_image;
+
+typedef struct cgltf_sampler
+{
+	cgltf_int mag_filter;
+	cgltf_int min_filter;
+	cgltf_int wrap_s;
+	cgltf_int wrap_t;
+	cgltf_extras extras;
+} cgltf_sampler;
+
+typedef struct cgltf_texture
+{
+	char* name;
+	cgltf_image* image;
+	cgltf_sampler* sampler;
+	cgltf_extras extras;
+} cgltf_texture;
+
+typedef struct cgltf_texture_transform
+{
+	cgltf_float offset[2];
+	cgltf_float rotation;
+	cgltf_float scale[2];
+	cgltf_int texcoord;
+} cgltf_texture_transform;
+
+typedef struct cgltf_texture_view
+{	
+	cgltf_texture* texture;
+	cgltf_int texcoord;
+	cgltf_float scale; /* equivalent to strength for occlusion_texture */
+	cgltf_bool has_transform;
+	cgltf_texture_transform transform;
+	cgltf_extras extras;
+} cgltf_texture_view;
+
+typedef struct cgltf_pbr_metallic_roughness
+{
+	cgltf_texture_view base_color_texture;
+	cgltf_texture_view metallic_roughness_texture;
+
+	cgltf_float base_color_factor[4];
+	cgltf_float metallic_factor;
+	cgltf_float roughness_factor;
+
+	cgltf_extras extras;
+} cgltf_pbr_metallic_roughness;
+
+typedef struct cgltf_pbr_specular_glossiness
+{
+	cgltf_texture_view diffuse_texture;
+	cgltf_texture_view specular_glossiness_texture;
+
+	cgltf_float diffuse_factor[4];
+	cgltf_float specular_factor[3];
+	cgltf_float glossiness_factor;
+} cgltf_pbr_specular_glossiness;
+
+typedef struct cgltf_material
+{
+	char* name;
+	cgltf_bool has_pbr_metallic_roughness;
+	cgltf_bool has_pbr_specular_glossiness;
+	cgltf_pbr_metallic_roughness pbr_metallic_roughness;
+	cgltf_pbr_specular_glossiness pbr_specular_glossiness;
+	cgltf_texture_view normal_texture;
+	cgltf_texture_view occlusion_texture;
+	cgltf_texture_view emissive_texture;
+	cgltf_float emissive_factor[3];
+	cgltf_alpha_mode alpha_mode;
+	cgltf_float alpha_cutoff;
+	cgltf_bool double_sided;
+	cgltf_bool unlit;
+	cgltf_extras extras;
+} cgltf_material;
+
+typedef struct cgltf_morph_target {
+	cgltf_attribute* attributes;
+	cgltf_size attributes_count;
+} cgltf_morph_target;
+
+typedef struct cgltf_primitive {
+	cgltf_primitive_type type;
+	cgltf_accessor* indices;
+	cgltf_material* material;
+	cgltf_attribute* attributes;
+	cgltf_size attributes_count;
+	cgltf_morph_target* targets;
+	cgltf_size targets_count;
+	cgltf_extras extras;
+} cgltf_primitive;
+
+typedef struct cgltf_mesh {
+	char* name;
+	cgltf_primitive* primitives;
+	cgltf_size primitives_count;
+	cgltf_float* weights;
+	cgltf_size weights_count;
+	cgltf_extras extras;
+} cgltf_mesh;
+
+typedef struct cgltf_node cgltf_node;
+
+typedef struct cgltf_skin {
+	char* name;
+	cgltf_node** joints;
+	cgltf_size joints_count;
+	cgltf_node* skeleton;
+	cgltf_accessor* inverse_bind_matrices;
+	cgltf_extras extras;
+} cgltf_skin;
+
+typedef struct cgltf_camera_perspective {
+	cgltf_float aspect_ratio;
+	cgltf_float yfov;
+	cgltf_float zfar;
+	cgltf_float znear;
+	cgltf_extras extras;
+} cgltf_camera_perspective;
+
+typedef struct cgltf_camera_orthographic {
+	cgltf_float xmag;
+	cgltf_float ymag;
+	cgltf_float zfar;
+	cgltf_float znear;
+	cgltf_extras extras;
+} cgltf_camera_orthographic;
+
+typedef struct cgltf_camera {
+	char* name;
+	cgltf_camera_type type;
+	union {
+		cgltf_camera_perspective perspective;
+		cgltf_camera_orthographic orthographic;
+	};
+	cgltf_extras extras;
+} cgltf_camera;
+
+typedef struct cgltf_light {
+	char* name;
+	cgltf_float color[3];
+	cgltf_float intensity;
+	cgltf_light_type type;
+	cgltf_float range;
+	cgltf_float spot_inner_cone_angle;
+	cgltf_float spot_outer_cone_angle;
+} cgltf_light;
+
+typedef struct cgltf_node {
+	char* name;
+	cgltf_node* parent;
+	cgltf_node** children;
+	cgltf_size children_count;
+	cgltf_skin* skin;
+	cgltf_mesh* mesh;
+	cgltf_camera* camera;
+	cgltf_light* light;
+	cgltf_float* weights;
+	cgltf_size weights_count;
+	cgltf_bool has_translation;
+	cgltf_bool has_rotation;
+	cgltf_bool has_scale;
+	cgltf_bool has_matrix;
+	cgltf_float translation[3];
+	cgltf_float rotation[4];
+	cgltf_float scale[3];
+	cgltf_float matrix[16];
+	cgltf_extras extras;
+} cgltf_node;
+
+typedef struct cgltf_scene {
+	char* name;
+	cgltf_node** nodes;
+	cgltf_size nodes_count;
+	cgltf_extras extras;
+} cgltf_scene;
+
+typedef struct cgltf_animation_sampler {
+	cgltf_accessor* input;
+	cgltf_accessor* output;
+	cgltf_interpolation_type interpolation;
+	cgltf_extras extras;
+} cgltf_animation_sampler;
+
+typedef struct cgltf_animation_channel {
+	cgltf_animation_sampler* sampler;
+	cgltf_node* target_node;
+	cgltf_animation_path_type target_path;
+	cgltf_extras extras;
+} cgltf_animation_channel;
+
+typedef struct cgltf_animation {
+	char* name;
+	cgltf_animation_sampler* samplers;
+	cgltf_size samplers_count;
+	cgltf_animation_channel* channels;
+	cgltf_size channels_count;
+	cgltf_extras extras;
+} cgltf_animation;
+
+typedef struct cgltf_asset {
+	char* copyright;
+	char* generator;
+	char* version;
+	char* min_version;
+	cgltf_extras extras;
+} cgltf_asset;
+
+typedef struct cgltf_data
+{
+	cgltf_file_type file_type;
+	void* file_data;
+
+	cgltf_asset asset;
+
+	cgltf_mesh* meshes;
+	cgltf_size meshes_count;
+
+	cgltf_material* materials;
+	cgltf_size materials_count;
+
+	cgltf_accessor* accessors;
+	cgltf_size accessors_count;
+
+	cgltf_buffer_view* buffer_views;
+	cgltf_size buffer_views_count;
+
+	cgltf_buffer* buffers;
+	cgltf_size buffers_count;
+
+	cgltf_image* images;
+	cgltf_size images_count;
+
+	cgltf_texture* textures;
+	cgltf_size textures_count;
+
+	cgltf_sampler* samplers;
+	cgltf_size samplers_count;
+
+	cgltf_skin* skins;
+	cgltf_size skins_count;
+
+	cgltf_camera* cameras;
+	cgltf_size cameras_count;
+
+	cgltf_light* lights;
+	cgltf_size lights_count;
+
+	cgltf_node* nodes;
+	cgltf_size nodes_count;
+
+	cgltf_scene* scenes;
+	cgltf_size scenes_count;
+
+	cgltf_scene* scene;
+
+	cgltf_animation* animations;
+	cgltf_size animations_count;
+
+	cgltf_extras extras;
+
+	char** extensions_used;
+	cgltf_size extensions_used_count;
+
+	char** extensions_required;
+	cgltf_size extensions_required_count;
+
+	const char* json;
+	cgltf_size json_size;
+
+	const void* bin;
+	cgltf_size bin_size;
+
+	void (*memory_free) (void* user, void* ptr);
+	void* memory_user_data;
+} cgltf_data;
+
+cgltf_result cgltf_parse(
+		const cgltf_options* options,
+		const void* data,
+		cgltf_size size,
+		cgltf_data** out_data);
+
+cgltf_result cgltf_parse_file(
+		const cgltf_options* options,
+		const char* path,
+		cgltf_data** out_data);
+
+cgltf_result cgltf_load_buffers(
+		const cgltf_options* options,
+		cgltf_data* data,
+		const char* gltf_path);
+
+
+cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data);
+
+cgltf_result cgltf_validate(
+		cgltf_data* data);
+
+void cgltf_free(cgltf_data* data);
+
+void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix);
+void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix);
+
+cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size);
+cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index);
+
+cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* #ifndef CGLTF_H_INCLUDED__ */
+
+/*
+ *
+ * Stop now, if you are only interested in the API.
+ * Below, you find the implementation.
+ *
+ */
+
+#ifdef __INTELLISENSE__
+/* This makes MSVC intellisense work. */
+#define CGLTF_IMPLEMENTATION
+#endif
+
+#ifdef CGLTF_IMPLEMENTATION
+
+#include <stdint.h> /* For uint8_t, uint32_t */
+#include <string.h> /* For strncpy */
+#include <stdlib.h> /* For malloc, free */
+#include <stdio.h>  /* For fopen */
+#include <limits.h> /* For UINT_MAX etc */
+
+/* JSMN_PARENT_LINKS is necessary to make parsing large structures linear in input size */
+#define JSMN_PARENT_LINKS
+
+/* JSMN_STRICT is necessary to reject invalid JSON documents */
+#define JSMN_STRICT
+
+/*
+ * -- jsmn.h start --
+ * Source: https://github.com/zserge/jsmn
+ * License: MIT
+ */
+typedef enum {
+	JSMN_UNDEFINED = 0,
+	JSMN_OBJECT = 1,
+	JSMN_ARRAY = 2,
+	JSMN_STRING = 3,
+	JSMN_PRIMITIVE = 4
+} jsmntype_t;
+enum jsmnerr {
+	/* Not enough tokens were provided */
+	JSMN_ERROR_NOMEM = -1,
+	/* Invalid character inside JSON string */
+	JSMN_ERROR_INVAL = -2,
+	/* The string is not a full JSON packet, more bytes expected */
+	JSMN_ERROR_PART = -3
+};
+typedef struct {
+	jsmntype_t type;
+	int start;
+	int end;
+	int size;
+#ifdef JSMN_PARENT_LINKS
+	int parent;
+#endif
+} jsmntok_t;
+typedef struct {
+	unsigned int pos; /* offset in the JSON string */
+	unsigned int toknext; /* next token to allocate */
+	int toksuper; /* superior token node, e.g parent object or array */
+} jsmn_parser;
+static void jsmn_init(jsmn_parser *parser);
+static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len, jsmntok_t *tokens, size_t num_tokens);
+/*
+ * -- jsmn.h end --
+ */
+
+
+static const cgltf_size GlbHeaderSize = 12;
+static const cgltf_size GlbChunkHeaderSize = 8;
+static const uint32_t GlbVersion = 2;
+static const uint32_t GlbMagic = 0x46546C67;
+static const uint32_t GlbMagicJsonChunk = 0x4E4F534A;
+static const uint32_t GlbMagicBinChunk = 0x004E4942;
+
+static void* cgltf_default_alloc(void* user, cgltf_size size)
+{
+	(void)user;
+	return malloc(size);
+}
+
+static void cgltf_default_free(void* user, void* ptr)
+{
+	(void)user;
+	free(ptr);
+}
+
+static void* cgltf_calloc(cgltf_options* options, size_t element_size, cgltf_size count)
+{
+	if (SIZE_MAX / element_size < count)
+	{
+		return NULL;
+	}
+	void* result = options->memory_alloc(options->memory_user_data, element_size * count);
+	if (!result)
+	{
+		return NULL;
+	}
+	memset(result, 0, element_size * count);
+	return result;
+}
+
+static cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data);
+
+cgltf_result cgltf_parse(const cgltf_options* options, const void* data, cgltf_size size, cgltf_data** out_data)
+{
+	if (size < GlbHeaderSize)
+	{
+		return cgltf_result_data_too_short;
+	}
+
+	if (options == NULL)
+	{
+		return cgltf_result_invalid_options;
+	}
+
+	cgltf_options fixed_options = *options;
+	if (fixed_options.memory_alloc == NULL)
+	{
+		fixed_options.memory_alloc = &cgltf_default_alloc;
+	}
+	if (fixed_options.memory_free == NULL)
+	{
+		fixed_options.memory_free = &cgltf_default_free;
+	}
+
+	uint32_t tmp;
+	// Magic
+	memcpy(&tmp, data, 4);
+	if (tmp != GlbMagic)
+	{
+		if (fixed_options.type == cgltf_file_type_invalid)
+		{
+			fixed_options.type = cgltf_file_type_gltf;
+		}
+		else if (fixed_options.type == cgltf_file_type_glb)
+		{
+			return cgltf_result_unknown_format;
+		}
+	}
+
+	if (fixed_options.type == cgltf_file_type_gltf)
+	{
+		cgltf_result json_result = cgltf_parse_json(&fixed_options, (const uint8_t*)data, size, out_data);
+		if (json_result != cgltf_result_success)
+		{
+			return json_result;
+		}
+
+		(*out_data)->file_type = cgltf_file_type_gltf;
+
+		return cgltf_result_success;
+	}
+
+	const uint8_t* ptr = (const uint8_t*)data;
+	// Version
+	memcpy(&tmp, ptr + 4, 4);
+	uint32_t version = tmp;
+	if (version != GlbVersion)
+	{
+		return cgltf_result_unknown_format;
+	}
+
+	// Total length
+	memcpy(&tmp, ptr + 8, 4);
+	if (tmp > size)
+	{
+		return cgltf_result_data_too_short;
+	}
+
+	const uint8_t* json_chunk = ptr + GlbHeaderSize;
+
+	if (GlbHeaderSize + GlbChunkHeaderSize > size)
+	{
+		return cgltf_result_data_too_short;
+	}
+
+	// JSON chunk: length
+	uint32_t json_length;
+	memcpy(&json_length, json_chunk, 4);
+	if (GlbHeaderSize + GlbChunkHeaderSize + json_length > size)
+	{
+		return cgltf_result_data_too_short;
+	}
+
+	// JSON chunk: magic
+	memcpy(&tmp, json_chunk + 4, 4);
+	if (tmp != GlbMagicJsonChunk)
+	{
+		return cgltf_result_unknown_format;
+	}
+
+	json_chunk += GlbChunkHeaderSize;
+
+	const void* bin = 0;
+	cgltf_size bin_size = 0;
+
+	if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize <= size)
+	{
+		// We can read another chunk
+		const uint8_t* bin_chunk = json_chunk + json_length;
+
+		// Bin chunk: length
+		uint32_t bin_length;
+		memcpy(&bin_length, bin_chunk, 4);
+		if (GlbHeaderSize + GlbChunkHeaderSize + json_length + GlbChunkHeaderSize + bin_length > size)
+		{
+			return cgltf_result_data_too_short;
+		}
+
+		// Bin chunk: magic
+		memcpy(&tmp, bin_chunk + 4, 4);
+		if (tmp != GlbMagicBinChunk)
+		{
+			return cgltf_result_unknown_format;
+		}
+
+		bin_chunk += GlbChunkHeaderSize;
+
+		bin = bin_chunk;
+		bin_size = bin_length;
+	}
+
+	cgltf_result json_result = cgltf_parse_json(&fixed_options, json_chunk, json_length, out_data);
+	if (json_result != cgltf_result_success)
+	{
+		return json_result;
+	}
+
+	(*out_data)->file_type = cgltf_file_type_glb;
+	(*out_data)->bin = bin;
+	(*out_data)->bin_size = bin_size;
+
+	return cgltf_result_success;
+}
+
+cgltf_result cgltf_parse_file(const cgltf_options* options, const char* path, cgltf_data** out_data)
+{
+	if (options == NULL)
+	{
+		return cgltf_result_invalid_options;
+	}
+
+	void* (*memory_alloc)(void*, cgltf_size) = options->memory_alloc ? options->memory_alloc : &cgltf_default_alloc;
+	void (*memory_free)(void*, void*) = options->memory_free ? options->memory_free : &cgltf_default_free;
+
+	FILE* file = fopen(path, "rb");
+	if (!file)
+	{
+		return cgltf_result_file_not_found;
+	}
+
+	fseek(file, 0, SEEK_END);
+
+	long length = ftell(file);
+	if (length < 0)
+	{
+		fclose(file);
+		return cgltf_result_io_error;
+	}
+
+	fseek(file, 0, SEEK_SET);
+
+	char* file_data = (char*)memory_alloc(options->memory_user_data, length);
+	if (!file_data)
+	{
+		fclose(file);
+		return cgltf_result_out_of_memory;
+	}
+
+	cgltf_size file_size = (cgltf_size)length;
+	cgltf_size read_size = fread(file_data, 1, file_size, file);
+
+	fclose(file);
+
+	if (read_size != file_size)
+	{
+		memory_free(options->memory_user_data, file_data);
+		return cgltf_result_io_error;
+	}
+
+	cgltf_result result = cgltf_parse(options, file_data, file_size, out_data);
+
+	if (result != cgltf_result_success)
+	{
+		memory_free(options->memory_user_data, file_data);
+		return result;
+	}
+
+	(*out_data)->file_data = file_data;
+
+	return cgltf_result_success;
+}
+
+static void cgltf_combine_paths(char* path, const char* base, const char* uri)
+{
+	const char* s0 = strrchr(base, '/');
+	const char* s1 = strrchr(base, '\\');
+	const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1;
+
+	if (slash)
+	{
+		size_t prefix = slash - base + 1;
+
+		strncpy(path, base, prefix);
+		strcpy(path + prefix, uri);
+	}
+	else
+	{
+		strcpy(path, uri);
+	}
+}
+
+static cgltf_result cgltf_load_buffer_file(const cgltf_options* options, cgltf_size size, const char* uri, const char* gltf_path, void** out_data)
+{
+	void* (*memory_alloc)(void*, cgltf_size) = options->memory_alloc ? options->memory_alloc : &cgltf_default_alloc;
+	void (*memory_free)(void*, void*) = options->memory_free ? options->memory_free : &cgltf_default_free;
+
+	char* path = (char*)memory_alloc(options->memory_user_data, strlen(uri) + strlen(gltf_path) + 1);
+	if (!path)
+	{
+		return cgltf_result_out_of_memory;
+	}
+
+	cgltf_combine_paths(path, gltf_path, uri);
+
+	FILE* file = fopen(path, "rb");
+
+	memory_free(options->memory_user_data, path);
+
+	if (!file)
+	{
+		return cgltf_result_file_not_found;
+	}
+
+	char* file_data = (char*)memory_alloc(options->memory_user_data, size);
+	if (!file_data)
+	{
+		fclose(file);
+		return cgltf_result_out_of_memory;
+	}
+
+	cgltf_size read_size = fread(file_data, 1, size, file);
+
+	fclose(file);
+
+	if (read_size != size)
+	{
+		memory_free(options->memory_user_data, file_data);
+		return cgltf_result_io_error;
+	}
+
+	*out_data = file_data;
+
+	return cgltf_result_success;
+}
+
+cgltf_result cgltf_load_buffer_base64(const cgltf_options* options, cgltf_size size, const char* base64, void** out_data)
+{
+	void* (*memory_alloc)(void*, cgltf_size) = options->memory_alloc ? options->memory_alloc : &cgltf_default_alloc;
+	void (*memory_free)(void*, void*) = options->memory_free ? options->memory_free : &cgltf_default_free;
+
+	unsigned char* data = (unsigned char*)memory_alloc(options->memory_user_data, size);
+	if (!data)
+	{
+		return cgltf_result_out_of_memory;
+	}
+
+	unsigned int buffer = 0;
+	unsigned int buffer_bits = 0;
+
+	for (cgltf_size i = 0; i < size; ++i)
+	{
+		while (buffer_bits < 8)
+		{
+			char ch = *base64++;
+
+			int index =
+				(unsigned)(ch - 'A') < 26 ? (ch - 'A') :
+				(unsigned)(ch - 'a') < 26 ? (ch - 'a') + 26 :
+				(unsigned)(ch - '0') < 10 ? (ch - '0') + 52 :
+				ch == '+' ? 62 :
+				ch == '/' ? 63 :
+				-1;
+
+			if (index < 0)
+			{
+				memory_free(options->memory_user_data, data);
+				return cgltf_result_io_error;
+			}
+
+			buffer = (buffer << 6) | index;
+			buffer_bits += 6;
+		}
+
+		data[i] = (unsigned char)(buffer >> (buffer_bits - 8));
+		buffer_bits -= 8;
+	}
+
+	*out_data = data;
+
+	return cgltf_result_success;
+}
+
+cgltf_result cgltf_load_buffers(const cgltf_options* options, cgltf_data* data, const char* gltf_path)
+{
+	if (options == NULL)
+	{
+		return cgltf_result_invalid_options;
+	}
+
+	if (data->buffers_count && data->buffers[0].data == NULL && data->buffers[0].uri == NULL && data->bin)
+	{
+		if (data->bin_size < data->buffers[0].size)
+		{
+			return cgltf_result_data_too_short;
+		}
+
+		data->buffers[0].data = (void*)data->bin;
+	}
+
+	for (cgltf_size i = 0; i < data->buffers_count; ++i)
+	{
+		if (data->buffers[i].data)
+		{
+			continue;
+		}
+
+		const char* uri = data->buffers[i].uri;
+
+		if (uri == NULL)
+		{
+			continue;
+		}
+
+		if (strncmp(uri, "data:", 5) == 0)
+		{
+			const char* comma = strchr(uri, ',');
+
+			if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0)
+			{
+				cgltf_result res = cgltf_load_buffer_base64(options, data->buffers[i].size, comma + 1, &data->buffers[i].data);
+
+				if (res != cgltf_result_success)
+				{
+					return res;
+				}
+			}
+			else
+			{
+				return cgltf_result_unknown_format;
+			}
+		}
+		else if (strstr(uri, "://") == NULL && gltf_path)
+		{
+			cgltf_result res = cgltf_load_buffer_file(options, data->buffers[i].size, uri, gltf_path, &data->buffers[i].data);
+
+			if (res != cgltf_result_success)
+			{
+				return res;
+			}
+		}
+		else
+		{
+			return cgltf_result_unknown_format;
+		}
+	}
+
+	return cgltf_result_success;
+}
+
+static cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type);
+
+static cgltf_size cgltf_calc_index_bound(cgltf_buffer_view* buffer_view, cgltf_size offset, cgltf_component_type component_type, cgltf_size count)
+{
+	char* data = (char*)buffer_view->buffer->data + offset + buffer_view->offset;
+	cgltf_size bound = 0;
+
+	switch (component_type)
+	{
+	case cgltf_component_type_r_8u:
+		for (size_t i = 0; i < count; ++i)
+		{
+			cgltf_size v = ((unsigned char*)data)[i];
+			bound = bound > v ? bound : v;
+		}
+		break;
+
+	case cgltf_component_type_r_16u:
+		for (size_t i = 0; i < count; ++i)
+		{
+			cgltf_size v = ((unsigned short*)data)[i];
+			bound = bound > v ? bound : v;
+		}
+		break;
+
+	case cgltf_component_type_r_32u:
+		for (size_t i = 0; i < count; ++i)
+		{
+			cgltf_size v = ((unsigned int*)data)[i];
+			bound = bound > v ? bound : v;
+		}
+		break;
+
+	default:
+		;
+	}
+
+	return bound;
+}
+
+cgltf_result cgltf_validate(cgltf_data* data)
+{
+	for (cgltf_size i = 0; i < data->accessors_count; ++i)
+	{
+		cgltf_accessor* accessor = &data->accessors[i];
+
+		cgltf_size element_size = cgltf_calc_size(accessor->type, accessor->component_type);
+
+		if (accessor->buffer_view)
+		{
+			cgltf_size req_size = accessor->offset + accessor->stride * (accessor->count - 1) + element_size;
+
+			if (accessor->buffer_view->size < req_size)
+			{
+				return cgltf_result_data_too_short;
+			}
+		}
+
+		if (accessor->is_sparse)
+		{
+			cgltf_accessor_sparse* sparse = &accessor->sparse;
+
+			cgltf_size indices_component_size = cgltf_calc_size(cgltf_type_scalar, sparse->indices_component_type);
+			cgltf_size indices_req_size = sparse->indices_byte_offset + indices_component_size * sparse->count;
+			cgltf_size values_req_size = sparse->values_byte_offset + element_size * sparse->count;
+
+			if (sparse->indices_buffer_view->size < indices_req_size ||
+				sparse->values_buffer_view->size < values_req_size)
+			{
+				return cgltf_result_data_too_short;
+			}
+
+			if (sparse->indices_component_type != cgltf_component_type_r_8u &&
+				sparse->indices_component_type != cgltf_component_type_r_16u &&
+				sparse->indices_component_type != cgltf_component_type_r_32u)
+			{
+				return cgltf_result_invalid_gltf;
+			}
+
+			if (sparse->indices_buffer_view->buffer->data)
+			{
+				cgltf_size index_bound = cgltf_calc_index_bound(sparse->indices_buffer_view, sparse->indices_byte_offset, sparse->indices_component_type, sparse->count);
+
+				if (index_bound >= accessor->count)
+				{
+					return cgltf_result_data_too_short;
+				}
+			}
+		}
+	}
+
+	for (cgltf_size i = 0; i < data->buffer_views_count; ++i)
+	{
+		cgltf_size req_size = data->buffer_views[i].offset + data->buffer_views[i].size;
+
+		if (data->buffer_views[i].buffer && data->buffer_views[i].buffer->size < req_size)
+		{
+			return cgltf_result_data_too_short;
+		}
+	}
+
+	for (cgltf_size i = 0; i < data->meshes_count; ++i)
+	{
+		if (data->meshes[i].weights)
+		{
+			if (data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].weights_count)
+			{
+				return cgltf_result_invalid_gltf;
+			}
+		}
+
+		for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j)
+		{
+			if (data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count)
+			{
+				return cgltf_result_invalid_gltf;
+			}
+
+			if (data->meshes[i].primitives[j].attributes_count)
+			{
+				cgltf_accessor* first = data->meshes[i].primitives[j].attributes[0].data;
+
+				for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k)
+				{
+					if (data->meshes[i].primitives[j].attributes[k].data->count != first->count)
+					{
+						return cgltf_result_invalid_gltf;
+					}
+				}
+
+				for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k)
+				{
+					for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m)
+					{
+						if (data->meshes[i].primitives[j].targets[k].attributes[m].data->count != first->count)
+						{
+							return cgltf_result_invalid_gltf;
+						}
+					}
+				}
+
+				cgltf_accessor* indices = data->meshes[i].primitives[j].indices;
+
+				if (indices &&
+					indices->component_type != cgltf_component_type_r_8u &&
+					indices->component_type != cgltf_component_type_r_16u &&
+					indices->component_type != cgltf_component_type_r_32u)
+				{
+					return cgltf_result_invalid_gltf;
+				}
+
+				if (indices && indices->buffer_view && indices->buffer_view->buffer->data)
+				{
+					cgltf_size index_bound = cgltf_calc_index_bound(indices->buffer_view, indices->offset, indices->component_type, indices->count);
+
+					if (index_bound >= first->count)
+					{
+						return cgltf_result_data_too_short;
+					}
+				}
+			}
+		}
+	}
+
+	for (cgltf_size i = 0; i < data->nodes_count; ++i)
+	{
+		if (data->nodes[i].weights && data->nodes[i].mesh)
+		{
+			if (data->nodes[i].mesh->primitives_count && data->nodes[i].mesh->primitives[0].targets_count != data->nodes[i].weights_count)
+			{
+				return cgltf_result_invalid_gltf;
+			}
+		}
+	}
+
+	return cgltf_result_success;
+}
+
+cgltf_result cgltf_copy_extras_json(const cgltf_data* data, const cgltf_extras* extras, char* dest, cgltf_size* dest_size)
+{
+	cgltf_size json_size = extras->end_offset - extras->start_offset;
+
+	if (!dest)
+	{
+		if (dest_size)
+		{
+			*dest_size = json_size + 1;
+			return cgltf_result_success;
+		}
+		return cgltf_result_invalid_options;
+	}
+
+	if (*dest_size + 1 < json_size)
+	{
+		strncpy(dest, data->json + extras->start_offset, *dest_size - 1);
+		dest[*dest_size - 1] = 0;
+	}
+	else
+	{
+		strncpy(dest, data->json + extras->start_offset, json_size);
+		dest[json_size] = 0;
+	}
+
+	return cgltf_result_success;
+}
+
+void cgltf_free(cgltf_data* data)
+{
+	if (!data)
+	{
+		return;
+	}
+
+	data->memory_free(data->memory_user_data, data->asset.copyright);
+	data->memory_free(data->memory_user_data, data->asset.generator);
+	data->memory_free(data->memory_user_data, data->asset.version);
+	data->memory_free(data->memory_user_data, data->asset.min_version);
+
+	data->memory_free(data->memory_user_data, data->accessors);
+	data->memory_free(data->memory_user_data, data->buffer_views);
+
+	for (cgltf_size i = 0; i < data->buffers_count; ++i)
+	{
+		if (data->buffers[i].data != data->bin)
+		{
+			data->memory_free(data->memory_user_data, data->buffers[i].data);
+		}
+
+		data->memory_free(data->memory_user_data, data->buffers[i].uri);
+	}
+
+	data->memory_free(data->memory_user_data, data->buffers);
+
+	for (cgltf_size i = 0; i < data->meshes_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->meshes[i].name);
+
+		for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j)
+		{
+			for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k)
+			{
+				data->memory_free(data->memory_user_data, data->meshes[i].primitives[j].attributes[k].name);
+			}
+
+			data->memory_free(data->memory_user_data, data->meshes[i].primitives[j].attributes);
+
+			for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k)
+			{
+				for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m)
+				{
+					data->memory_free(data->memory_user_data, data->meshes[i].primitives[j].targets[k].attributes[m].name);
+				}
+
+				data->memory_free(data->memory_user_data, data->meshes[i].primitives[j].targets[k].attributes);
+			}
+
+			data->memory_free(data->memory_user_data, data->meshes[i].primitives[j].targets);
+		}
+
+		data->memory_free(data->memory_user_data, data->meshes[i].primitives);
+		data->memory_free(data->memory_user_data, data->meshes[i].weights);
+	}
+
+	data->memory_free(data->memory_user_data, data->meshes);
+
+	for (cgltf_size i = 0; i < data->materials_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->materials[i].name);
+	}
+
+	data->memory_free(data->memory_user_data, data->materials);
+
+	for (cgltf_size i = 0; i < data->images_count; ++i) 
+	{
+		data->memory_free(data->memory_user_data, data->images[i].name);
+		data->memory_free(data->memory_user_data, data->images[i].uri);
+		data->memory_free(data->memory_user_data, data->images[i].mime_type);
+	}
+
+	data->memory_free(data->memory_user_data, data->images);
+
+	for (cgltf_size i = 0; i < data->textures_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->textures[i].name);
+	}
+
+	data->memory_free(data->memory_user_data, data->textures);
+
+	data->memory_free(data->memory_user_data, data->samplers);
+
+	for (cgltf_size i = 0; i < data->skins_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->skins[i].name);
+		data->memory_free(data->memory_user_data, data->skins[i].joints);
+	}
+
+	data->memory_free(data->memory_user_data, data->skins);
+
+	for (cgltf_size i = 0; i < data->cameras_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->cameras[i].name);
+	}
+
+	data->memory_free(data->memory_user_data, data->cameras);
+
+	for (cgltf_size i = 0; i < data->lights_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->lights[i].name);
+	}
+
+	data->memory_free(data->memory_user_data, data->lights);
+
+	for (cgltf_size i = 0; i < data->nodes_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->nodes[i].name);
+		data->memory_free(data->memory_user_data, data->nodes[i].children);
+		data->memory_free(data->memory_user_data, data->nodes[i].weights);
+	}
+
+	data->memory_free(data->memory_user_data, data->nodes);
+
+	for (cgltf_size i = 0; i < data->scenes_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->scenes[i].name);
+		data->memory_free(data->memory_user_data, data->scenes[i].nodes);
+	}
+
+	data->memory_free(data->memory_user_data, data->scenes);
+
+	for (cgltf_size i = 0; i < data->animations_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->animations[i].name);
+		data->memory_free(data->memory_user_data, data->animations[i].samplers);
+		data->memory_free(data->memory_user_data, data->animations[i].channels);
+	}
+
+	data->memory_free(data->memory_user_data, data->animations);
+
+	for (cgltf_size i = 0; i < data->extensions_used_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->extensions_used[i]);
+	}
+
+	data->memory_free(data->memory_user_data, data->extensions_used);
+
+	for (cgltf_size i = 0; i < data->extensions_required_count; ++i)
+	{
+		data->memory_free(data->memory_user_data, data->extensions_required[i]);
+	}
+
+	data->memory_free(data->memory_user_data, data->extensions_required);
+
+	data->memory_free(data->memory_user_data, data->file_data);
+
+	data->memory_free(data->memory_user_data, data);
+}
+
+void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix)
+{
+	cgltf_float* lm = out_matrix;
+
+	if (node->has_matrix)
+	{
+		memcpy(lm, node->matrix, sizeof(float) * 16);
+	}
+	else
+	{
+		float tx = node->translation[0];
+		float ty = node->translation[1];
+		float tz = node->translation[2];
+
+		float qx = node->rotation[0];
+		float qy = node->rotation[1];
+		float qz = node->rotation[2];
+		float qw = node->rotation[3];
+
+		float sx = node->scale[0];
+		float sy = node->scale[1];
+		float sz = node->scale[2];
+
+		lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx;
+		lm[1] = (2 * qx*qy + 2 * qz*qw) * sy;
+		lm[2] = (2 * qx*qz - 2 * qy*qw) * sz;
+		lm[3] = 0.f;
+
+		lm[4] = (2 * qx*qy - 2 * qz*qw) * sx;
+		lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy;
+		lm[6] = (2 * qy*qz + 2 * qx*qw) * sz;
+		lm[7] = 0.f;
+
+		lm[8] = (2 * qx*qz + 2 * qy*qw) * sx;
+		lm[9] = (2 * qy*qz - 2 * qx*qw) * sy;
+		lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz;
+		lm[11] = 0.f;
+
+		lm[12] = tx;
+		lm[13] = ty;
+		lm[14] = tz;
+		lm[15] = 1.f;
+	}
+}
+
+void cgltf_node_transform_world(const cgltf_node* node, cgltf_float* out_matrix)
+{
+	cgltf_float* lm = out_matrix;
+	cgltf_node_transform_local(node, lm);
+
+	const cgltf_node* parent = node->parent;
+
+	while (parent)
+	{
+		float pm[16];
+		cgltf_node_transform_local(parent, pm);
+
+		for (int i = 0; i < 4; ++i)
+		{
+			float l0 = lm[i * 4 + 0];
+			float l1 = lm[i * 4 + 1];
+			float l2 = lm[i * 4 + 2];
+
+			float r0 = l0 * pm[0] + l1 * pm[4] + l2 * pm[8];
+			float r1 = l0 * pm[1] + l1 * pm[5] + l2 * pm[9];
+			float r2 = l0 * pm[2] + l1 * pm[6] + l2 * pm[10];
+
+			lm[i * 4 + 0] = r0;
+			lm[i * 4 + 1] = r1;
+			lm[i * 4 + 2] = r2;
+		}
+
+		lm[12] += pm[12];
+		lm[13] += pm[13];
+		lm[14] += pm[14];
+
+		parent = parent->parent;
+	}
+}
+
+static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_type component_type)
+{
+	switch (component_type)
+	{
+		case cgltf_component_type_r_16:
+			return *((const int16_t*) in);
+		case cgltf_component_type_r_16u:
+			return *((const uint16_t*) in);
+		case cgltf_component_type_r_32u:
+			return *((const uint32_t*) in);
+		case cgltf_component_type_r_32f:
+			return (cgltf_size)*((const float*) in);
+		case cgltf_component_type_r_8:
+			return *((const int8_t*) in);
+		case cgltf_component_type_r_8u:
+		case cgltf_component_type_invalid:
+		default:
+			return *((const uint8_t*) in);
+	}
+}
+
+static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_type component_type, cgltf_bool normalized)
+{
+	if (component_type == cgltf_component_type_r_32f)
+	{
+		return *((const float*) in);
+	}
+
+	if (normalized)
+	{
+		switch (component_type)
+		{
+			case cgltf_component_type_r_32u:
+				return *((const uint32_t*) in) / (float) UINT_MAX;
+			case cgltf_component_type_r_16:
+				return *((const int16_t*) in) / (float) SHRT_MAX;
+			case cgltf_component_type_r_16u:
+				return *((const uint16_t*) in) / (float) USHRT_MAX;
+			case cgltf_component_type_r_8:
+				return *((const int8_t*) in) / (float) SCHAR_MAX;
+			case cgltf_component_type_r_8u:
+			case cgltf_component_type_invalid:
+			default:
+				return *((const uint8_t*) in) / (float) CHAR_MAX;
+		}
+	}
+
+	return (cgltf_float)cgltf_component_read_index(in, component_type);
+}
+
+static cgltf_size cgltf_num_components(cgltf_type type);
+static cgltf_size cgltf_component_size(cgltf_component_type component_type);
+
+static cgltf_bool cgltf_element_read_float(const uint8_t* element, cgltf_type type, cgltf_component_type component_type, cgltf_bool normalized, cgltf_float* out, cgltf_size element_size)
+{
+	cgltf_size num_components = cgltf_num_components(type);
+
+	if (element_size < num_components) {
+		return 0;
+	}
+
+	// There are three special cases for component extraction, see #data-alignment in the 2.0 spec.
+
+	cgltf_size component_size = cgltf_component_size(component_type);
+
+	if (type == cgltf_type_mat2 && component_size == 1)
+	{
+		out[0] = cgltf_component_read_float(element, component_type, normalized);
+		out[1] = cgltf_component_read_float(element + 1, component_type, normalized);
+		out[2] = cgltf_component_read_float(element + 4, component_type, normalized);
+		out[3] = cgltf_component_read_float(element + 5, component_type, normalized);
+		return 1;
+	}
+
+	if (type == cgltf_type_mat3 && component_size == 1)
+	{
+		out[0] = cgltf_component_read_float(element, component_type, normalized);
+		out[1] = cgltf_component_read_float(element + 1, component_type, normalized);
+		out[2] = cgltf_component_read_float(element + 2, component_type, normalized);
+		out[3] = cgltf_component_read_float(element + 4, component_type, normalized);
+		out[4] = cgltf_component_read_float(element + 5, component_type, normalized);
+		out[5] = cgltf_component_read_float(element + 6, component_type, normalized);
+		out[6] = cgltf_component_read_float(element + 8, component_type, normalized);
+		out[7] = cgltf_component_read_float(element + 9, component_type, normalized);
+		out[8] = cgltf_component_read_float(element + 10, component_type, normalized);
+		return 1;
+	}
+
+	if (type == cgltf_type_mat3 && component_size == 2)
+	{
+		out[0] = cgltf_component_read_float(element, component_type, normalized);
+		out[1] = cgltf_component_read_float(element + 2, component_type, normalized);
+		out[2] = cgltf_component_read_float(element + 4, component_type, normalized);
+		out[3] = cgltf_component_read_float(element + 8, component_type, normalized);
+		out[4] = cgltf_component_read_float(element + 10, component_type, normalized);
+		out[5] = cgltf_component_read_float(element + 12, component_type, normalized);
+		out[6] = cgltf_component_read_float(element + 16, component_type, normalized);
+		out[7] = cgltf_component_read_float(element + 18, component_type, normalized);
+		out[8] = cgltf_component_read_float(element + 20, component_type, normalized);
+		return 1;
+	}
+
+	for (cgltf_size i = 0; i < num_components; ++i)
+	{
+		out[i] = cgltf_component_read_float(element + component_size * i, component_type, normalized);
+	}
+	return 1;
+}
+
+
+cgltf_bool cgltf_accessor_read_float(const cgltf_accessor* accessor, cgltf_size index, cgltf_float* out, cgltf_size element_size)
+{
+	if (accessor->is_sparse || accessor->buffer_view == NULL)
+	{
+		return 0;
+	}
+
+	cgltf_size offset = accessor->offset + accessor->buffer_view->offset;
+	const uint8_t* element = (const uint8_t*) accessor->buffer_view->buffer->data;
+	element += offset + accessor->stride * index;
+	return cgltf_element_read_float(element, accessor->type, accessor->component_type, accessor->normalized, out, element_size);
+}
+
+cgltf_size cgltf_accessor_read_index(const cgltf_accessor* accessor, cgltf_size index)
+{
+	if (accessor->buffer_view)
+	{
+		cgltf_size offset = accessor->offset + accessor->buffer_view->offset;
+		const uint8_t* element = (const uint8_t*) accessor->buffer_view->buffer->data;
+		element += offset + accessor->stride * index;
+		return cgltf_component_read_index(element, accessor->component_type);
+	}
+
+	return 0;
+}
+
+#define CGLTF_ERROR_JSON -1
+#define CGLTF_ERROR_NOMEM -2
+
+#define CGLTF_CHECK_TOKTYPE(tok_, type_) if ((tok_).type != (type_)) { return CGLTF_ERROR_JSON; }
+#define CGLTF_CHECK_KEY(tok_) if ((tok_).type != JSMN_STRING || (tok_).size == 0) { return CGLTF_ERROR_JSON; } /* checking size for 0 verifies that a value follows the key */
+
+#define CGLTF_PTRINDEX(type, idx) (type*)(cgltf_size)(idx + 1)
+#define CGLTF_PTRFIXUP(var, data, size) if (var) { if ((cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1]; }
+#define CGLTF_PTRFIXUP_REQ(var, data, size) if (!var || (cgltf_size)var > size) { return CGLTF_ERROR_JSON; } var = &data[(cgltf_size)var-1];
+
+static int cgltf_json_strcmp(jsmntok_t const* tok, const uint8_t* json_chunk, const char* str)
+{
+	CGLTF_CHECK_TOKTYPE(*tok, JSMN_STRING);
+	size_t const str_len = strlen(str);
+	size_t const name_length = tok->end - tok->start;
+	return (str_len == name_length) ? strncmp((const char*)json_chunk + tok->start, str, str_len) : 128;
+}
+
+static int cgltf_json_to_int(jsmntok_t const* tok, const uint8_t* json_chunk)
+{
+	CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE);
+	char tmp[128];
+	int size = (cgltf_size)(tok->end - tok->start) < sizeof(tmp) ? tok->end - tok->start : sizeof(tmp) - 1;
+	strncpy(tmp, (const char*)json_chunk + tok->start, size);
+	tmp[size] = 0;
+	return atoi(tmp);
+}
+
+static cgltf_float cgltf_json_to_float(jsmntok_t const* tok, const uint8_t* json_chunk)
+{
+	CGLTF_CHECK_TOKTYPE(*tok, JSMN_PRIMITIVE);
+	char tmp[128];
+	int size = (cgltf_size)(tok->end - tok->start) < sizeof(tmp) ? tok->end - tok->start : sizeof(tmp) - 1;
+	strncpy(tmp, (const char*)json_chunk + tok->start, size);
+	tmp[size] = 0;
+	return (cgltf_float)atof(tmp);
+}
+
+static cgltf_bool cgltf_json_to_bool(jsmntok_t const* tok, const uint8_t* json_chunk)
+{
+	int size = tok->end - tok->start;
+	return size == 4 && memcmp(json_chunk + tok->start, "true", 4) == 0;
+}
+
+static int cgltf_skip_json(jsmntok_t const* tokens, int i)
+{
+	int end = i + 1;
+
+	while (i < end)
+	{
+		switch (tokens[i].type)
+		{
+		case JSMN_OBJECT:
+			end += tokens[i].size * 2;
+			break;
+
+		case JSMN_ARRAY:
+			end += tokens[i].size;
+			break;
+
+		case JSMN_PRIMITIVE:
+		case JSMN_STRING:
+			break;
+
+		default:
+			return -1;
+		}
+
+		i++;
+	}
+
+	return i;
+}
+
+static void cgltf_fill_float_array(float* out_array, int size, float value)
+{
+	for (int j = 0; j < size; ++j)
+	{
+		out_array[j] = value;
+	}
+}
+
+static int cgltf_parse_json_float_array(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, float* out_array, int size)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY);
+	if (tokens[i].size != size)
+	{
+		return CGLTF_ERROR_JSON;
+	}
+	++i;
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+		out_array[j] = cgltf_json_to_float(tokens + i, json_chunk);
+		++i;
+	}
+	return i;
+}
+
+static int cgltf_parse_json_string(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char** out_string)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_STRING);
+	if (*out_string)
+	{
+		return CGLTF_ERROR_JSON;
+	}
+	int size = tokens[i].end - tokens[i].start;
+	char* result = (char*)options->memory_alloc(options->memory_user_data, size + 1);
+	if (!result)
+	{
+		return CGLTF_ERROR_NOMEM;
+	}
+	strncpy(result, (const char*)json_chunk + tokens[i].start, size);
+	result[size] = 0;
+	*out_string = result;
+	return i + 1;
+}
+
+static int cgltf_parse_json_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, size_t element_size, void** out_array, cgltf_size* out_size)
+{
+	(void)json_chunk;
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY);
+	if (*out_array)
+	{
+		return CGLTF_ERROR_JSON;
+	}
+	int size = tokens[i].size;
+	void* result = cgltf_calloc(options, element_size, size);
+	if (!result)
+	{
+		return CGLTF_ERROR_NOMEM;
+	}
+	*out_array = result;
+	*out_size = size;
+	return i + 1;
+}
+
+static int cgltf_parse_json_string_array(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, char*** out_array, cgltf_size* out_size)
+{
+    CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_ARRAY);
+    i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(char*), (void**)out_array, out_size);
+    if (i < 0)
+    {
+        return i;
+    }
+
+    for (cgltf_size j = 0; j < *out_size; ++j)
+    {
+        i = cgltf_parse_json_string(options, tokens, i, json_chunk, j + (*out_array));
+        if (i < 0)
+        {
+            return i;
+        }
+    }
+    return i;
+}
+
+static void cgltf_parse_attribute_type(const char* name, cgltf_attribute_type* out_type, int* out_index)
+{
+	const char* us = strchr(name, '_');
+	size_t len = us ? us - name : strlen(name);
+
+	if (len == 8 && strncmp(name, "POSITION", 8) == 0)
+	{
+		*out_type = cgltf_attribute_type_position;
+	}
+	else if (len == 6 && strncmp(name, "NORMAL", 6) == 0)
+	{
+		*out_type = cgltf_attribute_type_normal;
+	}
+	else if (len == 7 && strncmp(name, "TANGENT", 7) == 0)
+	{
+		*out_type = cgltf_attribute_type_tangent;
+	}
+	else if (len == 8 && strncmp(name, "TEXCOORD", 8) == 0)
+	{
+		*out_type = cgltf_attribute_type_texcoord;
+	}
+	else if (len == 5 && strncmp(name, "COLOR", 5) == 0)
+	{
+		*out_type = cgltf_attribute_type_color;
+	}
+	else if (len == 6 && strncmp(name, "JOINTS", 6) == 0)
+	{
+		*out_type = cgltf_attribute_type_joints;
+	}
+	else if (len == 7 && strncmp(name, "WEIGHTS", 7) == 0)
+	{
+		*out_type = cgltf_attribute_type_weights;
+	}
+	else
+	{
+		*out_type = cgltf_attribute_type_invalid;
+	}
+
+	if (us && *out_type != cgltf_attribute_type_invalid)
+	{
+		*out_index = atoi(us + 1);
+	}
+}
+
+static int cgltf_parse_json_attribute_list(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_attribute** out_attributes, cgltf_size* out_attributes_count)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	if (*out_attributes)
+	{
+		return CGLTF_ERROR_JSON;
+	}
+
+	*out_attributes_count = tokens[i].size;
+	*out_attributes = (cgltf_attribute*)cgltf_calloc(options, sizeof(cgltf_attribute), *out_attributes_count);
+	++i;
+
+	if (!*out_attributes)
+	{
+		return CGLTF_ERROR_NOMEM;
+	}
+
+	for (cgltf_size j = 0; j < *out_attributes_count; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		i = cgltf_parse_json_string(options, tokens, i, json_chunk, &(*out_attributes)[j].name);
+		if (i < 0)
+		{
+			return CGLTF_ERROR_JSON;
+		}
+
+		cgltf_parse_attribute_type((*out_attributes)[j].name, &(*out_attributes)[j].type, &(*out_attributes)[j].index);
+
+		(*out_attributes)[j].data = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk));
+		++i;
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_extras(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_extras* out_extras)
+{
+	(void)json_chunk;
+	out_extras->start_offset = tokens[i].start;
+	out_extras->end_offset = tokens[i].end;
+	i = cgltf_skip_json(tokens, i);
+	return i;
+}
+
+static int cgltf_parse_json_primitive(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_primitive* out_prim)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	out_prim->type = cgltf_primitive_type_triangles;
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "mode") == 0)
+		{
+			++i;
+			out_prim->type
+					= (cgltf_primitive_type)
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0)
+		{
+			++i;
+			out_prim->indices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "material") == 0)
+		{
+			++i;
+			out_prim->material = CGLTF_PTRINDEX(cgltf_material, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "attributes") == 0)
+		{
+			i = cgltf_parse_json_attribute_list(options, tokens, i + 1, json_chunk, &out_prim->attributes, &out_prim->attributes_count);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "targets") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_morph_target), (void**)&out_prim->targets, &out_prim->targets_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_prim->targets_count; ++k)
+			{
+				i = cgltf_parse_json_attribute_list(options, tokens, i, json_chunk, &out_prim->targets[k].attributes, &out_prim->targets[k].attributes_count);
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_prim->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_mesh* out_mesh)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_mesh->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "primitives") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_primitive), (void**)&out_mesh->primitives, &out_mesh->primitives_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size prim_index = 0; prim_index < out_mesh->primitives_count; ++prim_index)
+			{
+				i = cgltf_parse_json_primitive(options, tokens, i, json_chunk, &out_mesh->primitives[prim_index]);
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_mesh->weights, &out_mesh->weights_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_mesh->weights, (int)out_mesh->weights_count);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_mesh->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_meshes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_mesh), (void**)&out_data->meshes, &out_data->meshes_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->meshes_count; ++j)
+	{
+		i = cgltf_parse_json_mesh(options, tokens, i, json_chunk, &out_data->meshes[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static cgltf_component_type cgltf_json_to_component_type(jsmntok_t const* tok, const uint8_t* json_chunk)
+{
+	int type = cgltf_json_to_int(tok, json_chunk);
+
+	switch (type)
+	{
+	case 5120:
+		return cgltf_component_type_r_8;
+	case 5121:
+		return cgltf_component_type_r_8u;
+	case 5122:
+		return cgltf_component_type_r_16;
+	case 5123:
+		return cgltf_component_type_r_16u;
+	case 5125:
+		return cgltf_component_type_r_32u;
+	case 5126:
+		return cgltf_component_type_r_32f;
+	default:
+		return cgltf_component_type_invalid;
+	}
+}
+
+static int cgltf_parse_json_accessor_sparse(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor_sparse* out_sparse)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0)
+		{
+			++i;
+			out_sparse->count = cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "indices") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int indices_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < indices_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0)
+				{
+					++i;
+					out_sparse->indices_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk));
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0)
+				{
+					++i;
+					out_sparse->indices_byte_offset = cgltf_json_to_int(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0)
+				{
+					++i;
+					out_sparse->indices_component_type = cgltf_json_to_component_type(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+				{
+					i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->indices_extras);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "values") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int values_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < values_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0)
+				{
+					++i;
+					out_sparse->values_buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk));
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0)
+				{
+					++i;
+					out_sparse->values_byte_offset = cgltf_json_to_int(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+				{
+					i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->values_extras);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sparse->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_accessor(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_accessor* out_accessor)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0)
+		{
+			++i;
+			out_accessor->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0)
+		{
+			++i;
+			out_accessor->offset =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "componentType") == 0)
+		{
+			++i;
+			out_accessor->component_type = cgltf_json_to_component_type(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "normalized") == 0)
+		{
+			++i;
+			out_accessor->normalized = cgltf_json_to_bool(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "count") == 0)
+		{
+			++i;
+			out_accessor->count =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0)
+		{
+			++i;
+			if (cgltf_json_strcmp(tokens+i, json_chunk, "SCALAR") == 0)
+			{
+				out_accessor->type = cgltf_type_scalar;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC2") == 0)
+			{
+				out_accessor->type = cgltf_type_vec2;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC3") == 0)
+			{
+				out_accessor->type = cgltf_type_vec3;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "VEC4") == 0)
+			{
+				out_accessor->type = cgltf_type_vec4;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT2") == 0)
+			{
+				out_accessor->type = cgltf_type_mat2;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT3") == 0)
+			{
+				out_accessor->type = cgltf_type_mat3;
+			}
+			else if (cgltf_json_strcmp(tokens+i, json_chunk, "MAT4") == 0)
+			{
+				out_accessor->type = cgltf_type_mat4;
+			}
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "min") == 0)
+		{
+			++i;
+			out_accessor->has_min = 1;
+			// note: we can't parse the precise number of elements since type may not have been computed yet
+			int min_size = tokens[i].size > 16 ? 16 : tokens[i].size;
+			i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->min, min_size);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "max") == 0)
+		{
+			++i;
+			out_accessor->has_max = 1;
+			// note: we can't parse the precise number of elements since type may not have been computed yet
+			int max_size = tokens[i].size > 16 ? 16 : tokens[i].size;
+			i = cgltf_parse_json_float_array(tokens, i, json_chunk, out_accessor->max, max_size);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "sparse") == 0)
+		{
+			out_accessor->is_sparse = 1;
+			i = cgltf_parse_json_accessor_sparse(tokens, i + 1, json_chunk, &out_accessor->sparse);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_accessor->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_texture_transform(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_transform* out_texture_transform)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens + i, json_chunk, "offset") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->offset, 2);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "rotation") == 0)
+		{
+			++i;
+			out_texture_transform->rotation = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_texture_transform->scale, 2);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0)
+		{
+			++i;
+			out_texture_transform->texcoord = cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_texture_view(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture_view* out_texture_view)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	out_texture_view->scale = 1.0f;
+	cgltf_fill_float_array(out_texture_view->transform.scale, 2, 1.0f);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens + i, json_chunk, "index") == 0)
+		{
+			++i;
+			out_texture_view->texture = CGLTF_PTRINDEX(cgltf_texture, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "texCoord") == 0)
+		{
+			++i;
+			out_texture_view->texcoord = cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "scale") == 0) 
+		{
+			++i;
+			out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "strength") == 0)
+		{
+			++i;
+			out_texture_view->scale = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_texture_view->extras);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int extensions_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < extensions_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_texture_transform") == 0)
+				{
+					out_texture_view->has_transform = 1;
+					i = cgltf_parse_json_texture_transform(tokens, i + 1, json_chunk, &out_texture_view->transform);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_pbr_metallic_roughness(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_metallic_roughness* out_pbr)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "metallicFactor") == 0)
+		{
+			++i;
+			out_pbr->metallic_factor = 
+				cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "roughnessFactor") == 0) 
+		{
+			++i;
+			out_pbr->roughness_factor =
+				cgltf_json_to_float(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorFactor") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->base_color_factor, 4);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "baseColorTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk,
+				&out_pbr->base_color_texture);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "metallicRoughnessTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk,
+				&out_pbr->metallic_roughness_texture);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_pbr->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_pbr_specular_glossiness(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_pbr_specular_glossiness* out_pbr)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseFactor") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->diffuse_factor, 4);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularFactor") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_pbr->specular_factor, 3);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "glossinessFactor") == 0)
+		{
+			++i;
+			out_pbr->glossiness_factor = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "diffuseTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk, &out_pbr->diffuse_texture);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "specularGlossinessTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk, &out_pbr->specular_glossiness_texture);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_image(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_image* out_image)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j) 
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens + i, json_chunk, "uri") == 0) 
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->uri);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "bufferView") == 0)
+		{
+			++i;
+			out_image->buffer_view = CGLTF_PTRINDEX(cgltf_buffer_view, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "mimeType") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->mime_type);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_image->name);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_image->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_sampler* out_sampler)
+{
+	(void)options;
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	out_sampler->wrap_s = 10497;
+	out_sampler->wrap_t = 10497;
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens + i, json_chunk, "magFilter") == 0) 
+		{
+			++i;
+			out_sampler->mag_filter
+				= cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "minFilter") == 0)
+		{
+			++i;
+			out_sampler->min_filter
+				= cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapS") == 0)
+		{
+			++i;
+			out_sampler->wrap_s
+				= cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "wrapT") == 0) 
+		{
+			++i;
+			out_sampler->wrap_t
+				= cgltf_json_to_int(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sampler->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+
+static int cgltf_parse_json_texture(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_texture* out_texture)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_texture->name);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "sampler") == 0)
+		{
+			++i;
+			out_texture->sampler = CGLTF_PTRINDEX(cgltf_sampler, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "source") == 0) 
+		{
+			++i;
+			out_texture->image = CGLTF_PTRINDEX(cgltf_image, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_texture->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_material(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_material* out_material)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	cgltf_fill_float_array(out_material->pbr_metallic_roughness.base_color_factor, 4, 1.0f);
+	out_material->pbr_metallic_roughness.metallic_factor = 1.0f;
+	out_material->pbr_metallic_roughness.roughness_factor = 1.0f;
+
+	cgltf_fill_float_array(out_material->pbr_specular_glossiness.diffuse_factor, 4, 1.0f);
+	cgltf_fill_float_array(out_material->pbr_specular_glossiness.specular_factor, 3, 1.0f);
+	out_material->pbr_specular_glossiness.glossiness_factor = 1.0f;
+
+	out_material->alpha_cutoff = 0.5f;
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_material->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "pbrMetallicRoughness") == 0)
+		{
+			out_material->has_pbr_metallic_roughness = 1;
+			i = cgltf_parse_json_pbr_metallic_roughness(tokens, i + 1, json_chunk, &out_material->pbr_metallic_roughness);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "emissiveFactor") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_material->emissive_factor, 3);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "normalTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk,
+				&out_material->normal_texture);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "occlusionTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk,
+				&out_material->occlusion_texture);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "emissiveTexture") == 0)
+		{
+			i = cgltf_parse_json_texture_view(tokens, i + 1, json_chunk,
+				&out_material->emissive_texture);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaMode") == 0)
+		{
+			++i;
+			if (cgltf_json_strcmp(tokens + i, json_chunk, "OPAQUE") == 0)
+			{
+				out_material->alpha_mode = cgltf_alpha_mode_opaque;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "MASK") == 0)
+			{
+				out_material->alpha_mode = cgltf_alpha_mode_mask;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "BLEND") == 0)
+			{
+				out_material->alpha_mode = cgltf_alpha_mode_blend;
+			}
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "alphaCutoff") == 0)
+		{
+			++i;
+			out_material->alpha_cutoff = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "doubleSided") == 0)
+		{
+			++i;
+			out_material->double_sided =
+				cgltf_json_to_bool(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_material->extras);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int extensions_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < extensions_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_pbrSpecularGlossiness") == 0)
+				{
+					out_material->has_pbr_specular_glossiness = 1;
+					i = cgltf_parse_json_pbr_specular_glossiness(tokens, i + 1, json_chunk, &out_material->pbr_specular_glossiness);
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_materials_unlit") == 0)
+				{
+					out_material->unlit = 1;
+					i = cgltf_skip_json(tokens, i+1);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_accessors(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_accessor), (void**)&out_data->accessors, &out_data->accessors_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->accessors_count; ++j)
+	{
+		i = cgltf_parse_json_accessor(tokens, i, json_chunk, &out_data->accessors[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_materials(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_material), (void**)&out_data->materials, &out_data->materials_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->materials_count; ++j)
+	{
+		i = cgltf_parse_json_material(options, tokens, i, json_chunk, &out_data->materials[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_images(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_image), (void**)&out_data->images, &out_data->images_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->images_count; ++j)
+	{
+		i = cgltf_parse_json_image(options, tokens, i, json_chunk, &out_data->images[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_textures(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_texture), (void**)&out_data->textures, &out_data->textures_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->textures_count; ++j)
+	{
+		i = cgltf_parse_json_texture(options, tokens, i, json_chunk, &out_data->textures[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_samplers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_sampler), (void**)&out_data->samplers, &out_data->samplers_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->samplers_count; ++j)
+	{
+		i = cgltf_parse_json_sampler(options, tokens, i, json_chunk, &out_data->samplers[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_buffer_view(jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer_view* out_buffer_view)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "buffer") == 0)
+		{
+			++i;
+			out_buffer_view->buffer = CGLTF_PTRINDEX(cgltf_buffer, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteOffset") == 0)
+		{
+			++i;
+			out_buffer_view->offset =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0)
+		{
+			++i;
+			out_buffer_view->size =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "byteStride") == 0)
+		{
+			++i;
+			out_buffer_view->stride =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0)
+		{
+			++i;
+			int type = cgltf_json_to_int(tokens+i, json_chunk);
+			switch (type)
+			{
+			case 34962:
+				type = cgltf_buffer_view_type_vertices;
+				break;
+			case 34963:
+				type = cgltf_buffer_view_type_indices;
+				break;
+			default:
+				type = cgltf_buffer_view_type_invalid;
+				break;
+			}
+			out_buffer_view->type = (cgltf_buffer_view_type)type;
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_buffer_view->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_buffer_views(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer_view), (void**)&out_data->buffer_views, &out_data->buffer_views_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->buffer_views_count; ++j)
+	{
+		i = cgltf_parse_json_buffer_view(tokens, i, json_chunk, &out_data->buffer_views[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_buffer(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_buffer* out_buffer)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "byteLength") == 0)
+		{
+			++i;
+			out_buffer->size =
+					cgltf_json_to_int(tokens+i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "uri") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_buffer->uri);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_buffer->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_buffers(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_buffer), (void**)&out_data->buffers, &out_data->buffers_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->buffers_count; ++j)
+	{
+		i = cgltf_parse_json_buffer(options, tokens, i, json_chunk, &out_data->buffers[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_skin(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_skin* out_skin)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_skin->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "joints") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_skin->joints, &out_skin->joints_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_skin->joints_count; ++k)
+			{
+				out_skin->joints[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk));
+				++i;
+			}
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "skeleton") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+			out_skin->skeleton = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "inverseBindMatrices") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+			out_skin->inverse_bind_matrices = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_skin->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_skins(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_skin), (void**)&out_data->skins, &out_data->skins_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->skins_count; ++j)
+	{
+		i = cgltf_parse_json_skin(options, tokens, i, json_chunk, &out_data->skins[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_camera(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_camera* out_camera)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_camera->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0)
+		{
+			++i;
+			if (cgltf_json_strcmp(tokens + i, json_chunk, "perspective") == 0)
+			{
+				out_camera->type = cgltf_camera_type_perspective;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "orthographic") == 0)
+			{
+				out_camera->type = cgltf_camera_type_orthographic;
+			}
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "perspective") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int data_size = tokens[i].size;
+			++i;
+
+			out_camera->type = cgltf_camera_type_perspective;
+
+			for (int k = 0; k < data_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "aspectRatio") == 0)
+				{
+					++i;
+					out_camera->perspective.aspect_ratio = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "yfov") == 0)
+				{
+					++i;
+					out_camera->perspective.yfov = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0)
+				{
+					++i;
+					out_camera->perspective.zfar = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0)
+				{
+					++i;
+					out_camera->perspective.znear = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+				{
+					i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->perspective.extras);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "orthographic") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int data_size = tokens[i].size;
+			++i;
+
+			out_camera->type = cgltf_camera_type_orthographic;
+
+			for (int k = 0; k < data_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "xmag") == 0)
+				{
+					++i;
+					out_camera->orthographic.xmag = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "ymag") == 0)
+				{
+					++i;
+					out_camera->orthographic.ymag = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "zfar") == 0)
+				{
+					++i;
+					out_camera->orthographic.zfar = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "znear") == 0)
+				{
+					++i;
+					out_camera->orthographic.znear = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+				{
+					i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->orthographic.extras);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_camera->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_cameras(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_camera), (void**)&out_data->cameras, &out_data->cameras_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->cameras_count; ++j)
+	{
+		i = cgltf_parse_json_camera(options, tokens, i, json_chunk, &out_data->cameras[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_light(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_light* out_light)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_light->name);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "color") == 0)
+		{
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_light->color, 3);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "intensity") == 0)
+		{
+			++i;
+			out_light->intensity = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "type") == 0)
+		{
+			++i;
+			if (cgltf_json_strcmp(tokens + i, json_chunk, "directional") == 0)
+			{
+				out_light->type = cgltf_light_type_directional;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "point") == 0)
+			{
+				out_light->type = cgltf_light_type_point;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "spot") == 0)
+			{
+				out_light->type = cgltf_light_type_spot;
+			}
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "range") == 0)
+		{
+			++i;
+			out_light->range = cgltf_json_to_float(tokens + i, json_chunk);
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "spot") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int data_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < data_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "innerConeAngle") == 0)
+				{
+					++i;
+					out_light->spot_inner_cone_angle = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "outerConeAngle") == 0)
+				{
+					++i;
+					out_light->spot_outer_cone_angle = cgltf_json_to_float(tokens + i, json_chunk);
+					++i;
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_lights(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_light), (void**)&out_data->lights, &out_data->lights_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->lights_count; ++j)
+	{
+		i = cgltf_parse_json_light(options, tokens, i, json_chunk, &out_data->lights[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_node(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_node* out_node)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	out_node->rotation[3] = 1.0f;
+	out_node->scale[0] = 1.0f;
+	out_node->scale[1] = 1.0f;
+	out_node->scale[2] = 1.0f;
+	out_node->matrix[0] = 1.0f;
+	out_node->matrix[5] = 1.0f;
+	out_node->matrix[10] = 1.0f;
+	out_node->matrix[15] = 1.0f;
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_node->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "children") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_node->children, &out_node->children_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_node->children_count; ++k)
+			{
+				out_node->children[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk));
+				++i;
+			}
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "mesh") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+			out_node->mesh = CGLTF_PTRINDEX(cgltf_mesh, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "skin") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+			out_node->skin = CGLTF_PTRINDEX(cgltf_skin, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "camera") == 0)
+		{
+			++i;
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+			out_node->camera = CGLTF_PTRINDEX(cgltf_camera, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0)
+		{
+			out_node->has_translation = 1;
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->translation, 3);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0)
+		{
+			out_node->has_rotation = 1;
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->rotation, 4);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0)
+		{
+			out_node->has_scale = 1;
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->scale, 3);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "matrix") == 0)
+		{
+			out_node->has_matrix = 1;
+			i = cgltf_parse_json_float_array(tokens, i + 1, json_chunk, out_node->matrix, 16);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "weights") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_float), (void**)&out_node->weights, &out_node->weights_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			i = cgltf_parse_json_float_array(tokens, i - 1, json_chunk, out_node->weights, (int)out_node->weights_count);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_node->extras);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int extensions_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < extensions_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0)
+				{
+					++i;
+
+					CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+					int data_size = tokens[i].size;
+					++i;
+
+					for (int m = 0; m < data_size; ++m)
+					{
+						CGLTF_CHECK_KEY(tokens[i]);
+
+						if (cgltf_json_strcmp(tokens + i, json_chunk, "light") == 0)
+						{
+							++i;
+							CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_PRIMITIVE);
+							out_node->light = CGLTF_PTRINDEX(cgltf_light, cgltf_json_to_int(tokens + i, json_chunk));
+							++i;
+						}
+						else
+						{
+							i = cgltf_skip_json(tokens, i + 1);
+						}
+
+						if (i < 0)
+						{
+							return i;
+						}
+					}
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_nodes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_node), (void**)&out_data->nodes, &out_data->nodes_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->nodes_count; ++j)
+	{
+		i = cgltf_parse_json_node(options, tokens, i, json_chunk, &out_data->nodes[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_scene(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_scene* out_scene)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_scene->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "nodes") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_node*), (void**)&out_scene->nodes, &out_scene->nodes_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_scene->nodes_count; ++k)
+			{
+				out_scene->nodes[k] = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk));
+				++i;
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_scene->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_scenes(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_scene), (void**)&out_data->scenes, &out_data->scenes_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->scenes_count; ++j)
+	{
+		i = cgltf_parse_json_scene(options, tokens, i, json_chunk, &out_data->scenes[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_animation_sampler(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_sampler* out_sampler)
+{
+	(void)options;
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "input") == 0)
+		{
+			++i;
+			out_sampler->input = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "output") == 0)
+		{
+			++i;
+			out_sampler->output = CGLTF_PTRINDEX(cgltf_accessor, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "interpolation") == 0)
+		{
+			++i;
+			if (cgltf_json_strcmp(tokens + i, json_chunk, "LINEAR") == 0)
+			{
+				out_sampler->interpolation = cgltf_interpolation_type_linear;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "STEP") == 0)
+			{
+				out_sampler->interpolation = cgltf_interpolation_type_step;
+			}
+			else if (cgltf_json_strcmp(tokens + i, json_chunk, "CUBICSPLINE") == 0)
+			{
+				out_sampler->interpolation = cgltf_interpolation_type_cubic_spline;
+			}
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_sampler->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_animation_channel(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation_channel* out_channel)
+{
+	(void)options;
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "sampler") == 0)
+		{
+			++i;
+			out_channel->sampler = CGLTF_PTRINDEX(cgltf_animation_sampler, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "target") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int target_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < target_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "node") == 0)
+				{
+					++i;
+					out_channel->target_node = CGLTF_PTRINDEX(cgltf_node, cgltf_json_to_int(tokens + i, json_chunk));
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens+i, json_chunk, "path") == 0)
+				{
+					++i;
+					if (cgltf_json_strcmp(tokens+i, json_chunk, "translation") == 0)
+					{
+						out_channel->target_path = cgltf_animation_path_type_translation;
+					}
+					else if (cgltf_json_strcmp(tokens+i, json_chunk, "rotation") == 0)
+					{
+						out_channel->target_path = cgltf_animation_path_type_rotation;
+					}
+					else if (cgltf_json_strcmp(tokens+i, json_chunk, "scale") == 0)
+					{
+						out_channel->target_path = cgltf_animation_path_type_scale;
+					}
+					else if (cgltf_json_strcmp(tokens+i, json_chunk, "weights") == 0)
+					{
+						out_channel->target_path = cgltf_animation_path_type_weights;
+					}
+					++i;
+				}
+				else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+				{
+					i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_channel->extras);
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i+1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_animation(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_animation* out_animation)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "name") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_animation->name);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "samplers") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_sampler), (void**)&out_animation->samplers, &out_animation->samplers_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_animation->samplers_count; ++k)
+			{
+				i = cgltf_parse_json_animation_sampler(options, tokens, i, json_chunk, &out_animation->samplers[k]);
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "channels") == 0)
+		{
+			i = cgltf_parse_json_array(options, tokens, i + 1, json_chunk, sizeof(cgltf_animation_channel), (void**)&out_animation->channels, &out_animation->channels_count);
+			if (i < 0)
+			{
+				return i;
+			}
+
+			for (cgltf_size k = 0; k < out_animation->channels_count; ++k)
+			{
+				i = cgltf_parse_json_animation_channel(options, tokens, i, json_chunk, &out_animation->channels[k]);
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_animation->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static int cgltf_parse_json_animations(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	i = cgltf_parse_json_array(options, tokens, i, json_chunk, sizeof(cgltf_animation), (void**)&out_data->animations, &out_data->animations_count);
+	if (i < 0)
+	{
+		return i;
+	}
+
+	for (cgltf_size j = 0; j < out_data->animations_count; ++j)
+	{
+		i = cgltf_parse_json_animation(options, tokens, i, json_chunk, &out_data->animations[j]);
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+	return i;
+}
+
+static int cgltf_parse_json_asset(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_asset* out_asset)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens+i, json_chunk, "copyright") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->copyright);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "generator") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->generator);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "version") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->version);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "minVersion") == 0)
+		{
+			i = cgltf_parse_json_string(options, tokens, i + 1, json_chunk, &out_asset->min_version);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_asset->extras);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i+1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+static cgltf_size cgltf_num_components(cgltf_type type) {
+	switch (type)
+	{
+	case cgltf_type_vec2:
+		return 2;
+	case cgltf_type_vec3:
+		return 3;
+	case cgltf_type_vec4:
+		return 4;
+	case cgltf_type_mat2:
+		return 4;
+	case cgltf_type_mat3:
+		return 9;
+	case cgltf_type_mat4:
+		return 16;
+	case cgltf_type_invalid:
+	case cgltf_type_scalar:
+	default:
+		return 1;
+	}
+}
+
+static cgltf_size cgltf_component_size(cgltf_component_type component_type) {
+	switch (component_type)
+	{
+	case cgltf_component_type_r_8:
+	case cgltf_component_type_r_8u:
+		return 1;
+	case cgltf_component_type_r_16:
+	case cgltf_component_type_r_16u:
+		return 2;
+	case cgltf_component_type_r_32u:
+	case cgltf_component_type_r_32f:
+		return 4;
+	case cgltf_component_type_invalid:
+	default:
+		return 0;
+	}
+}
+
+static cgltf_size cgltf_calc_size(cgltf_type type, cgltf_component_type component_type)
+{
+	cgltf_size component_size = cgltf_component_size(component_type);
+	if (type == cgltf_type_mat2 && component_size == 1)
+	{
+		return 8 * component_size;
+	}
+	else if (type == cgltf_type_mat3 && (component_size == 1 || component_size == 2))
+	{
+		return 12 * component_size;
+	}
+	return component_size * cgltf_num_components(type);
+}
+
+static int cgltf_fixup_pointers(cgltf_data* out_data);
+
+static int cgltf_parse_json_root(cgltf_options* options, jsmntok_t const* tokens, int i, const uint8_t* json_chunk, cgltf_data* out_data)
+{
+	CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+	int size = tokens[i].size;
+	++i;
+
+	for (int j = 0; j < size; ++j)
+	{
+		CGLTF_CHECK_KEY(tokens[i]);
+
+		if (cgltf_json_strcmp(tokens + i, json_chunk, "asset") == 0)
+		{
+			i = cgltf_parse_json_asset(options, tokens, i + 1, json_chunk, &out_data->asset);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "meshes") == 0)
+		{
+			i = cgltf_parse_json_meshes(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "accessors") == 0)
+		{
+			i = cgltf_parse_json_accessors(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "bufferViews") == 0)
+		{
+			i = cgltf_parse_json_buffer_views(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "buffers") == 0)
+		{
+			i = cgltf_parse_json_buffers(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "materials") == 0)
+		{
+			i = cgltf_parse_json_materials(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "images") == 0)
+		{
+			i = cgltf_parse_json_images(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "textures") == 0)
+		{
+			i = cgltf_parse_json_textures(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "samplers") == 0)
+		{
+			i = cgltf_parse_json_samplers(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "skins") == 0)
+		{
+			i = cgltf_parse_json_skins(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "cameras") == 0)
+		{
+			i = cgltf_parse_json_cameras(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "nodes") == 0)
+		{
+			i = cgltf_parse_json_nodes(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "scenes") == 0)
+		{
+			i = cgltf_parse_json_scenes(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "scene") == 0)
+		{
+			++i;
+			out_data->scene = CGLTF_PTRINDEX(cgltf_scene, cgltf_json_to_int(tokens + i, json_chunk));
+			++i;
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "animations") == 0)
+		{
+			i = cgltf_parse_json_animations(options, tokens, i + 1, json_chunk, out_data);
+		}
+		else if (cgltf_json_strcmp(tokens+i, json_chunk, "extras") == 0)
+		{
+			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_data->extras);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensions") == 0)
+		{
+			++i;
+
+			CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+			int extensions_size = tokens[i].size;
+			++i;
+
+			for (int k = 0; k < extensions_size; ++k)
+			{
+				CGLTF_CHECK_KEY(tokens[i]);
+
+				if (cgltf_json_strcmp(tokens+i, json_chunk, "KHR_lights_punctual") == 0)
+				{
+					++i;
+
+					CGLTF_CHECK_TOKTYPE(tokens[i], JSMN_OBJECT);
+
+					int data_size = tokens[i].size;
+					++i;
+
+					for (int m = 0; m < data_size; ++m)
+					{
+						CGLTF_CHECK_KEY(tokens[i]);
+
+						if (cgltf_json_strcmp(tokens + i, json_chunk, "lights") == 0)
+						{
+							i = cgltf_parse_json_lights(options, tokens, i + 1, json_chunk, out_data);
+						}
+						else
+						{
+							i = cgltf_skip_json(tokens, i + 1);
+						}
+
+						if (i < 0)
+						{
+							return i;
+						}
+					}
+				}
+				else
+				{
+					i = cgltf_skip_json(tokens, i + 1);
+				}
+
+				if (i < 0)
+				{
+					return i;
+				}
+			}
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsUsed") == 0)
+		{
+			i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_used, &out_data->extensions_used_count);
+		}
+		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extensionsRequired") == 0)
+		{
+			i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_data->extensions_required, &out_data->extensions_required_count);
+		}
+		else
+		{
+			i = cgltf_skip_json(tokens, i + 1);
+		}
+
+		if (i < 0)
+		{
+			return i;
+		}
+	}
+
+	return i;
+}
+
+cgltf_result cgltf_parse_json(cgltf_options* options, const uint8_t* json_chunk, cgltf_size size, cgltf_data** out_data)
+{
+	jsmn_parser parser = { 0, 0, 0 };
+
+	if (options->json_token_count == 0)
+	{
+		int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, NULL, 0);
+
+		if (token_count <= 0)
+		{
+			return cgltf_result_invalid_json;
+		}
+
+		options->json_token_count = token_count;
+	}
+
+	jsmntok_t* tokens = (jsmntok_t*)options->memory_alloc(options->memory_user_data, sizeof(jsmntok_t) * (options->json_token_count + 1));
+
+	if (!tokens)
+	{
+		return cgltf_result_out_of_memory;
+	}
+
+	jsmn_init(&parser);
+
+	int token_count = jsmn_parse(&parser, (const char*)json_chunk, size, tokens, options->json_token_count);
+
+	if (token_count <= 0)
+	{
+		options->memory_free(options->memory_user_data, tokens);
+		return cgltf_result_invalid_json;
+	}
+
+	// this makes sure that we always have an UNDEFINED token at the end of the stream
+	// for invalid JSON inputs this makes sure we don't perform out of bound reads of token data
+	tokens[token_count].type = JSMN_UNDEFINED;
+
+	cgltf_data* data = (cgltf_data*)options->memory_alloc(options->memory_user_data, sizeof(cgltf_data));
+
+	if (!data)
+	{
+		options->memory_free(options->memory_user_data, tokens);
+		return cgltf_result_out_of_memory;
+	}
+
+	memset(data, 0, sizeof(cgltf_data));
+	data->memory_free = options->memory_free;
+	data->memory_user_data = options->memory_user_data;
+
+	int i = cgltf_parse_json_root(options, tokens, 0, json_chunk, data);
+
+	options->memory_free(options->memory_user_data, tokens);
+
+	if (i < 0)
+	{
+		cgltf_free(data);
+		return (i == CGLTF_ERROR_NOMEM) ? cgltf_result_out_of_memory : cgltf_result_invalid_gltf;
+	}
+
+	if (cgltf_fixup_pointers(data) < 0)
+	{
+		cgltf_free(data);
+		return cgltf_result_invalid_gltf;
+	}
+
+	data->json = (const char*)json_chunk;
+	data->json_size = size;
+
+	*out_data = data;
+
+	return cgltf_result_success;
+}
+
+static int cgltf_fixup_pointers(cgltf_data* data)
+{
+	for (cgltf_size i = 0; i < data->meshes_count; ++i)
+	{
+		for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j)
+		{
+			CGLTF_PTRFIXUP(data->meshes[i].primitives[j].indices, data->accessors, data->accessors_count);
+			CGLTF_PTRFIXUP(data->meshes[i].primitives[j].material, data->materials, data->materials_count);
+
+			for (cgltf_size k = 0; k < data->meshes[i].primitives[j].attributes_count; ++k)
+			{
+				CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].attributes[k].data, data->accessors, data->accessors_count);
+			}
+
+			for (cgltf_size k = 0; k < data->meshes[i].primitives[j].targets_count; ++k)
+			{
+				for (cgltf_size m = 0; m < data->meshes[i].primitives[j].targets[k].attributes_count; ++m)
+				{
+					CGLTF_PTRFIXUP_REQ(data->meshes[i].primitives[j].targets[k].attributes[m].data, data->accessors, data->accessors_count);
+				}
+			}
+		}
+	}
+
+	for (cgltf_size i = 0; i < data->accessors_count; ++i)
+	{
+		CGLTF_PTRFIXUP(data->accessors[i].buffer_view, data->buffer_views, data->buffer_views_count);
+
+		if (data->accessors[i].is_sparse)
+		{
+			CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.indices_buffer_view, data->buffer_views, data->buffer_views_count);
+			CGLTF_PTRFIXUP_REQ(data->accessors[i].sparse.values_buffer_view, data->buffer_views, data->buffer_views_count);
+		}
+
+		if (data->accessors[i].buffer_view)
+		{
+			data->accessors[i].stride = data->accessors[i].buffer_view->stride;
+		}
+
+		if (data->accessors[i].stride == 0)
+		{
+			data->accessors[i].stride = cgltf_calc_size(data->accessors[i].type, data->accessors[i].component_type);
+		}
+	}
+
+	for (cgltf_size i = 0; i < data->textures_count; ++i)
+	{
+		CGLTF_PTRFIXUP(data->textures[i].image, data->images, data->images_count);
+		CGLTF_PTRFIXUP(data->textures[i].sampler, data->samplers, data->samplers_count);
+	}
+
+	for (cgltf_size i = 0; i < data->images_count; ++i)
+	{
+		CGLTF_PTRFIXUP(data->images[i].buffer_view, data->buffer_views, data->buffer_views_count);
+	}
+
+	for (cgltf_size i = 0; i < data->materials_count; ++i)
+	{
+		CGLTF_PTRFIXUP(data->materials[i].normal_texture.texture, data->textures, data->textures_count);
+		CGLTF_PTRFIXUP(data->materials[i].emissive_texture.texture, data->textures, data->textures_count);
+		CGLTF_PTRFIXUP(data->materials[i].occlusion_texture.texture, data->textures, data->textures_count);
+
+		CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.base_color_texture.texture, data->textures, data->textures_count);
+		CGLTF_PTRFIXUP(data->materials[i].pbr_metallic_roughness.metallic_roughness_texture.texture, data->textures, data->textures_count);
+
+		CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.diffuse_texture.texture, data->textures, data->textures_count);
+		CGLTF_PTRFIXUP(data->materials[i].pbr_specular_glossiness.specular_glossiness_texture.texture, data->textures, data->textures_count);
+	}
+
+	for (cgltf_size i = 0; i < data->buffer_views_count; ++i)
+	{
+		CGLTF_PTRFIXUP_REQ(data->buffer_views[i].buffer, data->buffers, data->buffers_count);
+	}
+
+	for (cgltf_size i = 0; i < data->skins_count; ++i)
+	{
+		for (cgltf_size j = 0; j < data->skins[i].joints_count; ++j)
+		{
+			CGLTF_PTRFIXUP_REQ(data->skins[i].joints[j], data->nodes, data->nodes_count);
+		}
+
+		CGLTF_PTRFIXUP(data->skins[i].skeleton, data->nodes, data->nodes_count);
+		CGLTF_PTRFIXUP(data->skins[i].inverse_bind_matrices, data->accessors, data->accessors_count);
+	}
+
+	for (cgltf_size i = 0; i < data->nodes_count; ++i)
+	{
+		for (cgltf_size j = 0; j < data->nodes[i].children_count; ++j)
+		{
+			CGLTF_PTRFIXUP_REQ(data->nodes[i].children[j], data->nodes, data->nodes_count);
+
+			if (data->nodes[i].children[j]->parent)
+			{
+				return CGLTF_ERROR_JSON;
+			}
+
+			data->nodes[i].children[j]->parent = &data->nodes[i];
+		}
+
+		CGLTF_PTRFIXUP(data->nodes[i].mesh, data->meshes, data->meshes_count);
+		CGLTF_PTRFIXUP(data->nodes[i].skin, data->skins, data->skins_count);
+		CGLTF_PTRFIXUP(data->nodes[i].camera, data->cameras, data->cameras_count);
+		CGLTF_PTRFIXUP(data->nodes[i].light, data->lights, data->lights_count);
+	}
+
+	for (cgltf_size i = 0; i < data->scenes_count; ++i)
+	{
+		for (cgltf_size j = 0; j < data->scenes[i].nodes_count; ++j)
+		{
+			CGLTF_PTRFIXUP_REQ(data->scenes[i].nodes[j], data->nodes, data->nodes_count);
+
+			if (data->scenes[i].nodes[j]->parent)
+			{
+				return CGLTF_ERROR_JSON;
+			}
+		}
+	}
+
+	CGLTF_PTRFIXUP(data->scene, data->scenes, data->scenes_count);
+
+	for (cgltf_size i = 0; i < data->animations_count; ++i)
+	{
+		for (cgltf_size j = 0; j < data->animations[i].samplers_count; ++j)
+		{
+			CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].input, data->accessors, data->accessors_count);
+			CGLTF_PTRFIXUP_REQ(data->animations[i].samplers[j].output, data->accessors, data->accessors_count);
+		}
+
+		for (cgltf_size j = 0; j < data->animations[i].channels_count; ++j)
+		{
+			CGLTF_PTRFIXUP_REQ(data->animations[i].channels[j].sampler, data->animations[i].samplers, data->animations[i].samplers_count);
+			CGLTF_PTRFIXUP(data->animations[i].channels[j].target_node, data->nodes, data->nodes_count);
+		}
+	}
+
+	return 0;
+}
+
+/*
+ * -- jsmn.c start --
+ * Source: https://github.com/zserge/jsmn
+ * License: MIT
+ *
+ * Copyright (c) 2010 Serge A. Zaitsev
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+/**
+ * Allocates a fresh unused token from the token pull.
+ */
+static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser,
+				   jsmntok_t *tokens, size_t num_tokens) {
+	jsmntok_t *tok;
+	if (parser->toknext >= num_tokens) {
+		return NULL;
+	}
+	tok = &tokens[parser->toknext++];
+	tok->start = tok->end = -1;
+	tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+	tok->parent = -1;
+#endif
+	return tok;
+}
+
+/**
+ * Fills token type and boundaries.
+ */
+static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type,
+			    int start, int end) {
+	token->type = type;
+	token->start = start;
+	token->end = end;
+	token->size = 0;
+}
+
+/**
+ * Fills next available token with JSON primitive.
+ */
+static int jsmn_parse_primitive(jsmn_parser *parser, const char *js,
+				size_t len, jsmntok_t *tokens, size_t num_tokens) {
+	jsmntok_t *token;
+	int start;
+
+	start = parser->pos;
+
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		switch (js[parser->pos]) {
+#ifndef JSMN_STRICT
+		/* In strict mode primitive must be followed by "," or "}" or "]" */
+		case ':':
+#endif
+		case '\t' : case '\r' : case '\n' : case ' ' :
+		case ','  : case ']'  : case '}' :
+			goto found;
+		}
+		if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
+			parser->pos = start;
+			return JSMN_ERROR_INVAL;
+		}
+	}
+#ifdef JSMN_STRICT
+	/* In strict mode primitive must be followed by a comma/object/array */
+	parser->pos = start;
+	return JSMN_ERROR_PART;
+#endif
+
+found:
+	if (tokens == NULL) {
+		parser->pos--;
+		return 0;
+	}
+	token = jsmn_alloc_token(parser, tokens, num_tokens);
+	if (token == NULL) {
+		parser->pos = start;
+		return JSMN_ERROR_NOMEM;
+	}
+	jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+	token->parent = parser->toksuper;
+#endif
+	parser->pos--;
+	return 0;
+}
+
+/**
+ * Fills next token with JSON string.
+ */
+static int jsmn_parse_string(jsmn_parser *parser, const char *js,
+			     size_t len, jsmntok_t *tokens, size_t num_tokens) {
+	jsmntok_t *token;
+
+	int start = parser->pos;
+
+	parser->pos++;
+
+	/* Skip starting quote */
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		char c = js[parser->pos];
+
+		/* Quote: end of string */
+		if (c == '\"') {
+			if (tokens == NULL) {
+				return 0;
+			}
+			token = jsmn_alloc_token(parser, tokens, num_tokens);
+			if (token == NULL) {
+				parser->pos = start;
+				return JSMN_ERROR_NOMEM;
+			}
+			jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+			token->parent = parser->toksuper;
+#endif
+			return 0;
+		}
+
+		/* Backslash: Quoted symbol expected */
+		if (c == '\\' && parser->pos + 1 < len) {
+			int i;
+			parser->pos++;
+			switch (js[parser->pos]) {
+			/* Allowed escaped symbols */
+			case '\"': case '/' : case '\\' : case 'b' :
+			case 'f' : case 'r' : case 'n'  : case 't' :
+				break;
+				/* Allows escaped symbol \uXXXX */
+			case 'u':
+				parser->pos++;
+				for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) {
+					/* If it isn't a hex character we have an error */
+					if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
+					     (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
+					     (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
+						parser->pos = start;
+						return JSMN_ERROR_INVAL;
+					}
+					parser->pos++;
+				}
+				parser->pos--;
+				break;
+				/* Unexpected symbol */
+			default:
+				parser->pos = start;
+				return JSMN_ERROR_INVAL;
+			}
+		}
+	}
+	parser->pos = start;
+	return JSMN_ERROR_PART;
+}
+
+/**
+ * Parse JSON string and fill tokens.
+ */
+static int jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
+	       jsmntok_t *tokens, size_t num_tokens) {
+	int r;
+	int i;
+	jsmntok_t *token;
+	int count = parser->toknext;
+
+	for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+		char c;
+		jsmntype_t type;
+
+		c = js[parser->pos];
+		switch (c) {
+		case '{': case '[':
+			count++;
+			if (tokens == NULL) {
+				break;
+			}
+			token = jsmn_alloc_token(parser, tokens, num_tokens);
+			if (token == NULL)
+				return JSMN_ERROR_NOMEM;
+			if (parser->toksuper != -1) {
+				tokens[parser->toksuper].size++;
+#ifdef JSMN_PARENT_LINKS
+				token->parent = parser->toksuper;
+#endif
+			}
+			token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+			token->start = parser->pos;
+			parser->toksuper = parser->toknext - 1;
+			break;
+		case '}': case ']':
+			if (tokens == NULL)
+				break;
+			type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+			if (parser->toknext < 1) {
+				return JSMN_ERROR_INVAL;
+			}
+			token = &tokens[parser->toknext - 1];
+			for (;;) {
+				if (token->start != -1 && token->end == -1) {
+					if (token->type != type) {
+						return JSMN_ERROR_INVAL;
+					}
+					token->end = parser->pos + 1;
+					parser->toksuper = token->parent;
+					break;
+				}
+				if (token->parent == -1) {
+					if(token->type != type || parser->toksuper == -1) {
+						return JSMN_ERROR_INVAL;
+					}
+					break;
+				}
+				token = &tokens[token->parent];
+			}
+#else
+			for (i = parser->toknext - 1; i >= 0; i--) {
+				token = &tokens[i];
+				if (token->start != -1 && token->end == -1) {
+					if (token->type != type) {
+						return JSMN_ERROR_INVAL;
+					}
+					parser->toksuper = -1;
+					token->end = parser->pos + 1;
+					break;
+				}
+			}
+			/* Error if unmatched closing bracket */
+			if (i == -1) return JSMN_ERROR_INVAL;
+			for (; i >= 0; i--) {
+				token = &tokens[i];
+				if (token->start != -1 && token->end == -1) {
+					parser->toksuper = i;
+					break;
+				}
+			}
+#endif
+			break;
+		case '\"':
+			r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
+			if (r < 0) return r;
+			count++;
+			if (parser->toksuper != -1 && tokens != NULL)
+				tokens[parser->toksuper].size++;
+			break;
+		case '\t' : case '\r' : case '\n' : case ' ':
+			break;
+		case ':':
+			parser->toksuper = parser->toknext - 1;
+			break;
+		case ',':
+			if (tokens != NULL && parser->toksuper != -1 &&
+					tokens[parser->toksuper].type != JSMN_ARRAY &&
+					tokens[parser->toksuper].type != JSMN_OBJECT) {
+#ifdef JSMN_PARENT_LINKS
+				parser->toksuper = tokens[parser->toksuper].parent;
+#else
+				for (i = parser->toknext - 1; i >= 0; i--) {
+					if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
+						if (tokens[i].start != -1 && tokens[i].end == -1) {
+							parser->toksuper = i;
+							break;
+						}
+					}
+				}
+#endif
+			}
+			break;
+#ifdef JSMN_STRICT
+			/* In strict mode primitives are: numbers and booleans */
+		case '-': case '0': case '1' : case '2': case '3' : case '4':
+		case '5': case '6': case '7' : case '8': case '9':
+		case 't': case 'f': case 'n' :
+			/* And they must not be keys of the object */
+			if (tokens != NULL && parser->toksuper != -1) {
+				jsmntok_t *t = &tokens[parser->toksuper];
+				if (t->type == JSMN_OBJECT ||
+						(t->type == JSMN_STRING && t->size != 0)) {
+					return JSMN_ERROR_INVAL;
+				}
+			}
+#else
+			/* In non-strict mode every unquoted value is a primitive */
+		default:
+#endif
+			r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
+			if (r < 0) return r;
+			count++;
+			if (parser->toksuper != -1 && tokens != NULL)
+				tokens[parser->toksuper].size++;
+			break;
+
+#ifdef JSMN_STRICT
+			/* Unexpected char in strict mode */
+		default:
+			return JSMN_ERROR_INVAL;
+#endif
+		}
+	}
+
+	if (tokens != NULL) {
+		for (i = parser->toknext - 1; i >= 0; i--) {
+			/* Unmatched opened object or array */
+			if (tokens[i].start != -1 && tokens[i].end == -1) {
+				return JSMN_ERROR_PART;
+			}
+		}
+	}
+
+	return count;
+}
+
+/**
+ * Creates a new parser based over a given  buffer with an array of tokens
+ * available.
+ */
+static void jsmn_init(jsmn_parser *parser) {
+	parser->pos = 0;
+	parser->toknext = 0;
+	parser->toksuper = -1;
+}
+/*
+ * -- jsmn.c end --
+ */
+
+#endif /* #ifdef CGLTF_IMPLEMENTATION */
+
+/* cgltf is distributed under MIT license:
+ *
+ * Copyright (c) 2018 Johannes Kuhlmann
+
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */

+ 1397 - 0
3rdparty/meshoptimizer/tools/fast_obj.h

@@ -0,0 +1,1397 @@
+/*
+ *
+ * MIT License
+ *
+ * Copyright (c) 2018 Richard Knight
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ *
+ */
+
+#ifndef FAST_OBJ_HDR
+#define FAST_OBJ_HDR
+
+
+typedef struct
+{
+    /* Texture name from .mtl file */
+    char*                       name;
+
+    /* Resolved path to texture */
+    char*                       path;
+
+} fastObjTexture;
+
+
+typedef struct
+{
+    /* Material name */
+    char*                       name;
+
+    /* Parameters */
+    float                       Ka[3];  /* Ambient */
+    float                       Kd[3];  /* Diffuse */
+    float                       Ks[3];  /* Specular */
+    float                       Ke[3];  /* Emission */
+    float                       Kt[3];  /* Transmittance */
+    float                       Ns;     /* Shininess */
+    float                       Ni;     /* Index of refraction */
+    float                       Tf[3];  /* Transmission filter */
+    float                       d;      /* Disolve (alpha) */
+    int                         illum;  /* Illumination model */
+
+    /* Texture maps */
+    fastObjTexture              map_Ka;
+    fastObjTexture              map_Kd;
+    fastObjTexture              map_Ks;
+    fastObjTexture              map_Ke;
+    fastObjTexture              map_Kt;
+    fastObjTexture              map_Ns;
+    fastObjTexture              map_Ni;
+    fastObjTexture              map_d;
+    fastObjTexture              map_bump;
+    
+
+} fastObjMaterial;
+
+
+typedef struct
+{
+    unsigned int                p;
+    unsigned int                t;
+    unsigned int                n;
+
+} fastObjIndex;
+
+
+typedef struct
+{
+    /* Group name */
+    char*                       name;
+
+    /* Number of faces */
+    unsigned int                face_count;
+
+    /* First face in fastObjMesh face_* arrays */
+    unsigned int                face_offset;
+
+    /* First index in fastObjMesh indices array */
+    unsigned int                index_offset;
+
+} fastObjGroup;
+
+
+typedef struct
+{
+    /* Vertex data */
+    unsigned int                position_count;
+    float*                      positions;
+
+    unsigned int                texcoord_count;
+    float*                      texcoords;
+
+    unsigned int                normal_count;
+    float*                      normals;
+
+    /* Face data: one element for each face */
+    unsigned int                face_count;
+    unsigned int*               face_vertices;
+    unsigned int*               face_materials;
+
+    /* Index data: one element for each face vertex */
+    fastObjIndex*               indices;
+
+    /* Materials */
+    unsigned int                material_count;
+    fastObjMaterial*            materials;
+
+    /* Mesh groups */
+    unsigned int                group_count;
+    fastObjGroup*               groups;
+
+} fastObjMesh;
+
+
+fastObjMesh*                    fast_obj_read(const char* path);
+void                            fast_obj_destroy(fastObjMesh* mesh);
+
+#endif
+
+
+#ifdef FAST_OBJ_IMPLEMENTATION
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef FAST_OBJ_REALLOC
+#define FAST_OBJ_REALLOC        realloc
+#endif
+
+#ifndef FAST_OBJ_FREE
+#define FAST_OBJ_FREE           free
+#endif
+
+#ifdef _WIN32
+#define FAST_OBJ_SEPARATOR      '\\'
+#define FAST_OBJ_OTHER_SEP      '/'
+#else
+#define FAST_OBJ_SEPARATOR      '/'
+#define FAST_OBJ_OTHER_SEP      '\\'
+#endif
+
+
+/* Size of buffer to read into */
+#define BUFFER_SIZE             65536
+
+/* Max supported power when parsing float */
+#define MAX_POWER               20
+
+typedef struct
+{
+    /* Final mesh */
+    fastObjMesh*                mesh;
+
+    /* Current group */
+    fastObjGroup                group;
+
+    /* Current material index */
+    unsigned int                material;
+
+    /* Current line in file */
+    unsigned int                line;
+
+    /* Base path for materials/textures */
+    char*                       base;
+
+} fastObjData;
+
+
+static const
+double POWER_10_POS[MAX_POWER] =
+{
+    1.0e0,  1.0e1,  1.0e2,  1.0e3,  1.0e4,  1.0e5,  1.0e6,  1.0e7,  1.0e8,  1.0e9,
+    1.0e10, 1.0e11, 1.0e12, 1.0e13, 1.0e14, 1.0e15, 1.0e16, 1.0e17, 1.0e18, 1.0e19,
+};
+
+static const
+double POWER_10_NEG[MAX_POWER] =
+{
+    1.0e0,   1.0e-1,  1.0e-2,  1.0e-3,  1.0e-4,  1.0e-5,  1.0e-6,  1.0e-7,  1.0e-8,  1.0e-9,
+    1.0e-10, 1.0e-11, 1.0e-12, 1.0e-13, 1.0e-14, 1.0e-15, 1.0e-16, 1.0e-17, 1.0e-18, 1.0e-19,
+};
+
+
+static
+void* memory_realloc(void* ptr, size_t bytes)
+{
+    return FAST_OBJ_REALLOC(ptr, bytes);
+}
+
+
+static
+void memory_dealloc(void* ptr)
+{
+    FAST_OBJ_FREE(ptr);
+}
+
+
+#define array_clean(_arr)       ((_arr) ? memory_dealloc(_array_header(_arr)), 0 : 0)
+#define array_push(_arr, _val)  (_array_mgrow(_arr, 1) ? ((_arr)[_array_size(_arr)++] = (_val), _array_size(_arr) - 1) : 0)
+#define array_size(_arr)        ((_arr) ? _array_size(_arr) : 0)
+#define array_capacity(_arr)    ((_arr) ? _array_capacity(_arr) : 0)
+#define array_empty(_arr)       (array_size(_arr) == 0)
+
+#define _array_header(_arr)     ((unsigned int*)(_arr) - 2)
+#define _array_size(_arr)       (_array_header(_arr)[0])
+#define _array_capacity(_arr)   (_array_header(_arr)[1])
+#define _array_ngrow(_arr, _n)  ((_arr) == 0 || (_array_size(_arr) + (_n) >= _array_capacity(_arr)))
+#define _array_mgrow(_arr, _n)  (_array_ngrow(_arr, _n) ? (_array_grow(_arr, _n) != 0) : 1)
+#define _array_grow(_arr, _n)   (*((void**)&(_arr)) = array_realloc(_arr, _n, sizeof(*(_arr))))
+
+
+static
+void* array_realloc(void* ptr, unsigned int n, unsigned int b)
+{
+    unsigned int  sz   = array_size(ptr);
+    unsigned int  nsz  = sz + n;
+    unsigned int  cap  = array_capacity(ptr);
+    unsigned int  ncap = 3 * cap / 2;
+    unsigned int* r;
+
+
+    if (ncap < nsz)
+        ncap = nsz;
+    ncap = (ncap + 15) & ~15u;
+
+    r = (unsigned int*)(memory_realloc(ptr ? _array_header(ptr) : 0, b * ncap + 2 * sizeof(unsigned int)));
+    if (!r)
+        return 0;
+
+    r[0] = sz;
+    r[1] = ncap;
+
+    return (r + 2);
+}
+
+
+static
+void* file_open(const char* path)
+{
+    return fopen(path, "rb");
+}
+
+
+static
+void file_close(void* file)
+{
+    FILE* f;
+    
+    f = (FILE*)(file);
+    fclose(f);
+}
+
+
+static
+size_t file_read(void* file, void* dst, size_t bytes)
+{
+    FILE* f;
+    
+    f = (FILE*)(file);
+    return fread(dst, 1, bytes, f);
+}
+
+
+static
+unsigned long file_size(void* file)
+{
+    FILE* f;
+    long p;
+    long n;
+    
+    f = (FILE*)(file);
+
+    p = ftell(f);
+    fseek(f, 0, SEEK_END);
+    n = ftell(f);
+    fseek(f, p, SEEK_SET);
+
+    if (n > 0)
+        return (unsigned long)(n);
+    else
+        return 0;
+}
+
+
+static
+char* string_copy(const char* s, const char* e)
+{
+    size_t n;
+    char*  p;
+        
+    n = (size_t)(e - s);
+    p = (char*)(memory_realloc(0, n + 1));
+    if (p)
+    {
+        memcpy(p, s, n);
+        p[n] = '\0';
+    }
+
+    return p;
+}
+
+
+static
+char* string_substr(const char* s, size_t a, size_t b)
+{
+    return string_copy(s + a, s + b);
+}
+
+
+static
+char* string_concat(const char* a, const char* s, const char* e)
+{
+    size_t an;
+    size_t sn;
+    char*  p;
+        
+    an = a ? strlen(a) : 0;
+    sn = (size_t)(e - s);
+    p = (char*)(memory_realloc(0, an + sn + 1));
+    if (p)
+    {
+        if (a)
+            memcpy(p, a, an);
+        memcpy(p + an, s, sn);
+        p[an + sn] = '\0';
+    }
+
+    return p;
+}
+
+
+static
+int string_equal(const char* a, const char* s, const char* e)
+{
+    while (*a++ == *s++ && s != e)
+        ;
+
+    return (*a == '\0' && s == e);
+}
+
+
+static
+int string_find_last(const char* s, char c, size_t* p)
+{
+    const char* e;
+
+    e = s + strlen(s);
+    while (e > s)
+    {
+        e--;
+
+        if (*e == c)
+        {
+            *p = (size_t)(e - s);
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+
+static
+void string_fix_separators(char* s)
+{
+    while (*s)
+    {
+        if (*s == FAST_OBJ_OTHER_SEP)
+            *s = FAST_OBJ_SEPARATOR;
+        s++;
+    }
+}
+
+
+static
+int is_whitespace(char c)
+{
+    return (c == ' ' || c == '\t' || c == '\r');
+}
+
+
+static
+int is_newline(char c)
+{
+    return (c == '\n');
+}
+
+
+static
+int is_digit(char c)
+{
+    return (c >= '0' && c <= '9');
+}
+
+
+static
+int is_exponent(char c)
+{
+    return (c == 'e' || c == 'E');
+}
+
+
+static
+const char* skip_whitespace(const char* ptr)
+{
+    while (is_whitespace(*ptr))
+        ptr++;
+
+    return ptr;
+}
+
+
+static
+const char* skip_line(const char* ptr)
+{
+    while (!is_newline(*ptr++))
+        ;
+
+    return ptr;
+}
+
+
+static
+fastObjGroup group_default(void)
+{
+    fastObjGroup group;
+
+    group.name         = 0;
+    group.face_count   = 0;
+    group.face_offset  = 0;
+    group.index_offset = 0;
+
+    return group;
+}
+
+
+static
+void group_clean(fastObjGroup* group)
+{
+    memory_dealloc(group->name);
+}
+
+
+static
+void flush_output(fastObjData* data)
+{
+    /* Add group if not empty */
+    if (data->group.face_count > 0)
+        array_push(data->mesh->groups, data->group);
+    else
+        group_clean(&data->group);
+
+    /* Reset for more data */
+    data->group = group_default();
+    data->group.face_offset = array_size(data->mesh->face_vertices);
+    data->group.index_offset = array_size(data->mesh->indices);
+}
+
+
+static
+const char* parse_int(const char* ptr, int* val)
+{
+    int sign;
+    int num;
+
+
+    if (*ptr == '-')
+    {
+        sign = -1;
+        ptr++;
+    }
+    else
+    {
+        sign = +1;
+    }
+
+    num = 0;
+    while (is_digit(*ptr))
+        num = 10 * num + (*ptr++ - '0');
+
+    *val = sign * num;
+
+    return ptr;
+}
+
+
+static
+const char* parse_float(const char* ptr, float* val)
+{
+    double        sign;
+    double        num;
+    double        fra;
+    double        div;
+    int           eval;
+    const double* powers;
+
+
+    ptr = skip_whitespace(ptr);
+
+    switch (*ptr)
+    {
+    case '+':
+        sign = 1.0;
+        ptr++;
+        break;
+
+    case '-':
+        sign = -1.0;
+        ptr++;
+        break;
+
+    default:
+        sign = 1.0;
+        break;
+    }
+
+
+    num = 0.0;
+    while (is_digit(*ptr))
+        num = 10.0 * num + (double)(*ptr++ - '0');
+
+    if (*ptr == '.')
+        ptr++;
+
+    fra = 0.0;
+    div = 1.0;
+
+    while (is_digit(*ptr))
+    {
+        fra  = 10.0 * fra + (double)(*ptr++ - '0');
+        div *= 10.0;
+    }
+
+    num += fra / div;
+
+    if (is_exponent(*ptr))
+    {
+        ptr++;
+
+        switch (*ptr)
+        {
+        case '+':
+            powers = POWER_10_POS;
+            ptr++;
+            break;
+
+        case '-':
+            powers = POWER_10_NEG;
+            ptr++;
+            break;
+
+        default:
+            powers = POWER_10_POS;
+            break;
+        }
+
+        eval = 0;
+        while (is_digit(*ptr))
+            eval = 10 * eval + (*ptr++ - '0');
+
+        num *= (eval >= MAX_POWER) ? 0.0 : powers[eval];
+    }
+
+    *val = (float)(sign * num);
+
+    return ptr;
+}
+
+
+static
+const char* parse_vertex(fastObjData* data, const char* ptr)
+{
+    unsigned int ii;
+    float        v;
+
+
+    for (ii = 0; ii < 3; ii++)
+    {
+        ptr = parse_float(ptr, &v);
+        array_push(data->mesh->positions, v);
+    }
+
+    return ptr;
+}
+
+
+static
+const char* parse_texcoord(fastObjData* data, const char* ptr)
+{
+    unsigned int ii;
+    float        v;
+
+
+    for (ii = 0; ii < 2; ii++)
+    {
+        ptr = parse_float(ptr, &v);
+        array_push(data->mesh->texcoords, v);
+    }
+
+    return ptr;
+}
+
+
+static
+const char* parse_normal(fastObjData* data, const char* ptr)
+{
+    unsigned int ii;
+    float        v;
+
+
+    for (ii = 0; ii < 3; ii++)
+    {
+        ptr = parse_float(ptr, &v);
+        array_push(data->mesh->normals, v);
+    }
+
+    return ptr;
+}
+
+
+static
+const char* parse_face(fastObjData* data, const char* ptr)
+{
+    unsigned int count;
+    fastObjIndex vn;
+    int          v;
+    int          t;
+    int          n;
+
+
+    ptr = skip_whitespace(ptr);
+
+    count = 0;
+    while (!is_newline(*ptr))
+    {
+        v = 0;
+        t = 0;
+        n = 0;
+
+        ptr = parse_int(ptr, &v);
+        if (*ptr == '/')
+        {
+            ptr++;
+            if (*ptr != '/')
+                ptr = parse_int(ptr, &t);
+
+            if (*ptr == '/')
+            {
+                ptr++;
+                ptr = parse_int(ptr, &n);
+            }
+        }
+
+        if (v < 0)
+            vn.p = (array_size(data->mesh->positions) / 3) - (unsigned int)(-v);
+        else
+            vn.p = (unsigned int)(v);
+
+        if (t < 0)
+            vn.t = (array_size(data->mesh->texcoords) / 2) - (unsigned int)(-t);
+        else if (t > 0)
+            vn.t = (unsigned int)(t);
+        else
+            vn.t = 0;
+
+        if (n < 0)
+            vn.n = (array_size(data->mesh->normals) / 3) - (unsigned int)(-n);
+        else if (n > 0)
+            vn.n = (unsigned int)(n);
+        else
+            vn.n = 0;
+
+        array_push(data->mesh->indices, vn);
+        count++;
+
+        ptr = skip_whitespace(ptr);
+    }
+
+    array_push(data->mesh->face_vertices, count);
+    array_push(data->mesh->face_materials, data->material);
+
+    data->group.face_count++;
+
+    return ptr;
+}
+
+
+static
+const char* parse_group(fastObjData* data, const char* ptr)
+{
+    const char* s;
+    const char* e;
+
+
+    ptr = skip_whitespace(ptr);
+
+    s = ptr;
+    while (!is_whitespace(*ptr) && !is_newline(*ptr))
+        ptr++;
+
+    e = ptr;
+
+    flush_output(data);
+    data->group.name = string_copy(s, e);
+
+    return ptr;
+}
+
+
+static
+fastObjTexture map_default(void)
+{
+    fastObjTexture map;
+
+    map.name = 0;
+    map.path = 0;
+
+    return map;
+}
+
+
+static
+fastObjMaterial mtl_default(void)
+{
+    fastObjMaterial mtl;
+
+    mtl.name = 0;
+
+    mtl.Ka[0] = 0.0;
+    mtl.Ka[1] = 0.0;
+    mtl.Ka[2] = 0.0;
+    mtl.Kd[0] = 1.0;
+    mtl.Kd[1] = 1.0;
+    mtl.Kd[2] = 1.0;
+    mtl.Ks[0] = 0.0;
+    mtl.Ks[1] = 0.0;
+    mtl.Ks[2] = 0.0;
+    mtl.Ke[0] = 0.0;
+    mtl.Ke[1] = 0.0;
+    mtl.Ke[2] = 0.0;
+    mtl.Kt[0] = 0.0;
+    mtl.Kt[1] = 0.0;
+    mtl.Kt[2] = 0.0;
+    mtl.Ns    = 1.0;
+    mtl.Ni    = 1.0;
+    mtl.Tf[0] = 1.0;
+    mtl.Tf[1] = 1.0;
+    mtl.Tf[2] = 1.0;
+    mtl.d     = 1.0;
+    mtl.illum = 1;
+
+    mtl.map_Ka   = map_default();
+    mtl.map_Kd   = map_default();
+    mtl.map_Ks   = map_default();
+    mtl.map_Ke   = map_default();
+    mtl.map_Kt   = map_default();
+    mtl.map_Ns   = map_default();
+    mtl.map_Ni   = map_default();
+    mtl.map_d    = map_default();
+    mtl.map_bump = map_default();
+
+    return mtl;
+}
+
+
+static
+const char* parse_usemtl(fastObjData* data, const char* ptr)
+{
+    const char*      s;
+    const char*      e;
+    unsigned int     idx;
+    fastObjMaterial* mtl;
+
+
+    ptr = skip_whitespace(ptr);
+
+    /* Parse the material name */
+    s = ptr;
+    while (!is_whitespace(*ptr) && !is_newline(*ptr))
+        ptr++;
+
+    e = ptr;
+
+
+    /* If there are no materials yet, add a dummy invalid material at index 0 */
+    if (array_empty(data->mesh->materials))
+        array_push(data->mesh->materials, mtl_default());
+
+
+    /* Find an existing material with the same name */
+    idx = 0;
+    while (idx < array_size(data->mesh->materials))
+    {
+        mtl = &data->mesh->materials[idx];
+        if (mtl->name && string_equal(mtl->name, s, e))
+            break;
+
+        idx++;
+    }
+
+    if (idx == array_size(data->mesh->materials))
+        idx = 0;
+
+    data->material = idx;
+
+    return ptr;
+}
+
+
+static
+void map_clean(fastObjTexture* map)
+{
+    memory_dealloc(map->name);
+    memory_dealloc(map->path);
+}
+
+
+static
+void mtl_clean(fastObjMaterial* mtl)
+{
+    map_clean(&mtl->map_Ka);
+    map_clean(&mtl->map_Kd);
+    map_clean(&mtl->map_Ks);
+    map_clean(&mtl->map_Ke);
+    map_clean(&mtl->map_Kt);
+    map_clean(&mtl->map_Ns);
+    map_clean(&mtl->map_Ni);
+    map_clean(&mtl->map_d);
+    map_clean(&mtl->map_bump);
+
+    memory_dealloc(mtl->name);
+}
+
+
+static
+const char* read_mtl_int(const char* p, int* v)
+{
+    return parse_int(p, v);
+}
+
+
+static
+const char* read_mtl_single(const char* p, float* v)
+{
+    return parse_float(p, v);
+}
+
+
+static
+const char* read_mtl_triple(const char* p, float v[3])
+{
+    p = read_mtl_single(p, &v[0]);
+    p = read_mtl_single(p, &v[1]);
+    p = read_mtl_single(p, &v[2]);
+
+    return p;
+}
+
+
+static
+const char* read_map(fastObjData* data, const char* ptr, fastObjTexture* map)
+{
+    const char* s;
+    const char* e;
+    char*       name;
+    char*       path;
+
+    ptr = skip_whitespace(ptr);
+
+    /* Don't support options at present */
+    if (*ptr == '-')
+        return ptr;
+
+
+    /* Read name */
+    s = ptr;
+    while (!is_whitespace(*ptr) && !is_newline(*ptr))
+        ptr++;
+
+    e = ptr;
+
+    name = string_copy(s, e);
+
+    path = string_concat(data->base, s, e);
+    string_fix_separators(path);
+
+    map->name = name;
+    map->path = path;
+
+    return e;
+}
+
+
+static
+int read_mtllib(fastObjData* data, void* file)
+{
+    unsigned long   n;
+    const char*     s;
+    char*           contents;
+    size_t          l;
+    const char*     p;
+    const char*     e;
+    int             found_d;
+    fastObjMaterial mtl;
+
+
+    /* Read entire file */
+    n = file_size(file);
+
+    contents = (char*)(memory_realloc(0, n + 1));
+    if (!contents)
+        return 0;
+
+    l = file_read(file, contents, n);
+    contents[l] = '\n';
+
+    mtl = mtl_default();
+
+    found_d = 0;
+
+    p = contents;
+    e = contents + l;
+    while (p < e)
+    {
+        p = skip_whitespace(p);
+
+        switch (*p)
+        {
+        case 'n':
+            p++;
+            if (p[0] == 'e' &&
+                p[1] == 'w' &&
+                p[2] == 'm' &&
+                p[3] == 't' &&
+                p[4] == 'l' &&
+                is_whitespace(p[5]))
+            {
+                /* Push previous material (if there is one) */
+                if (mtl.name)
+                {
+                    array_push(data->mesh->materials, mtl);
+                    mtl = mtl_default();
+                }
+
+
+                /* Read name */
+                p += 5;
+
+                while (is_whitespace(*p))
+                    p++;
+
+                s = p;
+                while (!is_whitespace(*p) && !is_newline(*p))
+                    p++;
+
+                mtl.name = string_copy(s, p);
+            }
+            break;
+
+        case 'K':
+            if (p[1] == 'a')
+                p = read_mtl_triple(p + 2, mtl.Ka);
+            else if (p[1] == 'd')
+                p = read_mtl_triple(p + 2, mtl.Kd);
+            else if (p[1] == 's')
+                p = read_mtl_triple(p + 2, mtl.Ks);
+            else if (p[1] == 'e')
+                p = read_mtl_triple(p + 2, mtl.Ke);
+            else if (p[1] == 't')
+                p = read_mtl_triple(p + 2, mtl.Kt);
+            break;
+
+        case 'N':
+            if (p[1] == 's')
+                p = read_mtl_single(p + 2, &mtl.Ns);
+            else if (p[1] == 'i')
+                p = read_mtl_single(p + 2, &mtl.Ni);
+            break;
+
+        case 'T':
+            if (p[1] == 'r')
+            {
+                float Tr;
+                p = read_mtl_single(p + 2, &Tr);
+                if (!found_d)
+                {
+                    /* Ignore Tr if we've already read d */
+                    mtl.d = 1.0f - Tr;
+                }
+            }
+            else if (p[1] == 'f')
+                p = read_mtl_triple(p + 2, mtl.Tf);
+            break;
+
+        case 'd':
+            if (is_whitespace(p[1]))
+            {
+                p = read_mtl_single(p + 1, &mtl.d);
+                found_d = 1;
+            }
+            break;
+
+        case 'i':
+            p++;
+            if (p[0] == 'l' &&
+                p[1] == 'l' &&
+                p[2] == 'u' &&
+                p[3] == 'm' &&
+                is_whitespace(p[4]))
+            {
+                p = read_mtl_int(p + 4, &mtl.illum);
+            }
+            break;
+
+        case 'm':
+            p++;
+            if (p[0] == 'a' &&
+                p[1] == 'p' &&
+                p[2] == '_')
+            {
+                p += 3;
+                if (*p == 'K')
+                {
+                    p++;
+                    if (is_whitespace(p[1]))
+                    {
+                        if (*p == 'a')
+                            p = read_map(data, p + 1, &mtl.map_Ka);
+                        else if (*p == 'd')
+                            p = read_map(data, p + 1, &mtl.map_Kd);
+                        else if (*p == 's')
+                            p = read_map(data, p + 1, &mtl.map_Ks);
+                        else if (*p == 'e')
+                            p = read_map(data, p + 1, &mtl.map_Ke);
+                        else if (*p == 't')
+                            p = read_map(data, p + 1, &mtl.map_Kt);
+                    }
+                }
+                else if (*p == 'N')
+                {
+                    p++;
+                    if (is_whitespace(p[1]))
+                    {
+                        if (*p == 's')
+                            p = read_map(data, p + 1, &mtl.map_Ns);
+                        else if (*p == 'i')
+                            p = read_map(data, p + 1, &mtl.map_Ni);
+                    }
+                }
+                else if (*p == 'd')
+                {
+                    p++;
+                    if (is_whitespace(*p))
+                        p = read_map(data, p, &mtl.map_d);
+                }
+                else if (p[0] == 'b' &&
+                         p[1] == 'u' &&
+                         p[2] == 'm' &&
+                         p[3] == 'p' &&
+                         is_whitespace(p[4]))
+                {
+                    p = read_map(data, p + 4, &mtl.map_d);
+                }
+            }
+            break;
+
+        case '#':
+            break;
+        }
+
+        p = skip_line(p);
+    }
+
+    /* Push final material */
+    if (mtl.name)
+        array_push(data->mesh->materials, mtl);
+
+    memory_dealloc(contents);
+
+    return 1;
+}
+
+
+static
+const char* parse_mtllib(fastObjData* data, const char* ptr)
+{
+    const char* s;
+    const char* e;
+    char*       lib;
+    void*       file;
+
+
+    ptr = skip_whitespace(ptr);
+
+    s = ptr;
+    while (!is_whitespace(*ptr) && !is_newline(*ptr))
+        ptr++;
+
+    e = ptr;
+
+    lib = string_concat(data->base, s, e);
+    if (lib)
+    {
+        string_fix_separators(lib);
+
+        file = file_open(lib);
+        if (file)
+        {
+            read_mtllib(data, file);
+            file_close(file);
+        }
+
+        memory_dealloc(lib);
+    }
+
+    return ptr;
+}
+
+
+static
+void parse_buffer(fastObjData* data, const char* ptr, const char* end)
+{
+    const char* p;
+    
+    
+    p = ptr;
+    while (p != end)
+    {
+        p = skip_whitespace(p);
+
+        switch (*p)
+        {
+        case 'v':
+            p++;
+
+            switch (*p++)
+            {
+            case ' ':
+            case '\t':
+                p = parse_vertex(data, p);
+                break;
+
+            case 't':
+                p = parse_texcoord(data, p);
+                break;
+
+            case 'n':
+                p = parse_normal(data, p);
+                break;
+
+            default:
+                p--; /* roll p++ back in case *p was a newline */
+            }
+            break;
+
+        case 'f':
+            p++;
+
+            switch (*p++)
+            {
+            case ' ':
+            case '\t':
+                p = parse_face(data, p);
+                break;
+
+            default:
+                p--; /* roll p++ back in case *p was a newline */
+            }
+            break;
+
+        case 'g':
+            p++;
+
+            switch (*p++)
+            {
+            case ' ':
+            case '\t':
+                p = parse_group(data, p);
+                break;
+
+            default:
+                p--; /* roll p++ back in case *p was a newline */
+            }
+            break;
+
+        case 'm':
+            p++;
+            if (p[0] == 't' &&
+                p[1] == 'l' &&
+                p[2] == 'l' &&
+                p[3] == 'i' &&
+                p[4] == 'b' &&
+                is_whitespace(p[5]))
+                p = parse_mtllib(data, p + 5);
+            break;
+
+        case 'u':
+            p++;
+            if (p[0] == 's' &&
+                p[1] == 'e' &&
+                p[2] == 'm' &&
+                p[3] == 't' &&
+                p[4] == 'l' &&
+                is_whitespace(p[5]))
+                p = parse_usemtl(data, p + 5);
+            break;
+
+        case '#':
+            break;
+        }
+
+        p = skip_line(p);
+
+        data->line++;
+    }
+}
+
+
+void fast_obj_destroy(fastObjMesh* m)
+{
+    unsigned int ii;
+
+
+    for (ii = 0; ii < array_size(m->groups); ii++)
+        group_clean(&m->groups[ii]);
+
+    for (ii = 0; ii < array_size(m->materials); ii++)
+        mtl_clean(&m->materials[ii]);
+
+    array_clean(m->positions);
+    array_clean(m->texcoords);
+    array_clean(m->normals);
+    array_clean(m->face_vertices);
+    array_clean(m->face_materials);
+    array_clean(m->indices);
+    array_clean(m->groups);
+    array_clean(m->materials);
+
+    memory_dealloc(m);
+}
+
+
+fastObjMesh* fast_obj_read(const char* path)
+{
+    fastObjData  data;
+    fastObjMesh* m;
+    void*        file;
+    char*        buffer;
+    char*        start;
+    char*        end;
+    char*        last;
+    unsigned int read;
+    size_t       sep;
+    unsigned int bytes;
+
+
+    /* Open file */
+    file = file_open(path);
+    if (!file)
+        return 0;
+
+
+    /* Empty mesh */
+    m = (fastObjMesh*)(memory_realloc(0, sizeof(fastObjMesh)));
+    if (!m)
+        return 0;
+
+    m->positions      = 0;
+    m->texcoords      = 0;
+    m->normals        = 0;
+    m->face_vertices  = 0;
+    m->face_materials = 0;
+    m->indices        = 0;
+    m->materials      = 0;
+    m->groups         = 0;
+
+
+    /* Add dummy position/texcoord/normal */
+    array_push(m->positions, 0.0f);
+    array_push(m->positions, 0.0f);
+    array_push(m->positions, 0.0f);
+
+    array_push(m->texcoords, 0.0f);
+    array_push(m->texcoords, 0.0f);
+
+    array_push(m->normals, 0.0f);
+    array_push(m->normals, 0.0f);
+    array_push(m->normals, 1.0f);
+
+
+    /* Data needed during parsing */
+    data.mesh     = m;
+    data.group    = group_default();
+    data.material = 0;
+    data.line     = 1;
+    data.base     = 0;
+
+
+    /* Find base path for materials/textures */
+    if (string_find_last(path, FAST_OBJ_SEPARATOR, &sep))
+        data.base = string_substr(path, 0, sep + 1);
+
+
+    /* Create buffer for reading file */
+    buffer = (char*)(memory_realloc(0, 2 * BUFFER_SIZE * sizeof(char)));
+    if (!buffer)
+        return 0;
+
+    start = buffer;
+    for (;;)
+    {
+        /* Read another buffer's worth from file */
+        read = (unsigned int)(file_read(file, start, BUFFER_SIZE));
+        if (read == 0 && start == buffer)
+            break;
+
+
+        /* Ensure buffer ends in a newline */
+        if (read < BUFFER_SIZE)
+        {
+            if (read == 0 || start[read - 1] != '\n')
+                start[read++] = '\n';
+        }
+
+        end = start + read;
+        if (end == buffer)
+            break;
+
+
+        /* Find last new line */
+        last = end;
+        while (last > buffer)
+        {
+            last--;
+            if (*last == '\n')
+                break;
+        }
+
+
+        /* Check there actually is a new line */
+        if (*last != '\n')
+            break;
+
+        last++;
+
+
+        /* Process buffer */
+        parse_buffer(&data, buffer, last);
+
+
+        /* Copy overflow for next buffer */
+        bytes = (unsigned int)(end - last);
+        memcpy(buffer, last, bytes);
+        start = buffer + bytes;
+    }
+
+
+    /* Flush final group */
+    flush_output(&data);
+    group_clean(&data.group);
+
+    m->position_count = array_size(m->positions) / 3;
+    m->texcoord_count = array_size(m->texcoords) / 2;
+    m->normal_count   = array_size(m->normals) / 3;
+    m->face_count     = array_size(m->face_vertices);
+    m->material_count = array_size(m->materials);
+    m->group_count    = array_size(m->groups);
+
+
+    /* Clean up */
+    memory_dealloc(buffer);
+    memory_dealloc(data.base);
+
+    file_close(file);
+
+    return m;
+}
+
+#endif
+

+ 3493 - 0
3rdparty/meshoptimizer/tools/gltfpack.cpp

@@ -0,0 +1,3493 @@
+// gltfpack is part of meshoptimizer library; see meshoptimizer.h for version/license details
+//
+// gltfpack is a command-line tool that takes a glTF file as an input and can produce two types of files:
+// - regular glb/gltf files that use data that has been optimized for GPU consumption using various cache optimizers
+// and quantization
+// - packed glb/gltf files that additionally use meshoptimizer codecs to reduce the size of vertex/index data; these
+// files can be further compressed with deflate/etc.
+//
+// To load regular glb files, it should be sufficient to use a standard glTF loader (although note that these files
+// use quantized position/texture coordinates that are technically invalid per spec; THREE.js and BabylonJS support
+// these files out of the box).
+// To load packed glb files, meshoptimizer vertex decoder needs to be integrated into the loader; demo/GLTFLoader.js
+// contains a work-in-progress loader - please note that the extension specification isn't ready yet so the format
+// will change!
+
+#ifndef _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#ifndef _CRT_NONSTDC_NO_WARNINGS
+#define _CRT_NONSTDC_NO_WARNINGS
+#endif
+
+#include "../src/meshoptimizer.h"
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include <float.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "cgltf.h"
+#include "fast_obj.h"
+
+struct Attr
+{
+	float f[4];
+};
+
+struct Stream
+{
+	cgltf_attribute_type type;
+	int index;
+	int target; // 0 = base mesh, 1+ = morph target
+
+	std::vector<Attr> data;
+};
+
+struct Mesh
+{
+	cgltf_node* node;
+
+	cgltf_material* material;
+	cgltf_skin* skin;
+
+	std::vector<Stream> streams;
+	std::vector<unsigned int> indices;
+
+	size_t targets;
+	std::vector<float> weights;
+};
+
+struct Settings
+{
+	int pos_bits;
+	int tex_bits;
+	int nrm_bits;
+	bool nrm_unit;
+
+	int anim_freq;
+	bool anim_const;
+
+	bool keep_named;
+
+	bool compress;
+	int verbose;
+};
+
+struct QuantizationParams
+{
+	float pos_offset[3];
+	float pos_scale;
+	int pos_bits;
+
+	float uv_offset[2];
+	float uv_scale[2];
+	int uv_bits;
+};
+
+struct StreamFormat
+{
+	cgltf_type type;
+	cgltf_component_type component_type;
+	bool normalized;
+	size_t stride;
+};
+
+struct NodeInfo
+{
+	bool keep;
+	bool animated;
+
+	unsigned int animated_paths;
+
+	int remap;
+	std::vector<size_t> meshes;
+};
+
+struct MaterialInfo
+{
+	bool keep;
+
+	int remap;
+};
+
+struct BufferView
+{
+	enum Kind
+	{
+		Kind_Vertex,
+		Kind_Index,
+		Kind_Skin,
+		Kind_Time,
+		Kind_Keyframe,
+		Kind_Image,
+		Kind_Count
+	};
+
+	Kind kind;
+	int variant;
+	size_t stride;
+	bool compressed;
+
+	std::string data;
+
+	size_t bytes;
+};
+
+const char* getError(cgltf_result result)
+{
+	switch (result)
+	{
+	case cgltf_result_file_not_found:
+		return "file not found";
+
+	case cgltf_result_io_error:
+		return "I/O error";
+
+	case cgltf_result_invalid_json:
+		return "invalid JSON";
+
+	case cgltf_result_invalid_gltf:
+		return "invalid GLTF";
+
+	case cgltf_result_out_of_memory:
+		return "out of memory";
+
+	default:
+		return "unknown error";
+	}
+}
+
+cgltf_accessor* getAccessor(const cgltf_attribute* attributes, size_t attribute_count, cgltf_attribute_type type, int index = 0)
+{
+	for (size_t i = 0; i < attribute_count; ++i)
+		if (attributes[i].type == type && attributes[i].index == index)
+			return attributes[i].data;
+
+	return 0;
+}
+
+void transformPosition(float* ptr, const float* transform)
+{
+	float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8] + transform[12];
+	float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9] + transform[13];
+	float z = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10] + transform[14];
+
+	ptr[0] = x;
+	ptr[1] = y;
+	ptr[2] = z;
+}
+
+void transformNormal(float* ptr, const float* transform)
+{
+	float x = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8];
+	float y = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9];
+	float z = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10];
+
+	float l = sqrtf(x * x + y * y + z * z);
+	float s = (l == 0.f) ? 0.f : 1 / l;
+
+	ptr[0] = x * s;
+	ptr[1] = y * s;
+	ptr[2] = z * s;
+}
+
+void transformMesh(Mesh& mesh, const cgltf_node* node)
+{
+	float transform[16];
+	cgltf_node_transform_world(node, transform);
+
+	for (size_t si = 0; si < mesh.streams.size(); ++si)
+	{
+		Stream& stream = mesh.streams[si];
+
+		if (stream.type == cgltf_attribute_type_position)
+		{
+			for (size_t i = 0; i < stream.data.size(); ++i)
+				transformPosition(stream.data[i].f, transform);
+		}
+		else if (stream.type == cgltf_attribute_type_normal || stream.type == cgltf_attribute_type_tangent)
+		{
+			for (size_t i = 0; i < stream.data.size(); ++i)
+				transformNormal(stream.data[i].f, transform);
+		}
+	}
+}
+
+void parseMeshesGltf(cgltf_data* data, std::vector<Mesh>& meshes)
+{
+	for (size_t ni = 0; ni < data->nodes_count; ++ni)
+	{
+		cgltf_node& node = data->nodes[ni];
+
+		if (!node.mesh)
+			continue;
+
+		const cgltf_mesh& mesh = *node.mesh;
+		int mesh_id = int(&mesh - data->meshes);
+
+		for (size_t pi = 0; pi < mesh.primitives_count; ++pi)
+		{
+			const cgltf_primitive& primitive = mesh.primitives[pi];
+
+			if (!primitive.indices || !primitive.indices->buffer_view)
+			{
+				fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because it has no index data\n", int(pi), mesh_id);
+				continue;
+			}
+
+			if (primitive.type != cgltf_primitive_type_triangles)
+			{
+				fprintf(stderr, "Warning: ignoring primitive %d of mesh %d because type %d is not supported\n", int(pi), mesh_id, primitive.type);
+				continue;
+			}
+
+			Mesh result;
+
+			result.node = &node;
+
+			result.material = primitive.material;
+			result.skin = node.skin;
+
+			result.indices.resize(primitive.indices->count);
+			for (size_t i = 0; i < primitive.indices->count; ++i)
+				result.indices[i] = unsigned(cgltf_accessor_read_index(primitive.indices, i));
+
+			for (size_t ai = 0; ai < primitive.attributes_count; ++ai)
+			{
+				const cgltf_attribute& attr = primitive.attributes[ai];
+
+				if (attr.type == cgltf_attribute_type_invalid)
+				{
+					fprintf(stderr, "Warning: ignoring unknown attribute %s in primitive %d of mesh %d\n", attr.name, int(pi), mesh_id);
+					continue;
+				}
+
+				Stream s = {attr.type, attr.index};
+				s.data.resize(attr.data->count);
+
+				for (size_t i = 0; i < attr.data->count; ++i)
+					cgltf_accessor_read_float(attr.data, i, s.data[i].f, 4);
+
+				result.streams.push_back(s);
+			}
+
+			for (size_t ti = 0; ti < primitive.targets_count; ++ti)
+			{
+				const cgltf_morph_target& target = primitive.targets[ti];
+
+				for (size_t ai = 0; ai < target.attributes_count; ++ai)
+				{
+					const cgltf_attribute& attr = target.attributes[ai];
+
+					if (attr.type == cgltf_attribute_type_invalid)
+					{
+						fprintf(stderr, "Warning: ignoring unknown attribute %s in morph target %d of primitive %d of mesh %d\n", attr.name, int(ti), int(pi), mesh_id);
+						continue;
+					}
+
+					Stream s = {attr.type, attr.index, int(ti + 1)};
+					s.data.resize(attr.data->count);
+
+					for (size_t i = 0; i < attr.data->count; ++i)
+						cgltf_accessor_read_float(attr.data, i, s.data[i].f, 4);
+
+					result.streams.push_back(s);
+				}
+			}
+
+			result.targets = primitive.targets_count;
+			result.weights.assign(mesh.weights, mesh.weights + mesh.weights_count);
+
+			meshes.push_back(result);
+		}
+	}
+}
+
+void defaultFree(void*, void* p)
+{
+	free(p);
+}
+
+size_t textureIndex(const std::vector<std::string>& textures, const std::string& name)
+{
+	std::vector<std::string>::const_iterator it = std::lower_bound(textures.begin(), textures.end(), name);
+	assert(it != textures.end());
+	assert(*it == name);
+
+	return size_t(it - textures.begin());
+}
+
+cgltf_data* parseSceneObj(fastObjMesh* obj)
+{
+	cgltf_data* data = (cgltf_data*)calloc(1, sizeof(cgltf_data));
+	data->memory_free = defaultFree;
+
+	std::vector<std::string> textures;
+
+	for (unsigned int mi = 0; mi < obj->material_count; ++mi)
+	{
+		fastObjMaterial& om = obj->materials[mi];
+
+		if (om.map_Kd.name)
+			textures.push_back(om.map_Kd.name);
+	}
+
+	std::sort(textures.begin(), textures.end());
+	textures.erase(std::unique(textures.begin(), textures.end()), textures.end());
+
+	data->images = (cgltf_image*)calloc(textures.size(), sizeof(cgltf_image));
+	data->images_count = textures.size();
+
+	for (size_t i = 0; i < textures.size(); ++i)
+	{
+		data->images[i].uri = strdup(textures[i].c_str());
+	}
+
+	data->textures = (cgltf_texture*)calloc(textures.size(), sizeof(cgltf_texture));
+	data->textures_count = textures.size();
+
+	for (size_t i = 0; i < textures.size(); ++i)
+	{
+		data->textures[i].image = &data->images[i];
+	}
+
+	data->materials = (cgltf_material*)calloc(obj->material_count, sizeof(cgltf_material));
+	data->materials_count = obj->material_count;
+
+	for (unsigned int mi = 0; mi < obj->material_count; ++mi)
+	{
+		cgltf_material& gm = data->materials[mi];
+		fastObjMaterial& om = obj->materials[mi];
+
+		gm.has_pbr_metallic_roughness = true;
+		gm.pbr_metallic_roughness.base_color_factor[0] = 1.0f;
+		gm.pbr_metallic_roughness.base_color_factor[1] = 1.0f;
+		gm.pbr_metallic_roughness.base_color_factor[2] = 1.0f;
+		gm.pbr_metallic_roughness.base_color_factor[3] = 1.0f;
+		gm.pbr_metallic_roughness.metallic_factor = 0.0f;
+		gm.pbr_metallic_roughness.roughness_factor = 1.0f;
+
+		gm.alpha_cutoff = 0.5f;
+
+		if (om.map_Kd.name)
+		{
+			gm.pbr_metallic_roughness.base_color_texture.texture = &data->textures[textureIndex(textures, om.map_Kd.name)];
+			gm.pbr_metallic_roughness.base_color_texture.scale = 1.0f;
+
+			gm.alpha_mode = (om.illum == 4 || om.illum == 6 || om.illum == 7 || om.illum == 9) ? cgltf_alpha_mode_mask : cgltf_alpha_mode_opaque;
+		}
+
+		if (om.map_d.name)
+		{
+			gm.alpha_mode = cgltf_alpha_mode_blend;
+		}
+	}
+
+	return data;
+}
+
+void parseMeshesObj(fastObjMesh* obj, cgltf_data* data, std::vector<Mesh>& meshes)
+{
+	unsigned int material_count = std::max(obj->material_count, 1u);
+
+	std::vector<size_t> vertex_count(material_count);
+	std::vector<size_t> index_count(material_count);
+
+	for (unsigned int fi = 0; fi < obj->face_count; ++fi)
+	{
+		unsigned int mi = obj->face_materials[fi];
+
+		vertex_count[mi] += obj->face_vertices[fi];
+		index_count[mi] += (obj->face_vertices[fi] - 2) * 3;
+	}
+
+	std::vector<size_t> mesh_index(material_count);
+
+	for (unsigned int mi = 0; mi < material_count; ++mi)
+	{
+		if (index_count[mi] == 0)
+			continue;
+
+		mesh_index[mi] = meshes.size();
+
+		meshes.push_back(Mesh());
+
+		Mesh& mesh = meshes.back();
+
+		if (data->materials_count)
+		{
+			assert(mi < data->materials_count);
+			mesh.material = &data->materials[mi];
+		}
+
+		mesh.streams.resize(3);
+		mesh.streams[0].type = cgltf_attribute_type_position;
+		mesh.streams[0].data.resize(vertex_count[mi]);
+		mesh.streams[1].type = cgltf_attribute_type_normal;
+		mesh.streams[1].data.resize(vertex_count[mi]);
+		mesh.streams[2].type = cgltf_attribute_type_texcoord;
+		mesh.streams[2].data.resize(vertex_count[mi]);
+		mesh.indices.resize(index_count[mi]);
+		mesh.targets = 0;
+	}
+
+	std::vector<size_t> vertex_offset(material_count);
+	std::vector<size_t> index_offset(material_count);
+
+	size_t group_offset = 0;
+
+	for (unsigned int fi = 0; fi < obj->face_count; ++fi)
+	{
+		unsigned int mi = obj->face_materials[fi];
+		Mesh& mesh = meshes[mesh_index[mi]];
+
+		size_t vo = vertex_offset[mi];
+		size_t io = index_offset[mi];
+
+		for (unsigned int vi = 0; vi < obj->face_vertices[fi]; ++vi)
+		{
+			fastObjIndex ii = obj->indices[group_offset + vi];
+
+			Attr p = {{obj->positions[ii.p * 3 + 0], obj->positions[ii.p * 3 + 1], obj->positions[ii.p * 3 + 2]}};
+			Attr n = {{obj->normals[ii.n * 3 + 0], obj->normals[ii.n * 3 + 1], obj->normals[ii.n * 3 + 2]}};
+			Attr t = {{obj->texcoords[ii.t * 2 + 0], 1.f - obj->texcoords[ii.t * 2 + 1]}};
+
+			mesh.streams[0].data[vo + vi] = p;
+			mesh.streams[1].data[vo + vi] = n;
+			mesh.streams[2].data[vo + vi] = t;
+		}
+
+		for (unsigned int vi = 2; vi < obj->face_vertices[fi]; ++vi)
+		{
+			size_t to = io + (vi - 2) * 3;
+
+			mesh.indices[to + 0] = unsigned(vo);
+			mesh.indices[to + 1] = unsigned(vo + vi - 1);
+			mesh.indices[to + 2] = unsigned(vo + vi);
+		}
+
+		vertex_offset[mi] += obj->face_vertices[fi];
+		index_offset[mi] += (obj->face_vertices[fi] - 2) * 3;
+		group_offset += obj->face_vertices[fi];
+	}
+}
+
+bool areTextureViewsEqual(const cgltf_texture_view& lhs, const cgltf_texture_view& rhs)
+{
+	if (lhs.has_transform != rhs.has_transform)
+		return false;
+
+	if (lhs.has_transform)
+	{
+		const cgltf_texture_transform& lt = lhs.transform;
+		const cgltf_texture_transform& rt = rhs.transform;
+
+		if (memcmp(lt.offset, rt.offset, sizeof(cgltf_float) * 2) != 0)
+			return false;
+
+		if (lt.rotation != rt.rotation)
+			return false;
+
+		if (memcmp(lt.scale, rt.scale, sizeof(cgltf_float) * 2) != 0)
+			return false;
+
+		if (lt.texcoord != rt.texcoord)
+			return false;
+	}
+
+	if (lhs.texture != rhs.texture)
+		return false;
+
+	if (lhs.texcoord != rhs.texcoord)
+		return false;
+
+	if (lhs.scale != rhs.scale)
+		return false;
+
+	return true;
+}
+
+bool areMaterialsEqual(const cgltf_material& lhs, const cgltf_material& rhs)
+{
+	if (lhs.has_pbr_metallic_roughness != rhs.has_pbr_metallic_roughness)
+		return false;
+
+	if (lhs.has_pbr_metallic_roughness)
+	{
+		const cgltf_pbr_metallic_roughness& lpbr = lhs.pbr_metallic_roughness;
+		const cgltf_pbr_metallic_roughness& rpbr = rhs.pbr_metallic_roughness;
+
+		if (!areTextureViewsEqual(lpbr.base_color_texture, rpbr.base_color_texture))
+			return false;
+
+		if (!areTextureViewsEqual(lpbr.metallic_roughness_texture, rpbr.metallic_roughness_texture))
+			return false;
+
+		if (memcmp(lpbr.base_color_factor, rpbr.base_color_factor, sizeof(cgltf_float) * 4) != 0)
+			return false;
+
+		if (lpbr.metallic_factor != rpbr.metallic_factor)
+			return false;
+
+		if (lpbr.roughness_factor != rpbr.roughness_factor)
+			return false;
+	}
+
+	if (lhs.has_pbr_specular_glossiness != rhs.has_pbr_specular_glossiness)
+		return false;
+
+	if (lhs.has_pbr_specular_glossiness)
+	{
+		const cgltf_pbr_specular_glossiness& lpbr = lhs.pbr_specular_glossiness;
+		const cgltf_pbr_specular_glossiness& rpbr = rhs.pbr_specular_glossiness;
+
+		if (!areTextureViewsEqual(lpbr.diffuse_texture, rpbr.diffuse_texture))
+			return false;
+
+		if (!areTextureViewsEqual(lpbr.specular_glossiness_texture, rpbr.specular_glossiness_texture))
+			return false;
+
+		if (memcmp(lpbr.diffuse_factor, rpbr.diffuse_factor, sizeof(cgltf_float) * 4) != 0)
+			return false;
+
+		if (memcmp(lpbr.specular_factor, rpbr.specular_factor, sizeof(cgltf_float) * 3) != 0)
+			return false;
+
+		if (lpbr.glossiness_factor != rpbr.glossiness_factor)
+			return false;
+	}
+
+	if (!areTextureViewsEqual(lhs.normal_texture, rhs.normal_texture))
+		return false;
+
+	if (!areTextureViewsEqual(lhs.occlusion_texture, rhs.occlusion_texture))
+		return false;
+
+	if (!areTextureViewsEqual(lhs.emissive_texture, rhs.emissive_texture))
+		return false;
+
+	if (memcmp(lhs.emissive_factor, rhs.emissive_factor, sizeof(cgltf_float) * 3) != 0)
+		return false;
+
+	if (lhs.alpha_mode != rhs.alpha_mode)
+		return false;
+
+	if (lhs.alpha_cutoff != rhs.alpha_cutoff)
+		return false;
+
+	if (lhs.double_sided != rhs.double_sided)
+		return false;
+
+	if (lhs.unlit != rhs.unlit)
+		return false;
+
+	return true;
+}
+
+void mergeMeshMaterials(cgltf_data* data, std::vector<Mesh>& meshes)
+{
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		Mesh& mesh = meshes[i];
+
+		if (mesh.indices.empty())
+			continue;
+
+		if (!mesh.material)
+			continue;
+
+		for (int j = 0; j < mesh.material - data->materials; ++j)
+		{
+			if (areMaterialsEqual(*mesh.material, data->materials[j]))
+			{
+				mesh.material = &data->materials[j];
+				break;
+			}
+		}
+	}
+}
+
+bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings)
+{
+	if (lhs.node != rhs.node)
+	{
+		if (!lhs.node || !rhs.node)
+			return false;
+
+		if (lhs.node->parent != rhs.node->parent)
+			return false;
+
+		bool lhs_transform = lhs.node->has_translation | lhs.node->has_rotation | lhs.node->has_scale | lhs.node->has_matrix | (!!lhs.node->weights);
+		bool rhs_transform = rhs.node->has_translation | rhs.node->has_rotation | rhs.node->has_scale | rhs.node->has_matrix | (!!rhs.node->weights);
+
+		if (lhs_transform || rhs_transform)
+			return false;
+
+		if (settings.keep_named)
+		{
+			if (lhs.node->name && *lhs.node->name)
+				return false;
+
+			if (rhs.node->name && *rhs.node->name)
+				return false;
+		}
+
+		// we can merge nodes that don't have transforms of their own and have the same parent
+		// this is helpful when instead of splitting mesh into primitives, DCCs split mesh into mesh nodes
+	}
+
+	if (lhs.material != rhs.material)
+		return false;
+
+	if (lhs.skin != rhs.skin)
+		return false;
+
+	if (lhs.targets != rhs.targets)
+		return false;
+
+	if (lhs.weights.size() != rhs.weights.size())
+		return false;
+
+	for (size_t i = 0; i < lhs.weights.size(); ++i)
+		if (lhs.weights[i] != rhs.weights[i])
+			return false;
+
+	if (lhs.streams.size() != rhs.streams.size())
+		return false;
+
+	for (size_t i = 0; i < lhs.streams.size(); ++i)
+		if (lhs.streams[i].type != rhs.streams[i].type || lhs.streams[i].index != rhs.streams[i].index || lhs.streams[i].target != rhs.streams[i].target)
+			return false;
+
+	return true;
+}
+
+void mergeMeshes(Mesh& target, const Mesh& mesh)
+{
+	assert(target.streams.size() == mesh.streams.size());
+
+	size_t vertex_offset = target.streams[0].data.size();
+	size_t index_offset = target.indices.size();
+
+	for (size_t i = 0; i < target.streams.size(); ++i)
+		target.streams[i].data.insert(target.streams[i].data.end(), mesh.streams[i].data.begin(), mesh.streams[i].data.end());
+
+	target.indices.resize(target.indices.size() + mesh.indices.size());
+
+	size_t index_count = mesh.indices.size();
+
+	for (size_t i = 0; i < index_count; ++i)
+		target.indices[index_offset + i] = unsigned(vertex_offset + mesh.indices[i]);
+}
+
+void mergeMeshes(std::vector<Mesh>& meshes, const Settings& settings)
+{
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		Mesh& mesh = meshes[i];
+
+		for (size_t j = 0; j < i; ++j)
+		{
+			Mesh& target = meshes[j];
+
+			if (target.indices.size() && canMergeMeshes(mesh, target, settings))
+			{
+				mergeMeshes(target, mesh);
+
+				mesh.streams.clear();
+				mesh.indices.clear();
+
+				break;
+			}
+		}
+	}
+}
+
+void reindexMesh(Mesh& mesh)
+{
+	size_t total_vertices = mesh.streams[0].data.size();
+	size_t total_indices = mesh.indices.size();
+
+	std::vector<meshopt_Stream> streams;
+	for (size_t i = 0; i < mesh.streams.size(); ++i)
+	{
+		if (mesh.streams[i].target)
+			continue;
+
+		assert(mesh.streams[i].data.size() == total_vertices);
+
+		meshopt_Stream stream = {&mesh.streams[i].data[0], sizeof(Attr), sizeof(Attr)};
+		streams.push_back(stream);
+	}
+
+	std::vector<unsigned int> remap(total_vertices);
+	size_t unique_vertices = meshopt_generateVertexRemapMulti(&remap[0], &mesh.indices[0], total_indices, total_vertices, &streams[0], streams.size());
+	assert(unique_vertices <= total_vertices);
+
+	meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], total_indices, &remap[0]);
+
+	for (size_t i = 0; i < mesh.streams.size(); ++i)
+	{
+		assert(mesh.streams[i].data.size() == total_vertices);
+
+		meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], total_vertices, sizeof(Attr), &remap[0]);
+		mesh.streams[i].data.resize(unique_vertices);
+	}
+}
+
+void optimizeMesh(Mesh& mesh)
+{
+	size_t vertex_count = mesh.streams[0].data.size();
+
+	meshopt_optimizeVertexCache(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), vertex_count);
+
+	std::vector<unsigned int> remap(vertex_count);
+	size_t unique_vertices = meshopt_optimizeVertexFetchRemap(&remap[0], &mesh.indices[0], mesh.indices.size(), vertex_count);
+
+	assert(unique_vertices == vertex_count);
+	(void)unique_vertices;
+
+	meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &remap[0]);
+
+	for (size_t i = 0; i < mesh.streams.size(); ++i)
+		meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], vertex_count, sizeof(Attr), &remap[0]);
+}
+
+bool getAttributeBounds(const std::vector<Mesh>& meshes, cgltf_attribute_type type, Attr& min, Attr& max)
+{
+	min.f[0] = min.f[1] = min.f[2] = min.f[3] = +FLT_MAX;
+	max.f[0] = max.f[1] = max.f[2] = max.f[3] = -FLT_MAX;
+
+	Attr pad = {};
+
+	bool valid = false;
+
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		const Mesh& mesh = meshes[i];
+
+		for (size_t j = 0; j < mesh.streams.size(); ++j)
+		{
+			const Stream& s = mesh.streams[j];
+
+			if (s.type == type)
+			{
+				if (s.target == 0)
+				{
+					for (size_t k = 0; k < s.data.size(); ++k)
+					{
+						const Attr& a = s.data[k];
+
+						min.f[0] = std::min(min.f[0], a.f[0]);
+						min.f[1] = std::min(min.f[1], a.f[1]);
+						min.f[2] = std::min(min.f[2], a.f[2]);
+						min.f[3] = std::min(min.f[3], a.f[3]);
+
+						max.f[0] = std::max(max.f[0], a.f[0]);
+						max.f[1] = std::max(max.f[1], a.f[1]);
+						max.f[2] = std::max(max.f[2], a.f[2]);
+						max.f[3] = std::max(max.f[3], a.f[3]);
+
+						valid = true;
+					}
+				}
+				else
+				{
+					for (size_t k = 0; k < s.data.size(); ++k)
+					{
+						const Attr& a = s.data[k];
+
+						pad.f[0] = std::max(pad.f[0], fabsf(a.f[0]));
+						pad.f[1] = std::max(pad.f[1], fabsf(a.f[1]));
+						pad.f[2] = std::max(pad.f[2], fabsf(a.f[2]));
+						pad.f[3] = std::max(pad.f[3], fabsf(a.f[3]));
+					}
+				}
+			}
+		}
+	}
+
+	if (valid)
+	{
+		for (int k = 0; k < 4; ++k)
+		{
+			min.f[k] -= pad.f[k];
+			max.f[k] += pad.f[k];
+		}
+	}
+
+	return valid;
+}
+
+QuantizationParams prepareQuantization(const std::vector<Mesh>& meshes, const Settings& settings)
+{
+	QuantizationParams result = {};
+
+	result.pos_bits = settings.pos_bits;
+
+	Attr pos_min, pos_max;
+	if (getAttributeBounds(meshes, cgltf_attribute_type_position, pos_min, pos_max))
+	{
+		result.pos_offset[0] = pos_min.f[0];
+		result.pos_offset[1] = pos_min.f[1];
+		result.pos_offset[2] = pos_min.f[2];
+		result.pos_scale = std::max(pos_max.f[0] - pos_min.f[0], std::max(pos_max.f[1] - pos_min.f[1], pos_max.f[2] - pos_min.f[2]));
+	}
+
+	result.uv_bits = settings.tex_bits;
+
+	Attr uv_min, uv_max;
+	if (getAttributeBounds(meshes, cgltf_attribute_type_texcoord, uv_min, uv_max))
+	{
+		result.uv_offset[0] = uv_min.f[0];
+		result.uv_offset[1] = uv_min.f[1];
+		result.uv_scale[0] = uv_max.f[0] - uv_min.f[0];
+		result.uv_scale[1] = uv_max.f[1] - uv_min.f[1];
+	}
+
+	return result;
+}
+
+void rescaleNormal(float& nx, float& ny, float& nz)
+{
+	// scale the normal to make sure the largest component is +-1.0
+	// this reduces the entropy of the normal by ~1.5 bits without losing precision
+	// it's better to use octahedral encoding but that requires special shader support
+	float nm = std::max(fabsf(nx), std::max(fabsf(ny), fabsf(nz)));
+	float ns = nm == 0.f ? 0.f : 1 / nm;
+
+	nx *= ns;
+	ny *= ns;
+	nz *= ns;
+}
+
+void renormalizeWeights(uint8_t (&w)[4])
+{
+	int sum = w[0] + w[1] + w[2] + w[3];
+
+	if (sum == 255)
+		return;
+
+	// we assume that the total error is limited to 0.5/component = 2
+	// this means that it's acceptable to adjust the max. component to compensate for the error
+	int max = 0;
+
+	for (int k = 1; k < 4; ++k)
+		if (w[k] > w[max])
+			max = k;
+
+	w[max] += uint8_t(255 - sum);
+}
+
+StreamFormat writeVertexStream(std::string& bin, const Stream& stream, const QuantizationParams& params, const Settings& settings, bool has_targets)
+{
+	if (stream.type == cgltf_attribute_type_position)
+	{
+		if (stream.target == 0)
+		{
+			float pos_rscale = params.pos_scale == 0.f ? 0.f : 1.f / params.pos_scale;
+
+			for (size_t i = 0; i < stream.data.size(); ++i)
+			{
+				const Attr& a = stream.data[i];
+
+				uint16_t v[4] = {
+				    uint16_t(meshopt_quantizeUnorm((a.f[0] - params.pos_offset[0]) * pos_rscale, params.pos_bits)),
+				    uint16_t(meshopt_quantizeUnorm((a.f[1] - params.pos_offset[1]) * pos_rscale, params.pos_bits)),
+				    uint16_t(meshopt_quantizeUnorm((a.f[2] - params.pos_offset[2]) * pos_rscale, params.pos_bits)),
+				    0};
+				bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+			}
+
+			StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16u, false, 8};
+			return format;
+		}
+		else
+		{
+			float pos_rscale = params.pos_scale == 0.f ? 0.f : 1.f / params.pos_scale;
+
+			for (size_t i = 0; i < stream.data.size(); ++i)
+			{
+				const Attr& a = stream.data[i];
+
+				int16_t v[4] = {
+				    int16_t((a.f[0] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[0]) * pos_rscale, params.pos_bits)),
+				    int16_t((a.f[1] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[1]) * pos_rscale, params.pos_bits)),
+				    int16_t((a.f[2] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[2]) * pos_rscale, params.pos_bits)),
+				    0};
+				bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+			}
+
+			StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_16, false, 8};
+			return format;
+		}
+	}
+	else if (stream.type == cgltf_attribute_type_texcoord)
+	{
+		float uv_rscale[2] = {
+		    params.uv_scale[0] == 0.f ? 0.f : 1.f / params.uv_scale[0],
+		    params.uv_scale[1] == 0.f ? 0.f : 1.f / params.uv_scale[1],
+		};
+
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			uint16_t v[2] = {
+			    uint16_t(meshopt_quantizeUnorm((a.f[0] - params.uv_offset[0]) * uv_rscale[0], params.uv_bits)),
+			    uint16_t(meshopt_quantizeUnorm((a.f[1] - params.uv_offset[1]) * uv_rscale[1], params.uv_bits)),
+			};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec2, cgltf_component_type_r_16u, false, 4};
+		return format;
+	}
+	else if (stream.type == cgltf_attribute_type_normal)
+	{
+		bool nrm_unit = has_targets || settings.nrm_unit;
+		int bits = nrm_unit ? 8 : settings.nrm_bits;
+
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			float nx = a.f[0], ny = a.f[1], nz = a.f[2];
+
+			if (!nrm_unit)
+				rescaleNormal(nx, ny, nz);
+
+			int8_t v[4] = {
+			    int8_t(meshopt_quantizeSnorm(nx, bits)),
+			    int8_t(meshopt_quantizeSnorm(ny, bits)),
+			    int8_t(meshopt_quantizeSnorm(nz, bits)),
+			    0};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_8, true, 4};
+		return format;
+	}
+	else if (stream.type == cgltf_attribute_type_tangent)
+	{
+		bool nrm_unit = has_targets || settings.nrm_unit;
+		int bits = nrm_unit ? 8 : settings.nrm_bits;
+
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			float nx = a.f[0], ny = a.f[1], nz = a.f[2], nw = a.f[3];
+
+			if (!nrm_unit)
+				rescaleNormal(nx, ny, nz);
+
+			int8_t v[4] = {
+			    int8_t(meshopt_quantizeSnorm(nx, bits)),
+			    int8_t(meshopt_quantizeSnorm(ny, bits)),
+			    int8_t(meshopt_quantizeSnorm(nz, bits)),
+			    int8_t(meshopt_quantizeSnorm(nw, 8))};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		if (stream.target == 0)
+		{
+			StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8, true, 4};
+			return format;
+		}
+		else
+		{
+			StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_8, true, 4};
+			return format;
+		}
+	}
+	else if (stream.type == cgltf_attribute_type_color)
+	{
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			uint8_t v[4] = {
+			    uint8_t(meshopt_quantizeUnorm(a.f[0], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[1], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[2], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[3], 8))};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4};
+		return format;
+	}
+	else if (stream.type == cgltf_attribute_type_weights)
+	{
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			uint8_t v[4] = {
+			    uint8_t(meshopt_quantizeUnorm(a.f[0], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[1], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[2], 8)),
+			    uint8_t(meshopt_quantizeUnorm(a.f[3], 8))};
+
+			renormalizeWeights(v);
+
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, true, 4};
+		return format;
+	}
+	else if (stream.type == cgltf_attribute_type_joints)
+	{
+		unsigned int maxj = 0;
+
+		for (size_t i = 0; i < stream.data.size(); ++i)
+			maxj = std::max(maxj, unsigned(stream.data[i].f[0]));
+
+		assert(maxj <= 65535);
+
+		if (maxj <= 255)
+		{
+			for (size_t i = 0; i < stream.data.size(); ++i)
+			{
+				const Attr& a = stream.data[i];
+
+				uint8_t v[4] = {
+				    uint8_t(a.f[0]),
+				    uint8_t(a.f[1]),
+				    uint8_t(a.f[2]),
+				    uint8_t(a.f[3])};
+				bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+			}
+
+			StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_8u, false, 4};
+			return format;
+		}
+		else
+		{
+			for (size_t i = 0; i < stream.data.size(); ++i)
+			{
+				const Attr& a = stream.data[i];
+
+				uint16_t v[4] = {
+				    uint16_t(a.f[0]),
+				    uint16_t(a.f[1]),
+				    uint16_t(a.f[2]),
+				    uint16_t(a.f[3])};
+				bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+			}
+
+			StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16u, false, 8};
+			return format;
+		}
+	}
+	else
+	{
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			float v[4] = {
+			    a.f[0],
+			    a.f[1],
+			    a.f[2],
+			    a.f[3]};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_32f, false, 16};
+		return format;
+	}
+}
+
+void getPositionBounds(int min[3], int max[3], const Stream& stream, const QuantizationParams& params)
+{
+	assert(stream.type == cgltf_attribute_type_position);
+	assert(stream.data.size() > 0);
+
+	min[0] = min[1] = min[2] = INT_MAX;
+	max[0] = max[1] = max[2] = INT_MIN;
+
+	float pos_rscale = params.pos_scale == 0.f ? 0.f : 1.f / params.pos_scale;
+
+	if (stream.target == 0)
+	{
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			for (int k = 0; k < 3; ++k)
+			{
+				int v = meshopt_quantizeUnorm((a.f[k] - params.pos_offset[k]) * pos_rscale, params.pos_bits);
+
+				min[k] = std::min(min[k], v);
+				max[k] = std::max(max[k], v);
+			}
+		}
+	}
+	else
+	{
+		for (size_t i = 0; i < stream.data.size(); ++i)
+		{
+			const Attr& a = stream.data[i];
+
+			for (int k = 0; k < 3; ++k)
+			{
+				int v = (a.f[k] >= 0.f ? 1 : -1) * meshopt_quantizeUnorm(fabsf(a.f[k]) * pos_rscale, params.pos_bits);
+
+				min[k] = std::min(min[k], v);
+				max[k] = std::max(max[k], v);
+			}
+		}
+	}
+}
+
+StreamFormat writeIndexStream(std::string& bin, const std::vector<unsigned int>& stream)
+{
+	unsigned int maxi = 0;
+	for (size_t i = 0; i < stream.size(); ++i)
+		maxi = std::max(maxi, stream[i]);
+
+	// save 16-bit indices if we can; note that we can't use restart index (65535)
+	if (maxi < 65535)
+	{
+		for (size_t i = 0; i < stream.size(); ++i)
+		{
+			uint16_t v[1] = {uint16_t(stream[i])};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_16u, false, 2};
+		return format;
+	}
+	else
+	{
+		for (size_t i = 0; i < stream.size(); ++i)
+		{
+			uint32_t v[1] = {stream[i]};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32u, false, 4};
+		return format;
+	}
+}
+
+StreamFormat writeTimeStream(std::string& bin, const std::vector<float>& data)
+{
+	for (size_t i = 0; i < data.size(); ++i)
+	{
+		float v[1] = {data[i]};
+		bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+	}
+
+	StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_32f, false, 4};
+	return format;
+}
+
+StreamFormat writeKeyframeStream(std::string& bin, cgltf_animation_path_type type, const std::vector<Attr>& data)
+{
+	if (type == cgltf_animation_path_type_rotation)
+	{
+		for (size_t i = 0; i < data.size(); ++i)
+		{
+			const Attr& a = data[i];
+
+			int16_t v[4] = {
+			    int16_t(meshopt_quantizeSnorm(a.f[0], 16)),
+			    int16_t(meshopt_quantizeSnorm(a.f[1], 16)),
+			    int16_t(meshopt_quantizeSnorm(a.f[2], 16)),
+			    int16_t(meshopt_quantizeSnorm(a.f[3], 16)),
+			};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec4, cgltf_component_type_r_16, true, 8};
+		return format;
+	}
+	else if (type == cgltf_animation_path_type_weights)
+	{
+		for (size_t i = 0; i < data.size(); ++i)
+		{
+			const Attr& a = data[i];
+
+			uint8_t v[1] = {uint8_t(meshopt_quantizeUnorm(a.f[0], 8))};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_scalar, cgltf_component_type_r_8u, true, 1};
+		return format;
+	}
+	else
+	{
+		for (size_t i = 0; i < data.size(); ++i)
+		{
+			const Attr& a = data[i];
+
+			float v[3] = {a.f[0], a.f[1], a.f[2]};
+			bin.append(reinterpret_cast<const char*>(v), sizeof(v));
+		}
+
+		StreamFormat format = {cgltf_type_vec3, cgltf_component_type_r_32f, false, 12};
+		return format;
+	}
+}
+
+void compressVertexStream(std::string& bin, const std::string& data, size_t count, size_t stride)
+{
+	assert(data.size() == count * stride);
+
+	std::vector<unsigned char> compressed(meshopt_encodeVertexBufferBound(count, stride));
+	size_t size = meshopt_encodeVertexBuffer(&compressed[0], compressed.size(), data.c_str(), count, stride);
+
+	bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
+}
+
+void compressIndexStream(std::string& bin, const std::string& data, size_t count, size_t stride)
+{
+	assert(stride == 2 || stride == 4);
+	assert(data.size() == count * stride);
+
+	std::vector<unsigned char> compressed(meshopt_encodeIndexBufferBound(count, count * 3));
+	size_t size = 0;
+
+	if (stride == 2)
+		size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint16_t*>(data.c_str()), count);
+	else
+		size = meshopt_encodeIndexBuffer(&compressed[0], compressed.size(), reinterpret_cast<const uint32_t*>(data.c_str()), count);
+
+	bin.append(reinterpret_cast<const char*>(&compressed[0]), size);
+}
+
+void comma(std::string& s)
+{
+	char ch = s.empty() ? 0 : s[s.size() - 1];
+
+	if (ch != 0 && ch != '[' && ch != '{')
+		s += ",";
+}
+
+void append(std::string& s, size_t v)
+{
+	char buf[32];
+	sprintf(buf, "%zu", v);
+	s += buf;
+}
+
+void append(std::string& s, float v)
+{
+	char buf[512];
+	sprintf(buf, "%.9g", v);
+	s += buf;
+}
+
+void append(std::string& s, const char* v)
+{
+	s += v;
+}
+
+void append(std::string& s, const std::string& v)
+{
+	s += v;
+}
+
+const char* componentType(cgltf_component_type type)
+{
+	switch (type)
+	{
+	case cgltf_component_type_r_8:
+		return "5120";
+	case cgltf_component_type_r_8u:
+		return "5121";
+	case cgltf_component_type_r_16:
+		return "5122";
+	case cgltf_component_type_r_16u:
+		return "5123";
+	case cgltf_component_type_r_32u:
+		return "5125";
+	case cgltf_component_type_r_32f:
+		return "5126";
+	default:
+		return "0";
+	}
+}
+
+const char* shapeType(cgltf_type type)
+{
+	switch (type)
+	{
+	case cgltf_type_scalar:
+		return "\"SCALAR\"";
+	case cgltf_type_vec2:
+		return "\"VEC2\"";
+	case cgltf_type_vec3:
+		return "\"VEC3\"";
+	case cgltf_type_vec4:
+		return "\"VEC4\"";
+	case cgltf_type_mat2:
+		return "\"MAT2\"";
+	case cgltf_type_mat3:
+		return "\"MAT3\"";
+	case cgltf_type_mat4:
+		return "\"MAT4\"";
+	default:
+		return "\"\"";
+	}
+}
+
+const char* attributeType(cgltf_attribute_type type)
+{
+	switch (type)
+	{
+	case cgltf_attribute_type_position:
+		return "POSITION";
+	case cgltf_attribute_type_normal:
+		return "NORMAL";
+	case cgltf_attribute_type_tangent:
+		return "TANGENT";
+	case cgltf_attribute_type_texcoord:
+		return "TEXCOORD";
+	case cgltf_attribute_type_color:
+		return "COLOR";
+	case cgltf_attribute_type_joints:
+		return "JOINTS";
+	case cgltf_attribute_type_weights:
+		return "WEIGHTS";
+	default:
+		return "ATTRIBUTE";
+	}
+}
+
+const char* animationPath(cgltf_animation_path_type type)
+{
+	switch (type)
+	{
+	case cgltf_animation_path_type_translation:
+		return "\"translation\"";
+	case cgltf_animation_path_type_rotation:
+		return "\"rotation\"";
+	case cgltf_animation_path_type_scale:
+		return "\"scale\"";
+	case cgltf_animation_path_type_weights:
+		return "\"weights\"";
+	default:
+		return "\"\"";
+	}
+}
+
+const char* lightType(cgltf_light_type type)
+{
+	switch (type)
+	{
+	case cgltf_light_type_directional:
+		return "\"directional\"";
+	case cgltf_light_type_point:
+		return "\"point\"";
+	case cgltf_light_type_spot:
+		return "\"spot\"";
+	default:
+		return "\"\"";
+	}
+}
+
+void writeTextureInfo(std::string& json, const cgltf_data* data, const cgltf_texture_view& view, const QuantizationParams& qp)
+{
+	assert(view.texture);
+
+	cgltf_texture_transform transform = view.transform;
+
+	transform.offset[0] += qp.uv_offset[0];
+	transform.offset[1] += qp.uv_offset[1];
+	transform.scale[0] *= qp.uv_scale[0] / float((1 << qp.uv_bits) - 1);
+	transform.scale[1] *= qp.uv_scale[1] / float((1 << qp.uv_bits) - 1);
+
+	append(json, "{\"index\":");
+	append(json, size_t(view.texture - data->textures));
+	append(json, ",\"texCoord\":");
+	append(json, size_t(view.texcoord));
+	append(json, ",\"extensions\":{\"KHR_texture_transform\":{");
+	append(json, "\"offset\":[");
+	append(json, transform.offset[0]);
+	append(json, ",");
+	append(json, transform.offset[1]);
+	append(json, "],\"scale\":[");
+	append(json, transform.scale[0]);
+	append(json, ",");
+	append(json, transform.scale[1]);
+	append(json, "]");
+	if (transform.rotation != 0.f)
+	{
+		append(json, ",\"rotation\":");
+		append(json, transform.rotation);
+	}
+	append(json, "}}}");
+}
+
+void writeMaterialInfo(std::string& json, const cgltf_data* data, const cgltf_material& material, const QuantizationParams& qp)
+{
+	static const float white[4] = {1, 1, 1, 1};
+	static const float black[4] = {0, 0, 0, 0};
+
+	if (material.has_pbr_metallic_roughness)
+	{
+		const cgltf_pbr_metallic_roughness& pbr = material.pbr_metallic_roughness;
+
+		comma(json);
+		append(json, "\"pbrMetallicRoughness\":{");
+		if (memcmp(pbr.base_color_factor, white, 16) != 0)
+		{
+			comma(json);
+			append(json, "\"baseColorFactor\":[");
+			append(json, pbr.base_color_factor[0]);
+			append(json, ",");
+			append(json, pbr.base_color_factor[1]);
+			append(json, ",");
+			append(json, pbr.base_color_factor[2]);
+			append(json, ",");
+			append(json, pbr.base_color_factor[3]);
+			append(json, "]");
+		}
+		if (pbr.base_color_texture.texture)
+		{
+			comma(json);
+			append(json, "\"baseColorTexture\":");
+			writeTextureInfo(json, data, pbr.base_color_texture, qp);
+		}
+		if (pbr.metallic_factor != 1)
+		{
+			comma(json);
+			append(json, "\"metallicFactor\":");
+			append(json, pbr.metallic_factor);
+		}
+		if (pbr.roughness_factor != 1)
+		{
+			comma(json);
+			append(json, "\"roughnessFactor\":");
+			append(json, pbr.roughness_factor);
+		}
+		if (pbr.metallic_roughness_texture.texture)
+		{
+			comma(json);
+			append(json, "\"metallicRoughnessTexture\":");
+			writeTextureInfo(json, data, pbr.metallic_roughness_texture, qp);
+		}
+		append(json, "}");
+	}
+
+	if (material.normal_texture.texture)
+	{
+		comma(json);
+		append(json, "\"normalTexture\":");
+		writeTextureInfo(json, data, material.normal_texture, qp);
+	}
+
+	if (material.occlusion_texture.texture)
+	{
+		comma(json);
+		append(json, "\"occlusionTexture\":");
+		writeTextureInfo(json, data, material.occlusion_texture, qp);
+	}
+
+	if (material.emissive_texture.texture)
+	{
+		comma(json);
+		append(json, "\"emissiveTexture\":");
+		writeTextureInfo(json, data, material.emissive_texture, qp);
+	}
+
+	if (memcmp(material.emissive_factor, black, 12) != 0)
+	{
+		comma(json);
+		append(json, "\"emissiveFactor\":[");
+		append(json, material.emissive_factor[0]);
+		append(json, ",");
+		append(json, material.emissive_factor[1]);
+		append(json, ",");
+		append(json, material.emissive_factor[2]);
+		append(json, "]");
+	}
+
+	if (material.alpha_mode != cgltf_alpha_mode_opaque)
+	{
+		comma(json);
+		append(json, "\"alphaMode\":");
+		append(json, (material.alpha_mode == cgltf_alpha_mode_blend) ? "\"BLEND\"" : "\"MASK\"");
+	}
+
+	if (material.alpha_cutoff != 0.5f)
+	{
+		comma(json);
+		append(json, "\"alphaCutoff\":");
+		append(json, material.alpha_cutoff);
+	}
+
+	if (material.double_sided)
+	{
+		comma(json);
+		append(json, "\"doubleSided\":true");
+	}
+
+	if (material.has_pbr_specular_glossiness || material.unlit)
+	{
+		comma(json);
+		append(json, "\"extensions\":{");
+
+		if (material.has_pbr_specular_glossiness)
+		{
+			const cgltf_pbr_specular_glossiness& pbr = material.pbr_specular_glossiness;
+
+			comma(json);
+			append(json, "\"KHR_materials_pbrSpecularGlossiness\":{");
+			if (pbr.diffuse_texture.texture)
+			{
+				comma(json);
+				append(json, "\"diffuseTexture\":");
+				writeTextureInfo(json, data, pbr.diffuse_texture, qp);
+			}
+			if (pbr.specular_glossiness_texture.texture)
+			{
+				comma(json);
+				append(json, "\"specularGlossinessTexture\":");
+				writeTextureInfo(json, data, pbr.specular_glossiness_texture, qp);
+			}
+			if (memcmp(pbr.diffuse_factor, white, 16) != 0)
+			{
+				comma(json);
+				append(json, "\"diffuseFactor\":[");
+				append(json, pbr.diffuse_factor[0]);
+				append(json, ",");
+				append(json, pbr.diffuse_factor[1]);
+				append(json, ",");
+				append(json, pbr.diffuse_factor[2]);
+				append(json, ",");
+				append(json, pbr.diffuse_factor[3]);
+				append(json, "]");
+			}
+			if (memcmp(pbr.specular_factor, white, 12) != 0)
+			{
+				comma(json);
+				append(json, "\"specularFactor\":[");
+				append(json, pbr.specular_factor[0]);
+				append(json, ",");
+				append(json, pbr.specular_factor[1]);
+				append(json, ",");
+				append(json, pbr.specular_factor[2]);
+				append(json, "]");
+			}
+			if (pbr.glossiness_factor != 1)
+			{
+				comma(json);
+				append(json, "\"glossinessFactor\":");
+				append(json, pbr.glossiness_factor);
+			}
+			append(json, "}");
+		}
+		if (material.unlit)
+		{
+			comma(json);
+			append(json, "\"KHR_materials_unlit\":{}");
+		}
+
+		append(json, "}");
+	}
+}
+
+bool usesTextureSet(const cgltf_material& material, int set)
+{
+	if (material.has_pbr_metallic_roughness)
+	{
+		const cgltf_pbr_metallic_roughness& pbr = material.pbr_metallic_roughness;
+
+		if (pbr.base_color_texture.texture && pbr.base_color_texture.texcoord == set)
+			return true;
+
+		if (pbr.metallic_roughness_texture.texture && pbr.metallic_roughness_texture.texcoord == set)
+			return true;
+	}
+
+	if (material.has_pbr_specular_glossiness)
+	{
+		const cgltf_pbr_specular_glossiness& pbr = material.pbr_specular_glossiness;
+
+		if (pbr.diffuse_texture.texture && pbr.diffuse_texture.texcoord == set)
+			return true;
+
+		if (pbr.specular_glossiness_texture.texture && pbr.specular_glossiness_texture.texcoord == set)
+			return true;
+	}
+
+	if (material.normal_texture.texture && material.normal_texture.texcoord == set)
+		return true;
+
+	if (material.occlusion_texture.texture && material.occlusion_texture.texcoord == set)
+		return true;
+
+	if (material.emissive_texture.texture && material.emissive_texture.texcoord == set)
+		return true;
+
+	return false;
+}
+
+size_t getBufferView(std::vector<BufferView>& views, BufferView::Kind kind, int variant, size_t stride, bool compressed)
+{
+	if (variant >= 0)
+	{
+		for (size_t i = 0; i < views.size(); ++i)
+			if (views[i].kind == kind && views[i].variant == variant && views[i].stride == stride && views[i].compressed == compressed)
+				return i;
+	}
+
+	BufferView view = {kind, variant, stride, compressed};
+	views.push_back(view);
+
+	return views.size() - 1;
+}
+
+void writeBufferView(std::string& json, BufferView::Kind kind, size_t count, size_t stride, size_t bin_offset, size_t bin_size, int compression)
+{
+	append(json, "{\"buffer\":0");
+	append(json, ",\"byteLength\":");
+	append(json, bin_size);
+	append(json, ",\"byteOffset\":");
+	append(json, bin_offset);
+	if (kind == BufferView::Kind_Vertex)
+	{
+		append(json, ",\"byteStride\":");
+		append(json, stride);
+	}
+	if (kind == BufferView::Kind_Vertex || kind == BufferView::Kind_Index)
+	{
+		append(json, ",\"target\":");
+		append(json, (kind == BufferView::Kind_Vertex) ? "34962" : "34963");
+	}
+	if (compression >= 0)
+	{
+		append(json, ",\"extensions\":{");
+		append(json, "\"MESHOPT_compression\":{");
+		append(json, "\"mode\":");
+		append(json, size_t(compression));
+		append(json, ",\"count\":");
+		append(json, count);
+		append(json, ",\"byteStride\":");
+		append(json, stride);
+		append(json, "}}");
+	}
+	append(json, "}");
+}
+
+void writeAccessor(std::string& json, size_t view, size_t offset, cgltf_type type, cgltf_component_type component_type, bool normalized, size_t count, const float* min = 0, const float* max = 0, size_t numminmax = 0)
+{
+	append(json, "{\"bufferView\":");
+	append(json, view);
+	append(json, ",\"byteOffset\":");
+	append(json, offset);
+	append(json, ",\"componentType\":");
+	append(json, componentType(component_type));
+	append(json, ",\"count\":");
+	append(json, count);
+	append(json, ",\"type\":");
+	append(json, shapeType(type));
+
+	if (normalized)
+	{
+		append(json, ",\"normalized\":true");
+	}
+
+	if (min && max)
+	{
+		assert(numminmax);
+
+		append(json, ",\"min\":[");
+		for (size_t k = 0; k < numminmax; ++k)
+		{
+			comma(json);
+			append(json, min[k]);
+		}
+		append(json, "],\"max\":[");
+		for (size_t k = 0; k < numminmax; ++k)
+		{
+			comma(json);
+			append(json, max[k]);
+		}
+		append(json, "]");
+	}
+
+	append(json, "}");
+}
+
+float getDelta(const Attr& l, const Attr& r, cgltf_animation_path_type type)
+{
+	if (type == cgltf_animation_path_type_rotation)
+	{
+		float error = 1.f - fabsf(l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3]);
+
+		return error;
+	}
+	else
+	{
+		float error = 0;
+		for (int k = 0; k < 4; ++k)
+			error += fabsf(r.f[k] - l.f[k]);
+
+		return error;
+	}
+}
+
+bool isTrackConstant(const cgltf_animation_sampler& sampler, cgltf_animation_path_type type, cgltf_node* target_node)
+{
+	const float tolerance = 1e-3f;
+
+	size_t value_stride = (sampler.interpolation == cgltf_interpolation_type_cubic_spline) ? 3 : 1;
+	size_t value_offset = (sampler.interpolation == cgltf_interpolation_type_cubic_spline) ? 1 : 0;
+
+	size_t components = (type == cgltf_animation_path_type_weights) ? target_node->mesh->primitives[0].targets_count : 1;
+
+	assert(sampler.input->count * value_stride * components == sampler.output->count);
+
+	for (size_t j = 0; j < components; ++j)
+	{
+		Attr first = {};
+		cgltf_accessor_read_float(sampler.output, j * value_stride + value_offset, first.f, 4);
+
+		for (size_t i = 1; i < sampler.input->count; ++i)
+		{
+			Attr attr = {};
+			cgltf_accessor_read_float(sampler.output, (i * components + j) * value_stride + value_offset, attr.f, 4);
+
+			if (getDelta(first, attr, type) > tolerance)
+				return false;
+		}
+
+		if (sampler.interpolation == cgltf_interpolation_type_cubic_spline)
+		{
+			for (size_t i = 0; i < sampler.input->count; ++i)
+			{
+				for (int k = 0; k < 2; ++k)
+				{
+					Attr t = {};
+					cgltf_accessor_read_float(sampler.output, (i * components + j) * 3 + k * 2, t.f, 4);
+
+					float error = fabsf(t.f[0]) + fabsf(t.f[1]) + fabsf(t.f[2]) + fabsf(t.f[3]);
+
+					if (error > tolerance)
+						return false;
+				}
+			}
+		}
+	}
+
+	return true;
+}
+
+Attr interpolateLinear(const Attr& l, const Attr& r, float t, cgltf_animation_path_type type)
+{
+	if (type == cgltf_animation_path_type_rotation)
+	{
+		// Approximating slerp, https://zeux.io/2015/07/23/approximating-slerp/
+		// We also handle quaternion double-cover
+		float ca = l.f[0] * r.f[0] + l.f[1] * r.f[1] + l.f[2] * r.f[2] + l.f[3] * r.f[3];
+
+		float d = fabsf(ca);
+		float A = 1.0904f + d * (-3.2452f + d * (3.55645f - d * 1.43519f));
+		float B = 0.848013f + d * (-1.06021f + d * 0.215638f);
+		float k = A * (t - 0.5f) * (t - 0.5f) + B;
+		float ot = t + t * (t - 0.5f) * (t - 1) * k;
+
+		float t0 = 1 - ot;
+		float t1 = ca > 0 ? ot : -ot;
+
+		Attr lerp = {{
+		    l.f[0] * t0 + r.f[0] * t1,
+		    l.f[1] * t0 + r.f[1] * t1,
+		    l.f[2] * t0 + r.f[2] * t1,
+		    l.f[3] * t0 + r.f[3] * t1,
+		}};
+
+		float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]);
+
+		if (len > 0.f)
+		{
+			lerp.f[0] /= len;
+			lerp.f[1] /= len;
+			lerp.f[2] /= len;
+			lerp.f[3] /= len;
+		}
+
+		return lerp;
+	}
+	else
+	{
+		Attr lerp = {{
+		    l.f[0] * (1 - t) + r.f[0] * t,
+		    l.f[1] * (1 - t) + r.f[1] * t,
+		    l.f[2] * (1 - t) + r.f[2] * t,
+		    l.f[3] * (1 - t) + r.f[3] * t,
+		}};
+
+		return lerp;
+	}
+}
+
+Attr interpolateHermite(const Attr& v0, const Attr& t0, const Attr& v1, const Attr& t1, float t, float dt, cgltf_animation_path_type type)
+{
+	float s0 = 1 + t * t * (2 * t - 3);
+	float s1 = t + t * t * (t - 2);
+	float s2 = 1 - s0;
+	float s3 = t * t * (t - 1);
+
+	float ts1 = dt * s1;
+	float ts3 = dt * s3;
+
+	Attr lerp = {{
+	    s0 * v0.f[0] + ts1 * t0.f[0] + s2 * v1.f[0] + ts3 * t1.f[0],
+	    s0 * v0.f[1] + ts1 * t0.f[1] + s2 * v1.f[1] + ts3 * t1.f[1],
+	    s0 * v0.f[2] + ts1 * t0.f[2] + s2 * v1.f[2] + ts3 * t1.f[2],
+	    s0 * v0.f[3] + ts1 * t0.f[3] + s2 * v1.f[3] + ts3 * t1.f[3],
+	}};
+
+	if (type == cgltf_animation_path_type_rotation)
+	{
+		float len = sqrtf(lerp.f[0] * lerp.f[0] + lerp.f[1] * lerp.f[1] + lerp.f[2] * lerp.f[2] + lerp.f[3] * lerp.f[3]);
+
+		if (len > 0.f)
+		{
+			lerp.f[0] /= len;
+			lerp.f[1] /= len;
+			lerp.f[2] /= len;
+			lerp.f[3] /= len;
+		}
+	}
+
+	return lerp;
+}
+
+void resampleKeyframes(std::vector<Attr>& data, const cgltf_animation_sampler& sampler, cgltf_animation_path_type type, cgltf_node* target_node, int frames, float mint, int freq)
+{
+	size_t components = (type == cgltf_animation_path_type_weights) ? target_node->mesh->primitives[0].targets_count : 1;
+
+	size_t cursor = 0;
+
+	for (int i = 0; i < frames; ++i)
+	{
+		float time = mint + float(i) / freq;
+
+		while (cursor + 1 < sampler.input->count)
+		{
+			float next_time = 0;
+			cgltf_accessor_read_float(sampler.input, cursor + 1, &next_time, 1);
+
+			if (next_time > time)
+				break;
+
+			cursor++;
+		}
+
+		if (cursor + 1 < sampler.input->count)
+		{
+			float cursor_time = 0;
+			float next_time = 0;
+			cgltf_accessor_read_float(sampler.input, cursor + 0, &cursor_time, 1);
+			cgltf_accessor_read_float(sampler.input, cursor + 1, &next_time, 1);
+
+			float range = next_time - cursor_time;
+			float inv_range = (range == 0.f) ? 0.f : 1.f / (next_time - cursor_time);
+			float t = std::max(0.f, std::min(1.f, (time - cursor_time) * inv_range));
+
+			for (size_t j = 0; j < components; ++j)
+			{
+				switch (sampler.interpolation)
+				{
+				case cgltf_interpolation_type_linear:
+				{
+					Attr v0 = {};
+					Attr v1 = {};
+					cgltf_accessor_read_float(sampler.output, (cursor + 0) * components + j, v0.f, 4);
+					cgltf_accessor_read_float(sampler.output, (cursor + 1) * components + j, v1.f, 4);
+					data.push_back(interpolateLinear(v0, v1, t, type));
+				}
+				break;
+
+				case cgltf_interpolation_type_step:
+				{
+					Attr v = {};
+					cgltf_accessor_read_float(sampler.output, cursor * components + j, v.f, 4);
+					data.push_back(v);
+				}
+				break;
+
+				case cgltf_interpolation_type_cubic_spline:
+				{
+					Attr v0 = {};
+					Attr b0 = {};
+					Attr a1 = {};
+					Attr v1 = {};
+					cgltf_accessor_read_float(sampler.output, (cursor * 3 + 1) * components + j, v0.f, 4);
+					cgltf_accessor_read_float(sampler.output, (cursor * 3 + 2) * components + j, b0.f, 4);
+					cgltf_accessor_read_float(sampler.output, (cursor * 3 + 3) * components + j, a1.f, 4);
+					cgltf_accessor_read_float(sampler.output, (cursor * 3 + 4) * components + j, v1.f, 4);
+					data.push_back(interpolateHermite(v0, b0, v1, a1, t, range, type));
+				}
+				break;
+
+				default:
+					assert(!"Unknown interpolation type");
+				}
+			}
+		}
+		else
+		{
+			size_t offset = (sampler.interpolation == cgltf_interpolation_type_cubic_spline) ? cursor * 3 + 1 : cursor;
+
+			for (size_t j = 0; j < components; ++j)
+			{
+				Attr v = {};
+				cgltf_accessor_read_float(sampler.output, offset * components + j, v.f, 4);
+				data.push_back(v);
+			}
+		}
+	}
+}
+
+void markAnimated(cgltf_data* data, std::vector<NodeInfo>& nodes)
+{
+	for (size_t i = 0; i < data->animations_count; ++i)
+	{
+		const cgltf_animation& animation = data->animations[i];
+
+		for (size_t j = 0; j < animation.channels_count; ++j)
+		{
+			const cgltf_animation_channel& channel = animation.channels[j];
+			const cgltf_animation_sampler& sampler = *channel.sampler;
+
+			if (!channel.target_node)
+				continue;
+
+			NodeInfo& ni = nodes[channel.target_node - data->nodes];
+
+			// mark nodes that have animation tracks that change their base transform as animated
+			if (!isTrackConstant(sampler, channel.target_path, channel.target_node))
+			{
+				ni.animated_paths |= (1 << channel.target_path);
+			}
+			else
+			{
+				Attr base = {};
+
+				switch (channel.target_path)
+				{
+				case cgltf_animation_path_type_translation:
+					memcpy(base.f, channel.target_node->translation, 3 * sizeof(float));
+					break;
+				case cgltf_animation_path_type_rotation:
+					memcpy(base.f, channel.target_node->rotation, 4 * sizeof(float));
+					break;
+				case cgltf_animation_path_type_scale:
+					memcpy(base.f, channel.target_node->scale, 3 * sizeof(float));
+					break;
+				default:;
+				}
+
+				Attr first = {};
+				cgltf_accessor_read_float(sampler.output, 0, first.f, 4);
+
+				const float tolerance = 1e-3f;
+
+				if (getDelta(base, first, channel.target_path) > tolerance)
+				{
+					ni.animated_paths |= (1 << channel.target_path);
+				}
+			}
+		}
+	}
+
+	for (size_t i = 0; i < data->nodes_count; ++i)
+	{
+		NodeInfo& ni = nodes[i];
+
+		for (cgltf_node* node = &data->nodes[i]; node; node = node->parent)
+			ni.animated |= nodes[node - data->nodes].animated_paths != 0;
+	}
+}
+
+void markNeededNodes(cgltf_data* data, std::vector<NodeInfo>& nodes, const std::vector<Mesh>& meshes, const Settings& settings)
+{
+	// mark all joints as kept
+	for (size_t i = 0; i < data->skins_count; ++i)
+	{
+		const cgltf_skin& skin = data->skins[i];
+
+		// for now we keep all joints directly referenced by the skin and the entire ancestry tree; we keep names for joints as well
+		for (size_t j = 0; j < skin.joints_count; ++j)
+		{
+			NodeInfo& ni = nodes[skin.joints[j] - data->nodes];
+
+			ni.keep = true;
+		}
+	}
+
+	// mark all animated nodes as kept
+	for (size_t i = 0; i < data->animations_count; ++i)
+	{
+		const cgltf_animation& animation = data->animations[i];
+
+		for (size_t j = 0; j < animation.channels_count; ++j)
+		{
+			const cgltf_animation_channel& channel = animation.channels[j];
+
+			if (channel.target_node)
+			{
+				NodeInfo& ni = nodes[channel.target_node - data->nodes];
+
+				ni.keep = true;
+			}
+		}
+	}
+
+	// mark all mesh nodes as kept
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		const Mesh& mesh = meshes[i];
+
+		if (mesh.indices.empty())
+			continue;
+
+		if (mesh.node)
+		{
+			NodeInfo& ni = nodes[mesh.node - data->nodes];
+
+			ni.keep = true;
+		}
+	}
+
+	// mark all light/camera nodes as kept
+	for (size_t i = 0; i < data->nodes_count; ++i)
+	{
+		const cgltf_node& node = data->nodes[i];
+
+		if (node.light || node.camera)
+		{
+			nodes[i].keep = true;
+		}
+	}
+
+	// mark all named nodes as needed (if -kn is specified)
+	if (settings.keep_named)
+	{
+		for (size_t i = 0; i < data->nodes_count; ++i)
+		{
+			const cgltf_node& node = data->nodes[i];
+
+			if (node.name && *node.name)
+			{
+				nodes[i].keep = true;
+			}
+		}
+	}
+}
+
+void markNeededMaterials(cgltf_data* data, std::vector<MaterialInfo>& materials, const std::vector<Mesh>& meshes)
+{
+	// mark all used materials as kept
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		const Mesh& mesh = meshes[i];
+
+		if (mesh.indices.empty())
+			continue;
+
+		if (mesh.material)
+		{
+			MaterialInfo& mi = materials[mesh.material - data->materials];
+
+			mi.keep = true;
+		}
+	}
+}
+
+void remapNodes(cgltf_data* data, std::vector<NodeInfo>& nodes, size_t& node_offset)
+{
+	// to keep a node, we currently need to keep the entire ancestry chain
+	for (size_t i = 0; i < data->nodes_count; ++i)
+	{
+		if (!nodes[i].keep)
+			continue;
+
+		for (cgltf_node* node = &data->nodes[i]; node; node = node->parent)
+			nodes[node - data->nodes].keep = true;
+	}
+
+	// generate sequential indices for all nodes; they aren't sorted topologically
+	for (size_t i = 0; i < data->nodes_count; ++i)
+	{
+		NodeInfo& ni = nodes[i];
+
+		if (ni.keep)
+		{
+			ni.remap = int(node_offset);
+
+			node_offset++;
+		}
+	}
+}
+
+bool parseDataUri(const char* uri, std::string& mime_type, std::string& result)
+{
+	if (strncmp(uri, "data:", 5) == 0)
+	{
+		const char* comma = strchr(uri, ',');
+
+		if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0)
+		{
+			const char* base64 = comma + 1;
+			size_t base64_size = strlen(base64);
+			size_t size = base64_size - base64_size / 4;
+
+			if (base64_size >= 2)
+			{
+				size -= base64[base64_size - 2] == '=';
+				size -= base64[base64_size - 1] == '=';
+			}
+
+			void* data = 0;
+
+			cgltf_options options = {};
+			cgltf_result res = cgltf_load_buffer_base64(&options, size, base64, &data);
+
+			if (res != cgltf_result_success)
+				return false;
+
+			mime_type = std::string(uri + 5, comma - 7);
+			result = std::string(static_cast<const char*>(data), size);
+
+			free(data);
+
+			return true;
+		}
+	}
+
+	return false;
+}
+
+void writeEmbeddedImage(std::string& json, std::vector<BufferView>& views, const char* data, size_t size, const char* mime_type)
+{
+	size_t view = getBufferView(views, BufferView::Kind_Image, -1, 1, false);
+
+	assert(views[view].data.empty());
+	views[view].data.append(data, size);
+
+	// each chunk must be aligned to 4 bytes
+	views[view].data.resize((views[view].data.size() + 3) & ~3);
+
+	append(json, "\"bufferView\":");
+	append(json, view);
+	append(json, ",\"mimeType\":\"");
+	append(json, mime_type);
+	append(json, "\"");
+}
+
+void writeMeshAttributes(std::string& json, std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, int target, const QuantizationParams& qp, const Settings& settings)
+{
+	std::string scratch;
+
+	for (size_t j = 0; j < mesh.streams.size(); ++j)
+	{
+		const Stream& stream = mesh.streams[j];
+
+		if (stream.target != target)
+			continue;
+
+		if (stream.type == cgltf_attribute_type_texcoord && (!mesh.material || !usesTextureSet(*mesh.material, stream.index)))
+			continue;
+
+		if ((stream.type == cgltf_attribute_type_joints || stream.type == cgltf_attribute_type_weights) && !mesh.skin)
+			continue;
+
+		scratch.clear();
+		StreamFormat format = writeVertexStream(scratch, stream, qp, settings, mesh.targets > 0);
+
+		size_t view = getBufferView(views, BufferView::Kind_Vertex, stream.type, format.stride, settings.compress);
+		size_t offset = views[view].data.size();
+		views[view].data += scratch;
+
+		comma(json_accessors);
+		if (stream.type == cgltf_attribute_type_position)
+		{
+			int min[3] = {};
+			int max[3] = {};
+			getPositionBounds(min, max, stream, qp);
+
+			float minf[3] = {float(min[0]), float(min[1]), float(min[2])};
+			float maxf[3] = {float(max[0]), float(max[1]), float(max[2])};
+
+			writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, stream.data.size(), minf, maxf, 3);
+		}
+		else
+		{
+			writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, stream.data.size());
+		}
+
+		size_t vertex_accr = accr_offset++;
+
+		comma(json);
+		append(json, "\"");
+		append(json, attributeType(stream.type));
+		if (stream.type != cgltf_attribute_type_position && stream.type != cgltf_attribute_type_normal && stream.type != cgltf_attribute_type_tangent)
+		{
+			append(json, "_");
+			append(json, size_t(stream.index));
+		}
+		append(json, "\":");
+		append(json, vertex_accr);
+	}
+}
+
+size_t writeMeshIndices(std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const Mesh& mesh, const Settings& settings)
+{
+	std::string scratch;
+	StreamFormat format = writeIndexStream(scratch, mesh.indices);
+
+	// note: we prefer to merge all index streams together; however, index codec currently doesn't handle concatenated index streams well and loses compression ratio
+	int variant = settings.compress ? -1 : 0;
+
+	size_t view = getBufferView(views, BufferView::Kind_Index, variant, format.stride, settings.compress);
+	size_t offset = views[view].data.size();
+	views[view].data += scratch;
+
+	comma(json_accessors);
+	writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, mesh.indices.size());
+
+	size_t index_accr = accr_offset++;
+
+	return index_accr;
+}
+
+size_t writeAnimationTime(std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, float mint, int frames, const Settings& settings)
+{
+	std::vector<float> time(frames);
+
+	for (int j = 0; j < frames; ++j)
+		time[j] = mint + float(j) / settings.anim_freq;
+
+	std::string scratch;
+	StreamFormat format = writeTimeStream(scratch, time);
+
+	size_t view = getBufferView(views, BufferView::Kind_Time, 0, format.stride, settings.compress);
+	size_t offset = views[view].data.size();
+	views[view].data += scratch;
+
+	comma(json_accessors);
+	writeAccessor(json_accessors, view, offset, cgltf_type_scalar, format.component_type, format.normalized, frames, &time.front(), &time.back(), 1);
+
+	size_t time_accr = accr_offset++;
+
+	return time_accr;
+}
+
+size_t writeJointBindMatrices(std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const cgltf_skin& skin, const QuantizationParams& qp, const Settings& settings)
+{
+	std::string scratch;
+
+	for (size_t j = 0; j < skin.joints_count; ++j)
+	{
+		float transform[16] = {1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1};
+
+		if (skin.inverse_bind_matrices)
+		{
+			cgltf_accessor_read_float(skin.inverse_bind_matrices, j, transform, 16);
+		}
+
+		float node_scale = qp.pos_scale / float((1 << qp.pos_bits) - 1);
+
+		// pos_offset has to be applied first, thus it results in an offset rotated by the bind matrix
+		transform[12] += qp.pos_offset[0] * transform[0] + qp.pos_offset[1] * transform[4] + qp.pos_offset[2] * transform[8];
+		transform[13] += qp.pos_offset[0] * transform[1] + qp.pos_offset[1] * transform[5] + qp.pos_offset[2] * transform[9];
+		transform[14] += qp.pos_offset[0] * transform[2] + qp.pos_offset[1] * transform[6] + qp.pos_offset[2] * transform[10];
+
+		// node_scale will be applied before the rotation/scale from transform
+		for (int k = 0; k < 12; ++k)
+			transform[k] *= node_scale;
+
+		scratch.append(reinterpret_cast<const char*>(transform), sizeof(transform));
+	}
+
+	size_t view = getBufferView(views, BufferView::Kind_Skin, 0, 64, settings.compress);
+	size_t offset = views[view].data.size();
+	views[view].data += scratch;
+
+	comma(json_accessors);
+	writeAccessor(json_accessors, view, offset, cgltf_type_mat4, cgltf_component_type_r_32f, false, skin.joints_count);
+
+	size_t matrix_accr = accr_offset++;
+
+	return matrix_accr;
+}
+
+void writeMeshNode(std::string& json, size_t mesh_offset, const Mesh& mesh, cgltf_data* data, const QuantizationParams& qp)
+{
+	float node_scale = qp.pos_scale / float((1 << qp.pos_bits) - 1);
+
+	comma(json);
+	append(json, "{\"mesh\":");
+	append(json, mesh_offset);
+	if (mesh.skin)
+	{
+		comma(json);
+		append(json, "\"skin\":");
+		append(json, size_t(mesh.skin - data->skins));
+	}
+	append(json, ",\"translation\":[");
+	append(json, qp.pos_offset[0]);
+	append(json, ",");
+	append(json, qp.pos_offset[1]);
+	append(json, ",");
+	append(json, qp.pos_offset[2]);
+	append(json, "],\"scale\":[");
+	append(json, node_scale);
+	append(json, ",");
+	append(json, node_scale);
+	append(json, ",");
+	append(json, node_scale);
+	append(json, "]");
+	if (mesh.node && mesh.node->weights_count)
+	{
+		append(json, ",\"weights\":[");
+		for (size_t j = 0; j < mesh.node->weights_count; ++j)
+		{
+			comma(json);
+			append(json, mesh.node->weights[j]);
+		}
+		append(json, "]");
+	}
+	append(json, "}");
+}
+
+void writeNode(std::string& json, const cgltf_node& node, const std::vector<NodeInfo>& nodes, cgltf_data* data)
+{
+	const NodeInfo& ni = nodes[&node - data->nodes];
+
+	comma(json);
+	append(json, "{");
+	if (node.name && *node.name)
+	{
+		comma(json);
+		append(json, "\"name\":\"");
+		append(json, node.name);
+		append(json, "\"");
+	}
+	if (node.has_translation)
+	{
+		comma(json);
+		append(json, "\"translation\":[");
+		append(json, node.translation[0]);
+		append(json, ",");
+		append(json, node.translation[1]);
+		append(json, ",");
+		append(json, node.translation[2]);
+		append(json, "]");
+	}
+	if (node.has_rotation)
+	{
+		comma(json);
+		append(json, "\"rotation\":[");
+		append(json, node.rotation[0]);
+		append(json, ",");
+		append(json, node.rotation[1]);
+		append(json, ",");
+		append(json, node.rotation[2]);
+		append(json, ",");
+		append(json, node.rotation[3]);
+		append(json, "]");
+	}
+	if (node.has_scale)
+	{
+		comma(json);
+		append(json, "\"scale\":[");
+		append(json, node.scale[0]);
+		append(json, ",");
+		append(json, node.scale[1]);
+		append(json, ",");
+		append(json, node.scale[2]);
+		append(json, "]");
+	}
+	if (node.has_matrix)
+	{
+		comma(json);
+		append(json, "\"matrix\":[");
+		for (int k = 0; k < 16; ++k)
+		{
+			comma(json);
+			append(json, node.matrix[k]);
+		}
+		append(json, "]");
+	}
+	if (node.children_count || !ni.meshes.empty())
+	{
+		comma(json);
+		append(json, "\"children\":[");
+		for (size_t j = 0; j < node.children_count; ++j)
+		{
+			const NodeInfo& ci = nodes[node.children[j] - data->nodes];
+
+			if (ci.keep)
+			{
+				comma(json);
+				append(json, size_t(ci.remap));
+			}
+		}
+		for (size_t j = 0; j < ni.meshes.size(); ++j)
+		{
+			comma(json);
+			append(json, ni.meshes[j]);
+		}
+		append(json, "]");
+	}
+	if (node.camera)
+	{
+		comma(json);
+		append(json, "\"camera\":");
+		append(json, size_t(node.camera - data->cameras));
+	}
+	if (node.light)
+	{
+		comma(json);
+		append(json, "\"extensions\":{\"KHR_lights_punctual\":{\"light\":");
+		append(json, size_t(node.light - data->lights));
+		append(json, "}}");
+	}
+	append(json, "}");
+}
+
+void writeAnimation(std::string& json, std::vector<BufferView>& views, std::string& json_accessors, size_t& accr_offset, const cgltf_animation& animation, cgltf_data* data, const std::vector<NodeInfo>& nodes, const Settings& settings)
+{
+	std::vector<const cgltf_animation_channel*> tracks;
+
+	for (size_t j = 0; j < animation.channels_count; ++j)
+	{
+		const cgltf_animation_channel& channel = animation.channels[j];
+
+		if (!channel.target_node)
+		{
+			fprintf(stderr, "Warning: ignoring channel %d of animation %d because it has no target node\n", int(j), int(&animation - data->animations));
+			continue;
+		}
+
+		const NodeInfo& ni = nodes[channel.target_node - data->nodes];
+
+		if (!ni.keep)
+			continue;
+
+		if (!settings.anim_const && (ni.animated_paths & (1 << channel.target_path)) == 0)
+			continue;
+
+		tracks.push_back(&channel);
+	}
+
+	if (tracks.empty())
+	{
+		fprintf(stderr, "Warning: ignoring animation %d because it has no valid tracks\n", int(&animation - data->animations));
+		return;
+	}
+
+	float mint = 0, maxt = 0;
+	bool needs_time = false;
+	bool needs_pose = false;
+
+	for (size_t j = 0; j < tracks.size(); ++j)
+	{
+		const cgltf_animation_channel& channel = *tracks[j];
+		const cgltf_animation_sampler& sampler = *channel.sampler;
+
+		mint = std::min(mint, sampler.input->min[0]);
+		maxt = std::max(maxt, sampler.input->max[0]);
+
+		bool tc = isTrackConstant(sampler, channel.target_path, channel.target_node);
+
+		needs_time = needs_time || !tc;
+		needs_pose = needs_pose || tc;
+	}
+
+	// round the number of frames to nearest but favor the "up" direction
+	// this means that at 10 Hz resampling, we will try to preserve the last frame <10ms
+	// but if the last frame is <2ms we favor just removing this data
+	int frames = 1 + int((maxt - mint) * settings.anim_freq + 0.8f);
+
+	size_t time_accr = needs_time ? writeAnimationTime(views, json_accessors, accr_offset, mint, frames, settings) : 0;
+	size_t pose_accr = needs_pose ? writeAnimationTime(views, json_accessors, accr_offset, mint, 1, settings) : 0;
+
+	std::string json_samplers;
+	std::string json_channels;
+
+	size_t track_offset = 0;
+
+	for (size_t j = 0; j < tracks.size(); ++j)
+	{
+		const cgltf_animation_channel& channel = *tracks[j];
+		const cgltf_animation_sampler& sampler = *channel.sampler;
+
+		bool tc = isTrackConstant(sampler, channel.target_path, channel.target_node);
+
+		std::vector<Attr> track;
+		resampleKeyframes(track, sampler, channel.target_path, channel.target_node, tc ? 1 : frames, mint, settings.anim_freq);
+
+		std::string scratch;
+		StreamFormat format = writeKeyframeStream(scratch, channel.target_path, track);
+
+		size_t view = getBufferView(views, BufferView::Kind_Keyframe, channel.target_path, format.stride, settings.compress && channel.target_path != cgltf_animation_path_type_weights);
+		size_t offset = views[view].data.size();
+		views[view].data += scratch;
+
+		comma(json_accessors);
+		writeAccessor(json_accessors, view, offset, format.type, format.component_type, format.normalized, track.size());
+
+		size_t data_accr = accr_offset++;
+
+		comma(json_samplers);
+		append(json_samplers, "{\"input\":");
+		append(json_samplers, tc ? pose_accr : time_accr);
+		append(json_samplers, ",\"output\":");
+		append(json_samplers, data_accr);
+		append(json_samplers, "}");
+
+		const NodeInfo& tni = nodes[channel.target_node - data->nodes];
+		size_t target_node = size_t(tni.remap);
+
+		if (channel.target_path == cgltf_animation_path_type_weights)
+		{
+			assert(tni.meshes.size() == 1);
+			target_node = tni.meshes[0];
+		}
+
+		comma(json_channels);
+		append(json_channels, "{\"sampler\":");
+		append(json_channels, track_offset);
+		append(json_channels, ",\"target\":{\"node\":");
+		append(json_channels, target_node);
+		append(json_channels, ",\"path\":");
+		append(json_channels, animationPath(channel.target_path));
+		append(json_channels, "}}");
+
+		track_offset++;
+	}
+
+	comma(json);
+	append(json, "{");
+	if (animation.name && *animation.name)
+	{
+		append(json, "\"name\":\"");
+		append(json, animation.name);
+		append(json, "\",");
+	}
+	append(json, "\"samplers\":[");
+	append(json, json_samplers);
+	append(json, "],\"channels\":[");
+	append(json, json_channels);
+	append(json, "]}");
+}
+
+void writeCamera(std::string& json, const cgltf_camera& camera)
+{
+	comma(json);
+	append(json, "{");
+
+	switch (camera.type)
+	{
+	case cgltf_camera_type_perspective:
+		append(json, "\"type\":\"perspective\",\"perspective\":{");
+		append(json, "\"yfov\":");
+		append(json, camera.perspective.yfov);
+		append(json, ",\"znear\":");
+		append(json, camera.perspective.znear);
+		if (camera.perspective.aspect_ratio != 0.f)
+		{
+			append(json, ",\"aspectRatio\":");
+			append(json, camera.perspective.aspect_ratio);
+		}
+		if (camera.perspective.zfar != 0.f)
+		{
+			append(json, ",\"zfar\":");
+			append(json, camera.perspective.zfar);
+		}
+		append(json, "}");
+		break;
+
+	case cgltf_camera_type_orthographic:
+		append(json, "\"type\":\"orthographic\",\"orthographic\":{");
+		append(json, "\"xmag\":");
+		append(json, camera.orthographic.xmag);
+		append(json, ",\"ymag\":");
+		append(json, camera.orthographic.ymag);
+		append(json, ",\"znear\":");
+		append(json, camera.orthographic.znear);
+		append(json, ",\"zfar\":");
+		append(json, camera.orthographic.zfar);
+		append(json, "}");
+		break;
+
+	default:
+		fprintf(stderr, "Warning: skipping camera of unknown type\n");
+	}
+
+	append(json, "}");
+}
+
+void writeLight(std::string& json, const cgltf_light& light)
+{
+	static const float white[3] = {1, 1, 1};
+
+	comma(json);
+	append(json, "{\"type\":");
+	append(json, lightType(light.type));
+	if (memcmp(light.color, white, sizeof(white)) != 0)
+	{
+		comma(json);
+		append(json, "\"color\":[");
+		append(json, light.color[0]);
+		append(json, ",");
+		append(json, light.color[1]);
+		append(json, ",");
+		append(json, light.color[2]);
+		append(json, "]");
+	}
+	if (light.intensity != 1.f)
+	{
+		comma(json);
+		append(json, "\"intensity\":");
+		append(json, light.intensity);
+	}
+	if (light.range != 0.f)
+	{
+		comma(json);
+		append(json, "\"range\":");
+		append(json, light.range);
+	}
+	if (light.type == cgltf_light_type_spot)
+	{
+		comma(json);
+		append(json, "\"spot\":{");
+		append(json, "\"innerConeAngle\":");
+		append(json, light.spot_inner_cone_angle);
+		append(json, ",\"outerConeAngle\":");
+		append(json, light.spot_outer_cone_angle == 0.f ? 0.78539816339f : light.spot_outer_cone_angle);
+		append(json, "}");
+	}
+	append(json, "}");
+}
+
+void printStats(const std::vector<BufferView>& views, BufferView::Kind kind)
+{
+	for (size_t i = 0; i < views.size(); ++i)
+	{
+		const BufferView& view = views[i];
+
+		if (view.kind != kind)
+			continue;
+
+		const char* variant = "unknown";
+
+		switch (kind)
+		{
+		case BufferView::Kind_Vertex:
+			variant = attributeType(cgltf_attribute_type(view.variant));
+			break;
+
+		case BufferView::Kind_Index:
+			variant = "index";
+			break;
+
+		case BufferView::Kind_Keyframe:
+			variant = animationPath(cgltf_animation_path_type(view.variant));
+			break;
+
+		default:;
+		}
+
+		size_t count = view.data.size() / view.stride;
+
+		printf("    %s: compressed %d bytes (%.1f bits), raw %d bytes (%d bits)\n",
+		       variant,
+		       int(view.bytes),
+		       double(view.bytes) / double(count) * 8,
+		       int(view.data.size()),
+		       int(view.stride * 8));
+	}
+}
+
+bool process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settings, std::string& json, std::string& bin)
+{
+	if (settings.verbose)
+	{
+		printf("input: %d nodes, %d meshes (%d primitives), %d materials, %d skins, %d animations\n",
+		       int(data->nodes_count), int(data->meshes_count), int(meshes.size()), int(data->materials_count), int(data->skins_count), int(data->animations_count));
+	}
+
+	std::vector<NodeInfo> nodes(data->nodes_count);
+
+	markAnimated(data, nodes);
+
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		Mesh& mesh = meshes[i];
+
+		// note: when -kn is specified, we keep mesh-node attachment so that named nodes can be transformed
+		if (mesh.node && !settings.keep_named)
+		{
+			NodeInfo& ni = nodes[mesh.node - data->nodes];
+
+			// we transform all non-skinned non-animated meshes to world space
+			// this makes sure that quantization doesn't introduce gaps if the original scene was watertight
+			if (!ni.animated && !mesh.skin && mesh.targets == 0)
+			{
+				transformMesh(mesh, mesh.node);
+				mesh.node = 0;
+			}
+
+			// skinned and animated meshes will be anchored to the same node that they used to be in
+			// for animated meshes, this is important since they need to be transformed by the same animation
+			// for skinned meshes, in theory this isn't important since the transform of the skinned node doesn't matter; in practice this affects generated bounding box in three.js
+		}
+	}
+
+	mergeMeshMaterials(data, meshes);
+	mergeMeshes(meshes, settings);
+
+	markNeededNodes(data, nodes, meshes, settings);
+
+	std::vector<MaterialInfo> materials(data->materials_count);
+
+	markNeededMaterials(data, materials, meshes);
+
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		Mesh& mesh = meshes[i];
+
+		if (mesh.indices.empty())
+			continue;
+
+		reindexMesh(mesh);
+		optimizeMesh(mesh);
+	}
+
+	if (settings.verbose)
+	{
+		size_t triangles = 0;
+		size_t vertices = 0;
+
+		for (size_t i = 0; i < meshes.size(); ++i)
+		{
+			const Mesh& mesh = meshes[i];
+
+			triangles += mesh.indices.size() / 3;
+			vertices += mesh.streams.empty() ? 0 : mesh.streams[0].data.size();
+		}
+
+		printf("meshes: %d triangles, %d vertices\n", int(triangles), int(vertices));
+	}
+
+	QuantizationParams qp = prepareQuantization(meshes, settings);
+
+	std::string json_images;
+	std::string json_textures;
+	std::string json_materials;
+	std::string json_accessors;
+	std::string json_meshes;
+	std::string json_nodes;
+	std::string json_skins;
+	std::string json_roots;
+	std::string json_animations;
+	std::string json_cameras;
+	std::string json_lights;
+
+	std::vector<BufferView> views;
+
+	bool ext_pbr_specular_glossiness = false;
+	bool ext_unlit = false;
+
+	size_t accr_offset = 0;
+	size_t node_offset = 0;
+	size_t mesh_offset = 0;
+	size_t material_offset = 0;
+
+	for (size_t i = 0; i < data->images_count; ++i)
+	{
+		const cgltf_image& image = data->images[i];
+
+		comma(json_images);
+		append(json_images, "{");
+		if (image.uri)
+		{
+			std::string mime_type;
+			std::string img;
+
+			if (parseDataUri(image.uri, mime_type, img))
+			{
+				writeEmbeddedImage(json_images, views, img.c_str(), img.size(), mime_type.c_str());
+			}
+			else
+			{
+				append(json_images, "\"uri\":\"");
+				append(json_images, image.uri);
+				append(json_images, "\"");
+			}
+		}
+		else if (image.buffer_view && image.buffer_view->buffer->data && image.mime_type)
+		{
+			const char* img = static_cast<const char*>(image.buffer_view->buffer->data) + image.buffer_view->offset;
+			size_t size = image.buffer_view->size;
+
+			writeEmbeddedImage(json_images, views, img, size, image.mime_type);
+		}
+		else
+		{
+			fprintf(stderr, "Warning: ignoring image %d since it has no URI and no valid buffer data\n", int(i));
+		}
+
+		append(json_images, "}");
+	}
+
+	for (size_t i = 0; i < data->textures_count; ++i)
+	{
+		const cgltf_texture& texture = data->textures[i];
+
+		comma(json_textures);
+		append(json_textures, "{");
+		if (texture.image)
+		{
+			append(json_textures, "\"source\":");
+			append(json_textures, size_t(texture.image - data->images));
+		}
+		append(json_textures, "}");
+	}
+
+	for (size_t i = 0; i < data->materials_count; ++i)
+	{
+		MaterialInfo& mi = materials[i];
+
+		if (!mi.keep)
+			continue;
+
+		const cgltf_material& material = data->materials[i];
+
+		comma(json_materials);
+		append(json_materials, "{");
+		writeMaterialInfo(json_materials, data, material, qp);
+		append(json_materials, "}");
+
+		mi.remap = int(material_offset);
+		material_offset++;
+
+		ext_pbr_specular_glossiness = ext_pbr_specular_glossiness || material.has_pbr_specular_glossiness;
+		ext_unlit = ext_unlit || material.unlit;
+	}
+
+	for (size_t i = 0; i < meshes.size(); ++i)
+	{
+		const Mesh& mesh = meshes[i];
+
+		if (mesh.indices.empty())
+			continue;
+
+		comma(json_meshes);
+		append(json_meshes, "{\"primitives\":[");
+
+		size_t pi = i;
+		for (; pi < meshes.size(); ++pi)
+		{
+			const Mesh& prim = meshes[pi];
+
+			if (prim.indices.empty())
+				continue;
+
+			if (prim.node != mesh.node || prim.skin != mesh.skin || prim.targets != mesh.targets)
+				break;
+
+			if (mesh.weights.size() && (prim.weights.size() != mesh.weights.size() || memcmp(&mesh.weights[0], &prim.weights[0], mesh.weights.size() * sizeof(float)) != 0))
+				break;
+
+			comma(json_meshes);
+			append(json_meshes, "{\"attributes\":{");
+			writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, 0, qp, settings);
+			append(json_meshes, "}");
+
+			if (mesh.targets)
+			{
+				append(json_meshes, ",\"targets\":[");
+				for (size_t j = 0; j < mesh.targets; ++j)
+				{
+					comma(json_meshes);
+					append(json_meshes, "{");
+					writeMeshAttributes(json_meshes, views, json_accessors, accr_offset, prim, int(1 + j), qp, settings);
+					append(json_meshes, "}");
+				}
+				append(json_meshes, "]");
+			}
+
+			size_t index_accr = writeMeshIndices(views, json_accessors, accr_offset, prim, settings);
+
+			append(json_meshes, ",\"indices\":");
+			append(json_meshes, index_accr);
+			if (prim.material)
+			{
+				MaterialInfo& mi = materials[prim.material - data->materials];
+
+				assert(mi.keep);
+				append(json_meshes, ",\"material\":");
+				append(json_meshes, size_t(mi.remap));
+			}
+			append(json_meshes, "}");
+		}
+
+		append(json_meshes, "]");
+
+		if (mesh.weights.size())
+		{
+			append(json_meshes, ",\"weights\":[");
+			for (size_t j = 0; j < mesh.weights.size(); ++j)
+			{
+				comma(json_meshes);
+				append(json_meshes, mesh.weights[j]);
+			}
+			append(json_meshes, "]");
+		}
+		append(json_meshes, "}");
+
+		writeMeshNode(json_nodes, mesh_offset, mesh, data, qp);
+
+		if (mesh.node)
+		{
+			NodeInfo& ni = nodes[mesh.node - data->nodes];
+
+			assert(ni.keep);
+			ni.meshes.push_back(node_offset);
+		}
+		else
+		{
+			comma(json_roots);
+			append(json_roots, node_offset);
+		}
+
+		node_offset++;
+		mesh_offset++;
+
+		// skip all meshes that we've written in this iteration
+		assert(pi > i);
+		i = pi - 1;
+	}
+
+	remapNodes(data, nodes, node_offset);
+
+	for (size_t i = 0; i < data->nodes_count; ++i)
+	{
+		NodeInfo& ni = nodes[i];
+
+		if (!ni.keep)
+			continue;
+
+		const cgltf_node& node = data->nodes[i];
+
+		if (!node.parent)
+		{
+			comma(json_roots);
+			append(json_roots, size_t(ni.remap));
+		}
+
+		writeNode(json_nodes, node, nodes, data);
+	}
+
+	for (size_t i = 0; i < data->skins_count; ++i)
+	{
+		const cgltf_skin& skin = data->skins[i];
+
+		size_t matrix_accr = writeJointBindMatrices(views, json_accessors, accr_offset, skin, qp, settings);
+
+		comma(json_skins);
+		append(json_skins, "{");
+		append(json_skins, "\"joints\":[");
+		for (size_t j = 0; j < skin.joints_count; ++j)
+		{
+			comma(json_skins);
+			append(json_skins, size_t(nodes[skin.joints[j] - data->nodes].remap));
+		}
+		append(json_skins, "]");
+		append(json_skins, ",\"inverseBindMatrices\":");
+		append(json_skins, matrix_accr);
+		if (skin.skeleton)
+		{
+			comma(json_skins);
+			append(json_skins, "\"skeleton\":");
+			append(json_skins, size_t(nodes[skin.skeleton - data->nodes].remap));
+		}
+		append(json_skins, "}");
+	}
+
+	for (size_t i = 0; i < data->animations_count; ++i)
+	{
+		const cgltf_animation& animation = data->animations[i];
+
+		writeAnimation(json_animations, views, json_accessors, accr_offset, animation, data, nodes, settings);
+	}
+
+	for (size_t i = 0; i < data->cameras_count; ++i)
+	{
+		const cgltf_camera& camera = data->cameras[i];
+
+		writeCamera(json_cameras, camera);
+	}
+
+	for (size_t i = 0; i < data->lights_count; ++i)
+	{
+		const cgltf_light& light = data->lights[i];
+
+		writeLight(json_lights, light);
+	}
+
+	char version[32];
+	sprintf(version, "%d.%d", MESHOPTIMIZER_VERSION / 1000, (MESHOPTIMIZER_VERSION % 1000) / 10);
+
+	append(json, "\"asset\":{");
+	append(json, "\"version\":\"2.0\",\"generator\":\"gltfpack ");
+	append(json, version);
+	append(json, "\"");
+	if (data->asset.extras.start_offset)
+	{
+		append(json, ",\"extras\":");
+		json.append(data->json + data->asset.extras.start_offset, data->json + data->asset.extras.end_offset);
+	}
+	append(json, "}");
+	append(json, ",\"extensionsUsed\":[");
+	append(json, "\"MESHOPT_quantized_geometry\"");
+	if (settings.compress)
+	{
+		comma(json);
+		append(json, "\"MESHOPT_compression\"");
+	}
+	if (!json_textures.empty())
+	{
+		comma(json);
+		append(json, "\"KHR_texture_transform\"");
+	}
+	if (ext_pbr_specular_glossiness)
+	{
+		comma(json);
+		append(json, "\"KHR_materials_pbrSpecularGlossiness\"");
+	}
+	if (ext_unlit)
+	{
+		comma(json);
+		append(json, "\"KHR_materials_unlit\"");
+	}
+	if (data->lights_count)
+	{
+		comma(json);
+		append(json, "\"KHR_lights_punctual\"");
+	}
+	append(json, "]");
+	if (settings.compress)
+	{
+		append(json, ",\"extensionsRequired\":[");
+		// Note: ideally we should include MESHOPT_quantized_geometry in the required extension list (regardless of compression)
+		// This extension *only* allows the use of quantized attributes for positions/normals/etc. This happens to be supported
+		// by popular JS frameworks, however, Babylon.JS refuses to load files with unsupported required extensions.
+		// For now we don't include it in the list, which will be fixed at some point once this extension becomes official.
+		append(json, "\"MESHOPT_compression\"");
+		append(json, "]");
+	}
+
+	size_t bytes[BufferView::Kind_Count] = {};
+
+	if (!views.empty())
+	{
+		append(json, ",\"bufferViews\":[");
+		for (size_t i = 0; i < views.size(); ++i)
+		{
+			BufferView& view = views[i];
+
+			size_t offset = bin.size();
+			size_t count = view.data.size() / view.stride;
+
+			int compression = -1;
+
+			if (view.compressed)
+			{
+				if (view.kind == BufferView::Kind_Index)
+				{
+					compressIndexStream(bin, view.data, count, view.stride);
+					compression = 1;
+				}
+				else
+				{
+					compressVertexStream(bin, view.data, count, view.stride);
+					compression = 0;
+				}
+			}
+			else
+			{
+				bin += view.data;
+			}
+
+			comma(json);
+			writeBufferView(json, view.kind, count, view.stride, offset, bin.size() - offset, compression);
+
+			view.bytes = bin.size() - offset;
+			bytes[view.kind] += view.bytes;
+
+			// align each bufferView by 4 bytes
+			bin.resize((bin.size() + 3) & ~3);
+		}
+		append(json, "]");
+	}
+	if (!json_accessors.empty())
+	{
+		append(json, ",\"accessors\":[");
+		append(json, json_accessors);
+		append(json, "]");
+	}
+	if (!json_images.empty())
+	{
+		append(json, ",\"images\":[");
+		append(json, json_images);
+		append(json, "]");
+	}
+	if (!json_textures.empty())
+	{
+		append(json, ",\"textures\":[");
+		append(json, json_textures);
+		append(json, "]");
+	}
+	if (!json_materials.empty())
+	{
+		append(json, ",\"materials\":[");
+		append(json, json_materials);
+		append(json, "]");
+	}
+	if (!json_meshes.empty())
+	{
+		append(json, ",\"meshes\":[");
+		append(json, json_meshes);
+		append(json, "]");
+	}
+	if (!json_skins.empty())
+	{
+		append(json, ",\"skins\":[");
+		append(json, json_skins);
+		append(json, "]");
+	}
+	if (!json_animations.empty())
+	{
+		append(json, ",\"animations\":[");
+		append(json, json_animations);
+		append(json, "]");
+	}
+	if (!json_roots.empty())
+	{
+		append(json, ",\"nodes\":[");
+		append(json, json_nodes);
+		append(json, "],\"scenes\":[");
+		append(json, "{\"nodes\":[");
+		append(json, json_roots);
+		append(json, "]}]");
+	}
+	if (!json_cameras.empty())
+	{
+		append(json, ",\"cameras\":[");
+		append(json, json_cameras);
+		append(json, "]");
+	}
+	if (!json_lights.empty())
+	{
+		append(json, ",\"extensions\":{\"KHR_lights_punctual\":{\"lights\":[");
+		append(json, json_lights);
+		append(json, "]}}");
+	}
+	if (!json_roots.empty())
+	{
+		append(json, ",\"scene\":0");
+	}
+
+	if (settings.verbose)
+	{
+		size_t primitives = 0;
+
+		for (size_t i = 0; i < meshes.size(); ++i)
+			primitives += !meshes[i].indices.empty();
+
+		printf("output: %d nodes, %d meshes (%d primitives), %d materials\n", int(node_offset), int(mesh_offset), int(primitives), int(material_offset));
+		printf("output: JSON %d bytes, buffers %d bytes\n", int(json.size()), int(bin.size()));
+		printf("output: buffers: vertex %d bytes, index %d bytes, skin %d bytes, time %d bytes, keyframe %d bytes, image %d bytes\n",
+		       int(bytes[BufferView::Kind_Vertex]), int(bytes[BufferView::Kind_Index]), int(bytes[BufferView::Kind_Skin]),
+		       int(bytes[BufferView::Kind_Time]), int(bytes[BufferView::Kind_Keyframe]), int(bytes[BufferView::Kind_Image]));
+	}
+
+	if (settings.verbose > 1)
+	{
+		printf("output: vertex stats:\n");
+		printStats(views, BufferView::Kind_Vertex);
+
+		printf("output: index stats:\n");
+		printStats(views, BufferView::Kind_Index);
+
+		printf("output: keyframe stats:\n");
+		printStats(views, BufferView::Kind_Keyframe);
+	}
+
+	return true;
+}
+
+void writeU32(FILE* out, uint32_t data)
+{
+	fwrite(&data, 4, 1, out);
+}
+
+int gltfpack(const char* input, const char* output, const Settings& settings)
+{
+	cgltf_data* data = 0;
+	std::vector<Mesh> meshes;
+
+	const char* iext = strrchr(input, '.');
+
+	if (iext && (strcmp(iext, ".gltf") == 0 || strcmp(iext, ".GLTF") == 0 || strcmp(iext, ".glb") == 0 || strcmp(iext, ".GLB") == 0))
+	{
+		cgltf_options options = {};
+		cgltf_result result = cgltf_parse_file(&options, input, &data);
+		result = (result == cgltf_result_success) ? cgltf_validate(data) : result;
+		result = (result == cgltf_result_success) ? cgltf_load_buffers(&options, data, input) : result;
+
+		if (result != cgltf_result_success)
+		{
+			fprintf(stderr, "Error loading %s: %s\n", input, getError(result));
+			cgltf_free(data);
+			return 2;
+		}
+
+		parseMeshesGltf(data, meshes);
+	}
+	else if (iext && (strcmp(iext, ".obj") == 0 || strcmp(iext, ".OBJ") == 0))
+	{
+		fastObjMesh* obj = fast_obj_read(input);
+
+		if (!obj)
+		{
+			fprintf(stderr, "Error loading %s: file not found\n", input);
+			cgltf_free(data);
+			return 2;
+		}
+
+		data = parseSceneObj(obj);
+		parseMeshesObj(obj, data, meshes);
+
+		fast_obj_destroy(obj);
+	}
+	else
+	{
+		fprintf(stderr, "Error loading %s: unknown extension (expected .gltf or .glb or .obj)\n", input);
+		return 2;
+	}
+
+	std::string json, bin;
+	if (!process(data, meshes, settings, json, bin))
+	{
+		fprintf(stderr, "Error processing %s\n", input);
+		cgltf_free(data);
+		return 3;
+	}
+
+	cgltf_free(data);
+
+	if (!output)
+	{
+		return 0;
+	}
+
+	const char* oext = strrchr(output, '.');
+
+	if (oext && (strcmp(oext, ".gltf") == 0 || strcmp(oext, ".GLTF") == 0))
+	{
+		std::string binpath = output;
+		binpath.replace(binpath.size() - 5, 5, ".bin");
+
+		std::string binname = binpath;
+		std::string::size_type slash = binname.find_last_of("/\\");
+		if (slash != std::string::npos)
+			binname.erase(0, slash + 1);
+
+		FILE* outjson = fopen(output, "wb");
+		FILE* outbin = fopen(binpath.c_str(), "wb");
+		if (!outjson || !outbin)
+		{
+			fprintf(stderr, "Error saving %s\n", output);
+			return 4;
+		}
+
+		fprintf(outjson, "{\"buffers\":[{\"uri\":\"%s\",\"byteLength\":%zu}],", binname.c_str(), bin.size());
+		fwrite(json.c_str(), json.size(), 1, outjson);
+		fprintf(outjson, "}");
+
+		fwrite(bin.c_str(), bin.size(), 1, outbin);
+
+		fclose(outjson);
+		fclose(outbin);
+	}
+	else if (oext && (strcmp(oext, ".glb") == 0 || strcmp(oext, ".GLB") == 0))
+	{
+		FILE* out = fopen(output, "wb");
+		if (!out)
+		{
+			fprintf(stderr, "Error saving %s\n", output);
+			return 4;
+		}
+
+		char bufferspec[64];
+		sprintf(bufferspec, "{\"buffers\":[{\"byteLength\":%zu}],", bin.size());
+
+		json.insert(0, bufferspec);
+		json.push_back('}');
+
+		while (json.size() % 4)
+			json.push_back(' ');
+
+		while (bin.size() % 4)
+			bin.push_back('\0');
+
+		writeU32(out, 0x46546C67);
+		writeU32(out, 2);
+		writeU32(out, uint32_t(12 + 8 + json.size() + 8 + bin.size()));
+
+		writeU32(out, uint32_t(json.size()));
+		writeU32(out, 0x4E4F534A);
+		fwrite(json.c_str(), json.size(), 1, out);
+
+		writeU32(out, uint32_t(bin.size()));
+		writeU32(out, 0x004E4942);
+		fwrite(bin.c_str(), bin.size(), 1, out);
+
+		fclose(out);
+	}
+	else
+	{
+		fprintf(stderr, "Error saving %s: unknown extension (expected .gltf or .glb)\n", output);
+		return 4;
+	}
+
+	return 0;
+}
+
+int main(int argc, char** argv)
+{
+	Settings settings = {};
+	settings.pos_bits = 14;
+	settings.tex_bits = 12;
+	settings.nrm_bits = 8;
+	settings.anim_freq = 30;
+
+	const char* input = 0;
+	const char* output = 0;
+	bool help = false;
+	int test = 0;
+
+	for (int i = 1; i < argc; ++i)
+	{
+		const char* arg = argv[i];
+
+		if (strcmp(arg, "-vp") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
+		{
+			settings.pos_bits = atoi(argv[++i]);
+		}
+		else if (strcmp(arg, "-vt") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
+		{
+			settings.tex_bits = atoi(argv[++i]);
+		}
+		else if (strcmp(arg, "-vn") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
+		{
+			settings.nrm_bits = atoi(argv[++i]);
+		}
+		else if (strcmp(arg, "-vu") == 0)
+		{
+			settings.nrm_unit = true;
+		}
+		else if (strcmp(arg, "-af") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
+		{
+			settings.anim_freq = atoi(argv[++i]);
+		}
+		else if (strcmp(arg, "-ac") == 0)
+		{
+			settings.anim_const = true;
+		}
+		else if (strcmp(arg, "-kn") == 0)
+		{
+			settings.keep_named = true;
+		}
+		else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
+		{
+			input = argv[++i];
+		}
+		else if (strcmp(arg, "-o") == 0 && i + 1 < argc && !output)
+		{
+			output = argv[++i];
+		}
+		else if (strcmp(arg, "-c") == 0)
+		{
+			settings.compress = true;
+		}
+		else if (strcmp(arg, "-v") == 0)
+		{
+			settings.verbose = 1;
+		}
+		else if (strcmp(arg, "-vv") == 0)
+		{
+			settings.verbose = 2;
+		}
+		else if (strcmp(arg, "-h") == 0)
+		{
+			help = true;
+		}
+		else if (strcmp(arg, "-test") == 0)
+		{
+			test = i + 1;
+			break;
+		}
+		else
+		{
+			fprintf(stderr, "Unrecognized option %s\n", arg);
+			return 1;
+		}
+	}
+
+	if (test)
+	{
+		for (int i = test; i < argc; ++i)
+			gltfpack(argv[i], NULL, settings);
+
+		return 0;
+	}
+
+	if (!input || !output || help)
+	{
+		fprintf(stderr, "Usage: gltfpack [options] -i input -o output\n");
+		fprintf(stderr, "\n");
+		fprintf(stderr, "Options:\n");
+		fprintf(stderr, "-i file: input file to process, .obj/.gltf/.glb\n");
+		fprintf(stderr, "-o file: output file path, .gltf/.glb\n");
+		fprintf(stderr, "-vp N: use N-bit quantization for positions (default: 14; N should be between 1 and 16)\n");
+		fprintf(stderr, "-vt N: use N-bit quantization for texture corodinates (default: 12; N should be between 1 and 16)\n");
+		fprintf(stderr, "-vn N: use N-bit quantization for normals and tangents (default: 8; N should be between 1 and 8)\n");
+		fprintf(stderr, "-vu: use unit-length normal/tangent vectors (default: off)\n");
+		fprintf(stderr, "-af N: resample animations at N Hz (default: 30)\n");
+		fprintf(stderr, "-ac: keep constant animation tracks even if they don't modify the node transform\n");
+		fprintf(stderr, "-kn: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally\n");
+		fprintf(stderr, "-c: produce compressed glb files\n");
+		fprintf(stderr, "-v: verbose output\n");
+		fprintf(stderr, "-h: display this help and exit\n");
+
+		return 1;
+	}
+
+	return gltfpack(input, output, settings);
+}

+ 52 - 64
3rdparty/meshoptimizer/tools/lodviewer.cpp

@@ -1,7 +1,8 @@
 #define _CRT_SECURE_NO_WARNINGS
 
 #include "../src/meshoptimizer.h"
-#include "objparser.h"
+#include "fast_obj.h"
+#include "cgltf.h"
 
 #include <algorithm>
 #include <cmath>
@@ -11,11 +12,6 @@
 
 #include <GLFW/glfw3.h>
 
-#ifdef GLTF
-#define CGLTF_IMPLEMENTATION
-#include "cgltf.h"
-#endif
-
 #ifdef _WIN32
 #pragma comment(lib, "opengl32.lib")
 #endif
@@ -60,41 +56,58 @@ struct Mesh
 
 Mesh parseObj(const char* path)
 {
-	ObjFile file;
-
-	if (!objParseFile(file, path) || !objValidate(file))
+	fastObjMesh* obj = fast_obj_read(path);
+	if (!obj)
 	{
-		printf("Error loading %s\n", path);
+		printf("Error loading %s: file not found\n", path);
 		return Mesh();
 	}
 
-	size_t total_indices = file.f_size / 3;
+	size_t total_indices = 0;
+
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+		total_indices += 3 * (obj->face_vertices[i] - 2);
 
 	std::vector<Vertex> vertices(total_indices);
 
-	for (size_t i = 0; i < total_indices; ++i)
-	{
-		int vi = file.f[i * 3 + 0];
-		int vti = file.f[i * 3 + 1];
-		int vni = file.f[i * 3 + 2];
+	size_t vertex_offset = 0;
+	size_t index_offset = 0;
 
-		Vertex v =
-		    {
-		        file.v[vi * 3 + 0],
-		        file.v[vi * 3 + 1],
-		        file.v[vi * 3 + 2],
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+	{
+		for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
+		{
+			fastObjIndex gi = obj->indices[index_offset + j];
 
-		        vni >= 0 ? file.vn[vni * 3 + 0] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 1] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 2] : 0,
+			Vertex v =
+			{
+				obj->positions[gi.p * 3 + 0],
+				obj->positions[gi.p * 3 + 1],
+				obj->positions[gi.p * 3 + 2],
+				obj->normals[gi.n * 3 + 0],
+				obj->normals[gi.n * 3 + 1],
+				obj->normals[gi.n * 3 + 2],
+				obj->texcoords[gi.t * 2 + 0],
+				obj->texcoords[gi.t * 2 + 1],
+			};
+
+			// triangulate polygon on the fly; offset-3 is always the first polygon vertex
+			if (j >= 3)
+			{
+				vertices[vertex_offset + 0] = vertices[vertex_offset - 3];
+				vertices[vertex_offset + 1] = vertices[vertex_offset - 1];
+				vertex_offset += 2;
+			}
 
-		        vti >= 0 ? file.vt[vti * 3 + 0] : 0,
-		        vti >= 0 ? file.vt[vti * 3 + 1] : 0,
-		    };
+			vertices[vertex_offset] = v;
+			vertex_offset++;
+		}
 
-		vertices[i] = v;
+		index_offset += obj->face_vertices[i];
 	}
 
+	fast_obj_destroy(obj);
+
 	Mesh result;
 
 	std::vector<unsigned int> remap(total_indices);
@@ -109,7 +122,6 @@ Mesh parseObj(const char* path)
 	return result;
 }
 
-#ifdef GLTF
 cgltf_accessor* getAccessor(const cgltf_attribute* attributes, size_t attribute_count, cgltf_attribute_type type, int index = 0)
 {
 	for (size_t i = 0; i < attribute_count; ++i)
@@ -119,15 +131,6 @@ cgltf_accessor* getAccessor(const cgltf_attribute* attributes, size_t attribute_
 	return 0;
 }
 
-template <typename T>
-const T* getComponentPtr(const cgltf_accessor* a)
-{
-	const char* buffer = (char*)a->buffer_view->buffer->data;
-	size_t offset = a->offset + a->buffer_view->offset;
-
-	return reinterpret_cast<const T*>(&buffer[offset]);
-}
-
 Mesh parseGltf(const char* path)
 {
 	cgltf_options options = {};
@@ -205,55 +208,43 @@ Mesh parseGltf(const char* path)
 			if (!ai || !ap)
 				continue;
 
-			if (ai->component_type == cgltf_component_type_r_32u)
-			{
-				const unsigned int* ptr = getComponentPtr<unsigned int>(ai);
-
-				for (size_t i = 0; i < ai->count; ++i)
-					result.indices[index_offset + i] = unsigned(vertex_offset + ptr[i]);
-			}
-			else
-			{
-				const unsigned short* ptr = getComponentPtr<unsigned short>(ai);
-
-				for (size_t i = 0; i < ai->count; ++i)
-					result.indices[index_offset + i] = unsigned(vertex_offset + ptr[i]);
-			}
+			for (size_t i = 0; i < ai->count; ++i)
+				result.indices[index_offset + i] = unsigned(vertex_offset + cgltf_accessor_read_index(ai, i));
 
 			{
-				const float* ptr = getComponentPtr<float>(ap);
-
 				for (size_t i = 0; i < ap->count; ++i)
 				{
+					float ptr[3];
+					cgltf_accessor_read_float(ap, i, ptr, 3);
+
 					result.vertices[vertex_offset + i].px = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8] + transform[12];
 					result.vertices[vertex_offset + i].py = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9] + transform[13];
 					result.vertices[vertex_offset + i].pz = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10] + transform[14];
-					ptr += ap->stride / 4;
 				}
 			}
 
 			if (cgltf_accessor* an = getAccessor(primitive.attributes, primitive.attributes_count, cgltf_attribute_type_normal))
 			{
-				const float* ptr = getComponentPtr<float>(an);
-
 				for (size_t i = 0; i < ap->count; ++i)
 				{
+					float ptr[3];
+					cgltf_accessor_read_float(an, i, ptr, 3);
+
 					result.vertices[vertex_offset + i].nx = ptr[0] * transform[0] + ptr[1] * transform[4] + ptr[2] * transform[8];
 					result.vertices[vertex_offset + i].ny = ptr[0] * transform[1] + ptr[1] * transform[5] + ptr[2] * transform[9];
 					result.vertices[vertex_offset + i].nz = ptr[0] * transform[2] + ptr[1] * transform[6] + ptr[2] * transform[10];
-					ptr += an->stride / 4;
 				}
 			}
 
 			if (cgltf_accessor* at = getAccessor(primitive.attributes, primitive.attributes_count, cgltf_attribute_type_texcoord))
 			{
-				const float* ptr = getComponentPtr<float>(at);
-
 				for (size_t i = 0; i < ap->count; ++i)
 				{
+					float ptr[2];
+					cgltf_accessor_read_float(at, i, ptr, 2);
+
 					result.vertices[vertex_offset + i].tx = ptr[0];
 					result.vertices[vertex_offset + i].ty = ptr[1];
-					ptr += at->stride / 4;
 				}
 			}
 
@@ -274,17 +265,14 @@ Mesh parseGltf(const char* path)
 
 	return result;
 }
-#endif
 
 Mesh loadMesh(const char* path)
 {
 	if (strstr(path, ".obj"))
 		return parseObj(path);
 
-#ifdef GLTF
 	if (strstr(path, ".gltf") || strstr(path, ".glb"))
 		return parseGltf(path);
-#endif
 
 	return Mesh();
 }

+ 0 - 255
3rdparty/meshoptimizer/tools/meshencoder.cpp

@@ -1,255 +0,0 @@
-// Converts .obj files to .optmesh files
-// Usage: meshencoder [.obj] [.optmesh]
-
-// Data layout:
-// Header: 64b
-// Object table: 16b * object_count
-// Object data
-// Vertex data
-// Index data
-
-#include "../src/meshoptimizer.h"
-#include "objparser.h"
-
-#include <algorithm>
-#include <vector>
-
-#include <float.h>
-#include <math.h>
-#include <stdio.h>
-#include <string.h>
-
-struct Header
-{
-	char magic[4]; // OPTM
-
-	unsigned int group_count;
-	unsigned int vertex_count;
-	unsigned int index_count;
-	unsigned int vertex_data_size;
-	unsigned int index_data_size;
-
-	float pos_offset[3];
-	float pos_scale;
-	float uv_offset[2];
-	float uv_scale[2];
-
-	unsigned int reserved[2];
-};
-
-struct Object
-{
-	unsigned int index_offset;
-	unsigned int index_count;
-	unsigned int material_length;
-	unsigned int reserved;
-};
-
-struct Vertex
-{
-	unsigned short px, py, pz, pw; // unsigned 16-bit value, use pos_offset/pos_scale to unpack
-	char nx, ny, nz, nw; // normalized signed 8-bit value
-	unsigned short tx, ty; // unsigned 16-bit value, use uv_offset/uv_scale to unpack
-};
-
-float rcpSafe(float v)
-{
-	return v == 0.f ? 0.f : 1.f / v;
-}
-
-int main(int argc, char** argv)
-{
-	if (argc <= 2)
-	{
-		printf("Usage: %s [.obj] [.optmesh]\n", argv[0]);
-		return 1;
-	}
-
-	const char* input = argv[1];
-	const char* output = argv[2];
-
-	ObjFile file;
-
-	if (!objParseFile(file, input))
-	{
-		printf("Error loading %s: file not found\n", input);
-		return 2;
-	}
-
-	if (!objValidate(file))
-	{
-		printf("Error loading %s: invalid file data\n", input);
-		return 3;
-	}
-
-	float pos_offset[3] = { FLT_MAX, FLT_MAX, FLT_MAX };
-	float pos_scale = 0.f;
-
-	for (size_t i = 0; i < file.v_size; i += 3)
-	{
-		pos_offset[0] = std::min(pos_offset[0], file.v[i + 0]);
-		pos_offset[1] = std::min(pos_offset[1], file.v[i + 1]);
-		pos_offset[2] = std::min(pos_offset[2], file.v[i + 2]);
-	}
-
-	for (size_t i = 0; i < file.v_size; i += 3)
-	{
-		pos_scale = std::max(pos_scale, file.v[i + 0] - pos_offset[0]);
-		pos_scale = std::max(pos_scale, file.v[i + 1] - pos_offset[1]);
-		pos_scale = std::max(pos_scale, file.v[i + 2] - pos_offset[2]);
-	}
-
-	float uv_offset[2] = { FLT_MAX, FLT_MAX };
-	float uv_scale[2] = { 0, 0 };
-
-	for (size_t i = 0; i < file.vt_size; i += 3)
-	{
-		uv_offset[0] = std::min(uv_offset[0], file.vt[i + 0]);
-		uv_offset[1] = std::min(uv_offset[1], file.vt[i + 1]);
-	}
-
-	for (size_t i = 0; i < file.vt_size; i += 3)
-	{
-		uv_scale[0] = std::max(uv_scale[0], file.vt[i + 0] - uv_offset[0]);
-		uv_scale[1] = std::max(uv_scale[1], file.vt[i + 1] - uv_offset[1]);
-	}
-
-	float pos_scale_inverse = rcpSafe(pos_scale);
-	float uv_scale_inverse[2] = { rcpSafe(uv_scale[0]), rcpSafe(uv_scale[1]) };
-
-	size_t total_indices = file.f_size / 3;
-
-	std::vector<Vertex> triangles(total_indices);
-
-	int pos_bits = 14;
-	int uv_bits = 12;
-
-	for (size_t i = 0; i < total_indices; ++i)
-	{
-		int vi = file.f[i * 3 + 0];
-		int vti = file.f[i * 3 + 1];
-		int vni = file.f[i * 3 + 2];
-
-		// note: we scale the vertices uniformly; this is not the best option wrt compression quality
-		// however, it means we can scale the mesh uniformly without distorting the normals
-		// this is helpful for backends like ThreeJS that apply mesh scaling to normals
-		float px = (file.v[vi * 3 + 0] - pos_offset[0]) * pos_scale_inverse;
-		float py = (file.v[vi * 3 + 1] - pos_offset[1]) * pos_scale_inverse;
-		float pz = (file.v[vi * 3 + 2] - pos_offset[2]) * pos_scale_inverse;
-
-		// normal is 0 if absent from the mesh
-		float nx = vni >= 0 ? file.vn[vni * 3 + 0] : 0;
-		float ny = vni >= 0 ? file.vn[vni * 3 + 1] : 0;
-		float nz = vni >= 0 ? file.vn[vni * 3 + 2] : 0;
-
-		// scale the normal to make sure the largest component is +-1.0
-		// this reduces the entropy of the normal by ~1.5 bits without losing precision
-		// it's better to use octahedral encoding but that requires special shader support
-		float nm = std::max(fabsf(nx), std::max(fabsf(ny), fabsf(nz)));
-		float ns = nm == 0.f ? 0.f : 1 / nm;
-
-		nx *= ns;
-		ny *= ns;
-		nz *= ns;
-
-		// texture coordinates are 0 if absent, and require a texture matrix to decode
-		float tx = vti >= 0 ? (file.vt[vti * 3 + 0] - uv_offset[0]) * uv_scale_inverse[0] : 0;
-		float ty = vti >= 0 ? (file.vt[vti * 3 + 1] - uv_offset[1]) * uv_scale_inverse[1] : 0;
-
-		Vertex v =
-		    {
-		        (unsigned short)(meshopt_quantizeUnorm(px, pos_bits)),
-		        (unsigned short)(meshopt_quantizeUnorm(py, pos_bits)),
-		        (unsigned short)(meshopt_quantizeUnorm(pz, pos_bits)),
-				0,
-
-		        char(meshopt_quantizeSnorm(nx, 8)),
-		        char(meshopt_quantizeSnorm(ny, 8)),
-		        char(meshopt_quantizeSnorm(nz, 8)),
-				0,
-
-		        (unsigned short)(meshopt_quantizeUnorm(tx, uv_bits)),
-		        (unsigned short)(meshopt_quantizeUnorm(ty, uv_bits)),
-		    };
-
-		triangles[i] = v;
-	}
-
-	std::vector<unsigned int> remap(total_indices);
-
-	size_t total_vertices = meshopt_generateVertexRemap(&remap[0], NULL, total_indices, &triangles[0], total_indices, sizeof(Vertex));
-
-	std::vector<unsigned int> indices(total_indices);
-	meshopt_remapIndexBuffer(&indices[0], NULL, total_indices, &remap[0]);
-
-	std::vector<Vertex> vertices(total_vertices);
-	meshopt_remapVertexBuffer(&vertices[0], &triangles[0], total_indices, sizeof(Vertex), &remap[0]);
-
-	for (size_t i = 0; i < file.g_size; ++i)
-	{
-		ObjGroup& g = file.g[i];
-
-		meshopt_optimizeVertexCache(&indices[g.index_offset], &indices[g.index_offset], g.index_count, vertices.size());
-	}
-
-	meshopt_optimizeVertexFetch(&vertices[0], &indices[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex));
-
-	std::vector<unsigned char> vbuf(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(Vertex)));
-	vbuf.resize(meshopt_encodeVertexBuffer(&vbuf[0], vbuf.size(), &vertices[0], vertices.size(), sizeof(Vertex)));
-
-	std::vector<unsigned char> ibuf(meshopt_encodeIndexBufferBound(indices.size(), vertices.size()));
-	ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), &indices[0], indices.size()));
-
-	FILE* result = fopen(output, "wb");
-	if (!result)
-	{
-		printf("Error saving %s: can't open file for writing\n", output);
-		return 4;
-	}
-
-	Header header = {};
-	memcpy(header.magic, "OPTM", 4);
-
-	header.group_count = unsigned(file.g_size);
-	header.vertex_count = unsigned(vertices.size());
-	header.index_count = unsigned(indices.size());
-	header.vertex_data_size = unsigned(vbuf.size());
-	header.index_data_size = unsigned(ibuf.size());
-
-	header.pos_offset[0] = pos_offset[0];
-	header.pos_offset[1] = pos_offset[1];
-	header.pos_offset[2] = pos_offset[2];
-	header.pos_scale = pos_scale / float((1 << pos_bits) - 1);
-
-	header.uv_offset[0] = uv_offset[0];
-	header.uv_offset[1] = uv_offset[1];
-	header.uv_scale[0] = uv_scale[0] / float((1 << uv_bits) - 1);
-	header.uv_scale[1] = uv_scale[1] / float((1 << uv_bits) - 1);
-
-	fwrite(&header, 1, sizeof(header), result);
-
-	for (size_t i = 0; i < file.g_size; ++i)
-	{
-		ObjGroup& g = file.g[i];
-
-		Object object = {};
-		object.index_offset = unsigned(g.index_offset);
-		object.index_count = unsigned(g.index_count);
-		object.material_length = unsigned(strlen(g.material));
-
-		fwrite(&object, 1, sizeof(object), result);
-	}
-
-	for (size_t i = 0; i < file.g_size; ++i)
-	{
-		ObjGroup& g = file.g[i];
-
-		fwrite(g.material, 1, strlen(g.material), result);
-	}
-
-	fwrite(&vbuf[0], 1, vbuf.size(), result);
-	fwrite(&ibuf[0], 1, ibuf.size(), result);
-	fclose(result);
-
-	return 0;
-}

+ 9 - 0
3rdparty/meshoptimizer/tools/meshloader.cpp

@@ -0,0 +1,9 @@
+#ifndef _CRT_SECURE_NO_WARNINGS
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#define CGLTF_IMPLEMENTATION
+#include "cgltf.h"
+
+#define FAST_OBJ_IMPLEMENTATION
+#include "fast_obj.h"

+ 0 - 383
3rdparty/meshoptimizer/tools/objparser.cpp

@@ -1,383 +0,0 @@
-#ifndef _CRT_SECURE_NO_WARNINGS
-#define _CRT_SECURE_NO_WARNINGS
-#endif
-
-#include "objparser.h"
-
-#include <cassert>
-#include <cmath>
-#include <cstdio>
-#include <cstdlib>
-#include <cstring>
-
-template <typename T>
-static void growArray(T*& data, size_t& capacity)
-{
-	size_t newcapacity = capacity == 0 ? 32 : capacity + capacity / 2;
-	T* newdata = new T[newcapacity];
-
-	if (data)
-	{
-		memcpy(newdata, data, capacity * sizeof(T));
-		delete[] data;
-	}
-
-	data = newdata;
-	capacity = newcapacity;
-}
-
-static int fixupIndex(int index, size_t size)
-{
-	return (index >= 0) ? index - 1 : int(size) + index;
-}
-
-static int parseInt(const char* s, const char** end)
-{
-	// skip whitespace
-	while (*s == ' ' || *s == '\t')
-		s++;
-
-	// read sign bit
-	int sign = (*s == '-');
-	s += (*s == '-' || *s == '+');
-
-	unsigned int result = 0;
-
-	for (;;)
-	{
-		if (unsigned(*s - '0') < 10)
-			result = result * 10 + (*s - '0');
-		else
-			break;
-
-		s++;
-	}
-
-	// return end-of-string
-	*end = s;
-
-	return sign ? -int(result) : int(result);
-}
-
-static float parseFloat(const char* s, const char** end)
-{
-	static const double digits[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
-	static const double powers[] = {1e0, 1e+1, 1e+2, 1e+3, 1e+4, 1e+5, 1e+6, 1e+7, 1e+8, 1e+9, 1e+10, 1e+11, 1e+12, 1e+13, 1e+14, 1e+15, 1e+16, 1e+17, 1e+18, 1e+19, 1e+20, 1e+21, 1e+22};
-
-	// skip whitespace
-	while (*s == ' ' || *s == '\t')
-		s++;
-
-	// read sign
-	double sign = (*s == '-') ? -1 : 1;
-	s += (*s == '-' || *s == '+');
-
-	// read integer part
-	double result = 0;
-	int power = 0;
-
-	while (unsigned(*s - '0') < 10)
-	{
-		result = result * 10 + digits[*s - '0'];
-		s++;
-	}
-
-	// read fractional part
-	if (*s == '.')
-	{
-		s++;
-
-		while (unsigned(*s - '0') < 10)
-		{
-			result = result * 10 + digits[*s - '0'];
-			s++;
-			power--;
-		}
-	}
-
-	// read exponent part
-	if ((*s | ' ') == 'e')
-	{
-		s++;
-
-		// read exponent sign
-		int expsign = (*s == '-') ? -1 : 1;
-		s += (*s == '-' || *s == '+');
-
-		// read exponent
-		int exppower = 0;
-
-		while (unsigned(*s - '0') < 10)
-		{
-			exppower = exppower * 10 + (*s - '0');
-			s++;
-		}
-
-		// done!
-		power += expsign * exppower;
-	}
-
-	// return end-of-string
-	*end = s;
-
-	// note: this is precise if result < 9e15
-	// for longer inputs we lose a bit of precision here
-	if (unsigned(-power) < sizeof(powers) / sizeof(powers[0]))
-		return float(sign * result / powers[-power]);
-	else if (unsigned(power) < sizeof(powers) / sizeof(powers[0]))
-		return float(sign * result * powers[power]);
-	else
-		return float(sign * result * pow(10.0, power));
-}
-
-static const char* parseFace(const char* s, int& vi, int& vti, int& vni)
-{
-	while (*s == ' ' || *s == '\t')
-		s++;
-
-	vi = parseInt(s, &s);
-
-	if (*s != '/')
-		return s;
-	s++;
-
-	// handle vi//vni indices
-	if (*s != '/')
-		vti = parseInt(s, &s);
-
-	if (*s != '/')
-		return s;
-	s++;
-
-	vni = parseInt(s, &s);
-
-	return s;
-}
-
-ObjFile::ObjFile()
-    : v(0)
-    , v_size(0)
-    , v_cap(0)
-    , vt(0)
-    , vt_size(0)
-    , vt_cap(0)
-    , vn(0)
-    , vn_size(0)
-    , vn_cap(0)
-    , f(0)
-    , f_size(0)
-    , f_cap(0)
-    , g(0)
-    , g_size(0)
-    , g_cap(0)
-{
-}
-
-ObjFile::~ObjFile()
-{
-	delete[] v;
-	delete[] vt;
-	delete[] vn;
-	delete[] f;
-	delete[] g;
-}
-
-void objParseLine(ObjFile& result, const char* line)
-{
-	if (line[0] == 'v' && line[1] == ' ')
-	{
-		const char* s = line + 2;
-
-		float x = parseFloat(s, &s);
-		float y = parseFloat(s, &s);
-		float z = parseFloat(s, &s);
-
-		if (result.v_size + 3 > result.v_cap)
-			growArray(result.v, result.v_cap);
-
-		result.v[result.v_size++] = x;
-		result.v[result.v_size++] = y;
-		result.v[result.v_size++] = z;
-	}
-	else if (line[0] == 'v' && line[1] == 't' && line[2] == ' ')
-	{
-		const char* s = line + 3;
-
-		float u = parseFloat(s, &s);
-		float v = parseFloat(s, &s);
-		float w = parseFloat(s, &s);
-
-		if (result.vt_size + 3 > result.vt_cap)
-			growArray(result.vt, result.vt_cap);
-
-		result.vt[result.vt_size++] = u;
-		result.vt[result.vt_size++] = v;
-		result.vt[result.vt_size++] = w;
-	}
-	else if (line[0] == 'v' && line[1] == 'n' && line[2] == ' ')
-	{
-		const char* s = line + 3;
-
-		float x = parseFloat(s, &s);
-		float y = parseFloat(s, &s);
-		float z = parseFloat(s, &s);
-
-		if (result.vn_size + 3 > result.vn_cap)
-			growArray(result.vn, result.vn_cap);
-
-		result.vn[result.vn_size++] = x;
-		result.vn[result.vn_size++] = y;
-		result.vn[result.vn_size++] = z;
-	}
-	else if (line[0] == 'f' && line[1] == ' ')
-	{
-		const char* s = line + 2;
-
-		if (!result.g)
-		{
-			growArray(result.g, result.g_cap);
-
-			ObjGroup g = {};
-			result.g[result.g_size++] = g;
-		}
-
-		size_t v = result.v_size / 3;
-		size_t vt = result.vt_size / 3;
-		size_t vn = result.vn_size / 3;
-
-		int fv = 0;
-		int f[3][3] = {};
-
-		while (*s)
-		{
-			int vi = 0, vti = 0, vni = 0;
-			s = parseFace(s, vi, vti, vni);
-
-			if (vi == 0)
-				break;
-
-			f[fv][0] = fixupIndex(vi, v);
-			f[fv][1] = fixupIndex(vti, vt);
-			f[fv][2] = fixupIndex(vni, vn);
-
-			if (fv == 2)
-			{
-				if (result.f_size + 9 > result.f_cap)
-					growArray(result.f, result.f_cap);
-
-				memcpy(&result.f[result.f_size], f, 9 * sizeof(int));
-				result.f_size += 9;
-
-				result.g[result.g_size - 1].index_count += 3;
-
-				f[1][0] = f[2][0];
-				f[1][1] = f[2][1];
-				f[1][2] = f[2][2];
-			}
-			else
-			{
-				fv++;
-			}
-		}
-	}
-	else if (strncmp(line, "usemtl", 6) == 0)
-	{
-		const char* s = line + 6;
-
-		// skip whitespace
-		while (*s == ' ' || *s == '\t')
-			s++;
-
-		if (result.g_size + 1 > result.g_cap)
-			growArray(result.g, result.g_cap);
-
-		ObjGroup g = {};
-		g.index_offset = result.f_size / 3;
-
-		strncpy(g.material, s, sizeof(g.material));
-		g.material[sizeof(g.material) - 1] = 0;
-
-		result.g[result.g_size++] = g;
-	}
-}
-
-bool objParseFile(ObjFile& result, const char* path)
-{
-	FILE* file = fopen(path, "rb");
-	if (!file)
-		return false;
-
-	char buffer[65536];
-	size_t size = 0;
-
-	while (!feof(file))
-	{
-		size += fread(buffer + size, 1, sizeof(buffer) - size, file);
-
-		size_t line = 0;
-
-		while (line < size)
-		{
-			// find the end of current line
-			void* eol = memchr(buffer + line, '\n', size - line);
-			if (!eol)
-				break;
-
-			// zero-terminate for objParseLine
-			size_t next = static_cast<char*>(eol) - buffer;
-
-			buffer[next] = 0;
-
-			// process next line
-			objParseLine(result, buffer + line);
-
-			line = next + 1;
-		}
-
-		// move prefix of the last line in the buffer to the beginning of the buffer for next iteration
-		assert(line <= size);
-
-		memmove(buffer, buffer + line, size - line);
-		size -= line;
-	}
-
-	if (size)
-	{
-		// process last line
-		assert(size < sizeof(buffer));
-		buffer[size] = 0;
-
-		objParseLine(result, buffer);
-	}
-
-	fclose(file);
-	return true;
-}
-
-bool objValidate(const ObjFile& result)
-{
-	size_t v = result.v_size / 3;
-	size_t vt = result.vt_size / 3;
-	size_t vn = result.vn_size / 3;
-
-	for (size_t i = 0; i < result.f_size; i += 3)
-	{
-		int vi = result.f[i + 0];
-		int vti = result.f[i + 1];
-		int vni = result.f[i + 2];
-
-		if (vi < 0)
-			return false;
-
-		if (vi >= 0 && size_t(vi) >= v)
-			return false;
-
-		if (vti >= 0 && size_t(vti) >= vt)
-			return false;
-
-		if (vni >= 0 && size_t(vni) >= vn)
-			return false;
-	}
-
-	return true;
-}

+ 0 - 42
3rdparty/meshoptimizer/tools/objparser.h

@@ -1,42 +0,0 @@
-#pragma once
-
-#include <stddef.h>
-
-struct ObjGroup
-{
-	char material[256];
-
-	size_t index_offset;
-	size_t index_count;
-};
-
-class ObjFile
-{
-public:
-	float* v; // positions; stride 3 (xyz)
-	size_t v_size, v_cap;
-
-	float* vt; // texture coordinates; stride 3 (uvw)
-	size_t vt_size, vt_cap;
-
-	float* vn; // vertex normals; stride 3 (xyz)
-	size_t vn_size, vn_cap;
-
-	int* f; // face elements; stride 9 (3 groups of indices into v/vt/vn)
-	size_t f_size, f_cap;
-
-	ObjGroup* g;
-	size_t g_size, g_cap;
-
-	ObjFile();
-	~ObjFile();
-
-private:
-	ObjFile(const ObjFile&);
-	ObjFile& operator=(const ObjFile&);
-};
-
-void objParseLine(ObjFile& result, const char* line);
-bool objParseFile(ObjFile& result, const char* path);
-
-bool objValidate(const ObjFile& result);

+ 26 - 12
3rdparty/meshoptimizer/tools/vcachetester.cpp

@@ -11,7 +11,7 @@
 #include <vector>
 
 #include "../src/meshoptimizer.h"
-#include "objparser.h"
+#include "fast_obj.h"
 
 #pragma comment(lib, "d3d11.lib")
 #pragma comment(lib, "d3dcompiler.lib")
@@ -396,26 +396,40 @@ void testCacheMeshes(IDXGIAdapter* adapter, int argc, char** argv)
 			continue;
 		}
 
-		ObjFile file;
-
-		if (!objParseFile(file, path))
+		fastObjMesh* obj = fast_obj_read(path);
+		if (!obj)
 		{
 			printf("Error loading %s: file not found\n", path);
 			continue;
 		}
 
-		if (!objValidate(file))
+		std::vector<unsigned int> ib1;
+
+		size_t index_offset = 0;
+
+		for (unsigned int i = 0; i < obj->face_count; ++i)
 		{
-			printf("Error loading %s: invalid file data\n", path);
-			continue;
-		}
+			for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
+			{
+				fastObjIndex gi = obj->indices[index_offset + j];
 
-		std::vector<unsigned int> ib1;
+				// triangulate polygon on the fly; offset-3 is always the first polygon vertex
+				if (j >= 3)
+				{
+					unsigned int i0 = ib1[ib1.size() - 3];
+					unsigned int i1 = ib1[ib1.size() - 1];
+
+					ib1.push_back(i0);
+					ib1.push_back(i1);
+				}
 
-		for (size_t i = 0; i < file.f_size; i += 3)
-			ib1.push_back(file.f[i]);
+				ib1.push_back(gi.p);
+			}
+
+			index_offset += obj->face_vertices[i];
+		}
 
-		unsigned int vertex_count = file.v_size / 3;
+		unsigned int vertex_count = obj->position_count;
 		unsigned int index_count = ib1.size();
 
 		unsigned int invocations1 = queryVSInvocations(device, context, ib1.data(), index_count);

+ 84 - 247
3rdparty/meshoptimizer/tools/vcachetuner.cpp

@@ -1,5 +1,5 @@
 #include "../src/meshoptimizer.h"
-#include "objparser.h"
+#include "fast_obj.h"
 
 #include <algorithm>
 #include <functional>
@@ -64,6 +64,7 @@ struct State
 {
 	float cache[kCacheSizeMax];
 	float live[kValenceMax];
+	float fitness;
 };
 
 struct Mesh
@@ -98,21 +99,17 @@ Mesh gridmesh(unsigned int N)
 
 Mesh objmesh(const char* path)
 {
-	ObjFile file;
-
-	if (!objParseFile(file, path))
+	fastObjMesh* obj = fast_obj_read(path);
+	if (!obj)
 	{
 		printf("Error loading %s: file not found\n", path);
 		return Mesh();
 	}
 
-	if (!objValidate(file))
-	{
-		printf("Error loading %s: invalid file data\n", path);
-		return Mesh();
-	}
+	size_t total_indices = 0;
 
-	size_t total_indices = file.f_size / 3;
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+		total_indices += 3 * (obj->face_vertices[i] - 2);
 
 	struct Vertex
 	{
@@ -123,29 +120,44 @@ Mesh objmesh(const char* path)
 
 	std::vector<Vertex> vertices(total_indices);
 
-	for (size_t i = 0; i < total_indices; ++i)
-	{
-		int vi = file.f[i * 3 + 0];
-		int vti = file.f[i * 3 + 1];
-		int vni = file.f[i * 3 + 2];
-
-		Vertex v =
-		    {
-		        file.v[vi * 3 + 0],
-		        file.v[vi * 3 + 1],
-		        file.v[vi * 3 + 2],
+	size_t vertex_offset = 0;
+	size_t index_offset = 0;
 
-		        vni >= 0 ? file.vn[vni * 3 + 0] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 1] : 0,
-		        vni >= 0 ? file.vn[vni * 3 + 2] : 0,
+	for (unsigned int i = 0; i < obj->face_count; ++i)
+	{
+		for (unsigned int j = 0; j < obj->face_vertices[i]; ++j)
+		{
+			fastObjIndex gi = obj->indices[index_offset + j];
+
+			Vertex v =
+			    {
+			        obj->positions[gi.p * 3 + 0],
+			        obj->positions[gi.p * 3 + 1],
+			        obj->positions[gi.p * 3 + 2],
+			        obj->normals[gi.n * 3 + 0],
+			        obj->normals[gi.n * 3 + 1],
+			        obj->normals[gi.n * 3 + 2],
+			        obj->texcoords[gi.t * 2 + 0],
+			        obj->texcoords[gi.t * 2 + 1],
+			    };
+
+			// triangulate polygon on the fly; offset-3 is always the first polygon vertex
+			if (j >= 3)
+			{
+				vertices[vertex_offset + 0] = vertices[vertex_offset - 3];
+				vertices[vertex_offset + 1] = vertices[vertex_offset - 1];
+				vertex_offset += 2;
+			}
 
-		        vti >= 0 ? file.vt[vti * 3 + 0] : 0,
-		        vti >= 0 ? file.vt[vti * 3 + 1] : 0,
-		    };
+			vertices[vertex_offset] = v;
+			vertex_offset++;
+		}
 
-		vertices[i] = v;
+		index_offset += obj->face_vertices[i];
 	}
 
+	fast_obj_destroy(obj);
+
 	Mesh result;
 
 	std::vector<unsigned int> remap(total_indices);
@@ -193,17 +205,7 @@ float fitness_score(const State& state, const std::vector<Mesh>& meshes)
 	return result / count;
 }
 
-float rndcache()
-{
-	return rand01();
-}
-
-float rndlive()
-{
-	return rand01();
-}
-
-std::vector<State> gen0(size_t count)
+std::vector<State> gen0(size_t count, const std::vector<Mesh>& meshes)
 {
 	std::vector<State> result;
 
@@ -212,250 +214,84 @@ std::vector<State> gen0(size_t count)
 		State state = {};
 
 		for (int j = 0; j < kCacheSizeMax; ++j)
-			state.cache[j] = rndcache();
+			state.cache[j] = rand01();
 
 		for (int j = 0; j < kValenceMax; ++j)
-			state.live[j] = rndlive();
-
-		result.push_back(state);
-	}
-
-	return result;
-}
-
-size_t rndindex(const std::vector<float>& prob)
-{
-	float r = rand01();
-
-	for (size_t i = 0; i < prob.size(); ++i)
-	{
-		r -= prob[i];
-
-		if (r <= 0)
-			return i;
-	}
+			state.live[j] = rand01();
 
-	return prob.size() - 1;
-}
-
-State mutate(const State& state)
-{
-	State result = state;
-
-	if (rand01() < 0.7f)
-	{
-		size_t idxcache = std::min(int(rand01() * kCacheSizeMax + 0.5f), int(kCacheSizeMax - 1));
-
-		result.cache[idxcache] = rndcache();
-	}
-
-	if (rand01() < 0.7f)
-	{
-		size_t idxlive = std::min(int(rand01() * kValenceMax + 0.5f), int(kValenceMax - 1));
-
-		result.live[idxlive] = rndlive();
-	}
-
-	if (rand01() < 0.2f)
-	{
-		uint32_t mask = rand32();
-
-		for (size_t i = 0; i < kCacheSizeMax; ++i)
-			if (mask & (1 << i))
-				result.cache[i] *= 0.9f + 0.2f * rand01();
-	}
+		state.fitness = fitness_score(state, meshes);
 
-	if (rand01() < 0.2f)
-	{
-		uint32_t mask = rand32();
-
-		for (size_t i = 0; i < kValenceMax; ++i)
-			if (mask & (1 << i))
-				result.live[i] *= 0.9f + 0.2f * rand01();
-	}
-
-	if (rand01() < 0.05f)
-	{
-		uint32_t mask = rand32();
-
-		for (size_t i = 0; i < kCacheSizeMax; ++i)
-			if (mask & (1 << i))
-				result.cache[i] = rndcache();
-	}
-
-	if (rand01() < 0.05f)
-	{
-		uint32_t mask = rand32();
-
-		for (size_t i = 0; i < kValenceMax; ++i)
-			if (mask & (1 << i))
-				result.live[i] = rndlive();
+		result.push_back(state);
 	}
 
 	return result;
 }
 
-bool accept(float fitnew, float fitold, float temp)
-{
-	if (fitnew >= fitold)
-		return true;
-
-	if (temp == 0)
-		return false;
-
-	float prob = exp2((fitnew - fitold) / temp);
-
-	return rand01() < prob;
-}
-
-std::pair<State, float> genN_SA(std::vector<State>& seed, const std::vector<Mesh>& meshes, size_t steps)
+// https://en.wikipedia.org/wiki/Differential_evolution
+// Good Parameters for Differential Evolution. Magnus Erik Hvass Pedersen, 2010
+std::pair<State, float> genN(std::vector<State>& seed, const std::vector<Mesh>& meshes, float crossover = 0.8803f, float weight = 0.4717f)
 {
-	std::vector<State> result;
-	result.reserve(seed.size() * (1 + steps));
+	std::vector<State> result(seed.size());
 
-	// perform several parallel steps of mutation for each temperature
 	for (size_t i = 0; i < seed.size(); ++i)
 	{
-		result.push_back(seed[i]);
-
-		for (size_t s = 0; s < steps; ++s)
-			result.push_back(mutate(seed[i]));
-	}
-
-	// compute fitness for all temperatures & mutations in parallel
-	std::vector<float> resultfit(result.size());
-
-#pragma omp parallel for
-	for (size_t i = 0; i < result.size(); ++i)
-	{
-		resultfit[i] = fitness_score(result[i], meshes);
-	}
+		for (;;)
+		{
+			int a = rand32() % seed.size();
+			int b = rand32() % seed.size();
+			int c = rand32() % seed.size();
 
-	// perform annealing for each temperature
-	std::vector<float> seedfit(seed.size());
+			if (a == b || a == c || b == c || a == int(i) || b == int(i) || c == int(i))
+				continue;
 
-	for (size_t i = 0; i < seed.size(); ++i)
-	{
-		size_t offset = i * (1 + steps);
+			int rc = rand32() % kCacheSizeMax;
+			int rl = rand32() % kValenceMax;
 
-		seedfit[i] = resultfit[offset];
+			for (int j = 0; j < kCacheSizeMax; ++j)
+			{
+				float r = rand01();
 
-		float temp = (float(i) / float(seed.size() - 1)) / 0.1f;
+				if (r < crossover || j == rc)
+					result[i].cache[j] = std::max(0.f, std::min(1.f, seed[a].cache[j] + weight * (seed[b].cache[j] - seed[c].cache[j])));
+				else
+					result[i].cache[j] = seed[i].cache[j];
+			}
 
-		for (size_t s = 0; s < steps; ++s)
-		{
-			if (accept(resultfit[offset + s + 1], seedfit[i], temp))
+			for (int j = 0; j < kValenceMax; ++j)
 			{
-				seedfit[i] = resultfit[offset + s + 1];
-				seed[i] = result[offset + s + 1];
+				float r = rand01();
+
+				if (r < crossover || j == rl)
+					result[i].live[j] = std::max(0.f, std::min(1.f, seed[a].live[j] + weight * (seed[b].live[j] - seed[c].live[j])));
+				else
+					result[i].live[j] = seed[i].live[j];
 			}
-		}
-	}
 
-	// perform promotion from each temperature to the next one
-	for (size_t i = seed.size() - 1; i > 0; --i)
-	{
-		if (seedfit[i] > seedfit[i - 1])
-		{
-			seedfit[i - 1] = seedfit[i];
-			seed[i - 1] = seed[i];
+			break;
 		}
 	}
 
-	return std::make_pair(seed[0], seedfit[0]);
-}
-
-std::pair<State, float> genN_GA(std::vector<State>& seed, const std::vector<Mesh>& meshes, float crossover, float mutate)
-{
-	std::vector<State> result;
-	result.reserve(seed.size());
-
-	std::vector<float> seedprob(seed.size());
-
-#pragma omp parallel for
+	#pragma omp parallel for
 	for (size_t i = 0; i < seed.size(); ++i)
 	{
-		seedprob[i] = fitness_score(seed[i], meshes);
+		result[i].fitness = fitness_score(result[i], meshes);
 	}
 
 	State best = {};
 	float bestfit = 0;
-	float probsum = 0;
 
 	for (size_t i = 0; i < seed.size(); ++i)
 	{
-		float score = seedprob[i];
-		probsum += score;
+		if (result[i].fitness > seed[i].fitness)
+			seed[i] = result[i];
 
-		if (score > bestfit)
+		if (seed[i].fitness > bestfit)
 		{
 			best = seed[i];
-			bestfit = score;
-		}
-	}
-
-	for (auto& prob : seedprob)
-	{
-		prob /= probsum;
-	}
-
-	std::vector<unsigned int> seedidx;
-	seedidx.reserve(seed.size());
-	for (size_t i = 0; i < seed.size(); ++i)
-		seedidx.push_back(i);
-
-	std::sort(seedidx.begin(), seedidx.end(), [&](size_t l, size_t r) { return seedprob[l] < seedprob[r]; });
-
-	while (result.size() < seed.size() / 4)
-	{
-		size_t idx = seedidx.back();
-		seedidx.pop_back();
-
-		result.push_back(seed[idx]);
-	}
-
-	while (result.size() < seed.size())
-	{
-		State s0 = seed[rndindex(seedprob)];
-		State s1 = seed[rndindex(seedprob)];
-
-		State state = s0;
-
-		// crossover
-		if (rand01() < crossover)
-		{
-			size_t idxcache = std::min(int(rand01() * kCacheSizeMax + 0.5f), 15);
-
-			memcpy(state.cache + idxcache, s1.cache + idxcache, (kCacheSizeMax - idxcache) * sizeof(float));
+			bestfit = seed[i].fitness;
 		}
-
-		if (rand01() < crossover)
-		{
-			size_t idxlive = std::min(int(rand01() * kValenceMax + 0.5f), 7);
-
-			memcpy(state.live + idxlive, s1.live + idxlive, (kValenceMax - idxlive) * sizeof(float));
-		}
-
-		// mutate
-		if (rand01() < mutate)
-		{
-			size_t idxcache = std::min(int(rand01() * kCacheSizeMax + 0.5f), 15);
-
-			state.cache[idxcache] = rndcache();
-		}
-
-		if (rand01() < mutate)
-		{
-			size_t idxlive = std::min(int(rand01() * kValenceMax + 0.5f), 7);
-
-			state.live[idxlive] = rndlive();
-		}
-
-		result.push_back(state);
 	}
 
-	seed.swap(result);
-
 	return std::make_pair(best, bestfit);
 }
 
@@ -545,7 +381,7 @@ int main(int argc, char** argv)
 	}
 	else
 	{
-		pop = gen0(annealing ? 32 : 1000);
+		pop = gen0(95, meshes);
 	}
 
 	printf("%d meshes, %.1fM triangles\n", int(meshes.size()), double(total_triangles) / 1e6);
@@ -559,7 +395,7 @@ int main(int argc, char** argv)
 
 	for (;;)
 	{
-		auto best = annealing ? genN_SA(pop, meshes, 31) : genN_GA(pop, meshes, 0.7f, 0.3f);
+		auto best = genN(pop, meshes);
 		gen++;
 
 		compute_atvr(best.first, meshes[0], atvr_0);
@@ -571,7 +407,8 @@ int main(int argc, char** argv)
 		{
 			char buf[128];
 			sprintf(buf, "gcloud logging write vcache-log \"fitness %f; grid %f %f %s %f %f\"", best.second, atvr_0[0], atvr_0[1], argv[argc - 1], atvr_N[0], atvr_N[1]);
-			system(buf);
+			int rc = system(buf);
+			(void)rc;
 		}
 
 		dump_state(best.first);