Pārlūkot izejas kodu

Updated meshoptimizer.

Бранимир Караџић 6 gadi atpakaļ
vecāks
revīzija
0c5c2fcdcf

+ 63 - 0
3rdparty/meshoptimizer/.github/workflows/build.yml

@@ -0,0 +1,63 @@
+name: build
+
+on: [push, pull_request]
+
+jobs:
+  linux:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v1
+    - name: make test
+      run: |
+        make -j2 config=sanitize test
+        make -j2 config=debug test
+        make -j2 config=release test
+        make -j2 config=release gltfpack
+    - name: make coverage
+      run: |
+        make -j2 config=coverage test
+        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 -t ${{secrets.CODECOV_TOKEN}}
+    - uses: actions/upload-artifact@v1
+      with:
+        name: gltfpack-linux
+        path: gltfpack
+
+  macos:
+    runs-on: macos-latest
+    steps:
+    - uses: actions/checkout@v1
+    - name: make test
+      run: |
+        make -j2 config=sanitize test
+        make -j2 config=debug test
+        make -j2 config=release test
+        make -j2 config=release gltfpack
+    - name: make iphone
+      run: make -j2 config=iphone
+    - uses: actions/upload-artifact@v1
+      with:
+        name: gltfpack-macos
+        path: gltfpack
+
+  windows:
+    runs-on: windows-latest
+    strategy:
+      matrix:
+        arch: [Win32, x64]
+    steps:
+    - uses: actions/checkout@v1
+    - name: cmake configure
+      run: cmake . -DBUILD_DEMO=ON -DBUILD_TOOLS=ON -A ${{matrix.arch}}
+    - name: cmake test
+      shell: bash # necessary for fail-fast
+      run: |
+        cmake --build . -- -property:Configuration=Debug -verbosity:minimal
+        Debug/demo.exe demo/pirate.obj
+        cmake --build . -- -property:Configuration=Release -verbosity:minimal
+        Release/demo.exe demo/pirate.obj
+    - uses: actions/upload-artifact@v1
+      with:
+        name: gltfpack-windows
+        path: Release/gltfpack.exe

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

@@ -18,17 +18,17 @@ matrix:
         - TARGET="Visual Studio 15 2017 Win64"
         - TARGET="Visual Studio 15 2017 Win64"
 
 
 script:
 script:
-  - if [[ "$TRAVIS_COMPILER" == "gcc" ]]; then make config=coverage test; fi
-  - 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 make config=release gltfpack; fi
+  - if [[ "$TRAVIS_COMPILER" == "gcc" ]]; then make -j2 config=coverage test; fi
+  - if [[ "$TRAVIS_COMPILER" == "clang" ]]; then make -j2 config=sanitize test; fi
+  - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make -j2 config=debug test; fi
+  - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make -j2 config=release test; fi
+  - if [[ "$TRAVIS_OS_NAME" != "windows" ]]; then make -j2 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 -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 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 ./Debug/demo.exe demo/pirate.obj; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake --build . -- -property:Configuration=Release -verbosity:minimal; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then cmake --build . -- -property:Configuration=Release -verbosity:minimal; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then ./Release/demo.exe demo/pirate.obj; fi
   - if [[ "$TRAVIS_OS_NAME" == "windows" ]]; then ./Release/demo.exe demo/pirate.obj; fi
-  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then make config=iphone; fi
+  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then make -j2 config=iphone; fi
 
 
 after_script:
 after_script:
   - if [[ "$TRAVIS_COMPILER" == "gcc" ]]; then
   - if [[ "$TRAVIS_COMPILER" == "gcc" ]]; then

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

@@ -1,6 +1,6 @@
 cmake_minimum_required(VERSION 3.0)
 cmake_minimum_required(VERSION 3.0)
 
 
-project(meshoptimizer VERSION 0.12)
+project(meshoptimizer VERSION 0.12 LANGUAGES CXX)
 
 
 option(BUILD_DEMO "Build demo" OFF)
 option(BUILD_DEMO "Build demo" OFF)
 option(BUILD_TOOLS "Build tools" OFF)
 option(BUILD_TOOLS "Build tools" OFF)

+ 1 - 1
3rdparty/meshoptimizer/Makefile

@@ -28,7 +28,7 @@ ifeq ($(config),iphone)
 	IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk
 	IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk
 	CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK)
 	CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK)
 	CXXFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -stdlib=libc++
 	CXXFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -stdlib=libc++
-	LDFLAGS+=-arch armv7 -arch arm64 -L $(IPHONESDK)/usr/lib -mios-version-min=7.0
+	LDFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -L $(IPHONESDK)/usr/lib -mios-version-min=7.0
 endif
 endif
 
 
 ifeq ($(config),trace)
 ifeq ($(config),trace)

+ 2 - 2
3rdparty/meshoptimizer/README.md

@@ -1,4 +1,4 @@
-# meshoptimizer [![Build Status](https://travis-ci.org/zeux/meshoptimizer.svg?branch=master)](https://travis-ci.org/zeux/meshoptimizer) [![codecov.io](https://codecov.io/github/zeux/meshoptimizer/coverage.svg?branch=master)](https://codecov.io/github/zeux/meshoptimizer?branch=master) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![GitHub](https://img.shields.io/badge/repo-github-green.svg)](https://github.com/zeux/meshoptimizer)
+# meshoptimizer [![Actions Status](https://github.com/zeux/meshoptimizer/workflows/build/badge.svg)](https://github.com/zeux/meshoptimizer/actions) [![Build Status](https://travis-ci.org/zeux/meshoptimizer.svg?branch=master)](https://travis-ci.org/zeux/meshoptimizer) [![codecov.io](https://codecov.io/github/zeux/meshoptimizer/coverage.svg?branch=master)](https://codecov.io/github/zeux/meshoptimizer?branch=master) ![MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![GitHub](https://img.shields.io/badge/repo-github-green.svg)](https://github.com/zeux/meshoptimizer)
 
 
 ## Purpose
 ## Purpose
 
 
@@ -288,7 +288,7 @@ You can then run the resulting command-line binary like this (run it without arg
 gltfpack -i scene.gltf -o scene.glb
 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 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. It will also simplify the meshes when requested to do so.
 
 
 gltfpack can produce two types of output files:
 gltfpack can produce two types of output files:
 
 

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

@@ -2270,6 +2270,7 @@ THREE.GLTFLoader = ( function () {
 				pointsMaterial.color.copy( material.color );
 				pointsMaterial.color.copy( material.color );
 				pointsMaterial.map = material.map;
 				pointsMaterial.map = material.map;
 				pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
 				pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet
+				pointsMaterial.sizeAttenuation = false; // glTF spec says points should be 1px
 
 
 				this.cache.add( cacheKey, pointsMaterial );
 				this.cache.add( cacheKey, pointsMaterial );
 
 

+ 50 - 0
3rdparty/meshoptimizer/demo/babylon.MESHOPT_compression.js

@@ -0,0 +1,50 @@
+/* Babylon.js extension for MESHOPT_compression; requires Babylon.js 4.1 */
+var NAME = "MESHOPT_compression";
+var MESHOPT_compression = /** @class */ (function () {
+    /** @hidden */
+    function MESHOPT_compression(loader, decoder) {
+        /** The name of this extension. */
+        this.name = NAME;
+        /** Defines whether this extension is enabled. */
+        this.enabled = true;
+        this._loader = loader;
+        this._decoder = decoder;
+    }
+    /** @hidden */
+    MESHOPT_compression.prototype.dispose = function () {
+        delete this._loader;
+    };
+    /** @hidden */
+    MESHOPT_compression.prototype.loadBufferViewAsync = function (context, bufferView) {
+        if (bufferView.extensions && bufferView.extensions[NAME]) {
+            if (bufferView._decoded) {
+                return bufferView._decoded;
+            }
+            var view = this._loader.loadBufferViewAsync(context, bufferView);
+            var decoder = this._decoder;
+            bufferView._decoded = Promise.all([view, decoder.ready]).then(function (res) {
+                var source = res[0];
+                var extensionDef = bufferView.extensions[NAME];
+                var count = extensionDef.count;
+                var stride = extensionDef.byteStride;
+                var result = new Uint8Array(new ArrayBuffer(count * stride));
+                switch (extensionDef.mode) {
+                    case 0:
+                        decoder.decodeVertexBuffer(result, count, stride, source);
+                        break;
+                    case 1:
+                        decoder.decodeIndexBuffer(result, count, stride, source);
+                        break;
+                    default:
+                        throw new Error("GLTFLoader: Unrecognized meshopt compression mode.");
+                }
+                return Promise.resolve(result);
+            });
+            return bufferView._decoded;
+        } else {
+            return null;
+        }
+    };
+    return MESHOPT_compression;
+}());
+/* BABYLON.GLTF2.GLTFLoader.RegisterExtension("MESHOPT_compression", (loader) => new MESHOPT_compression(loader, MeshoptDecoder)); */

+ 70 - 0
3rdparty/meshoptimizer/demo/demo.html

@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+
+        <title>meshoptimizer - demo</title>
+
+        <script src="https://preview.babylonjs.com/babylon.js"></script>
+        <script src="https://preview.babylonjs.com/loaders/babylon.glTF2FileLoader.js"></script>
+
+        <script src="babylon.MESHOPT_compression.js"></script>
+        <script src="../js/meshopt_decoder.js"></script>
+
+        <style>
+            html, body {
+                overflow: hidden;
+                width: 100%;
+                height: 100%;
+                margin: 0;
+                padding: 0;
+            }
+
+            #renderCanvas {
+                width: 100%;
+                height: 100%;
+                touch-action: none;
+            }
+        </style>
+    </head>
+<body>
+    <canvas id="renderCanvas"></canvas>
+    <script>
+        var canvas = document.getElementById("renderCanvas");
+
+        BABYLON.GLTF2.GLTFLoader.RegisterExtension("MESHOPT_compression", (loader) => new MESHOPT_compression(loader, MeshoptDecoder)); 
+
+        var createScene = function () {
+            var scene = new BABYLON.Scene(engine);
+        
+            var light = new BABYLON.HemisphericLight();
+        
+            var camera = new BABYLON.ArcRotateCamera("Camera", 0, 0.8, 10, BABYLON.Vector3.Zero(), scene);
+            camera.attachControl(canvas, false);
+        
+            BABYLON.SceneLoader.Append("", "pirate.glb", scene, function (newMeshes) {
+                scene.activeCamera = null;
+                scene.createDefaultCameraOrLight(true);
+                scene.activeCamera.attachControl(canvas, false);
+                scene.activeCamera.alpha = Math.PI / 2;
+            });
+        
+            return scene;
+        }
+
+        var engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
+        var scene = createScene();
+
+        engine.runRenderLoop(function () {
+            if (scene) {
+                scene.render();
+            }
+        });
+
+        // Resize
+        window.addEventListener("resize", function () {
+            engine.resize();
+        });
+    </script>
+</body>
+</html>

+ 18 - 9
3rdparty/meshoptimizer/demo/main.cpp

@@ -411,6 +411,22 @@ void simplifySloppy(const Mesh& mesh, float threshold = 0.2f)
 	       int(mesh.indices.size() / 3), int(lod.indices.size() / 3), (end - start) * 1000);
 	       int(mesh.indices.size() / 3), int(lod.indices.size() / 3), (end - start) * 1000);
 }
 }
 
 
+void simplifyPoints(const Mesh& mesh, float threshold = 0.2f)
+{
+	double start = timestamp();
+
+	size_t target_vertex_count = size_t(mesh.vertices.size() * threshold);
+
+	std::vector<unsigned int> indices(target_vertex_count);
+	indices.resize(meshopt_simplifyPoints(&indices[0], &mesh.vertices[0].px, mesh.vertices.size(), sizeof(Vertex), target_vertex_count));
+
+	double end = timestamp();
+
+	printf("%-9s: %d points => %d points in %.2f msec\n",
+	       "SimplifyP",
+	       int(mesh.vertices.size()), int(indices.size()), (end - start) * 1000);
+}
+
 void simplifyComplete(const Mesh& mesh)
 void simplifyComplete(const Mesh& mesh)
 {
 {
 	static const size_t lod_count = 5;
 	static const size_t lod_count = 5;
@@ -985,6 +1001,7 @@ void process(const char* path)
 	simplify(mesh);
 	simplify(mesh);
 	simplifySloppy(mesh);
 	simplifySloppy(mesh);
 	simplifyComplete(mesh);
 	simplifyComplete(mesh);
+	simplifyPoints(mesh);
 
 
 	spatialSort(mesh);
 	spatialSort(mesh);
 	spatialSortTriangles(mesh);
 	spatialSortTriangles(mesh);
@@ -999,15 +1016,7 @@ void processDev(const char* path)
 	if (!loadMesh(mesh, path))
 	if (!loadMesh(mesh, path))
 		return;
 		return;
 
 
-	Mesh copy = mesh;
-	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));
-
-	encodeIndex(copy);
-	encodeVertex<PackedVertexOct>(copy, "O");
-
-	spatialSort(mesh);
-	spatialSortTriangles(mesh);
+	simplifyPoints(mesh);
 }
 }
 
 
 int main(int argc, char** argv)
 int main(int argc, char** argv)

+ 12 - 0
3rdparty/meshoptimizer/src/meshoptimizer.h

@@ -215,6 +215,18 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplify(unsigned int* destination, co
  */
  */
 MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count);
 MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count);
 
 
+/**
+ * Experimental: Point cloud simplifier
+ * Reduces the number of points in the cloud to reach the given target
+ * Returns the number of points after simplification, with destination containing new index data
+ * The resulting index buffer references vertices from the original vertex buffer.
+ * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended.
+ *
+ * destination must contain enough space for the target index buffer
+ * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer
+ */
+MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_vertex_count);
+
 /**
 /**
  * Mesh stripifier
  * Mesh stripifier
  * Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index or degenerate triangles
  * Converts a previously vertex cache optimized triangle list to triangle strip, stitching strips using restart index or degenerate triangles

+ 178 - 0
3rdparty/meshoptimizer/src/simplifier.cpp

@@ -505,6 +505,22 @@ static void quadricFromPlane(Quadric& Q, float a, float b, float c, float d, flo
 	Q.w = w;
 	Q.w = w;
 }
 }
 
 
+static void quadricFromPoint(Quadric& Q, float x, float y, float z, float w)
+{
+	// we need to encode (x - X) ^ 2 + (y - Y)^2 + (z - Z)^2 into the quadric
+	Q.a00 = w;
+	Q.a11 = w;
+	Q.a22 = w;
+	Q.a10 = 0.f;
+	Q.a20 = 0.f;
+	Q.a21 = 0.f;
+	Q.b0 = -2.f * x * w;
+	Q.b1 = -2.f * y * w;
+	Q.b2 = -2.f * z * w;
+	Q.c = (x * x + y * y + z * z) * w;
+	Q.w = w;
+}
+
 static void quadricFromTriangle(Quadric& Q, const Vector3& p0, const Vector3& p1, const Vector3& p2, float weight)
 static void quadricFromTriangle(Quadric& Q, const Vector3& p0, const Vector3& p1, const Vector3& p2, float weight)
 {
 {
 	Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z};
 	Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z};
@@ -909,6 +925,25 @@ struct CellHasher
 	}
 	}
 };
 };
 
 
+struct IdHasher
+{
+	size_t hash(unsigned int id) const
+	{
+		unsigned int h = id;
+
+		// MurmurHash2 finalizer
+		h ^= h >> 13;
+		h *= 0x5bd1e995;
+		h ^= h >> 15;
+		return h;
+	}
+
+	bool equal(unsigned int lhs, unsigned int rhs) const
+	{
+		return lhs == rhs;
+	}
+};
+
 struct TriangleHasher
 struct TriangleHasher
 {
 {
 	unsigned int* indices;
 	unsigned int* indices;
@@ -989,6 +1024,26 @@ static size_t fillVertexCells(unsigned int* table, size_t table_size, unsigned i
 	return result;
 	return result;
 }
 }
 
 
+static size_t countVertexCells(unsigned int* table, size_t table_size, const unsigned int* vertex_ids, size_t vertex_count)
+{
+	IdHasher hasher;
+
+	memset(table, -1, table_size * sizeof(unsigned int));
+
+	size_t result = 0;
+
+	for (size_t i = 0; i < vertex_count; ++i)
+	{
+		unsigned int id = vertex_ids[i];
+		unsigned int* entry = hashLookup2(table, table_size, hasher, id, ~0u);
+
+		result += (*entry == ~0u);
+		*entry = id;
+	}
+
+	return result;
+}
+
 static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* vertex_cells)
 static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* vertex_cells)
 {
 {
 	for (size_t i = 0; i < index_count; i += 3)
 	for (size_t i = 0; i < index_count; i += 3)
@@ -1019,6 +1074,20 @@ static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices
 	}
 	}
 }
 }
 
 
+static void fillCellQuadrics(Quadric* cell_quadrics, const Vector3* vertex_positions, size_t vertex_count, const unsigned int* vertex_cells)
+{
+	for (size_t i = 0; i < vertex_count; ++i)
+	{
+		unsigned int c = vertex_cells[i];
+		const Vector3& v = vertex_positions[i];
+
+		Quadric Q;
+		quadricFromPoint(Q, v.x, v.y, v.z, 1.f);
+
+		quadricAdd(cell_quadrics[c], Q);
+	}
+}
+
 static void fillCellRemap(unsigned int* cell_remap, float* cell_errors, size_t cell_count, const unsigned int* vertex_cells, const Quadric* cell_quadrics, const Vector3* vertex_positions, size_t vertex_count)
 static void fillCellRemap(unsigned int* cell_remap, float* cell_errors, size_t cell_count, const unsigned int* vertex_cells, const Quadric* cell_quadrics, const Vector3* vertex_positions, size_t vertex_count)
 {
 {
 	memset(cell_remap, -1, cell_count * sizeof(unsigned int));
 	memset(cell_remap, -1, cell_count * sizeof(unsigned int));
@@ -1363,3 +1432,112 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind
 
 
 	return write;
 	return write;
 }
 }
+
+size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_vertex_count)
+{
+	using namespace meshopt;
+
+	assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256);
+	assert(vertex_positions_stride % sizeof(float) == 0);
+	assert(target_vertex_count <= vertex_count);
+
+	size_t target_cell_count = target_vertex_count;
+
+	meshopt_Allocator allocator;
+
+	Vector3* vertex_positions = allocator.allocate<Vector3>(vertex_count);
+	rescalePositions(vertex_positions, vertex_positions_data, vertex_count, vertex_positions_stride);
+
+	// find the optimal grid size using guided binary search
+#if TRACE
+	printf("source: %d vertices\n", int(vertex_count));
+	printf("target: %d cells\n", int(target_cell_count));
+#endif
+
+	unsigned int* vertex_ids = allocator.allocate<unsigned int>(vertex_count);
+
+	size_t table_size = hashBuckets2(vertex_count);
+	unsigned int* table = allocator.allocate<unsigned int>(table_size);
+
+	const int kInterpolationPasses = 5;
+
+	// invariant: # of vertices in min_grid <= target_count
+	int min_grid = 0;
+	int max_grid = 1025;
+	size_t min_vertices = 0;
+	size_t max_vertices = vertex_count;
+
+	// 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_vertices < target_vertex_count);
+		assert(max_grid - min_grid > 1);
+
+		// 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 vertices = countVertexCells(table, table_size, vertex_ids, vertex_count);
+
+#if TRACE
+		printf("pass %d (%s): grid size %d, vertices %d, %s\n",
+		       pass, (pass == 0) ? "guess" : (pass <= kInterpolationPasses) ? "lerp" : "binary",
+		       grid_size, int(vertices),
+		       (vertices <= target_vertex_count) ? "under" : "over");
+#endif
+
+		float tip = interpolate(float(target_vertex_count), float(min_grid), float(min_vertices), float(grid_size), float(vertices), float(max_grid), float(max_vertices));
+
+		if (vertices <= target_vertex_count)
+		{
+			min_grid = grid_size;
+			min_vertices = vertices;
+		}
+		else
+		{
+			max_grid = grid_size;
+			max_vertices = vertices;
+		}
+
+		if (vertices == target_vertex_count || 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_vertices == 0)
+		return 0;
+
+	// build vertex->cell association by mapping all vertices with the same quantized position to the same cell
+	unsigned int* vertex_cells = allocator.allocate<unsigned int>(vertex_count);
+
+	computeVertexIds(vertex_ids, vertex_positions, vertex_count, min_grid);
+	size_t cell_count = fillVertexCells(table, table_size, vertex_cells, vertex_ids, vertex_count);
+
+	// build a quadric for each target cell
+	Quadric* cell_quadrics = allocator.allocate<Quadric>(cell_count);
+	memset(cell_quadrics, 0, cell_count * sizeof(Quadric));
+
+	fillCellQuadrics(cell_quadrics, vertex_positions, vertex_count, vertex_cells);
+
+	// for each target cell, find the vertex with the minimal error
+	unsigned int* cell_remap = allocator.allocate<unsigned int>(cell_count);
+	float* cell_errors = allocator.allocate<float>(cell_count);
+
+	fillCellRemap(cell_remap, cell_errors, cell_count, vertex_cells, cell_quadrics, vertex_positions, vertex_count);
+
+	// copy results to the output
+	assert(cell_count <= target_vertex_count);
+	memcpy(destination, cell_remap, sizeof(unsigned int) * cell_count);
+
+#if TRACE
+	printf("result: %d cells\n", int(cell_count));
+#endif
+
+	return cell_count;
+}

+ 64 - 16
3rdparty/meshoptimizer/tools/cgltf.h

@@ -373,6 +373,8 @@ typedef struct cgltf_mesh {
 	cgltf_size primitives_count;
 	cgltf_size primitives_count;
 	cgltf_float* weights;
 	cgltf_float* weights;
 	cgltf_size weights_count;
 	cgltf_size weights_count;
+	char** target_names;
+	cgltf_size target_names_count;
 	cgltf_extras extras;
 	cgltf_extras extras;
 } cgltf_mesh;
 } cgltf_mesh;
 
 
@@ -1171,6 +1173,14 @@ cgltf_result cgltf_validate(cgltf_data* data)
 			}
 			}
 		}
 		}
 
 
+		if (data->meshes[i].target_names)
+		{
+			if (data->meshes[i].primitives_count && data->meshes[i].primitives[0].targets_count != data->meshes[i].target_names_count)
+			{
+				return cgltf_result_invalid_gltf;
+			}
+		}
+
 		for (cgltf_size j = 0; j < data->meshes[i].primitives_count; ++j)
 		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)
 			if (data->meshes[i].primitives[j].targets_count != data->meshes[i].primitives[0].targets_count)
@@ -1321,6 +1331,13 @@ void cgltf_free(cgltf_data* data)
 
 
 		data->memory_free(data->memory_user_data, data->meshes[i].primitives);
 		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[i].weights);
+
+		for (cgltf_size j = 0; j < data->meshes[i].target_names_count; ++j)
+		{
+			data->memory_free(data->memory_user_data, data->meshes[i].target_names[j]);
+		}
+
+		data->memory_free(data->memory_user_data, data->meshes[i].target_names);
 	}
 	}
 
 
 	data->memory_free(data->memory_user_data, data->meshes);
 	data->memory_free(data->memory_user_data, data->meshes);
@@ -1441,17 +1458,17 @@ void cgltf_node_transform_local(const cgltf_node* node, cgltf_float* out_matrix)
 		float sz = node->scale[2];
 		float sz = node->scale[2];
 
 
 		lm[0] = (1 - 2 * qy*qy - 2 * qz*qz) * sx;
 		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[1] = (2 * qx*qy + 2 * qz*qw) * sx;
+		lm[2] = (2 * qx*qz - 2 * qy*qw) * sx;
 		lm[3] = 0.f;
 		lm[3] = 0.f;
 
 
-		lm[4] = (2 * qx*qy - 2 * qz*qw) * sx;
+		lm[4] = (2 * qx*qy - 2 * qz*qw) * sy;
 		lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy;
 		lm[5] = (1 - 2 * qx*qx - 2 * qz*qz) * sy;
-		lm[6] = (2 * qy*qz + 2 * qx*qw) * sz;
+		lm[6] = (2 * qy*qz + 2 * qx*qw) * sy;
 		lm[7] = 0.f;
 		lm[7] = 0.f;
 
 
-		lm[8] = (2 * qx*qz + 2 * qy*qw) * sx;
-		lm[9] = (2 * qy*qz - 2 * qx*qw) * sy;
+		lm[8] = (2 * qx*qz + 2 * qy*qw) * sz;
+		lm[9] = (2 * qy*qz - 2 * qx*qw) * sz;
 		lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz;
 		lm[10] = (1 - 2 * qx*qx - 2 * qy*qy) * sz;
 		lm[11] = 0.f;
 		lm[11] = 0.f;
 
 
@@ -1512,9 +1529,9 @@ static cgltf_size cgltf_component_read_index(const void* in, cgltf_component_typ
 		case cgltf_component_type_r_8:
 		case cgltf_component_type_r_8:
 			return *((const int8_t*) in);
 			return *((const int8_t*) in);
 		case cgltf_component_type_r_8u:
 		case cgltf_component_type_r_8u:
-		case cgltf_component_type_invalid:
-		default:
 			return *((const uint8_t*) in);
 			return *((const uint8_t*) in);
+		default:
+			return 0;
 	}
 	}
 }
 }
 
 
@@ -1529,18 +1546,17 @@ static cgltf_float cgltf_component_read_float(const void* in, cgltf_component_ty
 	{
 	{
 		switch (component_type)
 		switch (component_type)
 		{
 		{
-			case cgltf_component_type_r_32u:
-				return *((const uint32_t*) in) / (float) UINT_MAX;
+			// note: glTF spec doesn't currently define normalized conversions for 32-bit integers
 			case cgltf_component_type_r_16:
 			case cgltf_component_type_r_16:
-				return *((const int16_t*) in) / (float) SHRT_MAX;
+				return *((const int16_t*) in) / (cgltf_float)32767;
 			case cgltf_component_type_r_16u:
 			case cgltf_component_type_r_16u:
-				return *((const uint16_t*) in) / (float) USHRT_MAX;
+				return *((const uint16_t*) in) / (cgltf_float)65535;
 			case cgltf_component_type_r_8:
 			case cgltf_component_type_r_8:
-				return *((const int8_t*) in) / (float) SCHAR_MAX;
+				return *((const int8_t*) in) / (cgltf_float)127;
 			case cgltf_component_type_r_8u:
 			case cgltf_component_type_r_8u:
-			case cgltf_component_type_invalid:
+				return *((const uint8_t*) in) / (cgltf_float)255;
 			default:
 			default:
-				return *((const uint8_t*) in) / (float) CHAR_MAX;
+				return 0;
 		}
 		}
 	}
 	}
 
 
@@ -1996,7 +2012,39 @@ static int cgltf_parse_json_mesh(cgltf_options* options, jsmntok_t const* tokens
 		}
 		}
 		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
 		else if (cgltf_json_strcmp(tokens + i, json_chunk, "extras") == 0)
 		{
 		{
-			i = cgltf_parse_json_extras(tokens, i + 1, json_chunk, &out_mesh->extras);
+			++i;
+
+			out_mesh->extras.start_offset = tokens[i].start;
+			out_mesh->extras.end_offset = tokens[i].end;
+
+			if (tokens[i].type == JSMN_OBJECT)
+			{
+				int extras_size = tokens[i].size;
+				++i;
+
+				for (int k = 0; k < extras_size; ++k)
+				{
+					CGLTF_CHECK_KEY(tokens[i]);
+
+					if (cgltf_json_strcmp(tokens+i, json_chunk, "targetNames") == 0)
+					{
+						i = cgltf_parse_json_string_array(options, tokens, i + 1, json_chunk, &out_mesh->target_names, &out_mesh->target_names_count);
+					}
+					else
+					{
+						i = cgltf_skip_json(tokens, i+1);
+					}
+
+					if (i < 0)
+					{
+						return i;
+					}
+				}
+			}
+			else
+			{
+				i = cgltf_skip_json(tokens, i);
+			}
 		}
 		}
 		else
 		else
 		{
 		{

+ 156 - 35
3rdparty/meshoptimizer/tools/gltfpack.cpp

@@ -64,7 +64,8 @@ struct Mesh
 	std::vector<unsigned int> indices;
 	std::vector<unsigned int> indices;
 
 
 	size_t targets;
 	size_t targets;
-	std::vector<float> weights;
+	std::vector<float> target_weights;
+	std::vector<const char*> target_names;
 };
 };
 
 
 struct Settings
 struct Settings
@@ -79,6 +80,9 @@ struct Settings
 
 
 	bool keep_named;
 	bool keep_named;
 
 
+	float simplify_threshold;
+	bool simplify_aggressive;
+
 	bool compress;
 	bool compress;
 	int verbose;
 	int verbose;
 };
 };
@@ -320,7 +324,8 @@ void parseMeshesGltf(cgltf_data* data, std::vector<Mesh>& meshes)
 			}
 			}
 
 
 			result.targets = primitive.targets_count;
 			result.targets = primitive.targets_count;
-			result.weights.assign(mesh.weights, mesh.weights + mesh.weights_count);
+			result.target_weights.assign(mesh.weights, mesh.weights + mesh.weights_count);
+			result.target_names.assign(mesh.target_names, mesh.target_names + mesh.target_names_count);
 
 
 			meshes.push_back(result);
 			meshes.push_back(result);
 		}
 		}
@@ -627,6 +632,28 @@ void mergeMeshMaterials(cgltf_data* data, std::vector<Mesh>& meshes)
 	}
 	}
 }
 }
 
 
+bool compareMeshTargets(const Mesh& lhs, const Mesh& rhs)
+{
+	if (lhs.targets != rhs.targets)
+		return false;
+
+	if (lhs.target_weights.size() != rhs.target_weights.size())
+		return false;
+
+	for (size_t i = 0; i < lhs.target_weights.size(); ++i)
+		if (lhs.target_weights[i] != rhs.target_weights[i])
+			return false;
+
+	if (lhs.target_names.size() != rhs.target_names.size())
+		return false;
+
+	for (size_t i = 0; i < lhs.target_names.size(); ++i)
+		if (strcmp(lhs.target_names[i], rhs.target_names[i]) != 0)
+			return false;
+
+	return true;
+}
+
 bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings)
 bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings)
 {
 {
 	if (lhs.node != rhs.node)
 	if (lhs.node != rhs.node)
@@ -665,16 +692,9 @@ bool canMergeMeshes(const Mesh& lhs, const Mesh& rhs, const Settings& settings)
 	if (lhs.type != rhs.type)
 	if (lhs.type != rhs.type)
 		return false;
 		return false;
 
 
-	if (lhs.targets != rhs.targets)
-		return false;
-
-	if (lhs.weights.size() != rhs.weights.size())
+	if (!compareMeshTargets(lhs, rhs))
 		return false;
 		return false;
 
 
-	for (size_t i = 0; i < lhs.weights.size(); ++i)
-		if (lhs.weights[i] != rhs.weights[i])
-			return false;
-
 	if (lhs.indices.empty() != rhs.indices.empty())
 	if (lhs.indices.empty() != rhs.indices.empty())
 		return false;
 		return false;
 
 
@@ -810,6 +830,38 @@ const Stream* getPositionStream(const Mesh& mesh)
 	return 0;
 	return 0;
 }
 }
 
 
+void simplifyMesh(Mesh& mesh, float threshold, bool aggressive)
+{
+	if (threshold >= 1)
+		return;
+
+	const Stream* positions = getPositionStream(mesh);
+	if (!positions)
+		return;
+
+	size_t vertex_count = mesh.streams[0].data.size();
+
+	size_t target_index_count = size_t(double(mesh.indices.size() / 3) * threshold) * 3;
+	float target_error = 1e-2f;
+
+	if (target_index_count < 1)
+		return;
+
+	std::vector<unsigned int> indices(mesh.indices.size());
+	indices.resize(meshopt_simplify(&indices[0], &mesh.indices[0], mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), target_index_count, target_error));
+	mesh.indices.swap(indices);
+
+	// Note: if the simplifier got stuck, we can try to reindex without normals/tangents and retry
+	// For now we simply fall back to aggressive simplifier instead
+
+	// if the mesh is complex enough and the precise simplifier got "stuck", we'll try to simplify using the sloppy simplifier which is guaranteed to reach the target count
+	if (aggressive && target_index_count > 50 * 3 && mesh.indices.size() > target_index_count)
+	{
+		indices.resize(meshopt_simplifySloppy(&indices[0], &mesh.indices[0], mesh.indices.size(), positions->data[0].f, vertex_count, sizeof(Attr), target_index_count));
+		mesh.indices.swap(indices);
+	}
+}
+
 void optimizeMesh(Mesh& mesh)
 void optimizeMesh(Mesh& mesh)
 {
 {
 	size_t vertex_count = mesh.streams[0].data.size();
 	size_t vertex_count = mesh.streams[0].data.size();
@@ -818,9 +870,7 @@ void optimizeMesh(Mesh& mesh)
 
 
 	std::vector<unsigned int> remap(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);
 	size_t unique_vertices = meshopt_optimizeVertexFetchRemap(&remap[0], &mesh.indices[0], mesh.indices.size(), vertex_count);
-
-	assert(unique_vertices == vertex_count);
-	(void)unique_vertices;
+	assert(unique_vertices <= vertex_count);
 
 
 	meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &remap[0]);
 	meshopt_remapIndexBuffer(&mesh.indices[0], &mesh.indices[0], mesh.indices.size(), &remap[0]);
 
 
@@ -829,16 +879,49 @@ void optimizeMesh(Mesh& mesh)
 		assert(mesh.streams[i].data.size() == vertex_count);
 		assert(mesh.streams[i].data.size() == vertex_count);
 
 
 		meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], vertex_count, sizeof(Attr), &remap[0]);
 		meshopt_remapVertexBuffer(&mesh.streams[i].data[0], &mesh.streams[i].data[0], vertex_count, sizeof(Attr), &remap[0]);
+		mesh.streams[i].data.resize(unique_vertices);
 	}
 	}
 }
 }
 
 
-void sortPointMesh(Mesh& mesh)
+void simplifyPointMesh(Mesh& mesh, float threshold)
 {
 {
+	if (threshold >= 1)
+		return;
+
 	const Stream* positions = getPositionStream(mesh);
 	const Stream* positions = getPositionStream(mesh);
 	if (!positions)
 	if (!positions)
 		return;
 		return;
 
 
-	assert(mesh.indices.empty());
+	size_t vertex_count = mesh.streams[0].data.size();
+
+	size_t target_vertex_count = size_t(double(vertex_count) * threshold);
+
+	if (target_vertex_count < 1)
+		return;
+
+	std::vector<unsigned int> indices(target_vertex_count);
+	indices.resize(meshopt_simplifyPoints(&indices[0], positions->data[0].f, vertex_count, sizeof(Attr), target_vertex_count));
+
+	std::vector<Attr> scratch(indices.size());
+
+	for (size_t i = 0; i < mesh.streams.size(); ++i)
+	{
+		std::vector<Attr>& data = mesh.streams[i].data;
+
+		assert(data.size() == vertex_count);
+
+		for (size_t j = 0; j < indices.size(); ++j)
+			scratch[j] = data[indices[j]];
+
+		data = scratch;
+	}
+}
+
+void sortPointMesh(Mesh& mesh)
+{
+	const Stream* positions = getPositionStream(mesh);
+	if (!positions)
+		return;
 
 
 	size_t vertex_count = mesh.streams[0].data.size();
 	size_t vertex_count = mesh.streams[0].data.size();
 
 
@@ -2813,7 +2896,23 @@ void writeLight(std::string& json, const cgltf_light& light)
 	append(json, "}");
 	append(json, "}");
 }
 }
 
 
-void printStats(const std::vector<BufferView>& views, BufferView::Kind kind, const char* name)
+void printMeshStats(const std::vector<Mesh>& meshes, const char* name)
+{
+	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("%s: %d triangles, %d vertices\n", name, int(triangles), int(vertices));
+}
+
+void printAttributeStats(const std::vector<BufferView>& views, BufferView::Kind kind, const char* name)
 {
 {
 	for (size_t i = 0; i < views.size(); ++i)
 	for (size_t i = 0; i < views.size(); ++i)
 	{
 	{
@@ -2897,6 +2996,11 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 
 
 	markNeededMaterials(data, materials, meshes);
 	markNeededMaterials(data, materials, meshes);
 
 
+	if (settings.verbose)
+	{
+		printMeshStats(meshes, "input");
+	}
+
 	for (size_t i = 0; i < meshes.size(); ++i)
 	for (size_t i = 0; i < meshes.size(); ++i)
 	{
 	{
 		Mesh& mesh = meshes[i];
 		Mesh& mesh = meshes[i];
@@ -2904,11 +3008,14 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 		switch (mesh.type)
 		switch (mesh.type)
 		{
 		{
 		case cgltf_primitive_type_points:
 		case cgltf_primitive_type_points:
+			assert(mesh.indices.empty());
+			simplifyPointMesh(mesh, settings.simplify_threshold);
 			sortPointMesh(mesh);
 			sortPointMesh(mesh);
 			break;
 			break;
 
 
 		case cgltf_primitive_type_triangles:
 		case cgltf_primitive_type_triangles:
 			reindexMesh(mesh);
 			reindexMesh(mesh);
+			simplifyMesh(mesh, settings.simplify_threshold, settings.simplify_aggressive);
 			optimizeMesh(mesh);
 			optimizeMesh(mesh);
 			break;
 			break;
 
 
@@ -2919,18 +3026,7 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 
 
 	if (settings.verbose)
 	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));
+		printMeshStats(meshes, "output");
 	}
 	}
 
 
 	QuantizationParams qp = prepareQuantization(meshes, settings);
 	QuantizationParams qp = prepareQuantization(meshes, settings);
@@ -3044,7 +3140,7 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 			if (prim.node != mesh.node || prim.skin != mesh.skin || prim.targets != mesh.targets)
 			if (prim.node != mesh.node || prim.skin != mesh.skin || prim.targets != mesh.targets)
 				break;
 				break;
 
 
-			if (mesh.weights.size() && (prim.weights.size() != mesh.weights.size() || memcmp(&mesh.weights[0], &prim.weights[0], mesh.weights.size() * sizeof(float)) != 0))
+			if (!compareMeshTargets(mesh, prim))
 				break;
 				break;
 
 
 			comma(json_meshes);
 			comma(json_meshes);
@@ -3089,16 +3185,30 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 
 
 		append(json_meshes, "]");
 		append(json_meshes, "]");
 
 
-		if (mesh.weights.size())
+		if (mesh.target_weights.size())
 		{
 		{
 			append(json_meshes, ",\"weights\":[");
 			append(json_meshes, ",\"weights\":[");
-			for (size_t j = 0; j < mesh.weights.size(); ++j)
+			for (size_t j = 0; j < mesh.target_weights.size(); ++j)
 			{
 			{
 				comma(json_meshes);
 				comma(json_meshes);
-				append(json_meshes, mesh.weights[j]);
+				append(json_meshes, mesh.target_weights[j]);
 			}
 			}
 			append(json_meshes, "]");
 			append(json_meshes, "]");
 		}
 		}
+
+		if (mesh.target_names.size())
+		{
+			append(json_meshes, ",\"extras\":{\"targetNames\":[");
+			for (size_t j = 0; j < mesh.target_names.size(); ++j)
+			{
+				comma(json_meshes);
+				append(json_meshes, "\"");
+				append(json_meshes, mesh.target_names[j]);
+				append(json_meshes, "\"");
+			}
+			append(json_meshes, "]}");
+		}
+
 		append(json_meshes, "}");
 		append(json_meshes, "}");
 
 
 		writeMeshNode(json_nodes, mesh_offset, mesh, data, qp);
 		writeMeshNode(json_nodes, mesh_offset, mesh, data, qp);
@@ -3365,9 +3475,9 @@ void process(cgltf_data* data, std::vector<Mesh>& meshes, const Settings& settin
 
 
 	if (settings.verbose > 1)
 	if (settings.verbose > 1)
 	{
 	{
-		printStats(views, BufferView::Kind_Vertex, "vertex");
-		printStats(views, BufferView::Kind_Index, "index");
-		printStats(views, BufferView::Kind_Keyframe, "keyframe");
+		printAttributeStats(views, BufferView::Kind_Vertex, "vertex");
+		printAttributeStats(views, BufferView::Kind_Index, "index");
+		printAttributeStats(views, BufferView::Kind_Keyframe, "keyframe");
 	}
 	}
 }
 }
 
 
@@ -3529,6 +3639,7 @@ int main(int argc, char** argv)
 	settings.tex_bits = 12;
 	settings.tex_bits = 12;
 	settings.nrm_bits = 8;
 	settings.nrm_bits = 8;
 	settings.anim_freq = 30;
 	settings.anim_freq = 30;
+	settings.simplify_threshold = 1.f;
 
 
 	const char* input = 0;
 	const char* input = 0;
 	const char* output = 0;
 	const char* output = 0;
@@ -3567,6 +3678,14 @@ int main(int argc, char** argv)
 		{
 		{
 			settings.keep_named = true;
 			settings.keep_named = true;
 		}
 		}
+		else if (strcmp(arg, "-si") == 0 && i + 1 < argc && isdigit(argv[i + 1][0]))
+		{
+			settings.simplify_threshold = float(atof(argv[++i]));
+		}
+		else if (strcmp(arg, "-sa") == 0)
+		{
+			settings.simplify_aggressive = true;
+		}
 		else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
 		else if (strcmp(arg, "-i") == 0 && i + 1 < argc && !input)
 		{
 		{
 			input = argv[++i];
 			input = argv[++i];
@@ -3628,6 +3747,8 @@ int main(int argc, char** argv)
 		fprintf(stderr, "-af N: resample animations at N Hz (default: 30)\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, "-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, "-kn: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally\n");
+		fprintf(stderr, "-si R: simplify meshes to achieve the ratio R (default: 1; R should be between 0 and 1)\n");
+		fprintf(stderr, "-sa: aggressively simplify to the target ratio disregarding quality\n");
 		fprintf(stderr, "-c: produce compressed glb files\n");
 		fprintf(stderr, "-c: produce compressed glb files\n");
 		fprintf(stderr, "-v: verbose output\n");
 		fprintf(stderr, "-v: verbose output\n");
 		fprintf(stderr, "-h: display this help and exit\n");
 		fprintf(stderr, "-h: display this help and exit\n");