├── gltf ├── wasistubs.txt ├── package.json ├── cli.js ├── json.cpp ├── wasistubs.cpp ├── basislib.cpp ├── fileio.cpp ├── README.md ├── basisenc.cpp ├── node.cpp ├── parseobj.cpp ├── image.cpp ├── library.js └── animation.cpp ├── .gitignore ├── demo ├── pirate.glb ├── ansi.c ├── demo.html ├── index.html └── simplify.html ├── config.cmake.in ├── js ├── index.module.d.ts ├── index.module.js ├── index.js ├── meshopt_simplifier.module.d.ts ├── package.json ├── meshopt_decoder.module.d.ts ├── meshopt_encoder.module.d.ts ├── benchmark.js ├── meshopt_simplifier.test.js ├── meshopt_encoder.test.js ├── meshopt_decoder.test.js └── meshopt_decoder_reference.js ├── codecov.yml ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── config.yml │ └── bug_report.md └── workflows │ ├── release.yml │ └── build.yml ├── .editorconfig ├── tools ├── meshloader.cpp ├── bitmask.py ├── wasmpack.py ├── codecfuzz.cpp ├── wasmstubs.cpp ├── codecbench.cpp └── vcachetuner.cpp ├── .clang-format ├── src ├── allocator.cpp ├── vfetchanalyzer.cpp ├── vfetchoptimizer.cpp ├── vcacheanalyzer.cpp ├── spatialorder.cpp ├── overdrawanalyzer.cpp ├── stripifier.cpp └── overdrawoptimizer.cpp ├── LICENSE.md ├── CONTRIBUTING.md ├── CMakeLists.txt └── Makefile /gltf/wasistubs.txt: -------------------------------------------------------------------------------- 1 | __wasi_path_open32 2 | __wasi_fd_seek32 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /data/ 3 | /gltf/library.wasm 4 | /.idea/ 5 | /cmake-build-*/ 6 | -------------------------------------------------------------------------------- /demo/pirate.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CesiumGS/meshoptimizer/master/demo/pirate.glb -------------------------------------------------------------------------------- /demo/ansi.c: -------------------------------------------------------------------------------- 1 | /* This file makes sure the library can be used by C89 code */ 2 | #include "../src/meshoptimizer.h" 3 | -------------------------------------------------------------------------------- /config.cmake.in: -------------------------------------------------------------------------------- 1 | @PACKAGE_INIT@ 2 | 3 | include("${CMAKE_CURRENT_LIST_DIR}/meshoptimizerTargets.cmake") 4 | check_required_components(meshoptimizer) 5 | -------------------------------------------------------------------------------- /js/index.module.d.ts: -------------------------------------------------------------------------------- 1 | export * from './meshopt_encoder.module'; 2 | export * from './meshopt_decoder.module'; 3 | export * from './meshopt_simplifier.module'; 4 | -------------------------------------------------------------------------------- /js/index.module.js: -------------------------------------------------------------------------------- 1 | export * from './meshopt_encoder.module.js'; 2 | export * from './meshopt_decoder.module.js'; 3 | export * from './meshopt_simplifier.module.js'; 4 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: off 6 | patch: off 7 | 8 | ignore: 9 | - demo 10 | - extern 11 | - tools 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # See https://editorconfig.org/ for more info 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Help and support 4 | url: https://github.com/zeux/meshoptimizer/discussions 5 | about: Please use GitHub Discussions if you have questions or need help. 6 | -------------------------------------------------------------------------------- /tools/meshloader.cpp: -------------------------------------------------------------------------------- 1 | #ifndef _CRT_SECURE_NO_WARNINGS 2 | #define _CRT_SECURE_NO_WARNINGS 3 | #endif 4 | 5 | #define CGLTF_IMPLEMENTATION 6 | #include "../extern/cgltf.h" 7 | 8 | #define FAST_OBJ_IMPLEMENTATION 9 | #include "../extern/fast_obj.h" 10 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | const MeshoptEncoder = require('./meshopt_encoder.js'); 2 | const MeshoptDecoder = require('./meshopt_decoder.js'); 3 | const MeshoptSimplifier = require('./meshopt_simplifier.js'); 4 | 5 | module.exports = {MeshoptEncoder, MeshoptDecoder, MeshoptSimplifier}; 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report if you believe you've found a bug in this project; please use GitHub Discussions instead if you think the bug may be in your code. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Standard: Cpp03 2 | UseTab: ForIndentation 3 | TabWidth: 4 4 | IndentWidth: 4 5 | AccessModifierOffset: -4 6 | BreakBeforeBraces: Allman 7 | IndentCaseLabels: false 8 | ColumnLimit: 0 9 | PointerAlignment: Left 10 | BreakConstructorInitializersBeforeComma: true 11 | NamespaceIndentation: None 12 | AlignEscapedNewlines: DontAlign 13 | AlignAfterOpenBracket: DontAlign 14 | -------------------------------------------------------------------------------- /src/allocator.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*)) 5 | { 6 | meshopt_Allocator::Storage::allocate = allocate; 7 | meshopt_Allocator::Storage::deallocate = deallocate; 8 | } 9 | -------------------------------------------------------------------------------- /tools/bitmask.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from z3 import * 4 | 5 | def same8(v): 6 | return Or(v == 0, v == 0xff) 7 | 8 | magic = BitVec('magic', 64) 9 | 10 | x = BitVec('x', 64) 11 | y = x * magic 12 | 13 | s = Solver() 14 | solve_using(s, ForAll([x], 15 | Or( 16 | Not(And([same8((x >> (i * 8)) & 0xff) for i in range(8)])), # x has bytes that aren't equal to 0xff or 0x00 17 | And([(x >> (i * 8 + 7)) & 1 == (y >> (56 + i)) & 1 for i in range(8)]) # every byte of x has bits that are equal to a corresponding top bit of y 18 | ))) 19 | 20 | print("magic =", hex(s.model().eval(magic).as_long())) 21 | -------------------------------------------------------------------------------- /gltf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gltfpack", 3 | "version": "0.18.0", 4 | "description": "A command-line tool that can optimize glTF files for size and speed", 5 | "author": "Arseny Kapoulkine", 6 | "license": "MIT", 7 | "bugs": "https://github.com/zeux/meshoptimizer/issues", 8 | "homepage": "https://github.com/zeux/meshoptimizer", 9 | "keywords": [ 10 | "gltf" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/zeux/meshoptimizer" 15 | }, 16 | "bin": "./cli.js", 17 | "main": "./library.js", 18 | "files": [ 19 | "*.js", "*.wasm" 20 | ], 21 | "scripts": { 22 | "prepublishOnly": "node cli.js -v" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gltf/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // This file is part of gltfpack and is distributed under the terms of MIT License. 3 | var gltfpack = require('./library.js'); 4 | 5 | var fs = require('fs'); 6 | 7 | var args = process.argv.slice(2); 8 | 9 | var interface = { 10 | read: function (path) { 11 | return fs.readFileSync(path); 12 | }, 13 | write: function (path, data) { 14 | fs.writeFileSync(path, data); 15 | }, 16 | }; 17 | 18 | gltfpack.pack(args, interface) 19 | .then(function (log) { 20 | process.stdout.write(log); 21 | process.exit(0); 22 | }) 23 | .catch(function (err) { 24 | process.stderr.write(err.message); 25 | process.exit(1); 26 | }); 27 | -------------------------------------------------------------------------------- /js/meshopt_simplifier.module.d.ts: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library and is distributed under the terms of MIT License. 2 | // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) 3 | export type Flags = "LockBorder"; 4 | 5 | export const MeshoptSimplifier: { 6 | supported: boolean; 7 | ready: Promise; 8 | 9 | compactMesh: (indices: Uint32Array) => [Uint32Array, number]; 10 | 11 | simplify: (indices: Uint32Array, vertex_positions: Float32Array, vertex_positions_stride: number, target_index_count: number, target_error: number, flags?: Flags[]) => [Uint32Array, number]; 12 | 13 | getScale: (vertex_positions: Float32Array, vertex_positions_stride: number) => number; 14 | }; 15 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meshoptimizer", 3 | "version": "0.18.1", 4 | "description": "Mesh optimizaiton library that makes meshes smaller and faster to render", 5 | "author": "Arseny Kapoulkine", 6 | "license": "MIT", 7 | "bugs": "https://github.com/zeux/meshoptimizer/issues", 8 | "homepage": "https://github.com/zeux/meshoptimizer", 9 | "keywords": [ 10 | "compression", 11 | "mesh" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/zeux/meshoptimizer" 16 | }, 17 | "files": [ 18 | "*.js", "*.ts" 19 | ], 20 | "main": "index.js", 21 | "module": "index.module.js", 22 | "types": "index.module.d.ts", 23 | "scripts": { 24 | "test": "node meshopt_encoder.test.js && node meshopt_decoder.test.js && node meshopt_simplifier.test.js", 25 | "prepublishOnly": "npm test" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /js/meshopt_decoder.module.d.ts: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library and is distributed under the terms of MIT License. 2 | // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) 3 | export const MeshoptDecoder: { 4 | supported: boolean; 5 | ready: Promise; 6 | 7 | decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void; 8 | decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void; 9 | decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void; 10 | 11 | decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void; 12 | 13 | useWorkers: (count: number) => void; 14 | decodeGltfBufferAsync: (count: number, size: number, source: Uint8Array, mode: string, filter?: string) => Promise; 15 | }; 16 | -------------------------------------------------------------------------------- /tools/wasmpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | # regenerate with wasmpack.py generate 6 | table = [32, 0, 65, 2, 1, 106, 34, 33, 3, 128, 11, 4, 13, 64, 6, 253, 10, 7, 15, 116, 127, 5, 8, 12, 40, 16, 19, 54, 20, 9, 27, 255, 113, 17, 42, 67, 24, 23, 146, 148, 18, 14, 22, 45, 70, 69, 56, 114, 101, 21, 25, 63, 75, 136, 108, 28, 118, 29, 73, 115] 7 | 8 | palette = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:;"; 9 | 10 | def encode(buffer): 11 | result = '' 12 | 13 | for ch in buffer.read(): 14 | if ch in table: 15 | index = table.index(ch) 16 | result += palette[index] 17 | else: 18 | result += palette[60 + ch // 64] 19 | result += palette[ch % 64] 20 | 21 | return result 22 | 23 | def stats(buffer): 24 | hist = [0] * 256 25 | for ch in buffer.read(): 26 | hist[ch] += 1 27 | 28 | result = [i for i in range(256)] 29 | result.sort(key=lambda i: hist[i], reverse=True) 30 | 31 | return result 32 | 33 | if sys.argv[-1] == 'generate': 34 | print(stats(sys.stdin.buffer)[:60]) 35 | else: 36 | print(encode(sys.stdin.buffer)) 37 | -------------------------------------------------------------------------------- /js/meshopt_encoder.module.d.ts: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library and is distributed under the terms of MIT License. 2 | // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) 3 | export const MeshoptEncoder: { 4 | supported: boolean; 5 | ready: Promise; 6 | 7 | reorderMesh: (indices: Uint32Array, triangles: boolean, optsize: boolean) => [Uint32Array, number]; 8 | 9 | encodeVertexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array; 10 | encodeIndexBuffer: (source: Uint8Array, count: number, size: number) => Uint8Array; 11 | encodeIndexSequence: (source: Uint8Array, count: number, size: number) => Uint8Array; 12 | 13 | encodeGltfBuffer: (source: Uint8Array, count: number, size: number, mode: string) => Uint8Array; 14 | 15 | encodeFilterOct: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array; 16 | encodeFilterQuat: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array; 17 | encodeFilterExp: (source: Float32Array, count: number, stride: number, bits: number) => Uint8Array; 18 | }; 19 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2022 Arseny Kapoulkine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /gltf/json.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include 5 | 6 | void comma(std::string& s) 7 | { 8 | char ch = s.empty() ? 0 : s[s.size() - 1]; 9 | 10 | if (ch != 0 && ch != '[' && ch != '{') 11 | s += ","; 12 | } 13 | 14 | void append(std::string& s, size_t v) 15 | { 16 | char buf[32]; 17 | snprintf(buf, sizeof(buf), "%zu", v); 18 | s += buf; 19 | } 20 | 21 | void append(std::string& s, float v) 22 | { 23 | char buf[64]; 24 | snprintf(buf, sizeof(buf), "%.9g", v); 25 | s += buf; 26 | } 27 | 28 | void append(std::string& s, const char* v) 29 | { 30 | s += v; 31 | } 32 | 33 | void append(std::string& s, const std::string& v) 34 | { 35 | s += v; 36 | } 37 | 38 | void appendJson(std::string& s, const char* data) 39 | { 40 | enum State 41 | { 42 | None, 43 | Escape, 44 | Quoted 45 | } state = None; 46 | 47 | for (const char* it = data; *it; ++it) 48 | { 49 | char ch = *it; 50 | 51 | // whitespace outside of quoted strings can be ignored 52 | if (state != None || !isspace(ch)) 53 | s += ch; 54 | 55 | // the finite automata tracks whether we're inside a quoted string 56 | switch (state) 57 | { 58 | case None: 59 | state = (ch == '"') ? Quoted : None; 60 | break; 61 | 62 | case Quoted: 63 | state = (ch == '"') ? None : (ch == '\\') ? Escape : Quoted; 64 | break; 65 | 66 | case Escape: 67 | state = Quoted; 68 | break; 69 | 70 | default: 71 | assert(!"Unexpected parsing state"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tools/codecfuzz.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/meshoptimizer.h" 2 | 3 | #include 4 | #include 5 | 6 | void fuzzDecoder(const uint8_t* data, size_t size, size_t stride, int (*decode)(void*, size_t, size_t, const unsigned char*, size_t)) 7 | { 8 | size_t count = 66; // must be divisible by 3 for decodeIndexBuffer; should be >=64 to cover large vertex blocks 9 | 10 | void* destination = malloc(count * stride); 11 | assert(destination); 12 | 13 | int rc = decode(destination, count, stride, reinterpret_cast(data), size); 14 | (void)rc; 15 | 16 | free(destination); 17 | } 18 | 19 | namespace meshopt 20 | { 21 | extern unsigned int cpuid; 22 | } 23 | 24 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) 25 | { 26 | // decodeIndexBuffer supports 2 and 4-byte indices 27 | fuzzDecoder(data, size, 2, meshopt_decodeIndexBuffer); 28 | fuzzDecoder(data, size, 4, meshopt_decodeIndexBuffer); 29 | 30 | // decodeIndexSequence supports 2 and 4-byte indices 31 | fuzzDecoder(data, size, 2, meshopt_decodeIndexSequence); 32 | fuzzDecoder(data, size, 4, meshopt_decodeIndexSequence); 33 | 34 | // decodeVertexBuffer supports any strides divisible by 4 in 4-256 interval 35 | // It's a waste of time to check all of them, so we'll just check a few with different alignment mod 16 36 | fuzzDecoder(data, size, 4, meshopt_decodeVertexBuffer); 37 | fuzzDecoder(data, size, 16, meshopt_decodeVertexBuffer); 38 | fuzzDecoder(data, size, 24, meshopt_decodeVertexBuffer); 39 | fuzzDecoder(data, size, 32, meshopt_decodeVertexBuffer); 40 | 41 | return 0; 42 | } 43 | -------------------------------------------------------------------------------- /gltf/wasistubs.cpp: -------------------------------------------------------------------------------- 1 | #ifdef __wasi__ 2 | #include 3 | #include 4 | 5 | #include 6 | 7 | extern "C" void __cxa_throw(void* ptr, void* type, void* destructor) 8 | { 9 | abort(); 10 | } 11 | 12 | extern "C" void* __cxa_allocate_exception(size_t thrown_size) 13 | { 14 | abort(); 15 | } 16 | 17 | extern "C" int32_t __wasi_path_open32(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int32_t arg5, int32_t arg6, int32_t arg7, int32_t arg8) 18 | __attribute__(( 19 | __import_module__("wasi_snapshot_preview1"), 20 | __import_name__("path_open32"), 21 | __warn_unused_result__)); 22 | 23 | extern "C" int32_t __imported_wasi_snapshot_preview1_path_open(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3, int32_t arg4, int64_t arg5, int64_t arg6, int32_t arg7, int32_t arg8) 24 | { 25 | return __wasi_path_open32(arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); 26 | } 27 | 28 | extern "C" int32_t __wasi_fd_seek32(int32_t arg0, int32_t arg1, int32_t arg2, int32_t arg3) 29 | __attribute__(( 30 | __import_module__("wasi_snapshot_preview1"), 31 | __import_name__("fd_seek32"), 32 | __warn_unused_result__)); 33 | 34 | extern "C" int32_t __imported_wasi_snapshot_preview1_fd_seek(int32_t arg0, int64_t arg1, int32_t arg2, int32_t arg3) 35 | { 36 | *(uint64_t*)arg3 = 0; 37 | return __wasi_fd_seek32(arg0, arg1, arg2, arg3); 38 | } 39 | 40 | extern "C" int32_t __imported_wasi_snapshot_preview1_clock_time_get(int32_t arg0, int64_t arg1, int32_t arg2) 41 | { 42 | return __WASI_ERRNO_NOSYS; 43 | } 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/vfetchanalyzer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | 7 | meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const unsigned int* indices, size_t index_count, size_t vertex_count, size_t vertex_size) 8 | { 9 | assert(index_count % 3 == 0); 10 | assert(vertex_size > 0 && vertex_size <= 256); 11 | 12 | meshopt_Allocator allocator; 13 | 14 | meshopt_VertexFetchStatistics result = {}; 15 | 16 | unsigned char* vertex_visited = allocator.allocate(vertex_count); 17 | memset(vertex_visited, 0, vertex_count); 18 | 19 | const size_t kCacheLine = 64; 20 | const size_t kCacheSize = 128 * 1024; 21 | 22 | // simple direct mapped cache; on typical mesh data this is close to 4-way cache, and this model is a gross approximation anyway 23 | size_t cache[kCacheSize / kCacheLine] = {}; 24 | 25 | for (size_t i = 0; i < index_count; ++i) 26 | { 27 | unsigned int index = indices[i]; 28 | assert(index < vertex_count); 29 | 30 | vertex_visited[index] = 1; 31 | 32 | size_t start_address = index * vertex_size; 33 | size_t end_address = start_address + vertex_size; 34 | 35 | size_t start_tag = start_address / kCacheLine; 36 | size_t end_tag = (end_address + kCacheLine - 1) / kCacheLine; 37 | 38 | assert(start_tag < end_tag); 39 | 40 | for (size_t tag = start_tag; tag < end_tag; ++tag) 41 | { 42 | size_t line = tag % (sizeof(cache) / sizeof(cache[0])); 43 | 44 | // we store +1 since cache is filled with 0 by default 45 | result.bytes_fetched += (cache[line] != tag + 1) * kCacheLine; 46 | cache[line] = tag + 1; 47 | } 48 | } 49 | 50 | size_t unique_vertex_count = 0; 51 | 52 | for (size_t i = 0; i < vertex_count; ++i) 53 | unique_vertex_count += vertex_visited[i]; 54 | 55 | result.overfetch = unique_vertex_count == 0 ? 0 : float(result.bytes_fetched) / float(unique_vertex_count * vertex_size); 56 | 57 | return result; 58 | } 59 | -------------------------------------------------------------------------------- /src/vfetchoptimizer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | 7 | size_t meshopt_optimizeVertexFetchRemap(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count) 8 | { 9 | assert(index_count % 3 == 0); 10 | 11 | memset(destination, -1, vertex_count * sizeof(unsigned int)); 12 | 13 | unsigned int next_vertex = 0; 14 | 15 | for (size_t i = 0; i < index_count; ++i) 16 | { 17 | unsigned int index = indices[i]; 18 | assert(index < vertex_count); 19 | 20 | if (destination[index] == ~0u) 21 | { 22 | destination[index] = next_vertex++; 23 | } 24 | } 25 | 26 | assert(next_vertex <= vertex_count); 27 | 28 | return next_vertex; 29 | } 30 | 31 | size_t meshopt_optimizeVertexFetch(void* destination, unsigned int* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size) 32 | { 33 | assert(index_count % 3 == 0); 34 | assert(vertex_size > 0 && vertex_size <= 256); 35 | 36 | meshopt_Allocator allocator; 37 | 38 | // support in-place optimization 39 | if (destination == vertices) 40 | { 41 | unsigned char* vertices_copy = allocator.allocate(vertex_count * vertex_size); 42 | memcpy(vertices_copy, vertices, vertex_count * vertex_size); 43 | vertices = vertices_copy; 44 | } 45 | 46 | // build vertex remap table 47 | unsigned int* vertex_remap = allocator.allocate(vertex_count); 48 | memset(vertex_remap, -1, vertex_count * sizeof(unsigned int)); 49 | 50 | unsigned int next_vertex = 0; 51 | 52 | for (size_t i = 0; i < index_count; ++i) 53 | { 54 | unsigned int index = indices[i]; 55 | assert(index < vertex_count); 56 | 57 | unsigned int& remap = vertex_remap[index]; 58 | 59 | if (remap == ~0u) // vertex was not added to destination VB 60 | { 61 | // add vertex 62 | memcpy(static_cast(destination) + next_vertex * vertex_size, static_cast(vertices) + index * vertex_size, vertex_size); 63 | 64 | remap = next_vertex++; 65 | } 66 | 67 | // modify indices in place 68 | indices[i] = remap; 69 | } 70 | 71 | assert(next_vertex <= vertex_count); 72 | 73 | return next_vertex; 74 | } 75 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | meshoptimizer - demo 7 | 8 | 9 | 10 | 11 | 26 | 27 | 28 | 29 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /gltf/basislib.cpp: -------------------------------------------------------------------------------- 1 | #ifdef WITH_BASISU 2 | 3 | #ifdef __clang__ 4 | #pragma GCC diagnostic ignored "-Wunknown-warning-option" 5 | #pragma GCC diagnostic ignored "-Wuninitialized-const-reference" 6 | #endif 7 | 8 | #ifdef __GNUC__ 9 | #pragma GCC diagnostic ignored "-Wclass-memaccess" 10 | #pragma GCC diagnostic ignored "-Wdeprecated-copy" 11 | #pragma GCC diagnostic ignored "-Wextra" 12 | #pragma GCC diagnostic ignored "-Wimplicit-fallthrough" 13 | #pragma GCC diagnostic ignored "-Wmisleading-indentation" 14 | #pragma GCC diagnostic ignored "-Wparentheses" 15 | #pragma GCC diagnostic ignored "-Wshadow" 16 | #pragma GCC diagnostic ignored "-Wsign-compare" 17 | #pragma GCC diagnostic ignored "-Wunused-value" 18 | #pragma GCC diagnostic ignored "-Wunused-variable" 19 | #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" 20 | #pragma GCC diagnostic ignored "-Wstrict-aliasing" // TODO: https://github.com/BinomialLLC/basis_universal/pull/275 21 | #pragma GCC diagnostic ignored "-Wstringop-overflow" 22 | #pragma GCC diagnostic ignored "-Wunused-but-set-variable" 23 | #endif 24 | 25 | #ifdef _MSC_VER 26 | #pragma warning(disable : 4702) // unreachable code 27 | #pragma warning(disable : 4005) // macro redefinition 28 | #endif 29 | 30 | #define BASISU_NO_ITERATOR_DEBUG_LEVEL 31 | 32 | #if defined(_MSC_VER) && !defined(__clang__) && (defined(_M_IX86) || defined(_M_X64)) 33 | #define BASISU_SUPPORT_SSE 1 34 | #endif 35 | 36 | #if defined(__SSE4_1__) 37 | #define BASISU_SUPPORT_SSE 1 38 | #endif 39 | 40 | #ifdef _WIN32 41 | #define WIN32_LEAN_AND_MEAN 42 | #define NOMINMAX 43 | #endif 44 | 45 | #include "encoder/basisu_backend.cpp" 46 | #include "encoder/basisu_basis_file.cpp" 47 | #include "encoder/basisu_bc7enc.cpp" 48 | #include "encoder/basisu_comp.cpp" 49 | #include "encoder/basisu_enc.cpp" 50 | #include "encoder/basisu_etc.cpp" 51 | #include "encoder/basisu_frontend.cpp" 52 | #include "encoder/basisu_gpu_texture.cpp" 53 | #include "encoder/basisu_kernels_sse.cpp" 54 | #include "encoder/basisu_opencl.cpp" 55 | #include "encoder/basisu_pvrtc1_4.cpp" 56 | #include "encoder/basisu_resample_filters.cpp" 57 | #include "encoder/basisu_resampler.cpp" 58 | #include "encoder/basisu_ssim.cpp" 59 | #include "encoder/basisu_uastc_enc.cpp" 60 | #include "encoder/jpgd.cpp" 61 | #include "encoder/pvpngreader.cpp" 62 | #include "transcoder/basisu_transcoder.cpp" 63 | 64 | #undef CLAMP 65 | #include "zstd/zstd.c" 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /tools/wasmstubs.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | extern unsigned char __heap_base; 6 | static intptr_t sbrkp = intptr_t(&__heap_base); 7 | 8 | static const int WASM_PAGE_SIZE = 64 * 1024; 9 | 10 | extern "C" void* sbrk(intptr_t increment) 11 | { 12 | intptr_t sbrko = sbrkp; 13 | 14 | increment = (increment + 3) & ~3; 15 | sbrkp += increment; 16 | 17 | size_t heap_size = __builtin_wasm_memory_size(0) * WASM_PAGE_SIZE; 18 | 19 | if (sbrkp > heap_size) 20 | { 21 | size_t diff = (sbrkp - heap_size + WASM_PAGE_SIZE - 1) / WASM_PAGE_SIZE; 22 | 23 | if (__builtin_wasm_memory_grow(0, diff) == size_t(-1)) 24 | return (void*)-1; 25 | } 26 | 27 | return (void*)sbrko; 28 | } 29 | 30 | extern "C" void* memcpy(void* destination, const void* source, size_t num) 31 | { 32 | char* d = (char*)destination; 33 | const char* s = (const char*)source; 34 | 35 | if (((uintptr_t(d) | uintptr_t(s)) & 3) == 0) 36 | { 37 | while (num > 15) 38 | { 39 | ((uint32_t*)d)[0] = ((uint32_t*)s)[0]; 40 | ((uint32_t*)d)[1] = ((uint32_t*)s)[1]; 41 | ((uint32_t*)d)[2] = ((uint32_t*)s)[2]; 42 | ((uint32_t*)d)[3] = ((uint32_t*)s)[3]; 43 | d += 16; 44 | s += 16; 45 | num -= 16; 46 | } 47 | 48 | while (num > 3) 49 | { 50 | ((uint32_t*)d)[0] = ((uint32_t*)s)[0]; 51 | d += 4; 52 | s += 4; 53 | num -= 4; 54 | } 55 | } 56 | 57 | while (num > 0) 58 | { 59 | *d++ = *s++; 60 | num--; 61 | } 62 | 63 | return destination; 64 | } 65 | 66 | extern "C" void* memset(void* ptr, int value, size_t num) 67 | { 68 | uint32_t v32 = ~0u / 255 * uint8_t(value); 69 | 70 | char* d = (char*)ptr; 71 | 72 | if ((uintptr_t(d) & 3) == 0) 73 | { 74 | while (num > 15) 75 | { 76 | ((uint32_t*)d)[0] = v32; 77 | ((uint32_t*)d)[1] = v32; 78 | ((uint32_t*)d)[2] = v32; 79 | ((uint32_t*)d)[3] = v32; 80 | d += 16; 81 | num -= 16; 82 | } 83 | 84 | while (num > 3) 85 | { 86 | ((uint32_t*)d)[0] = v32; 87 | d += 4; 88 | num -= 4; 89 | } 90 | } 91 | 92 | while (num > 0) 93 | { 94 | *d++ = char(value); 95 | num--; 96 | } 97 | 98 | return ptr; 99 | } 100 | 101 | void* operator new(size_t size) 102 | { 103 | return sbrk((size + 7) & ~7); 104 | } 105 | 106 | void operator delete(void* ptr) throw() 107 | { 108 | void* brk = sbrk(0); 109 | assert(ptr <= brk); 110 | 111 | sbrk((char*)ptr - (char*)brk); 112 | } 113 | -------------------------------------------------------------------------------- /src/vcacheanalyzer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | 7 | meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int primgroup_size) 8 | { 9 | assert(index_count % 3 == 0); 10 | assert(cache_size >= 3); 11 | assert(warp_size == 0 || warp_size >= 3); 12 | 13 | meshopt_Allocator allocator; 14 | 15 | meshopt_VertexCacheStatistics result = {}; 16 | 17 | unsigned int warp_offset = 0; 18 | unsigned int primgroup_offset = 0; 19 | 20 | unsigned int* cache_timestamps = allocator.allocate(vertex_count); 21 | memset(cache_timestamps, 0, vertex_count * sizeof(unsigned int)); 22 | 23 | unsigned int timestamp = cache_size + 1; 24 | 25 | for (size_t i = 0; i < index_count; i += 3) 26 | { 27 | unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; 28 | assert(a < vertex_count && b < vertex_count && c < vertex_count); 29 | 30 | bool ac = (timestamp - cache_timestamps[a]) > cache_size; 31 | bool bc = (timestamp - cache_timestamps[b]) > cache_size; 32 | bool cc = (timestamp - cache_timestamps[c]) > cache_size; 33 | 34 | // flush cache if triangle doesn't fit into warp or into the primitive buffer 35 | if ((primgroup_size && primgroup_offset == primgroup_size) || (warp_size && warp_offset + ac + bc + cc > warp_size)) 36 | { 37 | result.warps_executed += warp_offset > 0; 38 | 39 | warp_offset = 0; 40 | primgroup_offset = 0; 41 | 42 | // reset cache 43 | timestamp += cache_size + 1; 44 | } 45 | 46 | // update cache and add vertices to warp 47 | for (int j = 0; j < 3; ++j) 48 | { 49 | unsigned int index = indices[i + j]; 50 | 51 | if (timestamp - cache_timestamps[index] > cache_size) 52 | { 53 | cache_timestamps[index] = timestamp++; 54 | result.vertices_transformed++; 55 | warp_offset++; 56 | } 57 | } 58 | 59 | primgroup_offset++; 60 | } 61 | 62 | size_t unique_vertex_count = 0; 63 | 64 | for (size_t i = 0; i < vertex_count; ++i) 65 | unique_vertex_count += cache_timestamps[i] > 0; 66 | 67 | result.warps_executed += warp_offset > 0; 68 | 69 | result.acmr = index_count == 0 ? 0 : float(result.vertices_transformed) / float(index_count / 3); 70 | result.atvr = unique_vertex_count == 0 ? 0 : float(result.vertices_transformed) / float(unique_vertex_count); 71 | 72 | return result; 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths-ignore: 8 | - '*.md' 9 | 10 | jobs: 11 | gltfpack: 12 | strategy: 13 | matrix: 14 | os: [windows, ubuntu, macos] 15 | name: gltfpack-${{matrix.os}} 16 | runs-on: ${{matrix.os}}-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - uses: actions/checkout@v2 20 | with: 21 | repository: zeux/basis_universal 22 | ref: gltfpack 23 | path: basis_universal 24 | - name: cmake configure 25 | run: cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_BASISU_PATH=basis_universal -DMESHOPT_WERROR=ON -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded" -DCMAKE_BUILD_TYPE=Release 26 | - name: cmake build 27 | run: cmake --build . --target gltfpack --config Release -j 2 28 | - uses: actions/upload-artifact@v1 29 | with: 30 | name: gltfpack-windows 31 | path: Release/gltfpack.exe 32 | if: matrix.os == 'windows' 33 | - uses: actions/upload-artifact@v1 34 | with: 35 | name: gltfpack-${{matrix.os}} 36 | path: gltfpack 37 | if: matrix.os != 'windows' 38 | 39 | nodejs: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v1 43 | - uses: actions/setup-node@v1 44 | with: 45 | node-version: '14.x' 46 | - name: install wasi 47 | run: | 48 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-linux.tar.gz | tar xz 49 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/libclang_rt.builtins-wasm32-wasi-$VERSION.0.tar.gz | tar xz -C wasi-sdk-$VERSION.0 50 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sysroot-$VERSION.0.tar.gz | tar xz -C wasi-sdk-$VERSION.0/share 51 | mv wasi-sdk-$VERSION.0 wasi-sdk 52 | env: 53 | VERSION: 16 54 | - name: build 55 | run: | 56 | make -j2 -B WASI_SDK=wasi-sdk gltf/library.wasm js 57 | git status 58 | - name: npm pack 59 | run: | 60 | cp LICENSE.md gltf/ 61 | cp LICENSE.md js/ 62 | cd gltf && npm pack && cd .. 63 | cd js && npm pack && cd .. 64 | - uses: actions/upload-artifact@v2 65 | with: 66 | name: gltfpack-npm 67 | path: gltf/gltfpack-*.tgz 68 | - uses: actions/upload-artifact@v2 69 | with: 70 | name: meshoptimizer-npm 71 | path: js/meshoptimizer-*.tgz 72 | -------------------------------------------------------------------------------- /js/benchmark.js: -------------------------------------------------------------------------------- 1 | var encoder = require('./meshopt_encoder.js'); 2 | var decoder = require('./meshopt_decoder.js'); 3 | var { performance } = require('perf_hooks'); 4 | 5 | process.on('unhandledRejection', error => { 6 | console.log('unhandledRejection', error); 7 | process.exit(1); 8 | }); 9 | 10 | function bytes(view) { 11 | return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); 12 | } 13 | 14 | var tests = { 15 | roundtripVertexBuffer: function() { 16 | var N = 1024*1024; 17 | var data = new Uint8Array(N * 16); 18 | 19 | for (var i = 0; i < N * 16; i += 4) 20 | { 21 | data[i + 0] = 0; 22 | data[i + 1] = (i % 16) * 1; 23 | data[i + 2] = (i % 16) * 2; 24 | data[i + 3] = (i % 16) * 8; 25 | } 26 | 27 | var decoded = new Uint8Array(N * 16); 28 | 29 | var t0 = performance.now(); 30 | var encoded = encoder.encodeVertexBuffer(data, N, 16); 31 | var t1 = performance.now(); 32 | decoder.decodeVertexBuffer(decoded, N, 16, encoded); 33 | var t2 = performance.now(); 34 | 35 | return { encodeVertex: t1 - t0, decodeVertex: t2 - t1, bytes: N * 16 }; 36 | }, 37 | 38 | roundtripIndexBuffer: function() { 39 | var N = 1024*1024; 40 | var data = new Uint32Array(N * 3); 41 | 42 | for (var i = 0; i < N * 3; i += 6) 43 | { 44 | var v = i / 6; 45 | 46 | data[i + 0] = v; 47 | data[i + 1] = v + 1; 48 | data[i + 2] = v + 2; 49 | 50 | data[i + 3] = v + 2; 51 | data[i + 4] = v + 1; 52 | data[i + 5] = v + 3; 53 | } 54 | 55 | var decoded = new Uint32Array(data.length); 56 | 57 | var t0 = performance.now(); 58 | var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 4); 59 | var t1 = performance.now(); 60 | decoder.decodeIndexBuffer(bytes(decoded), data.length, 4, encoded); 61 | var t2 = performance.now(); 62 | 63 | return { encodeIndex: t1 - t0, decodeIndex: t2 - t1, bytes: N * 12 }; 64 | }, 65 | }; 66 | 67 | Promise.all([encoder.ready, decoder.ready]).then(() => { 68 | var reps = 10; 69 | var data = {} 70 | 71 | for (var key in tests) { 72 | data[key] = tests[key](); 73 | } 74 | 75 | for (var i = 1; i < reps; ++i) { 76 | for (var key in tests) { 77 | var nd = tests[key](); 78 | var od = data[key]; 79 | 80 | for (var idx in nd) { 81 | od[idx] = Math.min(od[idx], nd[idx]); 82 | } 83 | } 84 | } 85 | 86 | for (var key in tests) { 87 | var rep = key; 88 | rep += ":\n"; 89 | 90 | for (var idx in data[key]) { 91 | if (idx != "bytes") { 92 | rep += idx; 93 | rep += " "; 94 | rep += data[key][idx]; 95 | rep += " ms ("; 96 | rep += data[key].bytes / 1024 / 1024 / 1024 / data[key][idx] * 1000; 97 | rep += " GB/s)"; 98 | rep += "\n"; 99 | } 100 | } 101 | 102 | console.log(rep); 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /js/meshopt_simplifier.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert').strict; 2 | var simplifier = require('./meshopt_simplifier.js'); 3 | 4 | process.on('unhandledRejection', error => { 5 | console.log('unhandledRejection', error); 6 | process.exit(1); 7 | }); 8 | 9 | var tests = { 10 | compactMesh: function() { 11 | var indices = new Uint32Array([ 12 | 0, 1, 3, 13 | 3, 1, 5, 14 | ]); 15 | 16 | var expected = new Uint32Array([ 17 | 0, 1, 2, 18 | 2, 1, 3, 19 | ]); 20 | 21 | var missing = 2**32-1; 22 | 23 | var remap = new Uint32Array([ 24 | 0, 1, missing, 2, missing, 3 25 | ]); 26 | 27 | var res = simplifier.compactMesh(indices); 28 | assert.deepEqual(indices, expected); 29 | assert.deepEqual(res[0], remap); 30 | assert.equal(res[1], 4); // unique 31 | }, 32 | 33 | simplify: function() { 34 | // 0 35 | // 1 2 36 | // 3 4 5 37 | var indices = new Uint32Array([ 38 | 0, 2, 1, 39 | 1, 2, 3, 40 | 3, 2, 4, 41 | 2, 5, 4, 42 | ]); 43 | 44 | var positions = new Float32Array([ 45 | 0, 2, 0, 46 | 0, 1, 0, 47 | 1, 1, 0, 48 | 0, 0, 0, 49 | 1, 0, 0, 50 | 2, 0, 0, 51 | ]); 52 | 53 | var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01); 54 | 55 | var expected = new Uint32Array([ 56 | 3, 0, 5, 57 | ]); 58 | 59 | assert.deepEqual(res[0], expected); 60 | assert.equal(res[1], 0); // error 61 | }, 62 | 63 | simplify16: function() { 64 | // 0 65 | // 1 2 66 | // 3 4 5 67 | var indices = new Uint16Array([ 68 | 0, 2, 1, 69 | 1, 2, 3, 70 | 3, 2, 4, 71 | 2, 5, 4, 72 | ]); 73 | 74 | var positions = new Float32Array([ 75 | 0, 2, 0, 76 | 0, 1, 0, 77 | 1, 1, 0, 78 | 0, 0, 0, 79 | 1, 0, 0, 80 | 2, 0, 0, 81 | ]); 82 | 83 | var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01); 84 | 85 | var expected = new Uint16Array([ 86 | 3, 0, 5, 87 | ]); 88 | 89 | assert.deepEqual(res[0], expected); 90 | assert.equal(res[1], 0); // error 91 | }, 92 | 93 | simplifyLockBorder: function() { 94 | // 0 95 | // 1 2 96 | // 3 4 5 97 | var indices = new Uint32Array([ 98 | 0, 2, 1, 99 | 1, 2, 3, 100 | 3, 2, 4, 101 | 2, 5, 4, 102 | ]); 103 | 104 | var positions = new Float32Array([ 105 | 0, 2, 0, 106 | 0, 1, 0, 107 | 1, 1, 0, 108 | 0, 0, 0, 109 | 1, 0, 0, 110 | 2, 0, 0, 111 | ]); 112 | 113 | var res = simplifier.simplify(indices, positions, 3, /* target indices */ 3, /* target error */ 0.01, ["LockBorder"]); 114 | 115 | var expected = new Uint32Array([ 116 | 0, 2, 1, 117 | 1, 2, 3, 118 | 3, 2, 4, 119 | 2, 5, 4, 120 | ]); 121 | 122 | assert.deepEqual(res[0], expected); 123 | assert.equal(res[1], 0); // error 124 | }, 125 | 126 | getScale: function() { 127 | var positions = new Float32Array([ 128 | 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3 129 | ]); 130 | 131 | assert(simplifier.getScale(positions, 3) == 3.0); 132 | }, 133 | }; 134 | 135 | Promise.all([simplifier.ready]).then(() => { 136 | var count = 0; 137 | 138 | for (var key in tests) { 139 | tests[key](); 140 | count++; 141 | } 142 | 143 | console.log(count, 'tests passed'); 144 | }); 145 | -------------------------------------------------------------------------------- /gltf/fileio.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef _WIN32 9 | #include 10 | #else 11 | #include 12 | #endif 13 | 14 | TempFile::TempFile() 15 | : fd(-1) 16 | { 17 | } 18 | 19 | TempFile::TempFile(const char* suffix) 20 | : fd(-1) 21 | { 22 | create(suffix); 23 | } 24 | 25 | TempFile::~TempFile() 26 | { 27 | if (!path.empty()) 28 | remove(path.c_str()); 29 | 30 | #ifndef _WIN32 31 | if (fd >= 0) 32 | close(fd); 33 | #endif 34 | } 35 | 36 | void TempFile::create(const char* suffix) 37 | { 38 | assert(fd < 0 && path.empty()); 39 | 40 | #if defined(_WIN32) 41 | const char* temp_dir = getenv("TEMP"); 42 | path = temp_dir ? temp_dir : "."; 43 | path += "\\gltfpack-XXXXXX"; 44 | (void)_mktemp(&path[0]); 45 | path += suffix; 46 | #elif defined(__wasi__) 47 | static int id = 0; 48 | char ids[16]; 49 | snprintf(ids, sizeof(ids), "%d", id++); 50 | 51 | path = "gltfpack-temp-"; 52 | path += ids; 53 | path += suffix; 54 | #else 55 | path = "/tmp/gltfpack-XXXXXX"; 56 | path += suffix; 57 | fd = mkstemps(&path[0], strlen(suffix)); 58 | #endif 59 | } 60 | 61 | std::string getFullPath(const char* path, const char* base_path) 62 | { 63 | std::string result = base_path; 64 | 65 | std::string::size_type slash = result.find_last_of("/\\"); 66 | result.erase(slash == std::string::npos ? 0 : slash + 1); 67 | 68 | result += path; 69 | 70 | return result; 71 | } 72 | 73 | std::string getFileName(const char* path) 74 | { 75 | std::string result = path; 76 | 77 | std::string::size_type slash = result.find_last_of("/\\"); 78 | if (slash != std::string::npos) 79 | result.erase(0, slash + 1); 80 | 81 | std::string::size_type dot = result.find_last_of('.'); 82 | if (dot != std::string::npos) 83 | result.erase(dot); 84 | 85 | return result; 86 | } 87 | 88 | std::string getExtension(const char* path) 89 | { 90 | std::string result = path; 91 | 92 | std::string::size_type slash = result.find_last_of("/\\"); 93 | std::string::size_type dot = result.find_last_of('.'); 94 | 95 | if (slash != std::string::npos && dot != std::string::npos && dot < slash) 96 | dot = std::string::npos; 97 | 98 | result.erase(0, dot); 99 | 100 | for (size_t i = 0; i < result.length(); ++i) 101 | if (unsigned(result[i] - 'A') < 26) 102 | result[i] = (result[i] - 'A') + 'a'; 103 | 104 | return result; 105 | } 106 | 107 | bool readFile(const char* path, std::string& data) 108 | { 109 | FILE* file = fopen(path, "rb"); 110 | if (!file) 111 | return false; 112 | 113 | fseek(file, 0, SEEK_END); 114 | long length = ftell(file); 115 | fseek(file, 0, SEEK_SET); 116 | 117 | if (length <= 0) 118 | { 119 | fclose(file); 120 | return false; 121 | } 122 | 123 | data.resize(length); 124 | size_t result = fread(&data[0], 1, data.size(), file); 125 | int rc = fclose(file); 126 | 127 | return rc == 0 && result == data.size(); 128 | } 129 | 130 | bool writeFile(const char* path, const std::string& data) 131 | { 132 | FILE* file = fopen(path, "wb"); 133 | if (!file) 134 | return false; 135 | 136 | size_t result = fwrite(&data[0], 1, data.size(), file); 137 | int rc = fclose(file); 138 | 139 | return rc == 0 && result == data.size(); 140 | } 141 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | meshoptimizer - demo 5 | 6 | 7 | 26 | 27 | 28 | 29 |
30 | meshoptimizer 31 |
32 | 33 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for deciding to contribute to meshoptimizer! These guidelines will try to help make the process painless and efficient. 2 | 3 | ## Questions 4 | 5 | If you have a question regarding the library usage, please [open a GitHub issue](https://github.com/zeux/meshoptimizer/issues/new). 6 | Some questions just need answers, but it's nice to keep them for future reference in case other people want to know the same thing. 7 | Some questions help improve the library interface or documentation by inspiring future changes. 8 | 9 | ## Bugs 10 | 11 | If the library doesn't compile on your system, compiles with warnings, doesn't seem to run correctly for your input data or if anything else is amiss, please [open a GitHub issue](https://github.com/zeux/meshoptimizer/issues/new). 12 | It helps if you note the version of the library this issue happens in, the version of your compiler for compilation issues, and a reproduction case for runtime bugs. 13 | 14 | Of course, feel free to [create a pull request](https://help.github.com/articles/about-pull-requests/) to fix the bug yourself. 15 | 16 | ## Features 17 | 18 | New algorithms and improvements to existing algorithms are always welcome; you can open an issue or make the change yourself and submit a pull request. 19 | 20 | For major features, consider opening an issue describing an improvement you'd like to see or make before opening a pull request. 21 | This will give us a chance to discuss the idea before implementing it - some algorithms may not be easy to integrate into existing programs, may not be robust to arbitrary meshes or may be expensive to run or implement/maintain, so a discussion helps make sure these don't block the algorithm development. 22 | 23 | ## Code style 24 | 25 | Contributions to this project are expected to follow the existing code style. 26 | `.clang-format` file mostly defines syntactic styling rules (you can run `make format` to format the code accordingly). 27 | 28 | As for naming conventions, this library uses `snake_case` for variables, `lowerCamelCase` for functions, `UpperCamelCase` for types, `kCamelCase` for global constants and `SCARY_CASE` for macros. All public functions/types must additionally have an extra `meshopt_` prefix to avoid symbol conflicts. 29 | 30 | ## Dependencies 31 | 32 | Please note that this library uses C89 interface for all APIs and a C++98 implementation - C++11 features can not be used. 33 | This choice is made to maximize compatibility to make sure that any toolchain, including legacy proprietary gaming console toolchains, can compile this code. 34 | 35 | Additionally, the library code has zero external dependencies, does not depend on STL and does not use RTTI or exceptions. 36 | This, again, maximizes compatibility and makes sure the library can be used in environments where STL use is discouraged or prohibited, as well as maximizing runtime performance and minimizing compilation times. 37 | 38 | The demo program uses STL since it serves as an example of usage and as a test harness, not as production-ready code. 39 | 40 | ## Testing 41 | 42 | All pull requests will run through a continuous integration pipeline using GitHub Actions that will run the built-in unit tests and integration tests on Windows, macOS and Linux with gcc, clang and msvc compilers. 43 | You can run the tests yourself using `make test` or building the demo program with `cmake -DBUILD_DEMO=ON` and running it. 44 | 45 | Unit tests can be found in `demo/tests.cpp` and functional tests - in `demo/main.cpp`; when making code changes please try to make sure they are covered by an existing test or add a new test accordingly. 46 | 47 | ## Documentation 48 | 49 | Documentation for this library resides in the `meshoptimizer.h` header, with examples as part of a usage manual available in `README.md`. 50 | Changes to documentation are always welcome and should use issues/pull requests as outlined above; please note that `README.md` only contains documentation for stable algorithms, as experimental algorithms may change the interface without concern for backwards compatibility. 51 | 52 | ## Sensitive communication 53 | 54 | If you prefer to not disclose the issues or information relevant to the issue such as reproduction case to the public, you can always contact the author via e-mail (arseny.kapoulkine@gmail.com). 55 | -------------------------------------------------------------------------------- /gltf/README.md: -------------------------------------------------------------------------------- 1 | # 📦 gltfpack 2 | 3 | gltfpack is a tool that can automatically optimize glTF files to reduce the download size and improve loading and rendering speed. 4 | 5 | ## Installation 6 | 7 | You can download a pre-built binary for gltfpack on [Releases page](https://github.com/zeux/meshoptimizer/releases), or install [npm package](https://www.npmjs.com/package/gltfpack). Native binaries are recommended over npm since they can work with larger files, run faster, and support texture compression. 8 | 9 | ## Usage 10 | 11 | To convert a glTF file using gltfpack, run the command-line binary like this on an input `.gltf`/`.glb`/`.obj` file (run it without arguments for a list of options): 12 | 13 | ``` 14 | gltfpack -i scene.gltf -o scene.glb 15 | ``` 16 | 17 | 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. 18 | 19 | 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 GLTF loaders that support `KHR_mesh_quantization` extension such as [three.js](https://threejs.org/) (r111+) and [Babylon.js](https://www.babylonjs.com/) (4.1+). 20 | 21 | 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 support for [EXT_meshopt_compression](https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Vendor/EXT_meshopt_compression/README.md) extension; three.js supports it in r122+ (requires calling `GLTFLoader.setMeshoptDecoder`), Babylon.js supports it in 5.0+ without further setup. 22 | 23 | For better compression, you can use `-cc` option which applies additional compression; additionally make sure that your content delivery method is configured to use deflate (gzip) - meshoptimizer codecs are designed to produce output that can be compressed further with general purpose compressors. 24 | 25 | gltfpack can also compress textures using Basis Universal format stored in a KTX2 container (`-tc` flag, requires support for `KHR_texture_basisu`). Textures can also be embedded into `.bin`/`.glb` output using `-te` flag. 26 | 27 | ## Decompression 28 | 29 | When using compressed files, [js/meshopt_decoder.js](https://github.com/zeux/meshoptimizer/blob/master/js/meshopt_decoder.js) or `js/meshopt_decoder.module.js` needs to be loaded to provide the WebAssembly decoder module like this: 30 | 31 | ```js 32 | import { MeshoptDecoder } from './meshopt_decoder.module.js'; 33 | 34 | ... 35 | 36 | var loader = new GLTFLoader(); 37 | loader.setMeshoptDecoder(MeshoptDecoder); 38 | loader.load('pirate.glb', function (gltf) { scene.add(gltf.scene); }); 39 | ``` 40 | 41 | When using Three.js, this module can be imported from three.js repository from `examples/jsm/libs/meshopt_decoder.module.js`. 42 | 43 | Note that `meshopt_decoder` assumes that WebAssembly is supported. This is the case for all modern browsers; if support for legacy browsers such as Internet Explorer 11 is desired, it's recommended to use `-cf` flag when creating the glTF content. This will create and load fallback uncompressed buffers, but only on browsers that don't support WebAssembly. 44 | 45 | ## Options 46 | 47 | By default gltfpack makes certain assumptions when optimizing the scenes, for example meshes that belong to nodes that aren't animated can be merged together, and has some defaults that represent a tradeoff between precision and size that are picked to fit most use cases. However, in some cases the resulting `.gltf` file needs to retain some way for the application to manipulate individual scene elements, and in other cases precision or size are more important to optimize for. gltfpack has a rich set of command line options to control various aspects of its behavior, with the full list available via `gltfpack -h`. 48 | 49 | The following settings are frequently used to reduce the resulting data size: 50 | 51 | * `-cc`: produce compressed gltf/glb files (requires `EXT_meshopt_compression`) 52 | * `-tc`: convert all textures to KTX2 with BasisU supercompression (requires `KHR_texture_basisu` and may require `-tp` flag for compatibility with WebGL 1) 53 | * `-mi`: use mesh instancing when serializing references to the same meshes (requires `EXT_mesh_gpu_instancing`) 54 | * `-si R`: simplify meshes targeting triangle count ratio R (default: 1; R should be between 0 and 1) 55 | 56 | The following settings are frequently used to restrict some optimizations: 57 | 58 | * `-kn`: keep named nodes and meshes attached to named nodes so that named nodes can be transformed externally 59 | * `-km`: keep named materials and disable named material merging 60 | * `-ke`: keep extras data 61 | 62 | ## Building 63 | 64 | gltfpack can be built from source using CMake or Make. To build a full version of gltfpack that supports texture compression, CMake configuration needs to specify the path to https://github.com/zeux/basis_universal fork (branch gltfpack) via `MESHOPT_BASISU_PATH` variable: 65 | 66 | ``` 67 | git clone -b gltfpack https://github.com/zeux/basis_universal 68 | cmake . -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_BASISU_PATH=basis_universal -DCMAKE_BUILD_TYPE=Release 69 | cmake --build . --target gltfpack --config Release 70 | ``` 71 | 72 | ## License 73 | 74 | gltfpack is available to anybody free of charge, under the terms of MIT License (see LICENSE.md). 75 | -------------------------------------------------------------------------------- /gltf/basisenc.cpp: -------------------------------------------------------------------------------- 1 | #ifdef WITH_BASISU 2 | 3 | #define BASISU_NO_ITERATOR_DEBUG_LEVEL 4 | 5 | #ifdef __clang__ 6 | #pragma GCC diagnostic ignored "-Wunknown-warning-option" 7 | #endif 8 | 9 | #ifdef __GNUC__ 10 | #pragma GCC diagnostic ignored "-Wclass-memaccess" 11 | #pragma GCC diagnostic ignored "-Wunused-value" 12 | #endif 13 | 14 | #include "encoder/basisu_comp.h" 15 | 16 | #include "gltfpack.h" 17 | 18 | struct BasisSettings 19 | { 20 | int etc1s_l; 21 | int etc1s_q; 22 | int uastc_l; 23 | float uastc_q; 24 | }; 25 | 26 | static const BasisSettings kBasisSettings[10] = { 27 | {1, 1, 0, 1.5f}, 28 | {1, 6, 0, 1.f}, 29 | {1, 20, 1, 1.0f}, 30 | {1, 50, 1, 0.75f}, 31 | {1, 90, 1, 0.5f}, 32 | {1, 128, 1, 0.4f}, 33 | {1, 160, 1, 0.34f}, 34 | {1, 192, 1, 0.29f}, // default 35 | {1, 224, 2, 0.26f}, 36 | {1, 255, 2, 0.f}, 37 | }; 38 | 39 | static void fillParams(basisu::basis_compressor_params& params, const char* input, const char* output, bool uastc, int width, int height, const BasisSettings& bs, const ImageInfo& info, const Settings& settings) 40 | { 41 | if (uastc) 42 | { 43 | static const uint32_t s_level_flags[basisu::TOTAL_PACK_UASTC_LEVELS] = {basisu::cPackUASTCLevelFastest, basisu::cPackUASTCLevelFaster, basisu::cPackUASTCLevelDefault, basisu::cPackUASTCLevelSlower, basisu::cPackUASTCLevelVerySlow}; 44 | 45 | params.m_uastc = true; 46 | 47 | params.m_pack_uastc_flags &= ~basisu::cPackUASTCLevelMask; 48 | params.m_pack_uastc_flags |= s_level_flags[bs.uastc_l]; 49 | 50 | params.m_rdo_uastc = bs.uastc_q > 0; 51 | params.m_rdo_uastc_quality_scalar = bs.uastc_q; 52 | params.m_rdo_uastc_dict_size = 1024; 53 | } 54 | else 55 | { 56 | params.m_compression_level = bs.etc1s_l; 57 | params.m_quality_level = bs.etc1s_q; 58 | params.m_max_endpoint_clusters = 0; 59 | params.m_max_selector_clusters = 0; 60 | 61 | params.m_no_selector_rdo = info.normal_map; 62 | params.m_no_endpoint_rdo = info.normal_map; 63 | } 64 | 65 | params.m_perceptual = info.srgb; 66 | 67 | params.m_mip_gen = true; 68 | params.m_mip_srgb = info.srgb; 69 | 70 | params.m_resample_width = width; 71 | params.m_resample_height = height; 72 | 73 | params.m_y_flip = settings.texture_flipy; 74 | 75 | params.m_create_ktx2_file = true; 76 | params.m_ktx2_srgb_transfer_func = info.srgb; 77 | 78 | if (uastc) 79 | { 80 | params.m_ktx2_uastc_supercompression = basist::KTX2_SS_ZSTANDARD; 81 | params.m_ktx2_zstd_supercompression_level = 9; 82 | } 83 | 84 | params.m_read_source_images = true; 85 | params.m_write_output_basis_files = true; 86 | 87 | params.m_source_filenames.resize(1); 88 | params.m_source_filenames[0] = input; 89 | 90 | params.m_out_filename = output; 91 | 92 | params.m_status_output = false; 93 | } 94 | 95 | static const char* prepareEncode(basisu::basis_compressor_params& params, const cgltf_image& image, const char* input_path, const ImageInfo& info, const Settings& settings, TempFile& temp_input, TempFile& temp_output) 96 | { 97 | std::string img_data; 98 | std::string mime_type; 99 | std::string result; 100 | 101 | if (!readImage(image, input_path, img_data, mime_type)) 102 | return "error reading source file"; 103 | 104 | int width = 0, height = 0; 105 | if (!getDimensions(img_data, mime_type.c_str(), width, height)) 106 | return "error parsing image header"; 107 | 108 | adjustDimensions(width, height, settings); 109 | 110 | temp_input.create(mimeExtension(mime_type.c_str())); 111 | temp_output.create(".ktx2"); 112 | 113 | if (!writeFile(temp_input.path.c_str(), img_data)) 114 | return "error writing temporary file"; 115 | 116 | int quality = settings.texture_quality[info.kind]; 117 | bool uastc = settings.texture_mode[info.kind] == TextureMode_UASTC; 118 | 119 | const BasisSettings& bs = kBasisSettings[quality - 1]; 120 | 121 | fillParams(params, temp_input.path.c_str(),temp_output.path.c_str(), uastc, width, height, bs, info, settings); 122 | 123 | return nullptr; 124 | } 125 | 126 | void encodeImages(std::string* encoded, const cgltf_data* data, const std::vector& images, const char* input_path, const Settings& settings) 127 | { 128 | basisu::basisu_encoder_init(); 129 | 130 | basisu::vector params(data->images_count); 131 | basisu::vector results(data->images_count); 132 | 133 | std::vector temp_inputs(data->images_count); 134 | std::vector temp_outputs(data->images_count); 135 | 136 | for (size_t i = 0; i < data->images_count; ++i) 137 | { 138 | const cgltf_image& image = data->images[i]; 139 | ImageInfo info = images[i]; 140 | 141 | if (settings.texture_mode[info.kind] == TextureMode_Raw) 142 | continue; 143 | 144 | if (const char* error = prepareEncode(params[i], image, input_path, info, settings, temp_inputs[i], temp_outputs[i])) 145 | encoded[i] = error; 146 | 147 | // image is ready to encode in parallel 148 | } 149 | 150 | uint32_t num_threads = settings.texture_jobs == 0 ? std::thread::hardware_concurrency() : settings.texture_jobs; 151 | 152 | basisu::basis_parallel_compress(num_threads, params, results); 153 | 154 | for (size_t i = 0; i < data->images_count; ++i) 155 | { 156 | if (params[i].m_source_filenames.empty()) 157 | ; // encoding was skipped or preparation resulted in an error 158 | else if (results[i].m_error_code == basisu::basis_compressor::cECFailedReadingSourceImages) 159 | encoded[i] = "error decoding source image"; 160 | else if (results[i].m_error_code != basisu::basis_compressor::cECSuccess) 161 | encoded[i] = "error encoding image"; 162 | else if (!readFile(temp_outputs[i].path.c_str(), encoded[i])) 163 | encoded[i] = "error reading temporary file"; 164 | } 165 | } 166 | #endif 167 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | if(POLICY CMP0077) 4 | cmake_policy(SET CMP0077 NEW) # Enables override of options from parent CMakeLists.txt 5 | endif() 6 | 7 | if(POLICY CMP0091) 8 | cmake_policy(SET CMP0091 NEW) # Enables use of MSVC_RUNTIME_LIBRARY 9 | endif() 10 | if(POLICY CMP0092) 11 | cmake_policy(SET CMP0092 NEW) # Enables clean /W4 override for MSVC 12 | endif() 13 | 14 | project(meshoptimizer VERSION 0.18 LANGUAGES CXX) 15 | 16 | option(MESHOPT_BUILD_DEMO "Build demo" OFF) 17 | option(MESHOPT_BUILD_GLTFPACK "Build gltfpack" OFF) 18 | option(MESHOPT_BUILD_SHARED_LIBS "Build shared libraries" OFF) 19 | option(MESHOPT_WERROR "Treat warnings as errors" OFF) 20 | set(MESHOPT_BASISU_PATH "" CACHE STRING "") 21 | 22 | set(SOURCES 23 | src/meshoptimizer.h 24 | src/allocator.cpp 25 | src/clusterizer.cpp 26 | src/indexcodec.cpp 27 | src/indexgenerator.cpp 28 | src/overdrawanalyzer.cpp 29 | src/overdrawoptimizer.cpp 30 | src/simplifier.cpp 31 | src/spatialorder.cpp 32 | src/stripifier.cpp 33 | src/vcacheanalyzer.cpp 34 | src/vcacheoptimizer.cpp 35 | src/vertexcodec.cpp 36 | src/vertexfilter.cpp 37 | src/vfetchanalyzer.cpp 38 | src/vfetchoptimizer.cpp 39 | ) 40 | 41 | set(GLTF_SOURCES 42 | gltf/animation.cpp 43 | gltf/basisenc.cpp 44 | gltf/basislib.cpp 45 | gltf/fileio.cpp 46 | gltf/gltfpack.cpp 47 | gltf/image.cpp 48 | gltf/json.cpp 49 | gltf/material.cpp 50 | gltf/mesh.cpp 51 | gltf/node.cpp 52 | gltf/parseobj.cpp 53 | gltf/parsegltf.cpp 54 | gltf/stream.cpp 55 | gltf/write.cpp 56 | ) 57 | 58 | if(MSVC) 59 | add_compile_options(/W4) 60 | else() 61 | add_compile_options(-Wall -Wextra -Wshadow -Wno-missing-field-initializers) 62 | endif() 63 | 64 | if(MESHOPT_WERROR) 65 | if(MSVC) 66 | add_compile_options(/WX) 67 | else() 68 | add_compile_options(-Werror) 69 | endif() 70 | endif() 71 | 72 | if(MESHOPT_BUILD_SHARED_LIBS) 73 | add_library(meshoptimizer SHARED ${SOURCES}) 74 | else() 75 | add_library(meshoptimizer STATIC ${SOURCES}) 76 | endif() 77 | 78 | target_include_directories(meshoptimizer INTERFACE "$") 79 | 80 | if(MESHOPT_BUILD_SHARED_LIBS) 81 | set_target_properties(meshoptimizer PROPERTIES CXX_VISIBILITY_PRESET hidden) 82 | set_target_properties(meshoptimizer PROPERTIES VISIBILITY_INLINES_HIDDEN ON) 83 | 84 | if(WIN32) 85 | target_compile_definitions(meshoptimizer INTERFACE "MESHOPTIMIZER_API=__declspec(dllimport)") 86 | target_compile_definitions(meshoptimizer PRIVATE "MESHOPTIMIZER_API=__declspec(dllexport)") 87 | else() 88 | target_compile_definitions(meshoptimizer PUBLIC "MESHOPTIMIZER_API=__attribute__((visibility(\"default\")))") 89 | endif() 90 | endif() 91 | 92 | set(TARGETS meshoptimizer) 93 | 94 | if(MESHOPT_BUILD_DEMO) 95 | add_executable(demo demo/main.cpp demo/tests.cpp tools/meshloader.cpp) 96 | target_link_libraries(demo meshoptimizer) 97 | endif() 98 | 99 | if(MESHOPT_BUILD_GLTFPACK) 100 | add_executable(gltfpack ${GLTF_SOURCES} tools/meshloader.cpp) 101 | set_target_properties(gltfpack PROPERTIES CXX_STANDARD 11) 102 | target_link_libraries(gltfpack meshoptimizer) 103 | list(APPEND TARGETS gltfpack) 104 | 105 | if(MESHOPT_BUILD_SHARED_LIBS) 106 | string(CONCAT RPATH "$ORIGIN/../" ${CMAKE_INSTALL_LIBDIR}) 107 | set_target_properties(gltfpack PROPERTIES INSTALL_RPATH ${RPATH}) 108 | endif() 109 | 110 | if(NOT MESHOPT_BASISU_PATH STREQUAL "") 111 | get_filename_component(BASISU_PATH ${MESHOPT_BASISU_PATH} ABSOLUTE) 112 | 113 | target_compile_definitions(gltfpack PRIVATE WITH_BASISU) 114 | set_source_files_properties(gltf/basisenc.cpp gltf/basislib.cpp PROPERTIES INCLUDE_DIRECTORIES ${BASISU_PATH}) 115 | 116 | if(NOT MSVC AND CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "x86_64") 117 | set_source_files_properties(gltf/basislib.cpp PROPERTIES COMPILE_OPTIONS -msse4.1) 118 | endif() 119 | 120 | if(UNIX) 121 | target_link_libraries(gltfpack pthread) 122 | endif() 123 | endif() 124 | endif() 125 | 126 | include(GNUInstallDirs) 127 | 128 | install(TARGETS ${TARGETS} EXPORT meshoptimizerTargets 129 | COMPONENT meshoptimizer 130 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 131 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 132 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 133 | INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 134 | 135 | install(FILES src/meshoptimizer.h COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) 136 | install(EXPORT meshoptimizerTargets COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer NAMESPACE meshoptimizer::) 137 | 138 | # TARGET_PDB_FILE is available since 3.1 139 | if(MSVC AND NOT (CMAKE_VERSION VERSION_LESS "3.1")) 140 | foreach(TARGET ${TARGETS}) 141 | get_target_property(TARGET_TYPE ${TARGET} TYPE) 142 | if(NOT ${TARGET_TYPE} STREQUAL "STATIC_LIBRARY") 143 | install(FILES $ COMPONENT meshoptimizer DESTINATION ${CMAKE_INSTALL_BINDIR} OPTIONAL) 144 | endif() 145 | endforeach(TARGET) 146 | endif() 147 | 148 | include(CMakePackageConfigHelpers) 149 | 150 | configure_package_config_file(config.cmake.in 151 | ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake 152 | INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer NO_SET_AND_CHECK_MACRO) 153 | 154 | write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake COMPATIBILITY ExactVersion) 155 | 156 | install(FILES 157 | ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfig.cmake 158 | ${CMAKE_CURRENT_BINARY_DIR}/meshoptimizerConfigVersion.cmake 159 | COMPONENT meshoptimizer 160 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/meshoptimizer) 161 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | paths-ignore: 8 | - '*.md' 9 | pull_request: 10 | paths-ignore: 11 | - '*.md' 12 | 13 | jobs: 14 | unix: 15 | strategy: 16 | matrix: 17 | os: [ubuntu, macos] 18 | name: ${{matrix.os}} 19 | runs-on: ${{matrix.os}}-latest 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: make test 23 | run: | 24 | make -j2 config=sanitize test 25 | make -j2 config=debug test 26 | make -j2 config=release test 27 | make -j2 config=coverage test 28 | - name: make gltfpack 29 | run: | 30 | make -j2 config=release gltfpack 31 | strip gltfpack 32 | - name: upload coverage 33 | run: | 34 | find . -type f -name '*.gcno' -exec gcov -p {} + 35 | sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov 36 | bash <(curl -s https://codecov.io/bash) -f './src*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}} 37 | 38 | windows: 39 | runs-on: windows-latest 40 | strategy: 41 | matrix: 42 | arch: [Win32, x64] 43 | steps: 44 | - uses: actions/checkout@v1 45 | - name: cmake configure 46 | run: cmake . -DMESHOPT_BUILD_DEMO=ON -DMESHOPT_BUILD_GLTFPACK=ON -DMESHOPT_WERROR=ON -DCMAKE_MSVC_RUNTIME_LIBRARY="MultiThreaded$<$:Debug>" -A ${{matrix.arch}} 47 | - name: cmake test 48 | shell: bash # necessary for fail-fast 49 | run: | 50 | cmake --build . -- -property:Configuration=Debug -verbosity:minimal 51 | Debug/demo.exe demo/pirate.obj 52 | cmake --build . -- -property:Configuration=Release -verbosity:minimal 53 | Release/demo.exe demo/pirate.obj 54 | 55 | nodejs: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - uses: actions/checkout@v1 59 | - uses: actions/setup-node@v1 60 | with: 61 | node-version: '16' 62 | - name: test decoder 63 | run: node js/meshopt_decoder.test.js 64 | - name: test simd decoder 65 | run: node --experimental-wasm-simd js/meshopt_decoder.test.js 66 | - name: test encoder 67 | run: node js/meshopt_encoder.test.js 68 | - name: test simplifier 69 | run: node js/meshopt_simplifier.test.js 70 | 71 | gltfpack: 72 | runs-on: ubuntu-latest 73 | steps: 74 | - uses: actions/checkout@v1 75 | - uses: actions/checkout@v2 76 | with: 77 | repository: KhronosGroup/glTF-Sample-Models 78 | path: glTF-Sample-Models 79 | - name: make 80 | run: make -j2 config=sanitize gltfpack 81 | - name: test 82 | run: find glTF-Sample-Models/2.0/ -name *.gltf -or -name *.glb | xargs -d '\n' ./gltfpack -cc -test 83 | - name: pack 84 | run: find glTF-Sample-Models/2.0/ -name *.gltf | grep -v glTF-Draco | grep -v glTF-KTX-BasisU | xargs -d '\n' -I '{}' ./gltfpack -i '{}' -o '{}pack.gltf' 85 | - name: validate 86 | run: | 87 | curl -sL $VALIDATOR | tar xJ 88 | find glTF-Sample-Models/2.0/ -name *.gltfpack.gltf | xargs -d '\n' -L 1 ./gltf_validator -r -a 89 | env: 90 | VALIDATOR: https://github.com/KhronosGroup/glTF-Validator/releases/download/2.0.0-dev.3.3/gltf_validator-2.0.0-dev.3.3-linux64.tar.xz 91 | 92 | gltfpack-js: 93 | runs-on: ubuntu-latest 94 | steps: 95 | - uses: actions/checkout@v1 96 | - uses: actions/setup-node@v1 97 | with: 98 | node-version: '14.x' 99 | - name: install wasi 100 | run: | 101 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sdk-$VERSION.0-linux.tar.gz | tar xz 102 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/libclang_rt.builtins-wasm32-wasi-$VERSION.0.tar.gz | tar xz -C wasi-sdk-$VERSION.0 103 | curl -sL https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$VERSION/wasi-sysroot-$VERSION.0.tar.gz | tar xz -C wasi-sdk-$VERSION.0/share 104 | mv wasi-sdk-$VERSION.0 wasi-sdk 105 | env: 106 | VERSION: 16 107 | - name: build 108 | run: | 109 | make -j2 -B WASI_SDK=wasi-sdk gltf/library.wasm js 110 | git status 111 | - name: test 112 | run: | 113 | node gltf/cli.js -i demo/pirate.obj -o pirate.glb -v 114 | node gltf/cli.js -i `pwd`/pirate.glb -o pirate-repack.glb -cc -v 115 | wc -c pirate.glb pirate-repack.glb 116 | node js/meshopt_decoder.test.js 117 | node js/meshopt_encoder.test.js 118 | node js/meshopt_simplifier.test.js 119 | 120 | gltfpack-basis: 121 | runs-on: ubuntu-latest 122 | steps: 123 | - uses: actions/checkout@v1 124 | - uses: actions/checkout@v2 125 | with: 126 | repository: zeux/basis_universal 127 | ref: gltfpack 128 | path: basis_universal 129 | - name: make gltfpack 130 | run: make -j2 BASISU=basis_universal gltfpack 131 | 132 | arm64: 133 | runs-on: ubuntu-latest 134 | steps: 135 | - run: docker run --rm --privileged multiarch/qemu-user-static:register --reset 136 | - uses: docker://multiarch/ubuntu-core:arm64-focal 137 | with: 138 | args: 'uname -a' 139 | - uses: actions/checkout@v1 140 | - name: make test 141 | uses: docker://multiarch/ubuntu-core:arm64-focal 142 | with: 143 | args: 'bash -c "apt-get update && apt-get install -y build-essential && make -j2 config=coverage test"' 144 | - name: upload coverage 145 | run: | 146 | find . -type f -name '*.gcno' -exec gcov -p {} + 147 | sed -i -e "s/#####\(.*\)\(\/\/ unreachable.*\)/ -\1\2/" *.gcov 148 | bash <(curl -s https://codecov.io/bash) -f './src*.gcov' -X search -t ${{secrets.CODECOV_TOKEN}} -B ${{github.ref}} 149 | 150 | iphone: 151 | runs-on: macos-latest 152 | steps: 153 | - uses: actions/checkout@v1 154 | - name: make 155 | run: make -j2 config=iphone 156 | -------------------------------------------------------------------------------- /js/meshopt_encoder.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert').strict; 2 | var encoder = require('./meshopt_encoder.js'); 3 | var decoder = require('./meshopt_decoder.js'); 4 | 5 | process.on('unhandledRejection', error => { 6 | console.log('unhandledRejection', error); 7 | process.exit(1); 8 | }); 9 | 10 | function bytes(view) { 11 | return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); 12 | } 13 | 14 | var tests = { 15 | reorderMesh: function() { 16 | var indices = new Uint32Array([ 17 | 4, 2, 5, 18 | 3, 1, 4, 19 | 0, 1, 3, 20 | 1, 2, 4, 21 | ]); 22 | 23 | var expected = new Uint32Array([ 24 | 0, 1, 2, 25 | 3, 1, 0, 26 | 4, 3, 0, 27 | 5, 3, 4, 28 | ]); 29 | 30 | var remap = new Uint32Array([ 31 | 5, 3, 1, 4, 0, 2 32 | ]); 33 | 34 | var res = encoder.reorderMesh(indices, /* triangles= */ true, /* optsize= */ true); 35 | assert.deepEqual(indices, expected); 36 | assert.deepEqual(res[0], remap); 37 | assert.equal(res[1], 6); // unique 38 | }, 39 | 40 | roundtripVertexBuffer: function() { 41 | var data = new Uint8Array(16 * 4); 42 | 43 | // this tests 0/2/4/8 bit groups in one stream 44 | for (var i = 0; i < 16; ++i) 45 | { 46 | data[i * 4 + 0] = 0; 47 | data[i * 4 + 1] = i * 1; 48 | data[i * 4 + 2] = i * 2; 49 | data[i * 4 + 3] = i * 8; 50 | } 51 | 52 | var encoded = encoder.encodeVertexBuffer(data, 16, 4); 53 | var decoded = new Uint8Array(16 * 4); 54 | decoder.decodeVertexBuffer(decoded, 16, 4, encoded); 55 | 56 | assert.deepEqual(decoded, data); 57 | }, 58 | 59 | roundtripIndexBuffer: function() { 60 | var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]); 61 | 62 | var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 4); 63 | var decoded = new Uint32Array(data.length); 64 | decoder.decodeIndexBuffer(bytes(decoded), data.length, 4, encoded); 65 | 66 | assert.deepEqual(decoded, data); 67 | }, 68 | 69 | roundtripIndexBuffer16: function() { 70 | var data = new Uint16Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]); 71 | 72 | var encoded = encoder.encodeIndexBuffer(bytes(data), data.length, 2); 73 | var decoded = new Uint16Array(data.length); 74 | decoder.decodeIndexBuffer(bytes(decoded), data.length, 2, encoded); 75 | 76 | assert.deepEqual(decoded, data); 77 | }, 78 | 79 | roundtripIndexSequence: function() { 80 | var data = new Uint32Array([0, 1, 51, 2, 49, 1000]); 81 | 82 | var encoded = encoder.encodeIndexSequence(bytes(data), data.length, 4); 83 | var decoded = new Uint32Array(data.length); 84 | decoder.decodeIndexSequence(bytes(decoded), data.length, 4, encoded); 85 | 86 | assert.deepEqual(decoded, data); 87 | }, 88 | 89 | roundtripIndexSequence16: function() { 90 | var data = new Uint16Array([0, 1, 51, 2, 49, 1000]); 91 | 92 | var encoded = encoder.encodeIndexSequence(bytes(data), data.length, 2); 93 | var decoded = new Uint16Array(data.length); 94 | decoder.decodeIndexSequence(bytes(decoded), data.length, 2, encoded); 95 | 96 | assert.deepEqual(decoded, data); 97 | }, 98 | 99 | encodeFilterOct8: function() { 100 | var data = new Float32Array([ 101 | 1, 0, 0, 0, 102 | 0, -1, 0, 0, 103 | 0.7071068, 0, 0.707168, 1, 104 | -0.7071068, 0, -0.707168, 1, 105 | ]); 106 | 107 | var expected = new Uint8Array([ 108 | 0x7f, 0, 0x7f, 0, 109 | 0, 0x81, 0x7f, 0, 110 | 0x3f, 0, 0x7f, 0x7f, 111 | 0x81, 0x40, 0x7f, 0x7f, 112 | ]); 113 | 114 | // 4 vectors, encode each vector into 4 bytes with 8 bits of precision/component 115 | var encoded = encoder.encodeFilterOct(data, 4, 4, 8); 116 | assert.deepEqual(encoded, expected); 117 | }, 118 | 119 | encodeFilterOct12: function() { 120 | var data = new Float32Array([ 121 | 1, 0, 0, 0, 122 | 0, -1, 0, 0, 123 | 0.7071068, 0, 0.707168, 1, 124 | -0.7071068, 0, -0.707168, 1, 125 | ]); 126 | 127 | var expected = new Uint16Array([ 128 | 0x7ff, 0, 0x7ff, 0, 129 | 0x0, 0xf801, 0x7ff, 0, 130 | 0x3ff, 0, 0x7ff, 0x7fff, 131 | 0xf801, 0x400, 0x7ff, 0x7fff, 132 | ]); 133 | 134 | // 4 vectors, encode each vector into 8 bytes with 12 bits of precision/component 135 | var encoded = encoder.encodeFilterOct(data, 4, 8, 12); 136 | assert.deepEqual(encoded, bytes(expected)); 137 | }, 138 | 139 | encodeFilterQuat12: function() { 140 | var data = new Float32Array([ 141 | 1, 0, 0, 0, 142 | 0, -1, 0, 0, 143 | 0.7071068, 0, 0, 0.707168, 144 | -0.7071068, 0, 0, -0.707168, 145 | ]); 146 | 147 | var expected = new Uint16Array([ 148 | 0, 0, 0, 0x7fc, 149 | 0, 0, 0, 0x7fd, 150 | 0x7ff, 0, 0, 0x7ff, 151 | 0x7ff, 0, 0, 0x7ff, 152 | ]); 153 | 154 | // 4 quaternions, encode each quaternion into 8 bytes with 12 bits of precision/component 155 | var encoded = encoder.encodeFilterQuat(data, 4, 8, 12); 156 | assert.deepEqual(encoded, bytes(expected)); 157 | }, 158 | 159 | encodeFilterExp: function() { 160 | var data = new Float32Array([ 161 | 1, 162 | -23.4, 163 | -0.1, 164 | ]); 165 | 166 | var expected = new Uint32Array([ 167 | 0xf7000200, 168 | 0xf7ffd133, 169 | 0xf7ffffcd, 170 | ]); 171 | 172 | // 1 vector with 3 components (12 bytes), encode each vector into 12 bytes with 15 bits of precision/component 173 | var encoded = encoder.encodeFilterExp(data, 1, 12, 15); 174 | assert.deepEqual(encoded, bytes(expected)); 175 | }, 176 | 177 | encodeGltfBuffer: function() { 178 | var data = new Uint32Array([0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9]); 179 | 180 | var encoded = encoder.encodeGltfBuffer(bytes(data), data.length, 4, 'TRIANGLES'); 181 | var decoded = new Uint32Array(data.length); 182 | decoder.decodeGltfBuffer(bytes(decoded), data.length, 4, encoded, 'TRIANGLES'); 183 | 184 | assert.deepEqual(decoded, data); 185 | }, 186 | }; 187 | 188 | Promise.all([encoder.ready, decoder.ready]).then(() => { 189 | var count = 0; 190 | 191 | for (var key in tests) { 192 | tests[key](); 193 | count++; 194 | } 195 | 196 | console.log(count, 'tests passed'); 197 | }); 198 | -------------------------------------------------------------------------------- /gltf/node.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include 5 | #include 6 | 7 | void markScenes(cgltf_data* data, std::vector& nodes) 8 | { 9 | for (size_t i = 0; i < nodes.size(); ++i) 10 | nodes[i].scene = -1; 11 | 12 | for (size_t i = 0; i < data->scenes_count; ++i) 13 | for (size_t j = 0; j < data->scenes[i].nodes_count; ++j) 14 | { 15 | NodeInfo& ni = nodes[data->scenes[i].nodes[j] - data->nodes]; 16 | 17 | if (ni.scene >= 0) 18 | ni.scene = -2; // multiple scenes 19 | else 20 | ni.scene = int(i); 21 | } 22 | 23 | for (size_t i = 0; i < data->nodes_count; ++i) 24 | { 25 | cgltf_node* root = &data->nodes[i]; 26 | while (root->parent) 27 | root = root->parent; 28 | 29 | nodes[i].scene = nodes[root - data->nodes].scene; 30 | } 31 | } 32 | 33 | void markAnimated(cgltf_data* data, std::vector& nodes, const std::vector& animations) 34 | { 35 | for (size_t i = 0; i < animations.size(); ++i) 36 | { 37 | const Animation& animation = animations[i]; 38 | 39 | for (size_t j = 0; j < animation.tracks.size(); ++j) 40 | { 41 | const Track& track = animation.tracks[j]; 42 | 43 | // mark nodes that have animation tracks that change their base transform as animated 44 | if (!track.dummy) 45 | { 46 | NodeInfo& ni = nodes[track.node - data->nodes]; 47 | 48 | ni.animated_paths |= (1 << track.path); 49 | } 50 | } 51 | } 52 | 53 | for (size_t i = 0; i < data->nodes_count; ++i) 54 | { 55 | NodeInfo& ni = nodes[i]; 56 | 57 | for (cgltf_node* node = &data->nodes[i]; node; node = node->parent) 58 | ni.animated |= nodes[node - data->nodes].animated_paths != 0; 59 | } 60 | } 61 | 62 | void markNeededNodes(cgltf_data* data, std::vector& nodes, const std::vector& meshes, const std::vector& animations, const Settings& settings) 63 | { 64 | // mark all joints as kept 65 | for (size_t i = 0; i < data->skins_count; ++i) 66 | { 67 | const cgltf_skin& skin = data->skins[i]; 68 | 69 | // for now we keep all joints directly referenced by the skin and the entire ancestry tree; we keep names for joints as well 70 | for (size_t j = 0; j < skin.joints_count; ++j) 71 | { 72 | NodeInfo& ni = nodes[skin.joints[j] - data->nodes]; 73 | 74 | ni.keep = true; 75 | } 76 | } 77 | 78 | // mark all animated nodes as kept 79 | for (size_t i = 0; i < animations.size(); ++i) 80 | { 81 | const Animation& animation = animations[i]; 82 | 83 | for (size_t j = 0; j < animation.tracks.size(); ++j) 84 | { 85 | const Track& track = animation.tracks[j]; 86 | 87 | if (settings.anim_const || !track.dummy) 88 | { 89 | NodeInfo& ni = nodes[track.node - data->nodes]; 90 | 91 | ni.keep = true; 92 | } 93 | } 94 | } 95 | 96 | // mark all mesh nodes as kept 97 | for (size_t i = 0; i < meshes.size(); ++i) 98 | { 99 | const Mesh& mesh = meshes[i]; 100 | 101 | for (size_t j = 0; j < mesh.nodes.size(); ++j) 102 | { 103 | NodeInfo& ni = nodes[mesh.nodes[j] - data->nodes]; 104 | 105 | ni.keep = true; 106 | } 107 | } 108 | 109 | // mark all light/camera nodes as kept 110 | for (size_t i = 0; i < data->nodes_count; ++i) 111 | { 112 | const cgltf_node& node = data->nodes[i]; 113 | 114 | if (node.light || node.camera) 115 | { 116 | nodes[i].keep = true; 117 | } 118 | } 119 | 120 | // mark all named nodes as needed (if -kn is specified) 121 | if (settings.keep_nodes) 122 | { 123 | for (size_t i = 0; i < data->nodes_count; ++i) 124 | { 125 | const cgltf_node& node = data->nodes[i]; 126 | 127 | if (node.name && *node.name) 128 | { 129 | nodes[i].keep = true; 130 | } 131 | } 132 | } 133 | } 134 | 135 | void remapNodes(cgltf_data* data, std::vector& nodes, size_t& node_offset) 136 | { 137 | // to keep a node, we currently need to keep the entire ancestry chain 138 | for (size_t i = 0; i < data->nodes_count; ++i) 139 | { 140 | if (!nodes[i].keep) 141 | continue; 142 | 143 | for (cgltf_node* node = &data->nodes[i]; node; node = node->parent) 144 | nodes[node - data->nodes].keep = true; 145 | } 146 | 147 | // generate sequential indices for all nodes; they aren't sorted topologically 148 | for (size_t i = 0; i < data->nodes_count; ++i) 149 | { 150 | NodeInfo& ni = nodes[i]; 151 | 152 | if (ni.keep) 153 | { 154 | ni.remap = int(node_offset); 155 | 156 | node_offset++; 157 | } 158 | } 159 | } 160 | 161 | void decomposeTransform(float translation[3], float rotation[4], float scale[3], const float* transform) 162 | { 163 | float m[4][4] = {}; 164 | memcpy(m, transform, 16 * sizeof(float)); 165 | 166 | // extract translation from last row 167 | translation[0] = m[3][0]; 168 | translation[1] = m[3][1]; 169 | translation[2] = m[3][2]; 170 | 171 | // compute determinant to determine handedness 172 | float det = 173 | m[0][0] * (m[1][1] * m[2][2] - m[2][1] * m[1][2]) - 174 | m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + 175 | m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); 176 | 177 | float sign = (det < 0.f) ? -1.f : 1.f; 178 | 179 | // recover scale from axis lengths 180 | scale[0] = sqrtf(m[0][0] * m[0][0] + m[1][0] * m[1][0] + m[2][0] * m[2][0]) * sign; 181 | scale[1] = sqrtf(m[0][1] * m[0][1] + m[1][1] * m[1][1] + m[2][1] * m[2][1]) * sign; 182 | scale[2] = sqrtf(m[0][2] * m[0][2] + m[1][2] * m[1][2] + m[2][2] * m[2][2]) * sign; 183 | 184 | // normalize axes to get a pure rotation matrix 185 | float rsx = (scale[0] == 0.f) ? 0.f : 1.f / scale[0]; 186 | float rsy = (scale[1] == 0.f) ? 0.f : 1.f / scale[1]; 187 | float rsz = (scale[2] == 0.f) ? 0.f : 1.f / scale[2]; 188 | 189 | float r00 = m[0][0] * rsx, r10 = m[1][0] * rsx, r20 = m[2][0] * rsx; 190 | float r01 = m[0][1] * rsy, r11 = m[1][1] * rsy, r21 = m[2][1] * rsy; 191 | float r02 = m[0][2] * rsz, r12 = m[1][2] * rsz, r22 = m[2][2] * rsz; 192 | 193 | // "branchless" version of Mike Day's matrix to quaternion conversion 194 | int qc = r22 < 0 ? (r00 > r11 ? 0 : 1) : (r00 < -r11 ? 2 : 3); 195 | float qs1 = qc & 2 ? -1.f : 1.f; 196 | float qs2 = qc & 1 ? -1.f : 1.f; 197 | float qs3 = (qc - 1) & 2 ? -1.f : 1.f; 198 | 199 | float qt = 1.f - qs3 * r00 - qs2 * r11 - qs1 * r22; 200 | float qs = 0.5f / sqrtf(qt); 201 | 202 | rotation[qc ^ 0] = qs * qt; 203 | rotation[qc ^ 1] = qs * (r01 + qs1 * r10); 204 | rotation[qc ^ 2] = qs * (r20 + qs2 * r02); 205 | rotation[qc ^ 3] = qs * (r12 + qs3 * r21); 206 | } 207 | -------------------------------------------------------------------------------- /demo/simplify.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | meshoptimizer - demo 5 | 6 | 7 | 26 | 27 | 28 | 29 |
30 | meshoptimizer 31 |
32 | 33 | 214 | 215 | 216 | -------------------------------------------------------------------------------- /src/spatialorder.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // This work is based on: 9 | // Fabian Giesen. Decoding Morton codes. 2009 10 | namespace meshopt 11 | { 12 | 13 | // "Insert" two 0 bits after each of the 10 low bits of x 14 | inline unsigned int part1By2(unsigned int x) 15 | { 16 | x &= 0x000003ff; // x = ---- ---- ---- ---- ---- --98 7654 3210 17 | x = (x ^ (x << 16)) & 0xff0000ff; // x = ---- --98 ---- ---- ---- ---- 7654 3210 18 | x = (x ^ (x << 8)) & 0x0300f00f; // x = ---- --98 ---- ---- 7654 ---- ---- 3210 19 | x = (x ^ (x << 4)) & 0x030c30c3; // x = ---- --98 ---- 76-- --54 ---- 32-- --10 20 | x = (x ^ (x << 2)) & 0x09249249; // x = ---- 9--8 --7- -6-- 5--4 --3- -2-- 1--0 21 | return x; 22 | } 23 | 24 | static void computeOrder(unsigned int* result, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride) 25 | { 26 | size_t vertex_stride_float = vertex_positions_stride / sizeof(float); 27 | 28 | float minv[3] = {FLT_MAX, FLT_MAX, FLT_MAX}; 29 | float maxv[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX}; 30 | 31 | for (size_t i = 0; i < vertex_count; ++i) 32 | { 33 | const float* v = vertex_positions_data + i * vertex_stride_float; 34 | 35 | for (int j = 0; j < 3; ++j) 36 | { 37 | float vj = v[j]; 38 | 39 | minv[j] = minv[j] > vj ? vj : minv[j]; 40 | maxv[j] = maxv[j] < vj ? vj : maxv[j]; 41 | } 42 | } 43 | 44 | float extent = 0.f; 45 | 46 | extent = (maxv[0] - minv[0]) < extent ? extent : (maxv[0] - minv[0]); 47 | extent = (maxv[1] - minv[1]) < extent ? extent : (maxv[1] - minv[1]); 48 | extent = (maxv[2] - minv[2]) < extent ? extent : (maxv[2] - minv[2]); 49 | 50 | float scale = extent == 0 ? 0.f : 1.f / extent; 51 | 52 | // generate Morton order based on the position inside a unit cube 53 | for (size_t i = 0; i < vertex_count; ++i) 54 | { 55 | const float* v = vertex_positions_data + i * vertex_stride_float; 56 | 57 | int x = int((v[0] - minv[0]) * scale * 1023.f + 0.5f); 58 | int y = int((v[1] - minv[1]) * scale * 1023.f + 0.5f); 59 | int z = int((v[2] - minv[2]) * scale * 1023.f + 0.5f); 60 | 61 | result[i] = part1By2(x) | (part1By2(y) << 1) | (part1By2(z) << 2); 62 | } 63 | } 64 | 65 | static void computeHistogram(unsigned int (&hist)[1024][3], const unsigned int* data, size_t count) 66 | { 67 | memset(hist, 0, sizeof(hist)); 68 | 69 | // compute 3 10-bit histograms in parallel 70 | for (size_t i = 0; i < count; ++i) 71 | { 72 | unsigned int id = data[i]; 73 | 74 | hist[(id >> 0) & 1023][0]++; 75 | hist[(id >> 10) & 1023][1]++; 76 | hist[(id >> 20) & 1023][2]++; 77 | } 78 | 79 | unsigned int sumx = 0, sumy = 0, sumz = 0; 80 | 81 | // replace histogram data with prefix histogram sums in-place 82 | for (int i = 0; i < 1024; ++i) 83 | { 84 | unsigned int hx = hist[i][0], hy = hist[i][1], hz = hist[i][2]; 85 | 86 | hist[i][0] = sumx; 87 | hist[i][1] = sumy; 88 | hist[i][2] = sumz; 89 | 90 | sumx += hx; 91 | sumy += hy; 92 | sumz += hz; 93 | } 94 | 95 | assert(sumx == count && sumy == count && sumz == count); 96 | } 97 | 98 | static void radixPass(unsigned int* destination, const unsigned int* source, const unsigned int* keys, size_t count, unsigned int (&hist)[1024][3], int pass) 99 | { 100 | int bitoff = pass * 10; 101 | 102 | for (size_t i = 0; i < count; ++i) 103 | { 104 | unsigned int id = (keys[source[i]] >> bitoff) & 1023; 105 | 106 | destination[hist[id][pass]++] = source[i]; 107 | } 108 | } 109 | 110 | } // namespace meshopt 111 | 112 | void meshopt_spatialSortRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) 113 | { 114 | using namespace meshopt; 115 | 116 | assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); 117 | assert(vertex_positions_stride % sizeof(float) == 0); 118 | 119 | meshopt_Allocator allocator; 120 | 121 | unsigned int* keys = allocator.allocate(vertex_count); 122 | computeOrder(keys, vertex_positions, vertex_count, vertex_positions_stride); 123 | 124 | unsigned int hist[1024][3]; 125 | computeHistogram(hist, keys, vertex_count); 126 | 127 | unsigned int* scratch = allocator.allocate(vertex_count); 128 | 129 | for (size_t i = 0; i < vertex_count; ++i) 130 | destination[i] = unsigned(i); 131 | 132 | // 3-pass radix sort computes the resulting order into scratch 133 | radixPass(scratch, destination, keys, vertex_count, hist, 0); 134 | radixPass(destination, scratch, keys, vertex_count, hist, 1); 135 | radixPass(scratch, destination, keys, vertex_count, hist, 2); 136 | 137 | // since our remap table is mapping old=>new, we need to reverse it 138 | for (size_t i = 0; i < vertex_count; ++i) 139 | destination[scratch[i]] = unsigned(i); 140 | } 141 | 142 | void meshopt_spatialSortTriangles(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) 143 | { 144 | using namespace meshopt; 145 | 146 | assert(index_count % 3 == 0); 147 | assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); 148 | assert(vertex_positions_stride % sizeof(float) == 0); 149 | 150 | (void)vertex_count; 151 | 152 | size_t face_count = index_count / 3; 153 | size_t vertex_stride_float = vertex_positions_stride / sizeof(float); 154 | 155 | meshopt_Allocator allocator; 156 | 157 | float* centroids = allocator.allocate(face_count * 3); 158 | 159 | for (size_t i = 0; i < face_count; ++i) 160 | { 161 | unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2]; 162 | assert(a < vertex_count && b < vertex_count && c < vertex_count); 163 | 164 | const float* va = vertex_positions + a * vertex_stride_float; 165 | const float* vb = vertex_positions + b * vertex_stride_float; 166 | const float* vc = vertex_positions + c * vertex_stride_float; 167 | 168 | centroids[i * 3 + 0] = (va[0] + vb[0] + vc[0]) / 3.f; 169 | centroids[i * 3 + 1] = (va[1] + vb[1] + vc[1]) / 3.f; 170 | centroids[i * 3 + 2] = (va[2] + vb[2] + vc[2]) / 3.f; 171 | } 172 | 173 | unsigned int* remap = allocator.allocate(face_count); 174 | 175 | meshopt_spatialSortRemap(remap, centroids, face_count, sizeof(float) * 3); 176 | 177 | // support in-order remap 178 | if (destination == indices) 179 | { 180 | unsigned int* indices_copy = allocator.allocate(index_count); 181 | memcpy(indices_copy, indices, index_count * sizeof(unsigned int)); 182 | indices = indices_copy; 183 | } 184 | 185 | for (size_t i = 0; i < face_count; ++i) 186 | { 187 | unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2]; 188 | unsigned int r = remap[i]; 189 | 190 | destination[r * 3 + 0] = a; 191 | destination[r * 3 + 1] = b; 192 | destination[r * 3 + 2] = c; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /tools/codecbench.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/meshoptimizer.h" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #ifdef __EMSCRIPTEN__ 11 | #include 12 | 13 | double timestamp() 14 | { 15 | return emscripten_get_now() * 1e-3; 16 | } 17 | #elif defined(_WIN32) 18 | struct LARGE_INTEGER 19 | { 20 | __int64 QuadPart; 21 | }; 22 | extern "C" __declspec(dllimport) int __stdcall QueryPerformanceCounter(LARGE_INTEGER* lpPerformanceCount); 23 | extern "C" __declspec(dllimport) int __stdcall QueryPerformanceFrequency(LARGE_INTEGER* lpFrequency); 24 | 25 | double timestamp() 26 | { 27 | LARGE_INTEGER freq, counter; 28 | QueryPerformanceFrequency(&freq); 29 | QueryPerformanceCounter(&counter); 30 | return double(counter.QuadPart) / double(freq.QuadPart); 31 | } 32 | #else 33 | double timestamp() 34 | { 35 | timespec ts; 36 | clock_gettime(CLOCK_MONOTONIC, &ts); 37 | return double(ts.tv_sec) + 1e-9 * double(ts.tv_nsec); 38 | } 39 | #endif 40 | 41 | struct Vertex 42 | { 43 | uint16_t data[16]; 44 | }; 45 | 46 | uint32_t murmur3(uint32_t h) 47 | { 48 | h ^= h >> 16; 49 | h *= 0x85ebca6bu; 50 | h ^= h >> 13; 51 | h *= 0xc2b2ae35u; 52 | h ^= h >> 16; 53 | 54 | return h; 55 | } 56 | 57 | void benchCodecs(const std::vector& vertices, const std::vector& indices, double& bestvd, double& bestid, bool verbose) 58 | { 59 | std::vector vb(vertices.size()); 60 | std::vector ib(indices.size()); 61 | 62 | std::vector vc(meshopt_encodeVertexBufferBound(vertices.size(), sizeof(Vertex))); 63 | std::vector ic(meshopt_encodeIndexBufferBound(indices.size(), vertices.size())); 64 | 65 | if (verbose) 66 | printf("source: vertex data %d bytes, index data %d bytes\n", int(vertices.size() * sizeof(Vertex)), int(indices.size() * 4)); 67 | 68 | for (int pass = 0; pass < (verbose ? 2 : 1); ++pass) 69 | { 70 | if (pass == 1) 71 | meshopt_optimizeVertexCacheStrip(&ib[0], &indices[0], indices.size(), vertices.size()); 72 | else 73 | meshopt_optimizeVertexCache(&ib[0], &indices[0], indices.size(), vertices.size()); 74 | 75 | meshopt_optimizeVertexFetch(&vb[0], &ib[0], indices.size(), &vertices[0], vertices.size(), sizeof(Vertex)); 76 | 77 | vc.resize(vc.capacity()); 78 | vc.resize(meshopt_encodeVertexBuffer(&vc[0], vc.size(), &vb[0], vertices.size(), sizeof(Vertex))); 79 | 80 | ic.resize(ic.capacity()); 81 | ic.resize(meshopt_encodeIndexBuffer(&ic[0], ic.size(), &ib[0], indices.size())); 82 | 83 | if (verbose) 84 | printf("pass %d: vertex data %d bytes, index data %d bytes\n", pass, int(vc.size()), int(ic.size())); 85 | 86 | for (int attempt = 0; attempt < 10; ++attempt) 87 | { 88 | double t0 = timestamp(); 89 | 90 | int rv = meshopt_decodeVertexBuffer(&vb[0], vertices.size(), sizeof(Vertex), &vc[0], vc.size()); 91 | assert(rv == 0); 92 | (void)rv; 93 | 94 | double t1 = timestamp(); 95 | 96 | int ri = meshopt_decodeIndexBuffer(&ib[0], indices.size(), 4, &ic[0], ic.size()); 97 | assert(ri == 0); 98 | (void)ri; 99 | 100 | double t2 = timestamp(); 101 | 102 | double GB = 1024 * 1024 * 1024; 103 | 104 | if (verbose) 105 | printf("decode: vertex %.2f ms (%.2f GB/sec), index %.2f ms (%.2f GB/sec)\n", 106 | (t1 - t0) * 1000, double(vertices.size() * sizeof(Vertex)) / GB / (t1 - t0), 107 | (t2 - t1) * 1000, double(indices.size() * 4) / GB / (t2 - t1)); 108 | 109 | if (pass == 0) 110 | { 111 | bestvd = std::max(bestvd, double(vertices.size() * sizeof(Vertex)) / GB / (t1 - t0)); 112 | bestid = std::max(bestid, double(indices.size() * 4) / GB / (t2 - t1)); 113 | } 114 | } 115 | } 116 | } 117 | 118 | void benchFilters(size_t count, double& besto8, double& besto12, double& bestq12, double& bestexp, bool verbose) 119 | { 120 | // note: the filters are branchless so we just run them on runs of zeroes 121 | size_t count4 = (count + 3) & ~3; 122 | std::vector d4(count4 * 4); 123 | std::vector d8(count4 * 8); 124 | 125 | if (verbose) 126 | printf("filters: oct8 data %d bytes, oct12/quat12 data %d bytes\n", int(d4.size()), int(d8.size())); 127 | 128 | for (int attempt = 0; attempt < 10; ++attempt) 129 | { 130 | double t0 = timestamp(); 131 | 132 | meshopt_decodeFilterOct(&d4[0], count4, 4); 133 | 134 | double t1 = timestamp(); 135 | 136 | meshopt_decodeFilterOct(&d8[0], count4, 8); 137 | 138 | double t2 = timestamp(); 139 | 140 | meshopt_decodeFilterQuat(&d8[0], count4, 8); 141 | 142 | double t3 = timestamp(); 143 | 144 | meshopt_decodeFilterExp(&d8[0], count4, 8); 145 | 146 | double t4 = timestamp(); 147 | 148 | double GB = 1024 * 1024 * 1024; 149 | 150 | if (verbose) 151 | printf("filter: oct8 %.2f ms (%.2f GB/sec), oct12 %.2f ms (%.2f GB/sec), quat12 %.2f ms (%.2f GB/sec), exp %.2f ms (%.2f GB/sec)\n", 152 | (t1 - t0) * 1000, double(d4.size()) / GB / (t1 - t0), 153 | (t2 - t1) * 1000, double(d8.size()) / GB / (t2 - t1), 154 | (t3 - t2) * 1000, double(d8.size()) / GB / (t3 - t2), 155 | (t4 - t3) * 1000, double(d8.size()) / GB / (t4 - t3)); 156 | 157 | besto8 = std::max(besto8, double(d4.size()) / GB / (t1 - t0)); 158 | besto12 = std::max(besto12, double(d8.size()) / GB / (t2 - t1)); 159 | bestq12 = std::max(bestq12, double(d8.size()) / GB / (t3 - t2)); 160 | bestexp = std::max(bestexp, double(d8.size()) / GB / (t4 - t3)); 161 | } 162 | } 163 | 164 | int main(int argc, char** argv) 165 | { 166 | meshopt_encodeIndexVersion(1); 167 | 168 | bool verbose = false; 169 | 170 | for (int i = 1; i < argc; ++i) 171 | if (strcmp(argv[i], "-v") == 0) 172 | verbose = true; 173 | 174 | const int N = 1000; 175 | 176 | std::vector vertices; 177 | vertices.reserve((N + 1) * (N + 1)); 178 | 179 | for (int x = 0; x <= N; ++x) 180 | { 181 | for (int y = 0; y <= N; ++y) 182 | { 183 | Vertex v; 184 | 185 | for (int k = 0; k < 16; ++k) 186 | { 187 | uint32_t h = murmur3((x * (N + 1) + y) * 16 + k); 188 | 189 | // use random k-bit sequence for each word to test all encoding types 190 | // note: this doesn't stress the sentinel logic too much but it's all branchless so it's probably fine? 191 | v.data[k] = h & ((1 << (k + 1)) - 1); 192 | } 193 | 194 | vertices.push_back(v); 195 | } 196 | } 197 | 198 | std::vector indices; 199 | indices.reserve(N * N * 6); 200 | 201 | for (int x = 0; x < N; ++x) 202 | { 203 | for (int y = 0; y < N; ++y) 204 | { 205 | indices.push_back((x + 0) * N + (y + 0)); 206 | indices.push_back((x + 1) * N + (y + 0)); 207 | indices.push_back((x + 0) * N + (y + 1)); 208 | 209 | indices.push_back((x + 0) * N + (y + 1)); 210 | indices.push_back((x + 1) * N + (y + 0)); 211 | indices.push_back((x + 1) * N + (y + 1)); 212 | } 213 | } 214 | 215 | double bestvd = 0, bestid = 0; 216 | benchCodecs(vertices, indices, bestvd, bestid, verbose); 217 | 218 | double besto8 = 0, besto12 = 0, bestq12 = 0, bestexp = 0; 219 | benchFilters(8 * N * N, besto8, besto12, bestq12, bestexp, verbose); 220 | 221 | printf("Algorithm :\tvtx\tidx\toct8\toct12\tquat12\texp\n"); 222 | printf("Score (GB/s):\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\t%.2f\n", 223 | bestvd, bestid, besto8, besto12, bestq12, bestexp); 224 | } 225 | -------------------------------------------------------------------------------- /gltf/parseobj.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include "../extern/fast_obj.h" 5 | #include "../src/meshoptimizer.h" 6 | 7 | #include 8 | #include 9 | 10 | static void defaultFree(void*, void* p) 11 | { 12 | free(p); 13 | } 14 | 15 | static int textureIndex(const std::vector& textures, const char* name) 16 | { 17 | for (size_t i = 0; i < textures.size(); ++i) 18 | if (textures[i] == name) 19 | return int(i); 20 | 21 | return -1; 22 | } 23 | 24 | static cgltf_data* parseSceneObj(fastObjMesh* obj) 25 | { 26 | cgltf_data* data = (cgltf_data*)calloc(1, sizeof(cgltf_data)); 27 | data->memory.free_func = defaultFree; 28 | 29 | std::vector textures; 30 | 31 | for (unsigned int mi = 0; mi < obj->material_count; ++mi) 32 | { 33 | fastObjMaterial& om = obj->materials[mi]; 34 | 35 | if (om.map_Kd.name && textureIndex(textures, om.map_Kd.name) < 0) 36 | textures.push_back(om.map_Kd.name); 37 | } 38 | 39 | data->images = (cgltf_image*)calloc(textures.size(), sizeof(cgltf_image)); 40 | data->images_count = textures.size(); 41 | 42 | for (size_t i = 0; i < textures.size(); ++i) 43 | { 44 | data->images[i].uri = (char*)malloc(textures[i].size() + 1); 45 | strcpy(data->images[i].uri, textures[i].c_str()); 46 | } 47 | 48 | data->textures = (cgltf_texture*)calloc(textures.size(), sizeof(cgltf_texture)); 49 | data->textures_count = textures.size(); 50 | 51 | for (size_t i = 0; i < textures.size(); ++i) 52 | { 53 | data->textures[i].image = &data->images[i]; 54 | } 55 | 56 | data->materials = (cgltf_material*)calloc(obj->material_count, sizeof(cgltf_material)); 57 | data->materials_count = obj->material_count; 58 | 59 | for (unsigned int mi = 0; mi < obj->material_count; ++mi) 60 | { 61 | cgltf_material& gm = data->materials[mi]; 62 | fastObjMaterial& om = obj->materials[mi]; 63 | 64 | gm.has_pbr_metallic_roughness = true; 65 | gm.pbr_metallic_roughness.base_color_factor[0] = 1.0f; 66 | gm.pbr_metallic_roughness.base_color_factor[1] = 1.0f; 67 | gm.pbr_metallic_roughness.base_color_factor[2] = 1.0f; 68 | gm.pbr_metallic_roughness.base_color_factor[3] = 1.0f; 69 | gm.pbr_metallic_roughness.metallic_factor = 0.0f; 70 | gm.pbr_metallic_roughness.roughness_factor = 1.0f; 71 | 72 | gm.alpha_cutoff = 0.5f; 73 | 74 | if (om.map_Kd.name) 75 | { 76 | gm.pbr_metallic_roughness.base_color_texture.texture = &data->textures[textureIndex(textures, om.map_Kd.name)]; 77 | gm.pbr_metallic_roughness.base_color_texture.scale = 1.0f; 78 | 79 | gm.alpha_mode = (om.illum == 4 || om.illum == 6 || om.illum == 7 || om.illum == 9) ? cgltf_alpha_mode_mask : cgltf_alpha_mode_opaque; 80 | } 81 | 82 | if (om.map_d.name) 83 | { 84 | gm.alpha_mode = cgltf_alpha_mode_blend; 85 | } 86 | } 87 | 88 | data->scenes = (cgltf_scene*)calloc(1, sizeof(cgltf_scene)); 89 | data->scenes_count = 1; 90 | 91 | return data; 92 | } 93 | 94 | static void parseMeshObj(fastObjMesh* obj, unsigned int face_offset, unsigned int face_vertex_offset, unsigned int face_count, unsigned int face_vertex_count, unsigned int index_count, Mesh& mesh) 95 | { 96 | std::vector remap(face_vertex_count); 97 | size_t unique_vertices = meshopt_generateVertexRemap(remap.data(), nullptr, face_vertex_count, &obj->indices[face_vertex_offset], face_vertex_count, sizeof(fastObjIndex)); 98 | 99 | int pos_stream = 0; 100 | int nrm_stream = obj->normal_count > 1 ? 1 : -1; 101 | int tex_stream = obj->texcoord_count > 1 ? 1 + (nrm_stream >= 0) : -1; 102 | 103 | mesh.streams.resize(1 + (nrm_stream >= 0) + (tex_stream >= 0)); 104 | 105 | mesh.streams[pos_stream].type = cgltf_attribute_type_position; 106 | mesh.streams[pos_stream].data.resize(unique_vertices); 107 | 108 | if (nrm_stream >= 0) 109 | { 110 | mesh.streams[nrm_stream].type = cgltf_attribute_type_normal; 111 | mesh.streams[nrm_stream].data.resize(unique_vertices); 112 | } 113 | 114 | if (tex_stream >= 0) 115 | { 116 | mesh.streams[tex_stream].type = cgltf_attribute_type_texcoord; 117 | mesh.streams[tex_stream].data.resize(unique_vertices); 118 | } 119 | 120 | mesh.indices.resize(index_count); 121 | 122 | for (unsigned int vi = 0; vi < face_vertex_count; ++vi) 123 | { 124 | unsigned int target = remap[vi]; 125 | // TODO: this fills every target vertex multiple times 126 | 127 | fastObjIndex ii = obj->indices[face_vertex_offset + vi]; 128 | 129 | Attr p = {{obj->positions[ii.p * 3 + 0], obj->positions[ii.p * 3 + 1], obj->positions[ii.p * 3 + 2]}}; 130 | mesh.streams[pos_stream].data[target] = p; 131 | 132 | if (nrm_stream >= 0) 133 | { 134 | Attr n = {{obj->normals[ii.n * 3 + 0], obj->normals[ii.n * 3 + 1], obj->normals[ii.n * 3 + 2]}}; 135 | mesh.streams[nrm_stream].data[target] = n; 136 | } 137 | 138 | if (tex_stream >= 0) 139 | { 140 | Attr t = {{obj->texcoords[ii.t * 2 + 0], 1.f - obj->texcoords[ii.t * 2 + 1]}}; 141 | mesh.streams[tex_stream].data[target] = t; 142 | } 143 | } 144 | 145 | unsigned int vertex_offset = 0; 146 | unsigned int index_offset = 0; 147 | 148 | for (unsigned int fi = 0; fi < face_count; ++fi) 149 | { 150 | unsigned int face_vertices = obj->face_vertices[face_offset + fi]; 151 | 152 | for (unsigned int vi = 2; vi < face_vertices; ++vi) 153 | { 154 | size_t to = index_offset + (vi - 2) * 3; 155 | 156 | mesh.indices[to + 0] = remap[vertex_offset]; 157 | mesh.indices[to + 1] = remap[vertex_offset + vi - 1]; 158 | mesh.indices[to + 2] = remap[vertex_offset + vi]; 159 | } 160 | 161 | vertex_offset += face_vertices; 162 | index_offset += (face_vertices - 2) * 3; 163 | } 164 | 165 | assert(vertex_offset == face_vertex_count); 166 | assert(index_offset == index_count); 167 | } 168 | 169 | static void parseMeshesObj(fastObjMesh* obj, cgltf_data* data, std::vector& meshes) 170 | { 171 | unsigned int face_vertex_offset = 0; 172 | 173 | for (unsigned int face_offset = 0; face_offset < obj->face_count; ) 174 | { 175 | unsigned int mi = obj->face_materials[face_offset]; 176 | 177 | unsigned int face_count = 0; 178 | unsigned int face_vertex_count = 0; 179 | unsigned int index_count = 0; 180 | 181 | for (unsigned int fj = face_offset; fj < obj->face_count && obj->face_materials[fj] == mi; ++fj) 182 | { 183 | face_count += 1; 184 | face_vertex_count += obj->face_vertices[fj]; 185 | index_count += (obj->face_vertices[fj] - 2) * 3; 186 | } 187 | 188 | meshes.push_back(Mesh()); 189 | Mesh& mesh = meshes.back(); 190 | 191 | if (data->materials_count) 192 | { 193 | assert(mi < data->materials_count); 194 | mesh.material = &data->materials[mi]; 195 | } 196 | 197 | mesh.type = cgltf_primitive_type_triangles; 198 | mesh.targets = 0; 199 | 200 | parseMeshObj(obj, face_offset, face_vertex_offset, face_count, face_vertex_count, index_count, mesh); 201 | 202 | face_offset += face_count; 203 | face_vertex_offset += face_vertex_count; 204 | } 205 | } 206 | 207 | cgltf_data* parseObj(const char* path, std::vector& meshes, const char** error) 208 | { 209 | fastObjMesh* obj = fast_obj_read(path); 210 | 211 | if (!obj) 212 | { 213 | *error = "file not found"; 214 | return 0; 215 | } 216 | 217 | cgltf_data* data = parseSceneObj(obj); 218 | 219 | parseMeshesObj(obj, data, meshes); 220 | 221 | fast_obj_destroy(obj); 222 | 223 | return data; 224 | } 225 | -------------------------------------------------------------------------------- /gltf/image.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include 5 | 6 | static const char* kMimeTypes[][2] = { 7 | {"image/jpeg", ".jpg"}, 8 | {"image/jpeg", ".jpeg"}, 9 | {"image/png", ".png"}, 10 | }; 11 | 12 | static const char* inferMimeType(const char* path) 13 | { 14 | std::string ext = getExtension(path); 15 | 16 | for (size_t i = 0; i < sizeof(kMimeTypes) / sizeof(kMimeTypes[0]); ++i) 17 | if (ext == kMimeTypes[i][1]) 18 | return kMimeTypes[i][0]; 19 | 20 | return ""; 21 | } 22 | 23 | static bool parseDataUri(const char* uri, std::string& mime_type, std::string& result) 24 | { 25 | if (strncmp(uri, "data:", 5) == 0) 26 | { 27 | const char* comma = strchr(uri, ','); 28 | 29 | if (comma && comma - uri >= 7 && strncmp(comma - 7, ";base64", 7) == 0) 30 | { 31 | const char* base64 = comma + 1; 32 | size_t base64_size = strlen(base64); 33 | size_t size = base64_size - base64_size / 4; 34 | 35 | if (base64_size >= 2) 36 | { 37 | size -= base64[base64_size - 2] == '='; 38 | size -= base64[base64_size - 1] == '='; 39 | } 40 | 41 | void* data = 0; 42 | 43 | cgltf_options options = {}; 44 | cgltf_result res = cgltf_load_buffer_base64(&options, size, base64, &data); 45 | 46 | if (res != cgltf_result_success) 47 | return false; 48 | 49 | mime_type = std::string(uri + 5, comma - 7); 50 | result = std::string(static_cast(data), size); 51 | 52 | free(data); 53 | 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | 61 | bool readImage(const cgltf_image& image, const char* input_path, std::string& data, std::string& mime_type) 62 | { 63 | if (image.uri && parseDataUri(image.uri, mime_type, data)) 64 | { 65 | return true; 66 | } 67 | else if (image.buffer_view && image.buffer_view->buffer->data && image.mime_type) 68 | { 69 | const cgltf_buffer_view* view = image.buffer_view; 70 | 71 | data.assign(static_cast(view->buffer->data) + view->offset, view->size); 72 | mime_type = image.mime_type; 73 | return true; 74 | } 75 | else if (image.uri && *image.uri) 76 | { 77 | std::string path = image.uri; 78 | 79 | cgltf_decode_uri(&path[0]); 80 | path.resize(strlen(&path[0])); 81 | 82 | mime_type = image.mime_type ? image.mime_type : inferMimeType(path.c_str()); 83 | 84 | return readFile(getFullPath(path.c_str(), input_path).c_str(), data); 85 | } 86 | else 87 | { 88 | return false; 89 | } 90 | } 91 | 92 | static int readInt16(const std::string& data, size_t offset) 93 | { 94 | return (unsigned char)data[offset] * 256 + (unsigned char)data[offset + 1]; 95 | } 96 | 97 | static int readInt32(const std::string& data, size_t offset) 98 | { 99 | return (unsigned((unsigned char)data[offset]) << 24) | 100 | (unsigned((unsigned char)data[offset + 1]) << 16) | 101 | (unsigned((unsigned char)data[offset + 2]) << 8) | 102 | unsigned((unsigned char)data[offset + 3]); 103 | } 104 | 105 | static bool getDimensionsPng(const std::string& data, int& width, int& height) 106 | { 107 | if (data.size() < 8 + 8 + 13 + 4) 108 | return false; 109 | 110 | const char* signature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; 111 | if (data.compare(0, 8, signature) != 0) 112 | return false; 113 | 114 | if (data.compare(12, 4, "IHDR") != 0) 115 | return false; 116 | 117 | width = readInt32(data, 16); 118 | height = readInt32(data, 20); 119 | 120 | return true; 121 | } 122 | 123 | static bool getDimensionsJpeg(const std::string& data, int& width, int& height) 124 | { 125 | size_t offset = 0; 126 | 127 | // note, this can stop parsing before reaching the end but we stop at SOF anyway 128 | while (offset + 4 <= data.size()) 129 | { 130 | if (data[offset] != '\xff') 131 | return false; 132 | 133 | char marker = data[offset + 1]; 134 | 135 | if (marker == '\xff') 136 | { 137 | offset++; 138 | continue; // padding 139 | } 140 | 141 | // d0..d9 correspond to SOI, RSTn, EOI 142 | if (marker == 0 || unsigned(marker - '\xd0') <= 9) 143 | { 144 | offset += 2; 145 | continue; // no payload 146 | } 147 | 148 | // c0..c1 correspond to SOF0, SOF1 149 | if (marker == '\xc0' || marker == '\xc2') 150 | { 151 | if (offset + 10 > data.size()) 152 | return false; 153 | 154 | width = readInt16(data, offset + 7); 155 | height = readInt16(data, offset + 5); 156 | 157 | return true; 158 | } 159 | 160 | offset += 2 + readInt16(data, offset + 2); 161 | } 162 | 163 | return false; 164 | } 165 | 166 | static bool hasTransparencyPng(const std::string& data) 167 | { 168 | if (data.size() < 8 + 8 + 13 + 4) 169 | return false; 170 | 171 | const char* signature = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"; 172 | if (data.compare(0, 8, signature) != 0) 173 | return false; 174 | 175 | if (data.compare(12, 4, "IHDR") != 0) 176 | return false; 177 | 178 | int ctype = data[25]; 179 | 180 | if (ctype != 3) 181 | return ctype == 4 || ctype == 6; 182 | 183 | size_t offset = 8; // reparse IHDR chunk for simplicity 184 | 185 | while (offset + 12 <= data.size()) 186 | { 187 | int length = readInt32(data, offset); 188 | 189 | if (length < 0) 190 | return false; 191 | 192 | if (data.compare(offset + 4, 4, "tRNS") == 0) 193 | return true; 194 | 195 | offset += 12 + length; 196 | } 197 | 198 | return false; 199 | } 200 | 201 | bool hasAlpha(const std::string& data, const char* mime_type) 202 | { 203 | if (strcmp(mime_type, "image/png") == 0) 204 | return hasTransparencyPng(data); 205 | else 206 | return false; 207 | } 208 | 209 | bool getDimensions(const std::string& data, const char* mime_type, int& width, int& height) 210 | { 211 | if (strcmp(mime_type, "image/png") == 0) 212 | return getDimensionsPng(data, width, height); 213 | if (strcmp(mime_type, "image/jpeg") == 0) 214 | return getDimensionsJpeg(data, width, height); 215 | 216 | return false; 217 | } 218 | 219 | static int roundPow2(int value) 220 | { 221 | int result = 1; 222 | 223 | while (result < value) 224 | result <<= 1; 225 | 226 | // to prevent odd texture sizes from increasing the size too much, we round to nearest power of 2 above a certain size 227 | if (value > 128 && result * 3 / 4 > value) 228 | result >>= 1; 229 | 230 | return result; 231 | } 232 | 233 | static int roundBlock(int value, bool pow2) 234 | { 235 | if (value == 0) 236 | return 4; 237 | 238 | if (pow2 && value > 4) 239 | return roundPow2(value); 240 | 241 | return (value + 3) & ~3; 242 | } 243 | 244 | void adjustDimensions(int& width, int& height, const Settings& settings) 245 | { 246 | width = int(width * settings.texture_scale); 247 | height = int(height * settings.texture_scale); 248 | 249 | if (settings.texture_limit && (width > settings.texture_limit || height > settings.texture_limit)) 250 | { 251 | float limit_scale = float(settings.texture_limit) / float(width > height ? width : height); 252 | 253 | width = int(width * limit_scale); 254 | height = int(height * limit_scale); 255 | } 256 | 257 | width = roundBlock(width, settings.texture_pow2); 258 | height = roundBlock(height, settings.texture_pow2); 259 | } 260 | 261 | const char* mimeExtension(const char* mime_type) 262 | { 263 | for (size_t i = 0; i < sizeof(kMimeTypes) / sizeof(kMimeTypes[0]); ++i) 264 | if (strcmp(kMimeTypes[i][0], mime_type) == 0) 265 | return kMimeTypes[i][1]; 266 | 267 | return ".raw"; 268 | } 269 | -------------------------------------------------------------------------------- /src/overdrawanalyzer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // This work is based on: 9 | // Nicolas Capens. Advanced Rasterization. 2004 10 | namespace meshopt 11 | { 12 | 13 | const int kViewport = 256; 14 | 15 | struct OverdrawBuffer 16 | { 17 | float z[kViewport][kViewport][2]; 18 | unsigned int overdraw[kViewport][kViewport][2]; 19 | }; 20 | 21 | #ifndef min 22 | #define min(a, b) ((a) < (b) ? (a) : (b)) 23 | #endif 24 | 25 | #ifndef max 26 | #define max(a, b) ((a) > (b) ? (a) : (b)) 27 | #endif 28 | 29 | static float computeDepthGradients(float& dzdx, float& dzdy, float x1, float y1, float z1, float x2, float y2, float z2, float x3, float y3, float z3) 30 | { 31 | // z2 = z1 + dzdx * (x2 - x1) + dzdy * (y2 - y1) 32 | // z3 = z1 + dzdx * (x3 - x1) + dzdy * (y3 - y1) 33 | // (x2-x1 y2-y1)(dzdx) = (z2-z1) 34 | // (x3-x1 y3-y1)(dzdy) (z3-z1) 35 | // we'll solve it with Cramer's rule 36 | float det = (x2 - x1) * (y3 - y1) - (y2 - y1) * (x3 - x1); 37 | float invdet = (det == 0) ? 0 : 1 / det; 38 | 39 | dzdx = (z2 - z1) * (y3 - y1) - (y2 - y1) * (z3 - z1) * invdet; 40 | dzdy = (x2 - x1) * (z3 - z1) - (z2 - z1) * (x3 - x1) * invdet; 41 | 42 | return det; 43 | } 44 | 45 | // half-space fixed point triangle rasterizer 46 | static void rasterize(OverdrawBuffer* buffer, float v1x, float v1y, float v1z, float v2x, float v2y, float v2z, float v3x, float v3y, float v3z) 47 | { 48 | // compute depth gradients 49 | float DZx, DZy; 50 | float det = computeDepthGradients(DZx, DZy, v1x, v1y, v1z, v2x, v2y, v2z, v3x, v3y, v3z); 51 | int sign = det > 0; 52 | 53 | // flip backfacing triangles to simplify rasterization logic 54 | if (sign) 55 | { 56 | // flipping v2 & v3 preserves depth gradients since they're based on v1 57 | float t; 58 | t = v2x, v2x = v3x, v3x = t; 59 | t = v2y, v2y = v3y, v3y = t; 60 | t = v2z, v2z = v3z, v3z = t; 61 | 62 | // flip depth since we rasterize backfacing triangles to second buffer with reverse Z; only v1z is used below 63 | v1z = kViewport - v1z; 64 | DZx = -DZx; 65 | DZy = -DZy; 66 | } 67 | 68 | // coordinates, 28.4 fixed point 69 | int X1 = int(16.0f * v1x + 0.5f); 70 | int X2 = int(16.0f * v2x + 0.5f); 71 | int X3 = int(16.0f * v3x + 0.5f); 72 | 73 | int Y1 = int(16.0f * v1y + 0.5f); 74 | int Y2 = int(16.0f * v2y + 0.5f); 75 | int Y3 = int(16.0f * v3y + 0.5f); 76 | 77 | // bounding rectangle, clipped against viewport 78 | // since we rasterize pixels with covered centers, min >0.5 should round up 79 | // as for max, due to top-left filling convention we will never rasterize right/bottom edges 80 | // so max >= 0.5 should round down 81 | int minx = max((min(X1, min(X2, X3)) + 7) >> 4, 0); 82 | int maxx = min((max(X1, max(X2, X3)) + 7) >> 4, kViewport); 83 | int miny = max((min(Y1, min(Y2, Y3)) + 7) >> 4, 0); 84 | int maxy = min((max(Y1, max(Y2, Y3)) + 7) >> 4, kViewport); 85 | 86 | // deltas, 28.4 fixed point 87 | int DX12 = X1 - X2; 88 | int DX23 = X2 - X3; 89 | int DX31 = X3 - X1; 90 | 91 | int DY12 = Y1 - Y2; 92 | int DY23 = Y2 - Y3; 93 | int DY31 = Y3 - Y1; 94 | 95 | // fill convention correction 96 | int TL1 = DY12 < 0 || (DY12 == 0 && DX12 > 0); 97 | int TL2 = DY23 < 0 || (DY23 == 0 && DX23 > 0); 98 | int TL3 = DY31 < 0 || (DY31 == 0 && DX31 > 0); 99 | 100 | // half edge equations, 24.8 fixed point 101 | // note that we offset minx/miny by half pixel since we want to rasterize pixels with covered centers 102 | int FX = (minx << 4) + 8; 103 | int FY = (miny << 4) + 8; 104 | int CY1 = DX12 * (FY - Y1) - DY12 * (FX - X1) + TL1 - 1; 105 | int CY2 = DX23 * (FY - Y2) - DY23 * (FX - X2) + TL2 - 1; 106 | int CY3 = DX31 * (FY - Y3) - DY31 * (FX - X3) + TL3 - 1; 107 | float ZY = v1z + (DZx * float(FX - X1) + DZy * float(FY - Y1)) * (1 / 16.f); 108 | 109 | for (int y = miny; y < maxy; y++) 110 | { 111 | int CX1 = CY1; 112 | int CX2 = CY2; 113 | int CX3 = CY3; 114 | float ZX = ZY; 115 | 116 | for (int x = minx; x < maxx; x++) 117 | { 118 | // check if all CXn are non-negative 119 | if ((CX1 | CX2 | CX3) >= 0) 120 | { 121 | if (ZX >= buffer->z[y][x][sign]) 122 | { 123 | buffer->z[y][x][sign] = ZX; 124 | buffer->overdraw[y][x][sign]++; 125 | } 126 | } 127 | 128 | // signed left shift is UB for negative numbers so use unsigned-signed casts 129 | CX1 -= int(unsigned(DY12) << 4); 130 | CX2 -= int(unsigned(DY23) << 4); 131 | CX3 -= int(unsigned(DY31) << 4); 132 | ZX += DZx; 133 | } 134 | 135 | // signed left shift is UB for negative numbers so use unsigned-signed casts 136 | CY1 += int(unsigned(DX12) << 4); 137 | CY2 += int(unsigned(DX23) << 4); 138 | CY3 += int(unsigned(DX31) << 4); 139 | ZY += DZy; 140 | } 141 | } 142 | 143 | } // namespace meshopt 144 | 145 | meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) 146 | { 147 | using namespace meshopt; 148 | 149 | assert(index_count % 3 == 0); 150 | assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); 151 | assert(vertex_positions_stride % sizeof(float) == 0); 152 | 153 | meshopt_Allocator allocator; 154 | 155 | size_t vertex_stride_float = vertex_positions_stride / sizeof(float); 156 | 157 | meshopt_OverdrawStatistics result = {}; 158 | 159 | float minv[3] = {FLT_MAX, FLT_MAX, FLT_MAX}; 160 | float maxv[3] = {-FLT_MAX, -FLT_MAX, -FLT_MAX}; 161 | 162 | for (size_t i = 0; i < vertex_count; ++i) 163 | { 164 | const float* v = vertex_positions + i * vertex_stride_float; 165 | 166 | for (int j = 0; j < 3; ++j) 167 | { 168 | minv[j] = min(minv[j], v[j]); 169 | maxv[j] = max(maxv[j], v[j]); 170 | } 171 | } 172 | 173 | float extent = max(maxv[0] - minv[0], max(maxv[1] - minv[1], maxv[2] - minv[2])); 174 | float scale = kViewport / extent; 175 | 176 | float* triangles = allocator.allocate(index_count * 3); 177 | 178 | for (size_t i = 0; i < index_count; ++i) 179 | { 180 | unsigned int index = indices[i]; 181 | assert(index < vertex_count); 182 | 183 | const float* v = vertex_positions + index * vertex_stride_float; 184 | 185 | triangles[i * 3 + 0] = (v[0] - minv[0]) * scale; 186 | triangles[i * 3 + 1] = (v[1] - minv[1]) * scale; 187 | triangles[i * 3 + 2] = (v[2] - minv[2]) * scale; 188 | } 189 | 190 | OverdrawBuffer* buffer = allocator.allocate(1); 191 | 192 | for (int axis = 0; axis < 3; ++axis) 193 | { 194 | memset(buffer, 0, sizeof(OverdrawBuffer)); 195 | 196 | for (size_t i = 0; i < index_count; i += 3) 197 | { 198 | const float* vn0 = &triangles[3 * (i + 0)]; 199 | const float* vn1 = &triangles[3 * (i + 1)]; 200 | const float* vn2 = &triangles[3 * (i + 2)]; 201 | 202 | switch (axis) 203 | { 204 | case 0: 205 | rasterize(buffer, vn0[2], vn0[1], vn0[0], vn1[2], vn1[1], vn1[0], vn2[2], vn2[1], vn2[0]); 206 | break; 207 | case 1: 208 | rasterize(buffer, vn0[0], vn0[2], vn0[1], vn1[0], vn1[2], vn1[1], vn2[0], vn2[2], vn2[1]); 209 | break; 210 | case 2: 211 | rasterize(buffer, vn0[1], vn0[0], vn0[2], vn1[1], vn1[0], vn1[2], vn2[1], vn2[0], vn2[2]); 212 | break; 213 | } 214 | } 215 | 216 | for (int y = 0; y < kViewport; ++y) 217 | for (int x = 0; x < kViewport; ++x) 218 | for (int s = 0; s < 2; ++s) 219 | { 220 | unsigned int overdraw = buffer->overdraw[y][x][s]; 221 | 222 | result.pixels_covered += overdraw > 0; 223 | result.pixels_shaded += overdraw; 224 | } 225 | } 226 | 227 | result.overdraw = result.pixels_covered ? float(result.pixels_shaded) / float(result.pixels_covered) : 0.f; 228 | 229 | return result; 230 | } 231 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS+=-r -j 2 | 3 | config=debug 4 | files=demo/pirate.obj 5 | 6 | BUILD=build/$(config) 7 | 8 | LIBRARY_SOURCES=$(wildcard src/*.cpp) 9 | LIBRARY_OBJECTS=$(LIBRARY_SOURCES:%=$(BUILD)/%.o) 10 | 11 | DEMO_SOURCES=$(wildcard demo/*.c demo/*.cpp) tools/meshloader.cpp 12 | DEMO_OBJECTS=$(DEMO_SOURCES:%=$(BUILD)/%.o) 13 | 14 | GLTFPACK_SOURCES=$(wildcard gltf/*.cpp) tools/meshloader.cpp 15 | GLTFPACK_OBJECTS=$(GLTFPACK_SOURCES:%=$(BUILD)/%.o) 16 | 17 | OBJECTS=$(LIBRARY_OBJECTS) $(DEMO_OBJECTS) $(GLTFPACK_OBJECTS) 18 | 19 | LIBRARY=$(BUILD)/libmeshoptimizer.a 20 | DEMO=$(BUILD)/meshoptimizer 21 | 22 | CFLAGS=-g -Wall -Wextra -Werror -std=c89 23 | CXXFLAGS=-g -Wall -Wextra -Wshadow -Wno-missing-field-initializers -Werror -std=c++98 24 | LDFLAGS= 25 | 26 | $(GLTFPACK_OBJECTS): CXXFLAGS+=-std=c++11 27 | 28 | ifdef BASISU 29 | $(GLTFPACK_OBJECTS): CXXFLAGS+=-DWITH_BASISU 30 | $(BUILD)/gltf/basis%.cpp.o: CXXFLAGS+=-I$(BASISU) 31 | gltfpack: LDFLAGS+=-lpthread 32 | 33 | ifeq ($(HOSTTYPE),x86_64) 34 | $(BUILD)/gltf/basislib.cpp.o: CXXFLAGS+=-msse4.1 35 | endif 36 | endif 37 | 38 | WASMCC?=$(WASI_SDK)/bin/clang++ 39 | WASIROOT?=$(WASI_SDK)/share/wasi-sysroot 40 | 41 | WASM_FLAGS=--target=wasm32-wasi --sysroot=$(WASIROOT) 42 | WASM_FLAGS+=-O3 -DNDEBUG -nostartfiles -nostdlib -Wl,--no-entry -Wl,-s 43 | WASM_FLAGS+=-fno-slp-vectorize -fno-vectorize -fno-unroll-loops 44 | WASM_FLAGS+=-Wl,-z -Wl,stack-size=24576 -Wl,--initial-memory=65536 45 | WASM_EXPORT_PREFIX=-Wl,--export 46 | 47 | WASM_DECODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp tools/wasmstubs.cpp 48 | WASM_DECODER_EXPORTS=meshopt_decodeVertexBuffer meshopt_decodeIndexBuffer meshopt_decodeIndexSequence meshopt_decodeFilterOct meshopt_decodeFilterQuat meshopt_decodeFilterExp sbrk __wasm_call_ctors 49 | 50 | WASM_ENCODER_SOURCES=src/vertexcodec.cpp src/indexcodec.cpp src/vertexfilter.cpp src/vcacheoptimizer.cpp src/vfetchoptimizer.cpp tools/wasmstubs.cpp 51 | WASM_ENCODER_EXPORTS=meshopt_encodeVertexBuffer meshopt_encodeVertexBufferBound meshopt_encodeIndexBuffer meshopt_encodeIndexBufferBound meshopt_encodeIndexSequence meshopt_encodeIndexSequenceBound meshopt_encodeVertexVersion meshopt_encodeIndexVersion meshopt_encodeFilterOct meshopt_encodeFilterQuat meshopt_encodeFilterExp meshopt_optimizeVertexCache meshopt_optimizeVertexCacheStrip meshopt_optimizeVertexFetchRemap sbrk __wasm_call_ctors 52 | 53 | WASM_SIMPLIFIER_SOURCES=src/simplifier.cpp src/vfetchoptimizer.cpp tools/wasmstubs.cpp 54 | WASM_SIMPLIFIER_EXPORTS=meshopt_simplify meshopt_simplifyScale meshopt_optimizeVertexFetchRemap sbrk __wasm_call_ctors 55 | 56 | ifeq ($(config),iphone) 57 | IPHONESDK=/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk 58 | CFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) 59 | CXXFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -stdlib=libc++ 60 | LDFLAGS+=-arch armv7 -arch arm64 -isysroot $(IPHONESDK) -L $(IPHONESDK)/usr/lib -mios-version-min=7.0 61 | endif 62 | 63 | ifeq ($(config),trace) 64 | CXXFLAGS+=-DTRACE=1 65 | endif 66 | 67 | ifeq ($(config),scalar) 68 | CXXFLAGS+=-O3 -DNDEBUG -DMESHOPTIMIZER_NO_SIMD 69 | endif 70 | 71 | ifeq ($(config),release) 72 | CXXFLAGS+=-O3 -DNDEBUG 73 | endif 74 | 75 | ifeq ($(config),coverage) 76 | CXXFLAGS+=-coverage 77 | LDFLAGS+=-coverage 78 | endif 79 | 80 | ifeq ($(config),sanitize) 81 | CXXFLAGS+=-fsanitize=address,undefined -fno-sanitize-recover=all 82 | LDFLAGS+=-fsanitize=address,undefined 83 | endif 84 | 85 | ifeq ($(config),analyze) 86 | CXXFLAGS+=--analyze 87 | endif 88 | 89 | all: $(DEMO) 90 | 91 | test: $(DEMO) 92 | $(DEMO) $(files) 93 | 94 | check: $(DEMO) 95 | $(DEMO) 96 | 97 | dev: $(DEMO) 98 | $(DEMO) -d $(files) 99 | 100 | format: 101 | clang-format -i $(LIBRARY_SOURCES) $(DEMO_SOURCES) $(GLTFPACK_SOURCES) 102 | 103 | js: js/meshopt_decoder.js js/meshopt_decoder.module.js js/meshopt_encoder.js js/meshopt_encoder.module.js js/meshopt_simplifier.js js/meshopt_simplifier.module.js 104 | 105 | gltfpack: $(BUILD)/gltfpack 106 | ln -fs $^ $@ 107 | 108 | $(BUILD)/gltfpack: $(GLTFPACK_OBJECTS) $(LIBRARY) 109 | $(CXX) $^ $(LDFLAGS) -o $@ 110 | 111 | gltfpack.wasm: gltf/library.wasm 112 | 113 | gltf/library.wasm: ${LIBRARY_SOURCES} ${GLTFPACK_SOURCES} tools/meshloader.cpp 114 | $(WASMCC) $^ -o $@ -Os -DNDEBUG --target=wasm32-wasi --sysroot=$(WASIROOT) -nostartfiles -Wl,--no-entry -Wl,--export=pack -Wl,--export=malloc -Wl,--export=free -Wl,--export=__wasm_call_ctors -Wl,-s -Wl,--allow-undefined-file=gltf/wasistubs.txt 115 | 116 | build/decoder_base.wasm: $(WASM_DECODER_SOURCES) 117 | @mkdir -p build 118 | $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@ 119 | 120 | build/decoder_simd.wasm: $(WASM_DECODER_SOURCES) 121 | @mkdir -p build 122 | $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_DECODER_EXPORTS)) -o $@ -msimd128 -mbulk-memory 123 | 124 | build/encoder.wasm: $(WASM_ENCODER_SOURCES) 125 | @mkdir -p build 126 | $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_ENCODER_EXPORTS)) -lc -o $@ 127 | 128 | build/simplifier.wasm: $(WASM_SIMPLIFIER_SOURCES) 129 | @mkdir -p build 130 | $(WASMCC) $^ $(WASM_FLAGS) $(patsubst %,$(WASM_EXPORT_PREFIX)=%,$(WASM_SIMPLIFIER_EXPORTS)) -lc -o $@ 131 | 132 | js/meshopt_decoder.js: build/decoder_base.wasm build/decoder_simd.wasm tools/wasmpack.py 133 | sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@ 134 | sed -i "s#\(var wasm_base = \)\".*\";#\\1\"$$(cat build/decoder_base.wasm | python3 tools/wasmpack.py)\";#" $@ 135 | sed -i "s#\(var wasm_simd = \)\".*\";#\\1\"$$(cat build/decoder_simd.wasm | python3 tools/wasmpack.py)\";#" $@ 136 | 137 | js/meshopt_encoder.js: build/encoder.wasm tools/wasmpack.py 138 | sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@ 139 | sed -i "s#\(var wasm = \)\".*\";#\\1\"$$(cat build/encoder.wasm | python3 tools/wasmpack.py)\";#" $@ 140 | 141 | js/meshopt_simplifier.js: build/simplifier.wasm tools/wasmpack.py 142 | sed -i "s#Built with clang.*#Built with $$($(WASMCC) --version | head -n 1 | sed 's/\s\+(.*//')#" $@ 143 | sed -i "s#\(var wasm = \)\".*\";#\\1\"$$(cat build/simplifier.wasm | python3 tools/wasmpack.py)\";#" $@ 144 | 145 | js/%.module.js: js/%.js 146 | sed '/UMD-style export/,$$d' <$< >$@ 147 | sed -n "s#\s*module.exports = \(.*\);#export { \\1 };#p" <$< >>$@ 148 | 149 | $(DEMO): $(DEMO_OBJECTS) $(LIBRARY) 150 | $(CXX) $^ $(LDFLAGS) -o $@ 151 | 152 | vcachetuner: tools/vcachetuner.cpp $(BUILD)/tools/meshloader.cpp.o $(BUILD)/demo/miniz.cpp.o $(LIBRARY) 153 | $(CXX) $^ -fopenmp $(CXXFLAGS) -std=c++11 $(LDFLAGS) -o $@ 154 | 155 | codecbench: tools/codecbench.cpp $(LIBRARY) 156 | $(CXX) $^ $(CXXFLAGS) $(LDFLAGS) -o $@ 157 | 158 | codecbench.js: tools/codecbench.cpp ${LIBRARY_SOURCES} 159 | emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -o $@ 160 | 161 | codecbench-simd.js: tools/codecbench.cpp ${LIBRARY_SOURCES} 162 | emcc $^ -O3 -g -DNDEBUG -s TOTAL_MEMORY=268435456 -s SINGLE_FILE=1 -msimd128 -o $@ 163 | 164 | codecbench.wasm: tools/codecbench.cpp ${LIBRARY_SOURCES} 165 | $(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -o $@ 166 | 167 | codecbench-simd.wasm: tools/codecbench.cpp ${LIBRARY_SOURCES} 168 | $(WASMCC) $^ -fno-exceptions --target=wasm32-wasi --sysroot=$(WASIROOT) -lc++ -lc++abi -O3 -g -DNDEBUG -msimd128 -o $@ 169 | 170 | codecfuzz: tools/codecfuzz.cpp src/vertexcodec.cpp src/indexcodec.cpp 171 | $(CXX) $^ -fsanitize=fuzzer,address,undefined -O1 -g -o $@ 172 | 173 | $(LIBRARY): $(LIBRARY_OBJECTS) 174 | ar rcs $@ $^ 175 | 176 | $(BUILD)/%.cpp.o: %.cpp 177 | @mkdir -p $(dir $@) 178 | $(CXX) $< $(CXXFLAGS) -c -MMD -MP -o $@ 179 | 180 | $(BUILD)/%.c.o: %.c 181 | @mkdir -p $(dir $@) 182 | $(CC) $< $(CFLAGS) -c -MMD -MP -o $@ 183 | 184 | -include $(OBJECTS:.o=.d) 185 | 186 | clean: 187 | rm -rf $(BUILD) 188 | 189 | .PHONY: all clean format js 190 | -------------------------------------------------------------------------------- /src/stripifier.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // This work is based on: 9 | // Francine Evans, Steven Skiena and Amitabh Varshney. Optimizing Triangle Strips for Fast Rendering. 1996 10 | namespace meshopt 11 | { 12 | 13 | static unsigned int findStripFirst(const unsigned int buffer[][3], unsigned int buffer_size, const unsigned int* valence) 14 | { 15 | unsigned int index = 0; 16 | unsigned int iv = ~0u; 17 | 18 | for (size_t i = 0; i < buffer_size; ++i) 19 | { 20 | unsigned int va = valence[buffer[i][0]], vb = valence[buffer[i][1]], vc = valence[buffer[i][2]]; 21 | unsigned int v = (va < vb && va < vc) ? va : (vb < vc) ? vb : vc; 22 | 23 | if (v < iv) 24 | { 25 | index = unsigned(i); 26 | iv = v; 27 | } 28 | } 29 | 30 | return index; 31 | } 32 | 33 | static int findStripNext(const unsigned int buffer[][3], unsigned int buffer_size, unsigned int e0, unsigned int e1) 34 | { 35 | for (size_t i = 0; i < buffer_size; ++i) 36 | { 37 | unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2]; 38 | 39 | if (e0 == a && e1 == b) 40 | return (int(i) << 2) | 2; 41 | else if (e0 == b && e1 == c) 42 | return (int(i) << 2) | 0; 43 | else if (e0 == c && e1 == a) 44 | return (int(i) << 2) | 1; 45 | } 46 | 47 | return -1; 48 | } 49 | 50 | } // namespace meshopt 51 | 52 | size_t meshopt_stripify(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int restart_index) 53 | { 54 | assert(destination != indices); 55 | assert(index_count % 3 == 0); 56 | 57 | using namespace meshopt; 58 | 59 | meshopt_Allocator allocator; 60 | 61 | const size_t buffer_capacity = 8; 62 | 63 | unsigned int buffer[buffer_capacity][3] = {}; 64 | unsigned int buffer_size = 0; 65 | 66 | size_t index_offset = 0; 67 | 68 | unsigned int strip[2] = {}; 69 | unsigned int parity = 0; 70 | 71 | size_t strip_size = 0; 72 | 73 | // compute vertex valence; this is used to prioritize starting triangle for strips 74 | unsigned int* valence = allocator.allocate(vertex_count); 75 | memset(valence, 0, vertex_count * sizeof(unsigned int)); 76 | 77 | for (size_t i = 0; i < index_count; ++i) 78 | { 79 | unsigned int index = indices[i]; 80 | assert(index < vertex_count); 81 | 82 | valence[index]++; 83 | } 84 | 85 | int next = -1; 86 | 87 | while (buffer_size > 0 || index_offset < index_count) 88 | { 89 | assert(next < 0 || (size_t(next >> 2) < buffer_size && (next & 3) < 3)); 90 | 91 | // fill triangle buffer 92 | while (buffer_size < buffer_capacity && index_offset < index_count) 93 | { 94 | buffer[buffer_size][0] = indices[index_offset + 0]; 95 | buffer[buffer_size][1] = indices[index_offset + 1]; 96 | buffer[buffer_size][2] = indices[index_offset + 2]; 97 | 98 | buffer_size++; 99 | index_offset += 3; 100 | } 101 | 102 | assert(buffer_size > 0); 103 | 104 | if (next >= 0) 105 | { 106 | unsigned int i = next >> 2; 107 | unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2]; 108 | unsigned int v = buffer[i][next & 3]; 109 | 110 | // ordered removal from the buffer 111 | memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0])); 112 | buffer_size--; 113 | 114 | // update vertex valences for strip start heuristic 115 | valence[a]--; 116 | valence[b]--; 117 | valence[c]--; 118 | 119 | // find next triangle (note that edge order flips on every iteration) 120 | // in some cases we need to perform a swap to pick a different outgoing triangle edge 121 | // for [a b c], the default strip edge is [b c], but we might want to use [a c] 122 | int cont = findStripNext(buffer, buffer_size, parity ? strip[1] : v, parity ? v : strip[1]); 123 | int swap = cont < 0 ? findStripNext(buffer, buffer_size, parity ? v : strip[0], parity ? strip[0] : v) : -1; 124 | 125 | if (cont < 0 && swap >= 0) 126 | { 127 | // [a b c] => [a b a c] 128 | destination[strip_size++] = strip[0]; 129 | destination[strip_size++] = v; 130 | 131 | // next strip has same winding 132 | // ? a b => b a v 133 | strip[1] = v; 134 | 135 | next = swap; 136 | } 137 | else 138 | { 139 | // emit the next vertex in the strip 140 | destination[strip_size++] = v; 141 | 142 | // next strip has flipped winding 143 | strip[0] = strip[1]; 144 | strip[1] = v; 145 | parity ^= 1; 146 | 147 | next = cont; 148 | } 149 | } 150 | else 151 | { 152 | // if we didn't find anything, we need to find the next new triangle 153 | // we use a heuristic to maximize the strip length 154 | unsigned int i = findStripFirst(buffer, buffer_size, &valence[0]); 155 | unsigned int a = buffer[i][0], b = buffer[i][1], c = buffer[i][2]; 156 | 157 | // ordered removal from the buffer 158 | memmove(buffer[i], buffer[i + 1], (buffer_size - i - 1) * sizeof(buffer[0])); 159 | buffer_size--; 160 | 161 | // update vertex valences for strip start heuristic 162 | valence[a]--; 163 | valence[b]--; 164 | valence[c]--; 165 | 166 | // we need to pre-rotate the triangle so that we will find a match in the existing buffer on the next iteration 167 | int ea = findStripNext(buffer, buffer_size, c, b); 168 | int eb = findStripNext(buffer, buffer_size, a, c); 169 | int ec = findStripNext(buffer, buffer_size, b, a); 170 | 171 | // in some cases we can have several matching edges; since we can pick any edge, we pick the one with the smallest 172 | // triangle index in the buffer. this reduces the effect of stripification on ACMR and additionally - for unclear 173 | // reasons - slightly improves the stripification efficiency 174 | int mine = INT_MAX; 175 | mine = (ea >= 0 && mine > ea) ? ea : mine; 176 | mine = (eb >= 0 && mine > eb) ? eb : mine; 177 | mine = (ec >= 0 && mine > ec) ? ec : mine; 178 | 179 | if (ea == mine) 180 | { 181 | // keep abc 182 | next = ea; 183 | } 184 | else if (eb == mine) 185 | { 186 | // abc -> bca 187 | unsigned int t = a; 188 | a = b, b = c, c = t; 189 | 190 | next = eb; 191 | } 192 | else if (ec == mine) 193 | { 194 | // abc -> cab 195 | unsigned int t = c; 196 | c = b, b = a, a = t; 197 | 198 | next = ec; 199 | } 200 | 201 | if (restart_index) 202 | { 203 | if (strip_size) 204 | destination[strip_size++] = restart_index; 205 | 206 | destination[strip_size++] = a; 207 | destination[strip_size++] = b; 208 | destination[strip_size++] = c; 209 | 210 | // new strip always starts with the same edge winding 211 | strip[0] = b; 212 | strip[1] = c; 213 | parity = 1; 214 | } 215 | else 216 | { 217 | if (strip_size) 218 | { 219 | // connect last strip using degenerate triangles 220 | destination[strip_size++] = strip[1]; 221 | destination[strip_size++] = a; 222 | } 223 | 224 | // note that we may need to flip the emitted triangle based on parity 225 | // we always end up with outgoing edge "cb" in the end 226 | unsigned int e0 = parity ? c : b; 227 | unsigned int e1 = parity ? b : c; 228 | 229 | destination[strip_size++] = a; 230 | destination[strip_size++] = e0; 231 | destination[strip_size++] = e1; 232 | 233 | strip[0] = e0; 234 | strip[1] = e1; 235 | parity ^= 1; 236 | } 237 | } 238 | } 239 | 240 | return strip_size; 241 | } 242 | 243 | size_t meshopt_stripifyBound(size_t index_count) 244 | { 245 | assert(index_count % 3 == 0); 246 | 247 | // worst case without restarts is 2 degenerate indices and 3 indices per triangle 248 | // worst case with restarts is 1 restart index and 3 indices per triangle 249 | return (index_count / 3) * 5; 250 | } 251 | 252 | size_t meshopt_unstripify(unsigned int* destination, const unsigned int* indices, size_t index_count, unsigned int restart_index) 253 | { 254 | assert(destination != indices); 255 | 256 | size_t offset = 0; 257 | size_t start = 0; 258 | 259 | for (size_t i = 0; i < index_count; ++i) 260 | { 261 | if (restart_index && indices[i] == restart_index) 262 | { 263 | start = i + 1; 264 | } 265 | else if (i - start >= 2) 266 | { 267 | unsigned int a = indices[i - 2], b = indices[i - 1], c = indices[i]; 268 | 269 | // flip winding for odd triangles 270 | if ((i - start) & 1) 271 | { 272 | unsigned int t = a; 273 | a = b, b = t; 274 | } 275 | 276 | // although we use restart indices, strip swaps still produce degenerate triangles, so skip them 277 | if (a != b && a != c && b != c) 278 | { 279 | destination[offset + 0] = a; 280 | destination[offset + 1] = b; 281 | destination[offset + 2] = c; 282 | offset += 3; 283 | } 284 | } 285 | } 286 | 287 | return offset; 288 | } 289 | 290 | size_t meshopt_unstripifyBound(size_t index_count) 291 | { 292 | assert(index_count == 0 || index_count >= 3); 293 | 294 | return (index_count == 0) ? 0 : (index_count - 2) * 3; 295 | } 296 | -------------------------------------------------------------------------------- /js/meshopt_decoder.test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert').strict; 2 | var decoder = require('./meshopt_decoder.js'); 3 | 4 | process.on('unhandledRejection', error => { 5 | console.log('unhandledRejection', error); 6 | process.exit(1); 7 | }); 8 | 9 | var tests = { 10 | decodeVertexBuffer: function() { 11 | var encoded = new Uint8Array([ 12 | 0xa0, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x58, 0x57, 0x58, 0x01, 0x26, 0x00, 0x00, 0x00, 0x01, 13 | 0x0c, 0x00, 0x00, 0x00, 0x58, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 14 | 0x3f, 0x00, 0x00, 0x00, 0x17, 0x18, 0x17, 0x01, 0x26, 0x00, 0x00, 0x00, 0x01, 0x0c, 0x00, 15 | 0x00, 0x00, 0x17, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | ]); 19 | 20 | var expected = new Uint8Array([ 21 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 22 | 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 0, 0, 23 | 0, 0, 44, 1, 0, 0, 0, 0, 0, 0, 244, 1, 24 | 44, 1, 44, 1, 0, 0, 0, 0, 244, 1, 244, 1 25 | ]); 26 | 27 | var result = new Uint8Array(expected.length); 28 | decoder.decodeVertexBuffer(result, 4, 12, encoded); 29 | 30 | assert.deepStrictEqual(result, expected); 31 | }, 32 | 33 | decodeVertexBuffer_More: function() { 34 | var encoded = new Uint8Array([ 35 | 0xa0, 0x00, 0x01, 0x2a, 0xaa, 0xaa, 0xaa, 0x02, 0x04, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 36 | 0x44, 0x03, 0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 37 | 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 40 | ]); 41 | 42 | var expected = new Uint8Array([ 43 | 0, 0, 0, 0, 0, 1, 2, 8, 0, 2, 4, 16, 0, 3, 6, 24, 44 | 0, 4, 8, 32, 0, 5, 10, 40, 0, 6, 12, 48, 0, 7, 14, 56, 45 | 0, 8, 16, 64, 0, 9, 18, 72, 0, 10, 20, 80, 0, 11, 22, 88, 46 | 0, 12, 24, 96, 0, 13, 26, 104, 0, 14, 28, 112, 0, 15, 30, 120 47 | ]); 48 | 49 | var result = new Uint8Array(expected.length); 50 | decoder.decodeVertexBuffer(result, 16, 4, encoded); 51 | 52 | assert.deepStrictEqual(result, expected); 53 | }, 54 | 55 | decodeVertexBuffer_Mode2: function() { 56 | var encoded = new Uint8Array([ 57 | 0xa0, 0x02, 0x08, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x02, 0x0a, 0xaa, 0xaa, 0xaa, 58 | 0xaa, 0xaa, 0xaa, 0xaa, 0x02, 0x0c, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0x02, 0x0e, 59 | 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0xee, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 61 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 62 | ]); 63 | 64 | var expected = new Uint8Array([ 65 | 0, 0, 0, 0, 4, 5, 6, 7, 8, 10, 12, 14, 12, 15, 18, 21, 66 | 16, 20, 24, 28, 20, 25, 30, 35, 24, 30, 36, 42, 28, 35, 42, 49, 67 | 32, 40, 48, 56, 36, 45, 54, 63, 40, 50, 60, 70, 44, 55, 66, 77, 68 | 48, 60, 72, 84, 52, 65, 78, 91, 56, 70, 84, 98, 60, 75, 90, 105 69 | ]); 70 | 71 | var result = new Uint8Array(expected.length); 72 | decoder.decodeVertexBuffer(result, 16, 4, encoded); 73 | 74 | assert.deepStrictEqual(result, expected); 75 | }, 76 | 77 | decodeIndexBuffer16: function() { 78 | var encoded = new Uint8Array([ 79 | 0xe0, 0xf0, 0x10, 0xfe, 0xff, 0xf0, 0x0c, 0xff, 0x02, 0x02, 0x02, 0x00, 0x76, 0x87, 0x56, 0x67, 80 | 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00, 81 | ]); 82 | 83 | var expected = new Uint16Array([ 84 | 0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9 85 | ]); 86 | 87 | var result = new Uint16Array(expected.length); 88 | decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 2, encoded); 89 | 90 | assert.deepEqual(result, expected); 91 | }, 92 | 93 | decodeIndexBuffer32: function() { 94 | var encoded = new Uint8Array([ 95 | 0xe0, 0xf0, 0x10, 0xfe, 0xff, 0xf0, 0x0c, 0xff, 0x02, 0x02, 0x02, 0x00, 0x76, 0x87, 0x56, 0x67, 96 | 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00, 97 | ]); 98 | 99 | var expected = new Uint32Array([ 100 | 0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9 101 | ]); 102 | 103 | var result = new Uint32Array(expected.length); 104 | decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded); 105 | 106 | assert.deepStrictEqual(result, expected); 107 | }, 108 | 109 | decodeIndexBufferV1: function() { 110 | var encoded = new Uint8Array([ 111 | 0xe1, 0xf0, 0x10, 0xfe, 0x1f, 0x3d, 0x00, 0x0a, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 112 | 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00, 113 | ]); 114 | 115 | var expected = new Uint32Array([ 116 | 0, 1, 2, 2, 1, 3, 0, 1, 2, 2, 1, 5, 2, 1, 4 117 | ]); 118 | 119 | var result = new Uint32Array(expected.length); 120 | decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 15, 4, encoded); 121 | 122 | assert.deepStrictEqual(result, expected); 123 | }, 124 | 125 | decodeIndexBufferV1_More: function() { 126 | var encoded = new Uint8Array([ 127 | 0xe1, 0xf0, 0x10, 0xfe, 0xff, 0xf0, 0x0c, 0xff, 0x02, 0x02, 0x02, 0x00, 0x76, 0x87, 0x56, 0x67, 128 | 0x78, 0xa9, 0x86, 0x65, 0x89, 0x68, 0x98, 0x01, 0x69, 0x00, 0x00, 129 | ]); 130 | 131 | var expected = new Uint32Array([ 132 | 0, 1, 2, 2, 1, 3, 4, 6, 5, 7, 8, 9 133 | ]); 134 | 135 | var result = new Uint32Array(expected.length); 136 | decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded); 137 | 138 | assert.deepStrictEqual(result, expected); 139 | }, 140 | 141 | decodeIndexBufferV1_3Edges: function() { 142 | var encoded = new Uint8Array([ 143 | 0xe1, 0xf0, 0x20, 0x30, 0x40, 0x00, 0x76, 0x87, 0x56, 0x67, 0x78, 0xa9, 0x86, 0x65, 0x89, 144 | 0x68, 0x98, 0x01, 0x69, 0x00, 0x00, 145 | ]); 146 | var expected = new Uint32Array([ 147 | 0, 1, 2, 1, 0, 3, 2, 1, 4, 0, 2, 5 148 | ]); 149 | 150 | var result = new Uint32Array(expected.length); 151 | decoder.decodeIndexBuffer(new Uint8Array(result.buffer), 12, 4, encoded); 152 | 153 | assert.deepStrictEqual(result, expected); 154 | }, 155 | 156 | decodeIndexSequence: function() { 157 | var encoded = new Uint8Array([ 158 | 0xd1, 0x00, 0x04, 0xcd, 0x01, 0x04, 0x07, 0x98, 0x1f, 0x00, 0x00, 0x00, 0x00, 159 | ]); 160 | 161 | var expected = new Uint32Array([ 162 | 0, 1, 51, 2, 49, 1000 163 | ]); 164 | 165 | var result = new Uint32Array(expected.length); 166 | decoder.decodeIndexSequence(new Uint8Array(result.buffer), 6, 4, encoded); 167 | 168 | assert.deepStrictEqual(result, expected); 169 | }, 170 | 171 | decodeFilterOct8: function() { 172 | var encoded = new Uint8Array([ 173 | 0xa0, 0x01, 0x07, 0x00, 0x00, 0x00, 0x1e, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x8b, 0x8c, 0xfd, 0x00, 174 | 0x01, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 175 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 176 | 0x00, 0x00, 0x01, 0x7f, 0x00, 177 | ]); 178 | 179 | var expected = new Uint8Array([ 180 | 0, 1, 127, 0, 181 | 0, 159, 82, 1, 182 | 255, 1, 127, 0, 183 | 1, 130, 241, 1, 184 | ]); 185 | 186 | var result = new Uint8Array(expected.length); 187 | decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 4, encoded, /* filter= */ "OCTAHEDRAL"); 188 | 189 | assert.deepStrictEqual(result, expected); 190 | }, 191 | 192 | decodeFilterOct12: function() { 193 | var encoded = new Uint8Array([ 194 | 0xa0, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x3d, 0x5a, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x01, 195 | 0x3f, 0x00, 0x00, 0x00, 0x9a, 0x99, 0x26, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x0a, 0x00, 196 | 0x00, 0x01, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 197 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 198 | 0x00, 0x01, 0x00, 0xff, 0x07, 0x00, 0x00, 199 | ]); 200 | 201 | var expected = new Uint16Array([ 202 | 0, 16, 32767, 0, 203 | 0, 32621, 3088, 1, 204 | 32764, 16, 471, 0, 205 | 307, 28541, 16093, 1, 206 | ]); 207 | 208 | var result = new Uint16Array(expected.length); 209 | decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 8, encoded, /* filter= */ "OCTAHEDRAL"); 210 | 211 | assert.deepStrictEqual(result, expected); 212 | }, 213 | 214 | decodeFilterQuat12: function() { 215 | var encoded = new Uint8Array([ 216 | 0xa0, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x3d, 0x5a, 0x01, 0x0f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x01, 217 | 0x3f, 0x00, 0x00, 0x00, 0x9a, 0x99, 0x26, 0x01, 0x3f, 0x00, 0x00, 0x00, 0x0e, 0x0d, 0x0a, 0x00, 218 | 0x00, 0x01, 0x2a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 219 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 220 | 0x00, 0x01, 0x00, 0x00, 0x00, 0xfc, 0x07, 221 | ]); 222 | 223 | var expected = new Uint16Array([ 224 | 32767, 0, 11, 0, 225 | 0, 25013, 0, 21166, 226 | 11, 0, 23504, 22830, 227 | 158, 14715, 0, 29277, 228 | ]); 229 | 230 | var result = new Uint16Array(expected.length); 231 | decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 4, 8, encoded, /* filter= */ "QUATERNION"); 232 | 233 | assert.deepStrictEqual(result, expected); 234 | }, 235 | 236 | decodeFilterExp: function() { 237 | var encoded = new Uint8Array([ 238 | 0xa0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 239 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 240 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0xff, 0xf7, 0xff, 0xff, 0x02, 0xff, 0xff, 0x7f, 241 | 0xfe, 242 | ]); 243 | 244 | var expected = new Uint32Array([ 245 | 0, 246 | 0x3fc00000, 247 | 0xc2100000, 248 | 0x49fffffe, 249 | ]); 250 | 251 | var result = new Uint32Array(expected.length); 252 | decoder.decodeVertexBuffer(new Uint8Array(result.buffer), 1, 16, encoded, /* filter= */ "EXPONENTIAL"); 253 | 254 | assert.deepStrictEqual(result, expected); 255 | }, 256 | }; 257 | 258 | decoder.ready.then(() => { 259 | var count = 0; 260 | 261 | for (var key in tests) { 262 | tests[key](); 263 | count++; 264 | } 265 | 266 | console.log(count, 'tests passed'); 267 | }); 268 | -------------------------------------------------------------------------------- /gltf/library.js: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack and is distributed under the terms of MIT License. 2 | 3 | /** 4 | * Initialize the library with the Wasm module (library.wasm) 5 | * 6 | * @param wasm Promise with contents of library.wasm 7 | * 8 | * Note: this is called automatically in node.js 9 | */ 10 | function init(wasm) { 11 | if (ready) { 12 | throw new Error("init must be called once"); 13 | } 14 | 15 | ready = Promise.resolve(wasm) 16 | .then(function (buffer) { 17 | return WebAssembly.instantiate(buffer, { wasi_snapshot_preview1: wasi }); 18 | }) 19 | .then(function (result) { 20 | instance = result.instance; 21 | instance.exports.__wasm_call_ctors(); 22 | }); 23 | } 24 | 25 | /** 26 | * Pack the requested glTF data using the requested command line and access interface. 27 | * 28 | * @param args An array of strings with the input arguments; the paths for input and output files are interpreted by the interface 29 | * @param iface An interface to the system that will be used to service file requests and other system calls 30 | * @return Promise that indicates completion of the operation 31 | * 32 | * iface should contain the following methods: 33 | * read(path): Given a path, return a Uint8Array with the contents of that path 34 | * write(path, data): Write the specified Uint8Array to the provided path 35 | */ 36 | function pack(args, iface) { 37 | if (!ready) { 38 | throw new Error("init must be called before pack"); 39 | } 40 | 41 | var argv = args.slice(); 42 | argv.unshift("gltfpack"); 43 | 44 | return ready.then(function () { 45 | var buf = uploadArgv(argv); 46 | 47 | output.position = 0; 48 | output.size = 0; 49 | 50 | interface = iface; 51 | var result = instance.exports.pack(argv.length, buf); 52 | interface = undefined; 53 | 54 | instance.exports.free(buf); 55 | 56 | var log = getString(output.data.buffer, 0, output.size); 57 | 58 | if (result != 0) { 59 | throw new Error(log); 60 | } else { 61 | return log; 62 | } 63 | }); 64 | } 65 | 66 | // Library implementation (here be dragons) 67 | var WASI_EBADF = 8; 68 | var WASI_EINVAL = 28; 69 | var WASI_EIO = 29; 70 | var WASI_ENOSYS = 52; 71 | 72 | var ready; 73 | var instance; 74 | var interface; 75 | 76 | var output = { data: new Uint8Array(), position: 0, size: 0 }; 77 | var fds = { 1: output, 2: output, 3: { mount: "/", path: "/" }, 4: { mount: "/gltfpack-$pwd", path: "" } }; 78 | 79 | var wasi = { 80 | proc_exit: function(rval) { 81 | }, 82 | 83 | fd_close: function(fd) { 84 | if (!fds[fd]) { 85 | return WASI_EBADF; 86 | } 87 | 88 | try { 89 | if (fds[fd].close) { 90 | fds[fd].close(); 91 | } 92 | fds[fd] = undefined; 93 | return 0; 94 | } catch (err) { 95 | fds[fd] = undefined; 96 | return WASI_EIO; 97 | } 98 | }, 99 | 100 | fd_fdstat_get: function(fd, stat) { 101 | if (!fds[fd]) { 102 | return WASI_EBADF; 103 | } 104 | 105 | var heap = getHeap(); 106 | heap.setUint8(stat + 0, fds[fd].path !== undefined ? 3 : 4); 107 | heap.setUint16(stat + 2, 0, true); 108 | heap.setUint32(stat + 8, 0, true); 109 | heap.setUint32(stat + 12, 0, true); 110 | heap.setUint32(stat + 16, 0, true); 111 | heap.setUint32(stat + 20, 0, true); 112 | return 0; 113 | }, 114 | 115 | path_open32: function(parent_fd, dirflags, path, path_len, oflags, fs_rights_base, fs_rights_inheriting, fdflags, opened_fd) { 116 | if (!fds[parent_fd] || fds[parent_fd].path === undefined) { 117 | return WASI_EBADF; 118 | } 119 | 120 | var heap = getHeap(); 121 | 122 | var file = {}; 123 | file.name = fds[parent_fd].path + getString(heap.buffer, path, path_len); 124 | file.position = 0; 125 | 126 | if (oflags & 1) { 127 | file.data = new Uint8Array(4096); 128 | file.size = 0; 129 | file.close = function () { 130 | interface.write(file.name, new Uint8Array(file.data.buffer, 0, file.size)); 131 | }; 132 | } else { 133 | try { 134 | file.data = interface.read(file.name); 135 | 136 | if (!file.data) { 137 | return WASI_EIO; 138 | } 139 | 140 | file.size = file.data.length; 141 | } catch (err) { 142 | return WASI_EIO; 143 | } 144 | } 145 | 146 | var fd = nextFd(); 147 | fds[fd] = file; 148 | 149 | heap.setUint32(opened_fd, fd, true); 150 | return 0; 151 | }, 152 | 153 | path_filestat_get: function(parent_fd, flags, path, path_len, buf) { 154 | if (!fds[parent_fd] || fds[parent_fd].path === undefined) { 155 | return WASI_EBADF; 156 | } 157 | 158 | var heap = getHeap(); 159 | var name = getString(heap.buffer, path, path_len); 160 | 161 | var heap = getHeap(); 162 | for (var i = 0; i < 64; ++i) 163 | heap.setUint8(buf + i, 0); 164 | 165 | heap.setUint8(buf + 16, name == "." ? 3 : 4); 166 | return 0; 167 | }, 168 | 169 | fd_prestat_get: function(fd, buf) { 170 | if (!fds[fd] || fds[fd].path === undefined) { 171 | return WASI_EBADF; 172 | } 173 | 174 | var path_buf = stringBuffer(fds[fd].mount); 175 | 176 | var heap = getHeap(); 177 | heap.setUint8(buf, 0); 178 | heap.setUint32(buf + 4, path_buf.length, true); 179 | return 0; 180 | }, 181 | 182 | fd_prestat_dir_name: function(fd, path, path_len) { 183 | if (!fds[fd] || fds[fd].path === undefined) { 184 | return WASI_EBADF; 185 | } 186 | 187 | var path_buf = stringBuffer(fds[fd].mount); 188 | 189 | if (path_len != path_buf.length) { 190 | return WASI_EINVAL; 191 | } 192 | 193 | var heap = getHeap(); 194 | new Uint8Array(heap.buffer).set(path_buf, path); 195 | return 0; 196 | }, 197 | 198 | path_remove_directory: function(parent_fd, path, path_len) { 199 | return WASI_EINVAL; 200 | }, 201 | 202 | fd_fdstat_set_flags: function(fd, flags) { 203 | return WASI_ENOSYS; 204 | }, 205 | 206 | fd_seek32: function(fd, offset, whence, newoffset) { 207 | if (!fds[fd]) { 208 | return WASI_EBADF; 209 | } 210 | 211 | var newposition; 212 | 213 | switch (whence) { 214 | case 0: 215 | newposition = offset; 216 | break; 217 | 218 | case 1: 219 | newposition = fds[fd].position + offset; 220 | break; 221 | 222 | case 2: 223 | newposition = fds[fd].size; 224 | break; 225 | 226 | default: 227 | return WASI_EINVAL; 228 | } 229 | 230 | if (newposition > fds[fd].size) { 231 | return WASI_EINVAL; 232 | } 233 | 234 | fds[fd].position = newposition; 235 | 236 | var heap = getHeap(); 237 | heap.setUint32(newoffset, newposition, true); 238 | return 0; 239 | }, 240 | 241 | fd_read: function(fd, iovs, iovs_len, nread) { 242 | if (!fds[fd]) { 243 | return WASI_EBADF; 244 | } 245 | 246 | var heap = getHeap(); 247 | var read = 0; 248 | 249 | for (var i = 0; i < iovs_len; ++i) { 250 | var buf = heap.getUint32(iovs + 8 * i + 0, true); 251 | var buf_len = heap.getUint32(iovs + 8 * i + 4, true); 252 | 253 | var readi = Math.min(fds[fd].size - fds[fd].position, buf_len); 254 | 255 | new Uint8Array(heap.buffer).set(fds[fd].data.subarray(fds[fd].position, fds[fd].position + readi), buf); 256 | 257 | fds[fd].position += readi; 258 | read += readi; 259 | } 260 | 261 | heap.setUint32(nread, read, true); 262 | return 0; 263 | }, 264 | 265 | fd_write: function(fd, iovs, iovs_len, nwritten) { 266 | if (!fds[fd]) { 267 | return WASI_EBADF; 268 | } 269 | 270 | var heap = getHeap(); 271 | var written = 0; 272 | 273 | for (var i = 0; i < iovs_len; ++i) { 274 | var buf = heap.getUint32(iovs + 8 * i + 0, true); 275 | var buf_len = heap.getUint32(iovs + 8 * i + 4, true); 276 | 277 | if (fds[fd].position + buf_len > fds[fd].data.length) { 278 | fds[fd].data = growArray(fds[fd].data, fds[fd].position + buf_len); 279 | } 280 | 281 | fds[fd].data.set(new Uint8Array(heap.buffer, buf, buf_len), fds[fd].position); 282 | fds[fd].position += buf_len; 283 | fds[fd].size = Math.max(fds[fd].position, fds[fd].size); 284 | 285 | written += buf_len; 286 | } 287 | 288 | heap.setUint32(nwritten, written, true); 289 | return 0; 290 | }, 291 | }; 292 | 293 | function nextFd() { 294 | for (var i = 1; ; ++i) { 295 | if (fds[i] === undefined) { 296 | return i; 297 | } 298 | } 299 | } 300 | 301 | function getHeap() { 302 | return new DataView(instance.exports.memory.buffer); 303 | } 304 | 305 | function getString(buffer, offset, length) { 306 | return new TextDecoder().decode(new Uint8Array(buffer, offset, length)); 307 | } 308 | 309 | function stringBuffer(string) { 310 | return new TextEncoder().encode(string); 311 | } 312 | 313 | function growArray(data, len) { 314 | var new_length = Math.max(1, data.length); 315 | while (new_length < len) { 316 | new_length *= 2; 317 | } 318 | 319 | var new_data = new Uint8Array(new_length); 320 | new_data.set(data); 321 | 322 | return new_data; 323 | } 324 | 325 | function uploadArgv(argv) { 326 | var buf_size = argv.length * 4; 327 | for (var i = 0; i < argv.length; ++i) { 328 | buf_size += stringBuffer(argv[i]).length + 1; 329 | } 330 | 331 | var buf = instance.exports.malloc(buf_size); 332 | var argp = buf + argv.length * 4; 333 | 334 | var heap = getHeap(); 335 | 336 | for (var i = 0; i < argv.length; ++i) { 337 | var item = stringBuffer(argv[i]); 338 | 339 | heap.setUint32(buf + i * 4, argp, true); 340 | new Uint8Array(heap.buffer).set(item, argp); 341 | heap.setUint8(argp + item.length, 0); 342 | 343 | argp += item.length + 1; 344 | } 345 | 346 | return buf; 347 | } 348 | 349 | // Automatic initialization for node.js 350 | if (typeof window === 'undefined' && typeof process !== 'undefined' && process.release.name === 'node') { 351 | var fs = require('fs'); 352 | var util = require('util'); 353 | 354 | // Node versions before v12 don't support TextEncoder/TextDecoder natively, but util. provides compatible replacements 355 | if (typeof TextEncoder === 'undefined' && typeof TextDecoder === 'undefined') { 356 | TextEncoder = util.TextEncoder; 357 | TextDecoder = util.TextDecoder; 358 | } 359 | 360 | init(fs.readFileSync(__dirname + '/library.wasm')); 361 | } 362 | 363 | // UMD 364 | (function (root, factory) { 365 | if (typeof define === 'function' && define.amd) { 366 | define([], factory); 367 | } else if (typeof module === 'object' && module.exports) { 368 | module.exports = factory(); 369 | } else { 370 | root.gltfpack = factory(); 371 | } 372 | }(typeof self !== 'undefined' ? self : this, function () { 373 | return { init, pack }; 374 | })); 375 | -------------------------------------------------------------------------------- /gltf/animation.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of gltfpack; see gltfpack.h for version/license details 2 | #include "gltfpack.h" 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | static float getDelta(const Attr& l, const Attr& r, cgltf_animation_path_type type) 11 | { 12 | switch (type) 13 | { 14 | case cgltf_animation_path_type_translation: 15 | return std::max(std::max(fabsf(l.f[0] - r.f[0]), fabsf(l.f[1] - r.f[1])), fabsf(l.f[2] - r.f[2])); 16 | 17 | case cgltf_animation_path_type_rotation: 18 | return acosf(std::min(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]))); 19 | 20 | case cgltf_animation_path_type_scale: 21 | return std::max(std::max(fabsf(l.f[0] / r.f[0] - 1), fabsf(l.f[1] / r.f[1] - 1)), fabsf(l.f[2] / r.f[2] - 1)); 22 | 23 | case cgltf_animation_path_type_weights: 24 | return fabsf(l.f[0] - r.f[0]); 25 | 26 | default: 27 | assert(!"Uknown animation path"); 28 | return 0; 29 | } 30 | } 31 | 32 | static float getDeltaTolerance(cgltf_animation_path_type type) 33 | { 34 | switch (type) 35 | { 36 | case cgltf_animation_path_type_translation: 37 | return 0.0001f; // 0.1mm linear 38 | 39 | case cgltf_animation_path_type_rotation: 40 | return 0.1f * (3.1415926f / 180.f); // 0.1 degrees 41 | 42 | case cgltf_animation_path_type_scale: 43 | return 0.001f; // 0.1% ratio 44 | 45 | case cgltf_animation_path_type_weights: 46 | return 0.001f; // 0.1% linear 47 | 48 | default: 49 | assert(!"Uknown animation path"); 50 | return 0; 51 | } 52 | } 53 | 54 | static Attr interpolateLinear(const Attr& l, const Attr& r, float t, cgltf_animation_path_type type) 55 | { 56 | if (type == cgltf_animation_path_type_rotation) 57 | { 58 | // Approximating slerp, https://zeux.io/2015/07/23/approximating-slerp/ 59 | // We also handle quaternion double-cover 60 | 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]; 61 | 62 | float d = fabsf(ca); 63 | float A = 1.0904f + d * (-3.2452f + d * (3.55645f - d * 1.43519f)); 64 | float B = 0.848013f + d * (-1.06021f + d * 0.215638f); 65 | float k = A * (t - 0.5f) * (t - 0.5f) + B; 66 | float ot = t + t * (t - 0.5f) * (t - 1) * k; 67 | 68 | float t0 = 1 - ot; 69 | float t1 = ca > 0 ? ot : -ot; 70 | 71 | Attr lerp = {{ 72 | l.f[0] * t0 + r.f[0] * t1, 73 | l.f[1] * t0 + r.f[1] * t1, 74 | l.f[2] * t0 + r.f[2] * t1, 75 | l.f[3] * t0 + r.f[3] * t1, 76 | }}; 77 | 78 | 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]); 79 | 80 | if (len > 0.f) 81 | { 82 | lerp.f[0] /= len; 83 | lerp.f[1] /= len; 84 | lerp.f[2] /= len; 85 | lerp.f[3] /= len; 86 | } 87 | 88 | return lerp; 89 | } 90 | else 91 | { 92 | Attr lerp = {{ 93 | l.f[0] * (1 - t) + r.f[0] * t, 94 | l.f[1] * (1 - t) + r.f[1] * t, 95 | l.f[2] * (1 - t) + r.f[2] * t, 96 | l.f[3] * (1 - t) + r.f[3] * t, 97 | }}; 98 | 99 | return lerp; 100 | } 101 | } 102 | 103 | static Attr interpolateHermite(const Attr& v0, const Attr& t0, const Attr& v1, const Attr& t1, float t, float dt, cgltf_animation_path_type type) 104 | { 105 | float s0 = 1 + t * t * (2 * t - 3); 106 | float s1 = t + t * t * (t - 2); 107 | float s2 = 1 - s0; 108 | float s3 = t * t * (t - 1); 109 | 110 | float ts1 = dt * s1; 111 | float ts3 = dt * s3; 112 | 113 | Attr lerp = {{ 114 | s0 * v0.f[0] + ts1 * t0.f[0] + s2 * v1.f[0] + ts3 * t1.f[0], 115 | s0 * v0.f[1] + ts1 * t0.f[1] + s2 * v1.f[1] + ts3 * t1.f[1], 116 | s0 * v0.f[2] + ts1 * t0.f[2] + s2 * v1.f[2] + ts3 * t1.f[2], 117 | s0 * v0.f[3] + ts1 * t0.f[3] + s2 * v1.f[3] + ts3 * t1.f[3], 118 | }}; 119 | 120 | if (type == cgltf_animation_path_type_rotation) 121 | { 122 | 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]); 123 | 124 | if (len > 0.f) 125 | { 126 | lerp.f[0] /= len; 127 | lerp.f[1] /= len; 128 | lerp.f[2] /= len; 129 | lerp.f[3] /= len; 130 | } 131 | } 132 | 133 | return lerp; 134 | } 135 | 136 | static void resampleKeyframes(std::vector& data, const std::vector& input, const std::vector& output, cgltf_animation_path_type type, cgltf_interpolation_type interpolation, size_t components, int frames, float mint, int freq) 137 | { 138 | size_t cursor = 0; 139 | 140 | for (int i = 0; i < frames; ++i) 141 | { 142 | float time = mint + float(i) / freq; 143 | 144 | while (cursor + 1 < input.size()) 145 | { 146 | float next_time = input[cursor + 1]; 147 | 148 | if (next_time > time) 149 | break; 150 | 151 | cursor++; 152 | } 153 | 154 | if (cursor + 1 < input.size()) 155 | { 156 | float cursor_time = input[cursor + 0]; 157 | float next_time = input[cursor + 1]; 158 | 159 | float range = next_time - cursor_time; 160 | float inv_range = (range == 0.f) ? 0.f : 1.f / (next_time - cursor_time); 161 | float t = std::max(0.f, std::min(1.f, (time - cursor_time) * inv_range)); 162 | 163 | for (size_t j = 0; j < components; ++j) 164 | { 165 | switch (interpolation) 166 | { 167 | case cgltf_interpolation_type_linear: 168 | { 169 | const Attr& v0 = output[(cursor + 0) * components + j]; 170 | const Attr& v1 = output[(cursor + 1) * components + j]; 171 | data.push_back(interpolateLinear(v0, v1, t, type)); 172 | } 173 | break; 174 | 175 | case cgltf_interpolation_type_step: 176 | { 177 | const Attr& v = output[cursor * components + j]; 178 | data.push_back(v); 179 | } 180 | break; 181 | 182 | case cgltf_interpolation_type_cubic_spline: 183 | { 184 | const Attr& v0 = output[(cursor * 3 + 1) * components + j]; 185 | const Attr& b0 = output[(cursor * 3 + 2) * components + j]; 186 | const Attr& a1 = output[(cursor * 3 + 3) * components + j]; 187 | const Attr& v1 = output[(cursor * 3 + 4) * components + j]; 188 | data.push_back(interpolateHermite(v0, b0, v1, a1, t, range, type)); 189 | } 190 | break; 191 | 192 | default: 193 | assert(!"Unknown interpolation type"); 194 | } 195 | } 196 | } 197 | else 198 | { 199 | size_t offset = (interpolation == cgltf_interpolation_type_cubic_spline) ? cursor * 3 + 1 : cursor; 200 | 201 | for (size_t j = 0; j < components; ++j) 202 | { 203 | const Attr& v = output[offset * components + j]; 204 | data.push_back(v); 205 | } 206 | } 207 | } 208 | } 209 | 210 | static float getMaxDelta(const std::vector& data, cgltf_animation_path_type type, int frames, const Attr* value, size_t components) 211 | { 212 | assert(data.size() == frames * components); 213 | 214 | float result = 0; 215 | 216 | for (int i = 0; i < frames; ++i) 217 | { 218 | for (size_t j = 0; j < components; ++j) 219 | { 220 | float delta = getDelta(value[j], data[i * components + j], type); 221 | 222 | result = (result < delta) ? delta : result; 223 | } 224 | } 225 | 226 | return result; 227 | } 228 | 229 | static void getBaseTransform(Attr* result, size_t components, cgltf_animation_path_type type, cgltf_node* node) 230 | { 231 | switch (type) 232 | { 233 | case cgltf_animation_path_type_translation: 234 | memcpy(result->f, node->translation, 3 * sizeof(float)); 235 | break; 236 | 237 | case cgltf_animation_path_type_rotation: 238 | memcpy(result->f, node->rotation, 4 * sizeof(float)); 239 | break; 240 | 241 | case cgltf_animation_path_type_scale: 242 | memcpy(result->f, node->scale, 3 * sizeof(float)); 243 | break; 244 | 245 | case cgltf_animation_path_type_weights: 246 | if (node->weights_count) 247 | { 248 | assert(node->weights_count == components); 249 | memcpy(result->f, node->weights, components * sizeof(float)); 250 | } 251 | else if (node->mesh && node->mesh->weights_count) 252 | { 253 | assert(node->mesh->weights_count == components); 254 | memcpy(result->f, node->mesh->weights, components * sizeof(float)); 255 | } 256 | break; 257 | 258 | default: 259 | assert(!"Unknown animation path"); 260 | } 261 | } 262 | 263 | static float getWorldScale(cgltf_node* node) 264 | { 265 | float transform[16]; 266 | cgltf_node_transform_world(node, transform); 267 | 268 | // 3x3 determinant computes scale^3 269 | float a0 = transform[5] * transform[10] - transform[6] * transform[9]; 270 | float a1 = transform[4] * transform[10] - transform[6] * transform[8]; 271 | float a2 = transform[4] * transform[9] - transform[5] * transform[8]; 272 | float det = transform[0] * a0 - transform[1] * a1 + transform[2] * a2; 273 | 274 | return powf(fabsf(det), 1.f / 3.f); 275 | } 276 | 277 | void processAnimation(Animation& animation, const Settings& settings) 278 | { 279 | float mint = FLT_MAX, maxt = 0; 280 | 281 | for (size_t i = 0; i < animation.tracks.size(); ++i) 282 | { 283 | const Track& track = animation.tracks[i]; 284 | assert(!track.time.empty()); 285 | 286 | mint = std::min(mint, track.time.front()); 287 | maxt = std::max(maxt, track.time.back()); 288 | } 289 | 290 | mint = std::min(mint, maxt); 291 | 292 | // round the number of frames to nearest but favor the "up" direction 293 | // this means that at 10 Hz resampling, we will try to preserve the last frame <10ms 294 | // but if the last frame is <2ms we favor just removing this data 295 | int frames = 1 + int((maxt - mint) * settings.anim_freq + 0.8f); 296 | 297 | animation.start = mint; 298 | animation.frames = frames; 299 | 300 | std::vector base; 301 | 302 | for (size_t i = 0; i < animation.tracks.size(); ++i) 303 | { 304 | Track& track = animation.tracks[i]; 305 | 306 | std::vector result; 307 | resampleKeyframes(result, track.time, track.data, track.path, track.interpolation, track.components, frames, mint, settings.anim_freq); 308 | 309 | track.time.clear(); 310 | track.data.swap(result); 311 | 312 | float tolerance = getDeltaTolerance(track.path); 313 | 314 | // translation tracks use world space tolerance; in the future, we should compute all errors as linear using hierarchy 315 | if (track.node && track.path == cgltf_animation_path_type_translation) 316 | { 317 | float scale = getWorldScale(track.node); 318 | tolerance /= scale == 0.f ? 1.f : scale; 319 | } 320 | 321 | float deviation = getMaxDelta(track.data, track.path, frames, &track.data[0], track.components); 322 | 323 | if (deviation <= tolerance) 324 | { 325 | // track is constant (equal to first keyframe), we only need the first keyframe 326 | track.constant = true; 327 | track.data.resize(track.components); 328 | 329 | // track.dummy is true iff track redundantly sets up the value to be equal to default node transform 330 | base.resize(track.components); 331 | getBaseTransform(&base[0], track.components, track.path, track.node); 332 | 333 | track.dummy = getMaxDelta(track.data, track.path, 1, &base[0], track.components) <= tolerance; 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/overdrawoptimizer.cpp: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details 2 | #include "meshoptimizer.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | // This work is based on: 9 | // Pedro Sander, Diego Nehab and Joshua Barczak. Fast Triangle Reordering for Vertex Locality and Reduced Overdraw. 2007 10 | namespace meshopt 11 | { 12 | 13 | static void calculateSortData(float* sort_data, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_positions_stride, const unsigned int* clusters, size_t cluster_count) 14 | { 15 | size_t vertex_stride_float = vertex_positions_stride / sizeof(float); 16 | 17 | float mesh_centroid[3] = {}; 18 | 19 | for (size_t i = 0; i < index_count; ++i) 20 | { 21 | const float* p = vertex_positions + vertex_stride_float * indices[i]; 22 | 23 | mesh_centroid[0] += p[0]; 24 | mesh_centroid[1] += p[1]; 25 | mesh_centroid[2] += p[2]; 26 | } 27 | 28 | mesh_centroid[0] /= index_count; 29 | mesh_centroid[1] /= index_count; 30 | mesh_centroid[2] /= index_count; 31 | 32 | for (size_t cluster = 0; cluster < cluster_count; ++cluster) 33 | { 34 | size_t cluster_begin = clusters[cluster] * 3; 35 | size_t cluster_end = (cluster + 1 < cluster_count) ? clusters[cluster + 1] * 3 : index_count; 36 | assert(cluster_begin < cluster_end); 37 | 38 | float cluster_area = 0; 39 | float cluster_centroid[3] = {}; 40 | float cluster_normal[3] = {}; 41 | 42 | for (size_t i = cluster_begin; i < cluster_end; i += 3) 43 | { 44 | const float* p0 = vertex_positions + vertex_stride_float * indices[i + 0]; 45 | const float* p1 = vertex_positions + vertex_stride_float * indices[i + 1]; 46 | const float* p2 = vertex_positions + vertex_stride_float * indices[i + 2]; 47 | 48 | float p10[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; 49 | float p20[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; 50 | 51 | float normalx = p10[1] * p20[2] - p10[2] * p20[1]; 52 | float normaly = p10[2] * p20[0] - p10[0] * p20[2]; 53 | float normalz = p10[0] * p20[1] - p10[1] * p20[0]; 54 | 55 | float area = sqrtf(normalx * normalx + normaly * normaly + normalz * normalz); 56 | 57 | cluster_centroid[0] += (p0[0] + p1[0] + p2[0]) * (area / 3); 58 | cluster_centroid[1] += (p0[1] + p1[1] + p2[1]) * (area / 3); 59 | cluster_centroid[2] += (p0[2] + p1[2] + p2[2]) * (area / 3); 60 | cluster_normal[0] += normalx; 61 | cluster_normal[1] += normaly; 62 | cluster_normal[2] += normalz; 63 | cluster_area += area; 64 | } 65 | 66 | float inv_cluster_area = cluster_area == 0 ? 0 : 1 / cluster_area; 67 | 68 | cluster_centroid[0] *= inv_cluster_area; 69 | cluster_centroid[1] *= inv_cluster_area; 70 | cluster_centroid[2] *= inv_cluster_area; 71 | 72 | float cluster_normal_length = sqrtf(cluster_normal[0] * cluster_normal[0] + cluster_normal[1] * cluster_normal[1] + cluster_normal[2] * cluster_normal[2]); 73 | float inv_cluster_normal_length = cluster_normal_length == 0 ? 0 : 1 / cluster_normal_length; 74 | 75 | cluster_normal[0] *= inv_cluster_normal_length; 76 | cluster_normal[1] *= inv_cluster_normal_length; 77 | cluster_normal[2] *= inv_cluster_normal_length; 78 | 79 | float centroid_vector[3] = {cluster_centroid[0] - mesh_centroid[0], cluster_centroid[1] - mesh_centroid[1], cluster_centroid[2] - mesh_centroid[2]}; 80 | 81 | sort_data[cluster] = centroid_vector[0] * cluster_normal[0] + centroid_vector[1] * cluster_normal[1] + centroid_vector[2] * cluster_normal[2]; 82 | } 83 | } 84 | 85 | static void calculateSortOrderRadix(unsigned int* sort_order, const float* sort_data, unsigned short* sort_keys, size_t cluster_count) 86 | { 87 | // compute sort data bounds and renormalize, using fixed point snorm 88 | float sort_data_max = 1e-3f; 89 | 90 | for (size_t i = 0; i < cluster_count; ++i) 91 | { 92 | float dpa = fabsf(sort_data[i]); 93 | 94 | sort_data_max = (sort_data_max < dpa) ? dpa : sort_data_max; 95 | } 96 | 97 | const int sort_bits = 11; 98 | 99 | for (size_t i = 0; i < cluster_count; ++i) 100 | { 101 | // note that we flip distribution since high dot product should come first 102 | float sort_key = 0.5f - 0.5f * (sort_data[i] / sort_data_max); 103 | 104 | sort_keys[i] = meshopt_quantizeUnorm(sort_key, sort_bits) & ((1 << sort_bits) - 1); 105 | } 106 | 107 | // fill histogram for counting sort 108 | unsigned int histogram[1 << sort_bits]; 109 | memset(histogram, 0, sizeof(histogram)); 110 | 111 | for (size_t i = 0; i < cluster_count; ++i) 112 | { 113 | histogram[sort_keys[i]]++; 114 | } 115 | 116 | // compute offsets based on histogram data 117 | size_t histogram_sum = 0; 118 | 119 | for (size_t i = 0; i < 1 << sort_bits; ++i) 120 | { 121 | size_t count = histogram[i]; 122 | histogram[i] = unsigned(histogram_sum); 123 | histogram_sum += count; 124 | } 125 | 126 | assert(histogram_sum == cluster_count); 127 | 128 | // compute sort order based on offsets 129 | for (size_t i = 0; i < cluster_count; ++i) 130 | { 131 | sort_order[histogram[sort_keys[i]]++] = unsigned(i); 132 | } 133 | } 134 | 135 | static unsigned int updateCache(unsigned int a, unsigned int b, unsigned int c, unsigned int cache_size, unsigned int* cache_timestamps, unsigned int& timestamp) 136 | { 137 | unsigned int cache_misses = 0; 138 | 139 | // if vertex is not in cache, put it in cache 140 | if (timestamp - cache_timestamps[a] > cache_size) 141 | { 142 | cache_timestamps[a] = timestamp++; 143 | cache_misses++; 144 | } 145 | 146 | if (timestamp - cache_timestamps[b] > cache_size) 147 | { 148 | cache_timestamps[b] = timestamp++; 149 | cache_misses++; 150 | } 151 | 152 | if (timestamp - cache_timestamps[c] > cache_size) 153 | { 154 | cache_timestamps[c] = timestamp++; 155 | cache_misses++; 156 | } 157 | 158 | return cache_misses; 159 | } 160 | 161 | static size_t generateHardBoundaries(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int* cache_timestamps) 162 | { 163 | memset(cache_timestamps, 0, vertex_count * sizeof(unsigned int)); 164 | 165 | unsigned int timestamp = cache_size + 1; 166 | 167 | size_t face_count = index_count / 3; 168 | 169 | size_t result = 0; 170 | 171 | for (size_t i = 0; i < face_count; ++i) 172 | { 173 | unsigned int m = updateCache(indices[i * 3 + 0], indices[i * 3 + 1], indices[i * 3 + 2], cache_size, &cache_timestamps[0], timestamp); 174 | 175 | // when all three vertices are not in the cache it's usually relatively safe to assume that this is a new patch in the mesh 176 | // that is disjoint from previous vertices; sometimes it might come back to reference existing vertices but that frequently 177 | // suggests an inefficiency in the vertex cache optimization algorithm 178 | // usually the first triangle has 3 misses unless it's degenerate - thus we make sure the first cluster always starts with 0 179 | if (i == 0 || m == 3) 180 | { 181 | destination[result++] = unsigned(i); 182 | } 183 | } 184 | 185 | assert(result <= index_count / 3); 186 | 187 | return result; 188 | } 189 | 190 | static size_t generateSoftBoundaries(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const unsigned int* clusters, size_t cluster_count, unsigned int cache_size, float threshold, unsigned int* cache_timestamps) 191 | { 192 | memset(cache_timestamps, 0, vertex_count * sizeof(unsigned int)); 193 | 194 | unsigned int timestamp = 0; 195 | 196 | size_t result = 0; 197 | 198 | for (size_t it = 0; it < cluster_count; ++it) 199 | { 200 | size_t start = clusters[it]; 201 | size_t end = (it + 1 < cluster_count) ? clusters[it + 1] : index_count / 3; 202 | assert(start < end); 203 | 204 | // reset cache 205 | timestamp += cache_size + 1; 206 | 207 | // measure cluster ACMR 208 | unsigned int cluster_misses = 0; 209 | 210 | for (size_t i = start; i < end; ++i) 211 | { 212 | unsigned int m = updateCache(indices[i * 3 + 0], indices[i * 3 + 1], indices[i * 3 + 2], cache_size, &cache_timestamps[0], timestamp); 213 | 214 | cluster_misses += m; 215 | } 216 | 217 | float cluster_threshold = threshold * (float(cluster_misses) / float(end - start)); 218 | 219 | // first cluster always starts from the hard cluster boundary 220 | destination[result++] = unsigned(start); 221 | 222 | // reset cache 223 | timestamp += cache_size + 1; 224 | 225 | unsigned int running_misses = 0; 226 | unsigned int running_faces = 0; 227 | 228 | for (size_t i = start; i < end; ++i) 229 | { 230 | unsigned int m = updateCache(indices[i * 3 + 0], indices[i * 3 + 1], indices[i * 3 + 2], cache_size, &cache_timestamps[0], timestamp); 231 | 232 | running_misses += m; 233 | running_faces += 1; 234 | 235 | if (float(running_misses) / float(running_faces) <= cluster_threshold) 236 | { 237 | // we have reached the target ACMR with the current triangle so we need to start a new cluster on the next one 238 | // note that this may mean that we add 'end` to destination for the last triangle, which will imply that the last 239 | // cluster is empty; however, the 'pop_back' after the loop will clean it up 240 | destination[result++] = unsigned(i + 1); 241 | 242 | // reset cache 243 | timestamp += cache_size + 1; 244 | 245 | running_misses = 0; 246 | running_faces = 0; 247 | } 248 | } 249 | 250 | // each time we reach the target ACMR we flush the cluster 251 | // this means that the last cluster is by definition not very good - there are frequent cases where we are left with a few triangles 252 | // in the last cluster, producing a very bad ACMR and significantly penalizing the overall results 253 | // thus we remove the last cluster boundary, merging the last complete cluster with the last incomplete one 254 | // there are sometimes cases when the last cluster is actually good enough - in which case the code above would have added 'end' 255 | // to the cluster boundary array which we need to remove anyway - this code will do that automatically 256 | if (destination[result - 1] != start) 257 | { 258 | result--; 259 | } 260 | } 261 | 262 | assert(result >= cluster_count); 263 | assert(result <= index_count / 3); 264 | 265 | return result; 266 | } 267 | 268 | } // namespace meshopt 269 | 270 | void meshopt_optimizeOverdraw(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float threshold) 271 | { 272 | using namespace meshopt; 273 | 274 | assert(index_count % 3 == 0); 275 | assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); 276 | assert(vertex_positions_stride % sizeof(float) == 0); 277 | 278 | meshopt_Allocator allocator; 279 | 280 | // guard for empty meshes 281 | if (index_count == 0 || vertex_count == 0) 282 | return; 283 | 284 | // support in-place optimization 285 | if (destination == indices) 286 | { 287 | unsigned int* indices_copy = allocator.allocate(index_count); 288 | memcpy(indices_copy, indices, index_count * sizeof(unsigned int)); 289 | indices = indices_copy; 290 | } 291 | 292 | unsigned int cache_size = 16; 293 | 294 | unsigned int* cache_timestamps = allocator.allocate(vertex_count); 295 | 296 | // generate hard boundaries from full-triangle cache misses 297 | unsigned int* hard_clusters = allocator.allocate(index_count / 3); 298 | size_t hard_cluster_count = generateHardBoundaries(hard_clusters, indices, index_count, vertex_count, cache_size, cache_timestamps); 299 | 300 | // generate soft boundaries 301 | unsigned int* soft_clusters = allocator.allocate(index_count / 3 + 1); 302 | size_t soft_cluster_count = generateSoftBoundaries(soft_clusters, indices, index_count, vertex_count, hard_clusters, hard_cluster_count, cache_size, threshold, cache_timestamps); 303 | 304 | const unsigned int* clusters = soft_clusters; 305 | size_t cluster_count = soft_cluster_count; 306 | 307 | // fill sort data 308 | float* sort_data = allocator.allocate(cluster_count); 309 | calculateSortData(sort_data, indices, index_count, vertex_positions, vertex_positions_stride, clusters, cluster_count); 310 | 311 | // sort clusters using sort data 312 | unsigned short* sort_keys = allocator.allocate(cluster_count); 313 | unsigned int* sort_order = allocator.allocate(cluster_count); 314 | calculateSortOrderRadix(sort_order, sort_data, sort_keys, cluster_count); 315 | 316 | // fill output buffer 317 | size_t offset = 0; 318 | 319 | for (size_t it = 0; it < cluster_count; ++it) 320 | { 321 | unsigned int cluster = sort_order[it]; 322 | assert(cluster < cluster_count); 323 | 324 | size_t cluster_begin = clusters[cluster] * 3; 325 | size_t cluster_end = (cluster + 1 < cluster_count) ? clusters[cluster + 1] * 3 : index_count; 326 | assert(cluster_begin < cluster_end); 327 | 328 | memcpy(destination + offset, indices + cluster_begin, (cluster_end - cluster_begin) * sizeof(unsigned int)); 329 | offset += cluster_end - cluster_begin; 330 | } 331 | 332 | assert(offset == index_count); 333 | } 334 | -------------------------------------------------------------------------------- /js/meshopt_decoder_reference.js: -------------------------------------------------------------------------------- 1 | // This file is part of meshoptimizer library and is distributed under the terms of MIT License. 2 | // Copyright (C) 2016-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) 3 | 4 | // This is the reference decoder implementation by Jasper St. Pierre. 5 | // It follows the decoder interface and should be a drop-in replacement for the actual decoder from meshopt_decoder.js 6 | // It is provided for educational value and is not recommended for use in production because it's not performance-optimized. 7 | 8 | const MeshoptDecoder = {}; 9 | MeshoptDecoder.supported = true; 10 | MeshoptDecoder.ready = Promise.resolve(); 11 | 12 | function assert(cond) { 13 | if (!cond) { 14 | throw new Error("Assertion failed"); 15 | } 16 | } 17 | 18 | function dezig(v) { 19 | return ((v & 1) !== 0) ? ~(v >>> 1) : v >>> 1; 20 | } 21 | 22 | MeshoptDecoder.decodeVertexBuffer = (target, elementCount, byteStride, source, filter) => { 23 | assert(source[0] === 0xA0); 24 | 25 | const maxBlockElements = Math.min((0x2000 / byteStride) & ~0x000F, 0x100); 26 | 27 | const deltas = new Uint8Array(0x10); 28 | 29 | const tailDataOffs = source.length - byteStride; 30 | 31 | // What deltas are stored relative to 32 | const tempData = source.slice(tailDataOffs, tailDataOffs + byteStride); 33 | 34 | let srcOffs = 0x01; 35 | 36 | // Attribute Blocks 37 | for (let dstElemBase = 0; dstElemBase < elementCount; dstElemBase += maxBlockElements) { 38 | const attrBlockElementCount = Math.min(elementCount - dstElemBase, maxBlockElements); 39 | const groupCount = ((attrBlockElementCount + 0x0F) & ~0x0F) >>> 4; 40 | const headerByteCount = ((groupCount + 0x03) & ~0x03) >>> 2; 41 | 42 | // Data blocks 43 | for (let byte = 0; byte < byteStride; byte++) { 44 | let headerBitsOffs = srcOffs; 45 | 46 | srcOffs += headerByteCount; 47 | for (let group = 0; group < groupCount; group++) { 48 | const mode = (source[headerBitsOffs] >>> ((group & 0x03) << 1)) & 0x03; 49 | // If this is the last group, move to the next byte of header bits. 50 | if ((group & 0x03) === 0x03) 51 | headerBitsOffs++; 52 | 53 | const dstElemGroup = dstElemBase + (group << 4); 54 | 55 | if (mode === 0) { 56 | // bits 0: All 16 byte deltas are 0; the size of the encoded block is 0 bytes 57 | deltas.fill(0x00); 58 | } else if (mode === 1) { 59 | // bits 1: Deltas are using 2-bit sentinel encoding; the size of the encoded block is [4..20] bytes 60 | const srcBase = srcOffs; 61 | srcOffs += 0x04; 62 | for (let m = 0; m < 0x10; m++) { 63 | // 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0 64 | const shift = (6 - ((m & 0x03) << 1)); 65 | let delta = (source[srcBase + (m >>> 2)] >>> shift) & 0x03; 66 | if (delta === 3) 67 | delta = source[srcOffs++]; 68 | deltas[m] = delta; 69 | } 70 | } else if (mode === 2) { 71 | // bits 2: Deltas are using 4-bit sentinel encoding; the size of the encoded block is [8..24] bytes 72 | const srcBase = srcOffs; 73 | srcOffs += 0x08; 74 | for (let m = 0; m < 0x10; m++) { 75 | // 0 = >>> 6, 1 = >>> 4, 2 = >>> 2, 3 = >>> 0 76 | const shift = (m & 0x01) ? 0 : 4; 77 | let delta = (source[srcBase + (m >>> 1)] >>> shift) & 0x0f; 78 | if (delta === 0xf) 79 | delta = source[srcOffs++]; 80 | deltas[m] = delta; 81 | } 82 | } else { 83 | // bits 3: All 16 byte deltas are stored verbatim; the size of the encoded block is 16 bytes 84 | deltas.set(source.subarray(srcOffs, srcOffs + 0x10)); 85 | srcOffs += 0x10; 86 | } 87 | 88 | // Go through and apply deltas to data 89 | for (let m = 0; m < 0x10; m++) { 90 | const dstElem = dstElemGroup + m; 91 | if (dstElem >= elementCount) 92 | break; 93 | 94 | const delta = dezig(deltas[m]); 95 | const dstOffs = dstElem * byteStride + byte; 96 | target[dstOffs] = (tempData[byte] += delta); 97 | } 98 | } 99 | } 100 | } 101 | 102 | // Filters - only applied if filter isn't undefined or NONE 103 | if (filter === 'OCTAHEDRAL') { 104 | assert(byteStride === 4 || byteStride === 8); 105 | 106 | let dst, maxInt; 107 | if (byteStride === 4) { 108 | dst = new Int8Array(target.buffer); 109 | maxInt = 127; 110 | } else { 111 | dst = new Int16Array(target.buffer); 112 | maxInt = 32767; 113 | } 114 | 115 | for (let i = 0; i < 4 * elementCount; i += 4) { 116 | let x = dst[i + 0], y = dst[i + 1], one = dst[i + 2]; 117 | x /= one; 118 | y /= one; 119 | const z = 1.0 - Math.abs(x) - Math.abs(y); 120 | const t = Math.max(-z, 0.0); 121 | x -= (x >= 0) ? t : -t; 122 | y -= (y >= 0) ? t : -t; 123 | const h = maxInt / Math.hypot(x, y, z); 124 | dst[i + 0] = Math.round(x * h); 125 | dst[i + 1] = Math.round(y * h); 126 | dst[i + 2] = Math.round(z * h); 127 | // keep dst[i + 3] as is 128 | } 129 | } else if (filter === 'QUATERNION') { 130 | assert(byteStride === 8); 131 | 132 | const dst = new Int16Array(target.buffer); 133 | 134 | for (let i = 0; i < 4 * elementCount; i += 4) { 135 | const inputW = dst[i + 3]; 136 | const maxComponent = inputW & 0x03; 137 | const s = Math.SQRT1_2 / (inputW | 0x03); 138 | let x = dst[i + 0] * s; 139 | let y = dst[i + 1] * s; 140 | let z = dst[i + 2] * s; 141 | let w = Math.sqrt(Math.max(0.0, 1.0 - x**2 - y**2 - z**2)); 142 | dst[i + (maxComponent + 1) % 4] = Math.round(x * 32767); 143 | dst[i + (maxComponent + 2) % 4] = Math.round(y * 32767); 144 | dst[i + (maxComponent + 3) % 4] = Math.round(z * 32767); 145 | dst[i + (maxComponent + 0) % 4] = Math.round(w * 32767); 146 | } 147 | } else if (filter === 'EXPONENTIAL') { 148 | assert((byteStride & 0x03) === 0x00); 149 | 150 | const src = new Int32Array(target.buffer); 151 | const dst = new Float32Array(target.buffer); 152 | for (let i = 0; i < (byteStride * elementCount) / 4; i++) { 153 | const v = src[i], exp = v >> 24, mantissa = (v << 8) >> 8; 154 | dst[i] = 2.0**exp * mantissa; 155 | } 156 | } 157 | }; 158 | 159 | function pushfifo(fifo, n) { 160 | for (let i = fifo.length - 1; i > 0; i--) 161 | fifo[i] = fifo[i - 1]; 162 | fifo[0] = n; 163 | } 164 | 165 | MeshoptDecoder.decodeIndexBuffer = (target, count, byteStride, source) => { 166 | assert(source[0] === 0xE1); 167 | assert(count % 3 === 0); 168 | assert(byteStride === 2 || byteStride === 4); 169 | 170 | let dst; 171 | if (byteStride === 2) 172 | dst = new Uint16Array(target.buffer); 173 | else 174 | dst = new Uint32Array(target.buffer); 175 | 176 | const triCount = count / 3; 177 | 178 | let codeOffs = 0x01; 179 | let dataOffs = codeOffs + triCount; 180 | let codeauxOffs = source.length - 0x10; 181 | 182 | function readLEB128() { 183 | let n = 0; 184 | for (let i = 0; ; i += 7) { 185 | const b = source[dataOffs++]; 186 | n |= (b & 0x7F) << i; 187 | 188 | if (b < 0x80) 189 | return n; 190 | } 191 | } 192 | 193 | let next = 0, last = 0; 194 | const edgefifo = new Uint32Array(32); 195 | const vertexfifo = new Uint32Array(16); 196 | 197 | function decodeIndex(v) { 198 | return (last += dezig(v)); 199 | } 200 | 201 | let dstOffs = 0; 202 | for (let i = 0; i < triCount; i++) { 203 | const code = source[codeOffs++]; 204 | const b0 = code >>> 4, b1 = code & 0x0F; 205 | 206 | if (b0 < 0x0F) { 207 | const a = edgefifo[(b0 << 1) + 0], b = edgefifo[(b0 << 1) + 1]; 208 | let c = -1; 209 | 210 | if (b1 === 0x00) { 211 | c = next++; 212 | pushfifo(vertexfifo, c); 213 | } else if (b1 < 0x0D) { 214 | c = vertexfifo[b1]; 215 | } else if (b1 === 0x0D) { 216 | c = --last; 217 | pushfifo(vertexfifo, c); 218 | } else if (b1 === 0x0E) { 219 | c = ++last; 220 | pushfifo(vertexfifo, c); 221 | } else if (b1 === 0x0F) { 222 | const v = readLEB128(); 223 | c = decodeIndex(v); 224 | pushfifo(vertexfifo, c); 225 | } 226 | 227 | // fifo pushes happen backwards 228 | pushfifo(edgefifo, b); pushfifo(edgefifo, c); 229 | pushfifo(edgefifo, c); pushfifo(edgefifo, a); 230 | 231 | dst[dstOffs++] = a; 232 | dst[dstOffs++] = b; 233 | dst[dstOffs++] = c; 234 | } else { // b0 === 0x0F 235 | let a = -1, b = -1, c = -1; 236 | 237 | if (b1 < 0x0E) { 238 | const e = source[codeauxOffs + b1]; 239 | const z = e >>> 4, w = e & 0x0F; 240 | 241 | a = next++; 242 | 243 | if (z === 0x00) 244 | b = next++; 245 | else 246 | b = vertexfifo[z - 1]; 247 | 248 | if (w === 0x00) 249 | c = next++; 250 | else 251 | c = vertexfifo[w - 1]; 252 | 253 | pushfifo(vertexfifo, a); 254 | if (z === 0x00) 255 | pushfifo(vertexfifo, b); 256 | if (w === 0x00) 257 | pushfifo(vertexfifo, c); 258 | } else { 259 | const e = source[dataOffs++]; 260 | if (e === 0x00) 261 | next = 0; 262 | 263 | const z = e >>> 4, w = e & 0x0F; 264 | 265 | if (b1 === 0x0E) 266 | a = next++; 267 | else 268 | a = decodeIndex(readLEB128()); 269 | 270 | if (z === 0x00) 271 | b = next++; 272 | else if (z === 0x0F) 273 | b = decodeIndex(readLEB128()); 274 | else 275 | b = vertexfifo[z - 1]; 276 | 277 | if (w === 0x00) 278 | c = next++; 279 | else if (w === 0x0F) 280 | c = decodeIndex(readLEB128()); 281 | else 282 | c = vertexfifo[w - 1]; 283 | 284 | pushfifo(vertexfifo, a); 285 | if (z === 0x00 || z === 0x0F) 286 | pushfifo(vertexfifo, b); 287 | if (w === 0x00 || w === 0x0F) 288 | pushfifo(vertexfifo, c); 289 | } 290 | 291 | pushfifo(edgefifo, a); pushfifo(edgefifo, b); 292 | pushfifo(edgefifo, b); pushfifo(edgefifo, c); 293 | pushfifo(edgefifo, c); pushfifo(edgefifo, a); 294 | 295 | dst[dstOffs++] = a; 296 | dst[dstOffs++] = b; 297 | dst[dstOffs++] = c; 298 | } 299 | } 300 | }; 301 | 302 | MeshoptDecoder.decodeIndexSequence = (target, count, byteStride, source) => { 303 | assert(source[0] === 0xD1); 304 | assert(byteStride === 2 || byteStride === 4); 305 | 306 | let dst; 307 | if (byteStride === 2) 308 | dst = new Uint16Array(target.buffer); 309 | else 310 | dst = new Uint32Array(target.buffer); 311 | 312 | let dataOffs = 0x01; 313 | 314 | function readLEB128() { 315 | let n = 0; 316 | for (let i = 0; ; i += 7) { 317 | const b = source[dataOffs++]; 318 | n |= (b & 0x7F) << i; 319 | 320 | if (b < 0x80) 321 | return n; 322 | } 323 | } 324 | 325 | const last = new Uint32Array(2); 326 | 327 | for (let i = 0; i < count; i++) { 328 | const v = readLEB128(); 329 | const b = (v & 0x01); 330 | const delta = dezig(v >>> 1); 331 | dst[i] = (last[b] += delta); 332 | } 333 | }; 334 | 335 | MeshoptDecoder.decodeGltfBuffer = (target, count, size, source, mode, filter) => { 336 | var table = { 337 | ATTRIBUTES: MeshoptDecoder.decodeVertexBuffer, 338 | TRIANGLES: MeshoptDecoder.decodeIndexBuffer, 339 | INDICES: MeshoptDecoder.decodeIndexSequence, 340 | }; 341 | assert(table[mode] !== undefined); 342 | table[mode](target, count, size, source, filter); 343 | }; 344 | 345 | // node.js interface: 346 | // for (let k in MeshoptDecoder) { exports[k] = MeshoptDecoder[k]; } 347 | 348 | export { MeshoptDecoder }; 349 | -------------------------------------------------------------------------------- /tools/vcachetuner.cpp: -------------------------------------------------------------------------------- 1 | #include "../src/meshoptimizer.h" 2 | #include "../extern/fast_obj.h" 3 | 4 | #define SDEFL_IMPLEMENTATION 5 | #include "../extern/sdefl.h" 6 | 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | const int kCacheSizeMax = 16; 17 | const int kValenceMax = 8; 18 | 19 | namespace meshopt 20 | { 21 | struct VertexScoreTable 22 | { 23 | float cache[1 + kCacheSizeMax]; 24 | float live[1 + kValenceMax]; 25 | }; 26 | } // namespace meshopt 27 | 28 | void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const meshopt::VertexScoreTable* table); 29 | 30 | struct Profile 31 | { 32 | float weight; 33 | int cache, warp, triangle; // vcache tuning parameters 34 | int compression; 35 | }; 36 | 37 | Profile profiles[] = 38 | { 39 | {1.f, 0, 0, 0, 0}, // Compression 40 | {1.f, 0, 0, 0, 1}, // Compression w/deflate 41 | // {1.f, 14, 64, 128}, // AMD GCN 42 | // {1.f, 32, 32, 32}, // NVidia Pascal 43 | // {1.f, 16, 32, 32}, // NVidia Kepler, Maxwell 44 | // {1.f, 128, 0, 0}, // Intel 45 | }; 46 | 47 | const int Profile_Count = sizeof(profiles) / sizeof(profiles[0]); 48 | 49 | struct pcg32_random_t 50 | { 51 | uint64_t state; 52 | uint64_t inc; 53 | }; 54 | 55 | #define PCG32_INITIALIZER { 0x853c49e6748fea9bULL, 0xda3e39cb94b95bdbULL } 56 | 57 | uint32_t pcg32_random_r(pcg32_random_t* rng) 58 | { 59 | uint64_t oldstate = rng->state; 60 | // Advance internal state 61 | rng->state = oldstate * 6364136223846793005ULL + (rng->inc | 1); 62 | // Calculate output function (XSH RR), uses old state for max ILP 63 | uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u; 64 | uint32_t rot = oldstate >> 59u; 65 | return (xorshifted >> rot) | (xorshifted << ((-rot) & 31)); 66 | } 67 | 68 | pcg32_random_t rngstate = PCG32_INITIALIZER; 69 | 70 | float rand01() 71 | { 72 | return pcg32_random_r(&rngstate) / float(1ull << 32); 73 | } 74 | 75 | uint32_t rand32() 76 | { 77 | return pcg32_random_r(&rngstate); 78 | } 79 | 80 | struct State 81 | { 82 | float cache[kCacheSizeMax]; 83 | float live[kValenceMax]; 84 | float fitness; 85 | }; 86 | 87 | struct Mesh 88 | { 89 | const char* name; 90 | 91 | size_t vertex_count; 92 | std::vector indices; 93 | 94 | float metric_base[Profile_Count]; 95 | }; 96 | 97 | Mesh gridmesh(unsigned int N) 98 | { 99 | Mesh result; 100 | 101 | result.name = "grid"; 102 | 103 | result.vertex_count = (N + 1) * (N + 1); 104 | result.indices.reserve(N * N * 6); 105 | 106 | for (unsigned int y = 0; y < N; ++y) 107 | for (unsigned int x = 0; x < N; ++x) 108 | { 109 | result.indices.push_back((y + 0) * (N + 1) + (x + 0)); 110 | result.indices.push_back((y + 0) * (N + 1) + (x + 1)); 111 | result.indices.push_back((y + 1) * (N + 1) + (x + 0)); 112 | 113 | result.indices.push_back((y + 1) * (N + 1) + (x + 0)); 114 | result.indices.push_back((y + 0) * (N + 1) + (x + 1)); 115 | result.indices.push_back((y + 1) * (N + 1) + (x + 1)); 116 | } 117 | 118 | return result; 119 | } 120 | 121 | Mesh objmesh(const char* path) 122 | { 123 | fastObjMesh* obj = fast_obj_read(path); 124 | if (!obj) 125 | { 126 | printf("Error loading %s: file not found\n", path); 127 | return Mesh(); 128 | } 129 | 130 | size_t total_indices = 0; 131 | 132 | for (unsigned int i = 0; i < obj->face_count; ++i) 133 | total_indices += 3 * (obj->face_vertices[i] - 2); 134 | 135 | struct Vertex 136 | { 137 | float px, py, pz; 138 | float nx, ny, nz; 139 | float tx, ty; 140 | }; 141 | 142 | std::vector vertices(total_indices); 143 | 144 | size_t vertex_offset = 0; 145 | size_t index_offset = 0; 146 | 147 | for (unsigned int i = 0; i < obj->face_count; ++i) 148 | { 149 | for (unsigned int j = 0; j < obj->face_vertices[i]; ++j) 150 | { 151 | fastObjIndex gi = obj->indices[index_offset + j]; 152 | 153 | Vertex v = 154 | { 155 | obj->positions[gi.p * 3 + 0], 156 | obj->positions[gi.p * 3 + 1], 157 | obj->positions[gi.p * 3 + 2], 158 | obj->normals[gi.n * 3 + 0], 159 | obj->normals[gi.n * 3 + 1], 160 | obj->normals[gi.n * 3 + 2], 161 | obj->texcoords[gi.t * 2 + 0], 162 | obj->texcoords[gi.t * 2 + 1], 163 | }; 164 | 165 | // triangulate polygon on the fly; offset-3 is always the first polygon vertex 166 | if (j >= 3) 167 | { 168 | vertices[vertex_offset + 0] = vertices[vertex_offset - 3]; 169 | vertices[vertex_offset + 1] = vertices[vertex_offset - 1]; 170 | vertex_offset += 2; 171 | } 172 | 173 | vertices[vertex_offset] = v; 174 | vertex_offset++; 175 | } 176 | 177 | index_offset += obj->face_vertices[i]; 178 | } 179 | 180 | fast_obj_destroy(obj); 181 | 182 | Mesh result; 183 | 184 | result.name = path; 185 | 186 | std::vector remap(total_indices); 187 | 188 | size_t total_vertices = meshopt_generateVertexRemap(&remap[0], NULL, total_indices, &vertices[0], total_indices, sizeof(Vertex)); 189 | 190 | result.indices.resize(total_indices); 191 | meshopt_remapIndexBuffer(&result.indices[0], NULL, total_indices, &remap[0]); 192 | 193 | result.vertex_count = total_vertices; 194 | 195 | return result; 196 | } 197 | 198 | template 199 | size_t compress(const std::vector& data, int level = SDEFL_LVL_DEF) 200 | { 201 | std::vector cbuf(sdefl_bound(int(data.size() * sizeof(T)))); 202 | sdefl s = {}; 203 | return sdeflate(&s, &cbuf[0], reinterpret_cast(&data[0]), int(data.size() * sizeof(T)), level); 204 | } 205 | 206 | void compute_metric(const State* state, const Mesh& mesh, float result[Profile_Count]) 207 | { 208 | std::vector indices(mesh.indices.size()); 209 | 210 | if (state) 211 | { 212 | meshopt::VertexScoreTable table = {}; 213 | memcpy(table.cache + 1, state->cache, kCacheSizeMax * sizeof(float)); 214 | memcpy(table.live + 1, state->live, kValenceMax * sizeof(float)); 215 | meshopt_optimizeVertexCacheTable(&indices[0], &mesh.indices[0], mesh.indices.size(), mesh.vertex_count, &table); 216 | } 217 | else 218 | { 219 | meshopt_optimizeVertexCache(&indices[0], &mesh.indices[0], mesh.indices.size(), mesh.vertex_count); 220 | } 221 | 222 | meshopt_optimizeVertexFetch(NULL, &indices[0], indices.size(), NULL, mesh.vertex_count, 0); 223 | 224 | std::vector ibuf; 225 | 226 | for (int profile = 0; profile < Profile_Count; ++profile) 227 | { 228 | if (profiles[profile].cache == 0) 229 | { 230 | ibuf.resize(meshopt_encodeIndexBufferBound(indices.size(), mesh.vertex_count)); 231 | ibuf.resize(meshopt_encodeIndexBuffer(&ibuf[0], ibuf.size(), &indices[0], indices.size())); 232 | } 233 | } 234 | 235 | for (int profile = 0; profile < Profile_Count; ++profile) 236 | { 237 | if (profiles[profile].cache) 238 | { 239 | meshopt_VertexCacheStatistics stats = meshopt_analyzeVertexCache(&indices[0], indices.size(), mesh.vertex_count, profiles[profile].cache, profiles[profile].warp, profiles[profile].triangle); 240 | result[profile] = stats.atvr; 241 | } 242 | else 243 | { 244 | // take into account both pre-deflate and post-deflate size but focus a bit more on post-deflate 245 | size_t csize = profiles[profile].compression ? compress(ibuf) : ibuf.size(); 246 | 247 | result[profile] = double(csize) / double(indices.size() / 3); 248 | } 249 | } 250 | } 251 | 252 | float fitness_score(const State& state, const std::vector& meshes) 253 | { 254 | float result = 0; 255 | float count = 0; 256 | 257 | for (auto& mesh : meshes) 258 | { 259 | float metric[Profile_Count]; 260 | compute_metric(&state, mesh, metric); 261 | 262 | for (int profile = 0; profile < Profile_Count; ++profile) 263 | { 264 | result += mesh.metric_base[profile] / metric[profile] * profiles[profile].weight; 265 | count += profiles[profile].weight; 266 | } 267 | } 268 | 269 | return result / count; 270 | } 271 | 272 | std::vector gen0(size_t count, const std::vector& meshes) 273 | { 274 | std::vector result; 275 | 276 | for (size_t i = 0; i < count; ++i) 277 | { 278 | State state = {}; 279 | 280 | for (int j = 0; j < kCacheSizeMax; ++j) 281 | state.cache[j] = rand01(); 282 | 283 | for (int j = 0; j < kValenceMax; ++j) 284 | state.live[j] = rand01(); 285 | 286 | state.fitness = fitness_score(state, meshes); 287 | 288 | result.push_back(state); 289 | } 290 | 291 | return result; 292 | } 293 | 294 | // https://en.wikipedia.org/wiki/Differential_evolution 295 | // Good Parameters for Differential Evolution. Magnus Erik Hvass Pedersen, 2010 296 | std::pair genN(std::vector& seed, const std::vector& meshes, float crossover = 0.8803f, float weight = 0.4717f) 297 | { 298 | std::vector result(seed.size()); 299 | 300 | for (size_t i = 0; i < seed.size(); ++i) 301 | { 302 | for (;;) 303 | { 304 | int a = rand32() % seed.size(); 305 | int b = rand32() % seed.size(); 306 | int c = rand32() % seed.size(); 307 | 308 | if (a == b || a == c || b == c || a == int(i) || b == int(i) || c == int(i)) 309 | continue; 310 | 311 | int rc = rand32() % kCacheSizeMax; 312 | int rl = rand32() % kValenceMax; 313 | 314 | for (int j = 0; j < kCacheSizeMax; ++j) 315 | { 316 | float r = rand01(); 317 | 318 | if (r < crossover || j == rc) 319 | 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]))); 320 | else 321 | result[i].cache[j] = seed[i].cache[j]; 322 | } 323 | 324 | for (int j = 0; j < kValenceMax; ++j) 325 | { 326 | float r = rand01(); 327 | 328 | if (r < crossover || j == rl) 329 | 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]))); 330 | else 331 | result[i].live[j] = seed[i].live[j]; 332 | } 333 | 334 | break; 335 | } 336 | } 337 | 338 | #pragma omp parallel for 339 | for (size_t i = 0; i < seed.size(); ++i) 340 | { 341 | result[i].fitness = fitness_score(result[i], meshes); 342 | } 343 | 344 | State best = {}; 345 | float bestfit = 0; 346 | 347 | for (size_t i = 0; i < seed.size(); ++i) 348 | { 349 | if (result[i].fitness > seed[i].fitness) 350 | seed[i] = result[i]; 351 | 352 | if (seed[i].fitness > bestfit) 353 | { 354 | best = seed[i]; 355 | bestfit = seed[i].fitness; 356 | } 357 | } 358 | 359 | return std::make_pair(best, bestfit); 360 | } 361 | 362 | bool load_state(const char* path, std::vector& result) 363 | { 364 | FILE* file = fopen(path, "rb"); 365 | if (!file) 366 | return false; 367 | 368 | State state; 369 | 370 | result.clear(); 371 | 372 | while (fread(&state, sizeof(State), 1, file) == 1) 373 | result.push_back(state); 374 | 375 | fclose(file); 376 | 377 | return true; 378 | } 379 | 380 | bool save_state(const char* path, const std::vector& result) 381 | { 382 | FILE* file = fopen(path, "wb"); 383 | if (!file) 384 | return false; 385 | 386 | for (auto& state : result) 387 | { 388 | if (fwrite(&state, sizeof(State), 1, file) != 1) 389 | { 390 | fclose(file); 391 | return false; 392 | } 393 | } 394 | 395 | return fclose(file) == 0; 396 | } 397 | 398 | void dump_state(const State& state) 399 | { 400 | printf("cache:"); 401 | for (int i = 0; i < kCacheSizeMax; ++i) 402 | { 403 | printf(" %.3f", state.cache[i]); 404 | } 405 | printf("\n"); 406 | 407 | printf("live:"); 408 | for (int i = 0; i < kValenceMax; ++i) 409 | { 410 | printf(" %.3f", state.live[i]); 411 | } 412 | printf("\n"); 413 | } 414 | 415 | void dump_stats(const State& state, const std::vector& meshes) 416 | { 417 | float improvement[Profile_Count] = {}; 418 | 419 | for (size_t i = 0; i < meshes.size(); ++i) 420 | { 421 | float metric[Profile_Count]; 422 | compute_metric(&state, meshes[i], metric); 423 | 424 | printf(" %s", meshes[i].name); 425 | for (int profile = 0; profile < Profile_Count; ++profile) 426 | printf(" %f", metric[profile]); 427 | 428 | for (int profile = 0; profile < Profile_Count; ++profile) 429 | improvement[profile] += meshes[i].metric_base[profile] / metric[profile]; 430 | } 431 | 432 | printf("; improvement"); 433 | for (int profile = 0; profile < Profile_Count; ++profile) 434 | printf(" %f", improvement[profile] / float(meshes.size())); 435 | 436 | printf("\n"); 437 | } 438 | 439 | int main(int argc, char** argv) 440 | { 441 | meshopt_encodeIndexVersion(1); 442 | 443 | std::vector meshes; 444 | 445 | meshes.push_back(gridmesh(50)); 446 | 447 | for (int i = 1; i < argc; ++i) 448 | meshes.push_back(objmesh(argv[i])); 449 | 450 | size_t total_triangles = 0; 451 | 452 | for (auto& mesh : meshes) 453 | { 454 | compute_metric(nullptr, mesh, mesh.metric_base); 455 | 456 | total_triangles += mesh.indices.size() / 3; 457 | } 458 | 459 | std::vector pop; 460 | size_t gen = 0; 461 | 462 | if (load_state("mutator.state", pop)) 463 | { 464 | printf("Loaded %d state vectors\n", int(pop.size())); 465 | } 466 | else 467 | { 468 | pop = gen0(95, meshes); 469 | } 470 | 471 | printf("%d meshes, %.1fM triangles\n", int(meshes.size()), double(total_triangles) / 1e6); 472 | 473 | for (;;) 474 | { 475 | auto best = genN(pop, meshes); 476 | gen++; 477 | 478 | if (gen % 10 == 0) 479 | { 480 | printf("%d: fitness %f;", int(gen), best.second); 481 | dump_stats(best.first, meshes); 482 | } 483 | else 484 | { 485 | printf("%d: fitness %f\n", int(gen), best.second); 486 | } 487 | 488 | dump_state(best.first); 489 | 490 | if (save_state("mutator.state-temp", pop) && rename("mutator.state-temp", "mutator.state") == 0) 491 | { 492 | } 493 | else 494 | { 495 | printf("ERROR: Can't save state\n"); 496 | } 497 | } 498 | } 499 | --------------------------------------------------------------------------------