├── .circleci └── config.yml ├── .github └── workflows │ ├── release.yml │ └── smoke.yml ├── .gitignore ├── BUILDING.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── assets ├── pinecones │ ├── matte │ │ ├── Pinecone.gltf │ │ └── Pinecone_data.bin │ ├── shiny │ │ ├── Pinecone.gltf │ │ └── Pinecone_data.bin │ ├── tinted │ │ ├── Pinecone.gltf │ │ └── Pinecone_data.bin │ └── variational │ │ ├── Pinecone.gltf │ │ └── Pinecone_data.bin └── tank_teapots │ ├── TexturedTankTeapot-export.gltf │ ├── camouflage-pattern.jpg │ ├── green-brushed.jpg │ ├── teapot-camo-pink-bronze.gltf │ ├── teapot-camo-pink-silver.gltf │ ├── teapot-green-pink-bronze.gltf │ ├── teapot-green-pink-silver.gltf │ └── teapot_data.bin ├── build-wasm-pkg.sh ├── native ├── .vscode │ └── tasks.json ├── Cargo.toml ├── assets │ ├── Cargo.toml │ └── src │ │ └── lib.rs ├── src │ ├── bin │ │ └── meldtool │ │ │ ├── args.rs │ │ │ └── mod.rs │ ├── extension │ │ ├── mod.rs │ │ ├── on_primitive.rs │ │ └── on_root.rs │ ├── glb.rs │ ├── gltfext.rs │ ├── lib.rs │ ├── meld_keys │ │ ├── fingerprints.rs │ │ ├── key_trait.rs │ │ └── mod.rs │ ├── variational_asset │ │ ├── metadata.rs │ │ ├── mod.rs │ │ └── wasm.rs │ └── work_asset │ │ ├── construct.rs │ │ ├── export.rs │ │ ├── meld.rs │ │ └── mod.rs └── tests │ ├── basic_parse_tests.rs │ ├── meld_tests.rs │ └── variational_asset_tests.rs ├── variationator.code-workspace └── web └── cli ├── .gitignore ├── build-node-app.sh ├── package-lock.json ├── package.json ├── src ├── index.ts └── wasmpkg.ts ├── tsconfig.json └── webpack.config.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # largely lifted from https://gist.github.com/zargony/de209b1a790c3cb2176c86405a51b33c 2 | 3 | version: 2 4 | jobs: 5 | build_and_test: 6 | docker: 7 | - image: circleci/rust:buster 8 | steps: 9 | - checkout 10 | - restore_cache: 11 | keys: 12 | - v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} 13 | - run: 14 | name: Update Rust 15 | command: rustup update 16 | - run: 17 | name: Version information 18 | command: rustc --version; cargo --version; rustup --version 19 | - run: 20 | name: Build all targets 21 | command: cargo build --all --all-targets 22 | - save_cache: 23 | paths: 24 | - /usr/local/cargo/registry 25 | - target/debug/.fingerprint 26 | - target/debug/build 27 | - target/debug/deps 28 | key: v4-cargo-cache-{{ arch }}-{{ checksum "Cargo.lock" }} 29 | - run: 30 | name: Run all tests 31 | command: cargo test --all 32 | workflows: 33 | version: 2 34 | build_and_test: 35 | jobs: 36 | - build_and_test 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | name: "Build '${{ matrix.rust }}' on ${{ matrix.os.human-os-name }}" 10 | runs-on: ${{ matrix.os.github-os-name }} 11 | strategy: 12 | matrix: 13 | os: 14 | - human-os-name: Linux 15 | github-os-name: ubuntu-latest 16 | meldtool-filename: meldtool 17 | - human-os-name: Windows 18 | github-os-name: windows-latest 19 | meldtool-filename: meldtool.exe 20 | - human-os-name: MacOS 21 | github-os-name: macOS-latest 22 | meldtool-filename: meldtool 23 | rust: [stable] 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v1 27 | - name: Setup 28 | uses: hecrj/setup-rust-action@v1 29 | with: 30 | rust-version: ${{ matrix.rust }} 31 | - name: Test 32 | run: cargo test --release 33 | - name: Build 34 | run: cargo build --release 35 | - name: Upload 36 | uses: svenstaro/upload-release-action@v1-release 37 | with: 38 | repo_token: ${{ secrets.GITHUB_TOKEN }} 39 | file: target/release/${{ matrix.os.meldtool-filename}} 40 | asset_name: ${{ matrix.os.meldtool-filename }}-${{ matrix.os.human-os-name }}-x64 41 | tag: ${{ github.ref }} 42 | overwrite: true 43 | -------------------------------------------------------------------------------- /.github/workflows/smoke.yml: -------------------------------------------------------------------------------- 1 | name: Smoke 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: "Build/Test '${{ matrix.rust }}' on ${{ matrix.os }}" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | rust: [stable, nightly] 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v1 22 | - name: Setup 23 | uses: hecrj/setup-rust-action@v1 24 | with: 25 | rust-version: ${{ matrix.rust }} 26 | - name: Build 27 | run: cargo build --verbose 28 | - name: Run tests 29 | run: cargo test --verbose 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build output 2 | /target/ 3 | 4 | # Backup files from rustfmt 5 | /native/**/*.rs.bk 6 | 7 | /**/*~ 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | There are a number of prerequisites for building the code in this repo. 4 | 5 | Here follow terse descriptions on how to get there. 6 | 7 | ### Setup for Native: The Rust Toolchain 8 | 9 | - Follow [these instructions](https://www.rust-lang.org/tools/install) to install the Rust 10 | toolchain. 11 | 12 | - There are other installation paths, but `rustup` is heartily recommended. 13 | - Installing `rustup` should also install `cargo`, Rust's day-to-day workhorse tool. 14 | - Run `rustup update` both now & occasionally, to get updates to languages & components. 15 | 16 | ### Setup for Web: Node & WebAssembly 17 | 18 | - Follow [these](https://rustwasm.github.io/wasm-pack/installer/) to install wasm-bindgen. 19 | - This is the tool that binds Rust and WebAssembly together. 20 | - For sturdiness, we require specific versions of Node.js. 21 | - To that end, [install nvm](https://github.com/nvm-sh/nvm#install--update-script), the Node 22 | Version Manager. 23 | - Note the instructions on how to enable `nvm` for your current shell session. 24 | - Grab a stable node version for general use: `nvm install --lts --latest-npm` 25 | - Further down, you will run a script that installs a specific node version for our use. 26 | 27 | ### Fetching the Code 28 | 29 | Grab the actual repository: 30 | 31 | ``` 32 | > git clone https://github.com/facebookincubator/glTFVariantMeld 33 | > cd glTFVariantMeld 34 | ``` 35 | 36 | ## Generate Rust Binaries 37 | 38 | At this point you should be able to run `cargo build`, which will recurse into `./native` where the Rust source lives. Binaries will end up in `./target/debug/`. 39 | 40 | ## Generate WebAssembly Package 41 | 42 | If you now try: 43 | 44 | ``` 45 | > ./build-wasm-pkg.sh 46 | ``` 47 | 48 | you should end up with a generated NPM package in `./web/wasmpkg/`. 49 | 50 | You can eyeball it for fun, but we don't do anything with the generated package directly. We just reference it from elsewhere, as per the next section. 51 | 52 | ## Run the Node.js test app 53 | 54 | Now simply run: 55 | 56 | ``` 57 | > cd web/cli 58 | > npm install 59 | > ./build-node-app.sh 60 | ``` 61 | 62 | This script should make sure you've got NVM installed, and then use it to make sure you're running the recommended Node.js version, then execute the TypeScript-blessed WebPack to generate the required files in `./dist`. Something like this: 63 | 64 | ``` 65 | > ls -l ./dist 66 | -rw-r--r-- 1 zell staff 31497 Sep 17 17:05 0.app.js 67 | -rwxr-xr-x 1 zell staff 31707 Sep 17 17:05 app.js 68 | -rw-r--r-- 1 zell staff 1010595 Sep 17 17:05 d4d094458c23e79dfea6.module.wasm 69 | ``` 70 | 71 | Finally, the script executes `dist/app.js` which actually runs the app. 72 | 73 | Tada! You have built a native library, converted it to WebAssembly, bundled it all into executable JavaScript, and successfully run it. It's not much harder to run it in the browser, but instructions for that will come later. 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | Facebook has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please read the [full text](https://code.fb.com/codeofconduct/) 5 | so that you can understand what actions will and will not be tolerated. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to glTFVariantMeld 2 | 3 | We want to make contributing to this project as easy and transparent as 4 | possible. 5 | 6 | ## Pull Requests 7 | 8 | We actively welcome your pull requests. 9 | 10 | 1. Fork the repo and create your branch from `master`. 11 | 2. If you've added code that should be tested, add tests. 12 | 3. If you've changed APIs, update the documentation. 13 | 4. Ensure the test suite passes. 14 | 5. If you haven't already, complete the Contributor License Agreement ("CLA"). 15 | 16 | ## Contributor License Agreement ("CLA") 17 | 18 | In order to accept your pull request, we need you to submit a CLA. You only need 19 | to do this once to work on any of Facebook's open source projects. 20 | 21 | Complete your CLA here: 22 | 23 | ## Issues 24 | 25 | We use GitHub issues to track public bugs. Please ensure your description is 26 | clear and has sufficient instructions to be able to reproduce the issue. 27 | 28 | ## License 29 | 30 | By contributing to glTFVariantMeld, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "adler32" 5 | version = "1.0.4" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "ansi_term" 10 | version = "0.11.0" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | dependencies = [ 13 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 14 | ] 15 | 16 | [[package]] 17 | name = "assets" 18 | version = "0.1.0" 19 | 20 | [[package]] 21 | name = "atty" 22 | version = "0.2.13" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | dependencies = [ 25 | "libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "autocfg" 31 | version = "0.1.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | 34 | [[package]] 35 | name = "base64" 36 | version = "0.10.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | dependencies = [ 39 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 40 | ] 41 | 42 | [[package]] 43 | name = "bitflags" 44 | version = "1.2.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | 47 | [[package]] 48 | name = "bumpalo" 49 | version = "2.6.0" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | 52 | [[package]] 53 | name = "byteorder" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | 57 | [[package]] 58 | name = "cfg-if" 59 | version = "0.1.10" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | 62 | [[package]] 63 | name = "clap" 64 | version = "2.33.0" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | dependencies = [ 67 | "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 68 | "atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)", 69 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 70 | "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 71 | "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 72 | "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 73 | "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", 74 | ] 75 | 76 | [[package]] 77 | name = "deflate" 78 | version = "0.7.20" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | dependencies = [ 81 | "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 82 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 83 | ] 84 | 85 | [[package]] 86 | name = "gltf" 87 | version = "0.13.0" 88 | source = "git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld#28263402838adfffe376e08a9dc84c36b62b8339" 89 | dependencies = [ 90 | "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", 91 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 92 | "gltf-json 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)", 93 | "image 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)", 94 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 95 | ] 96 | 97 | [[package]] 98 | name = "gltf-derive" 99 | version = "0.13.0" 100 | source = "git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld#28263402838adfffe376e08a9dc84c36b62b8339" 101 | dependencies = [ 102 | "inflections 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 103 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 104 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 105 | "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 106 | ] 107 | 108 | [[package]] 109 | name = "gltf-json" 110 | version = "0.13.0" 111 | source = "git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld#28263402838adfffe376e08a9dc84c36b62b8339" 112 | dependencies = [ 113 | "gltf-derive 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)", 114 | "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", 115 | "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", 116 | "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", 117 | ] 118 | 119 | [[package]] 120 | name = "gltf_variant_meld" 121 | version = "0.1.0" 122 | dependencies = [ 123 | "assets 0.1.0", 124 | "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "gltf 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)", 126 | "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", 127 | "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", 128 | "serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)", 129 | "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 130 | "spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 131 | "wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 132 | ] 133 | 134 | [[package]] 135 | name = "image" 136 | version = "0.21.3" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | dependencies = [ 139 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 140 | "jpeg-decoder 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 141 | "lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", 142 | "num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", 143 | "num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 144 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 145 | "png 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)", 146 | ] 147 | 148 | [[package]] 149 | name = "inflate" 150 | version = "0.4.5" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | dependencies = [ 153 | "adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 154 | ] 155 | 156 | [[package]] 157 | name = "inflections" 158 | version = "1.1.1" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | 161 | [[package]] 162 | name = "itoa" 163 | version = "0.4.4" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | 166 | [[package]] 167 | name = "jpeg-decoder" 168 | version = "0.1.16" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | dependencies = [ 171 | "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 172 | ] 173 | 174 | [[package]] 175 | name = "lazy_static" 176 | version = "1.4.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | 179 | [[package]] 180 | name = "libc" 181 | version = "0.2.65" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | 184 | [[package]] 185 | name = "log" 186 | version = "0.4.8" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | dependencies = [ 189 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 190 | ] 191 | 192 | [[package]] 193 | name = "lzw" 194 | version = "0.10.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | 197 | [[package]] 198 | name = "num-integer" 199 | version = "0.1.41" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | dependencies = [ 202 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 203 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 204 | ] 205 | 206 | [[package]] 207 | name = "num-iter" 208 | version = "0.1.39" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | dependencies = [ 211 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 212 | "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", 213 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 214 | ] 215 | 216 | [[package]] 217 | name = "num-rational" 218 | version = "0.2.2" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | dependencies = [ 221 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 222 | "num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)", 223 | "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 224 | ] 225 | 226 | [[package]] 227 | name = "num-traits" 228 | version = "0.2.8" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | dependencies = [ 231 | "autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 232 | ] 233 | 234 | [[package]] 235 | name = "png" 236 | version = "0.14.1" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | dependencies = [ 239 | "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 240 | "deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", 241 | "inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", 242 | "num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", 243 | ] 244 | 245 | [[package]] 246 | name = "proc-macro2" 247 | version = "1.0.5" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | dependencies = [ 250 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 251 | ] 252 | 253 | [[package]] 254 | name = "quote" 255 | version = "1.0.2" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | dependencies = [ 258 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 259 | ] 260 | 261 | [[package]] 262 | name = "ryu" 263 | version = "1.0.2" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | 266 | [[package]] 267 | name = "serde" 268 | version = "1.0.101" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | 271 | [[package]] 272 | name = "serde_derive" 273 | version = "1.0.101" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | dependencies = [ 276 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 277 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 278 | "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 279 | ] 280 | 281 | [[package]] 282 | name = "serde_json" 283 | version = "1.0.41" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | dependencies = [ 286 | "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", 287 | "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 288 | "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", 289 | ] 290 | 291 | [[package]] 292 | name = "sha1" 293 | version = "0.6.0" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | 296 | [[package]] 297 | name = "spectral" 298 | version = "0.6.0" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | 301 | [[package]] 302 | name = "strsim" 303 | version = "0.8.0" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | 306 | [[package]] 307 | name = "syn" 308 | version = "1.0.5" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | dependencies = [ 311 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 312 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 313 | "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 314 | ] 315 | 316 | [[package]] 317 | name = "textwrap" 318 | version = "0.11.0" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | dependencies = [ 321 | "unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 322 | ] 323 | 324 | [[package]] 325 | name = "unicode-width" 326 | version = "0.1.6" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | 329 | [[package]] 330 | name = "unicode-xid" 331 | version = "0.2.0" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | 334 | [[package]] 335 | name = "vec_map" 336 | version = "0.8.1" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | 339 | [[package]] 340 | name = "wasm-bindgen" 341 | version = "0.2.51" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | dependencies = [ 344 | "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", 345 | "wasm-bindgen-macro 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 346 | ] 347 | 348 | [[package]] 349 | name = "wasm-bindgen-backend" 350 | version = "0.2.51" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | dependencies = [ 353 | "bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 354 | "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 355 | "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", 356 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 357 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 358 | "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 359 | "wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 360 | ] 361 | 362 | [[package]] 363 | name = "wasm-bindgen-macro" 364 | version = "0.2.51" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | dependencies = [ 367 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 368 | "wasm-bindgen-macro-support 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 369 | ] 370 | 371 | [[package]] 372 | name = "wasm-bindgen-macro-support" 373 | version = "0.2.51" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | dependencies = [ 376 | "proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 377 | "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 378 | "syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", 379 | "wasm-bindgen-backend 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 380 | "wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", 381 | ] 382 | 383 | [[package]] 384 | name = "wasm-bindgen-shared" 385 | version = "0.2.51" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | 388 | [[package]] 389 | name = "winapi" 390 | version = "0.3.8" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | dependencies = [ 393 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 394 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 395 | ] 396 | 397 | [[package]] 398 | name = "winapi-i686-pc-windows-gnu" 399 | version = "0.4.0" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | 402 | [[package]] 403 | name = "winapi-x86_64-pc-windows-gnu" 404 | version = "0.4.0" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | 407 | [metadata] 408 | "checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2" 409 | "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 410 | "checksum atty 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1803c647a3ec87095e7ae7acfca019e98de5ec9a7d01343f611cf3152ed71a90" 411 | "checksum autocfg 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "b671c8fb71b457dd4ae18c4ba1e59aa81793daacc361d82fcd410cef0d491875" 412 | "checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e" 413 | "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 414 | "checksum bumpalo 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ad807f2fc2bf185eeb98ff3a901bd46dc5ad58163d0fa4577ba0d25674d71708" 415 | "checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5" 416 | "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 417 | "checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" 418 | "checksum deflate 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)" = "707b6a7b384888a70c8d2e8650b3e60170dfc6a67bb4aa67b6dfca57af4bedb4" 419 | "checksum gltf 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)" = "" 420 | "checksum gltf-derive 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)" = "" 421 | "checksum gltf-json 0.13.0 (git+https://github.com/zellski/gltf-rs?branch=for-gltf-variant-meld)" = "" 422 | "checksum image 0.21.3 (registry+https://github.com/rust-lang/crates.io-index)" = "35371e467cd7b0b3d1d6013d619203658467df12d61b0ca43cd67b743b1965eb" 423 | "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" 424 | "checksum inflections 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" 425 | "checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" 426 | "checksum jpeg-decoder 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "c1aae18ffeeae409c6622c3b6a7ee49792a7e5a062eea1b135fbb74e301792ba" 427 | "checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 428 | "checksum libc 0.2.65 (registry+https://github.com/rust-lang/crates.io-index)" = "1a31a0627fdf1f6a39ec0dd577e101440b7db22672c0901fe00a9a6fbb5c24e8" 429 | "checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" 430 | "checksum lzw 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7d947cbb889ed21c2a84be6ffbaebf5b4e0f4340638cba0444907e38b56be084" 431 | "checksum num-integer 0.1.41 (registry+https://github.com/rust-lang/crates.io-index)" = "b85e541ef8255f6cf42bbfe4ef361305c6c135d10919ecc26126c4e5ae94bc09" 432 | "checksum num-iter 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "76bd5272412d173d6bf9afdf98db8612bbabc9a7a830b7bfc9c188911716132e" 433 | "checksum num-rational 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f2885278d5fe2adc2f75ced642d52d879bffaceb5a2e0b1d4309ffdfb239b454" 434 | "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" 435 | "checksum png 0.14.1 (registry+https://github.com/rust-lang/crates.io-index)" = "63daf481fdd0defa2d1d2be15c674fbfa1b0fd71882c303a91f9a79b3252c359" 436 | "checksum proc-macro2 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90cf5f418035b98e655e9cdb225047638296b862b42411c4e45bb88d700f7fc0" 437 | "checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" 438 | "checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" 439 | "checksum serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "9796c9b7ba2ffe7a9ce53c2287dfc48080f4b2b362fcc245a259b3a7201119dd" 440 | "checksum serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)" = "4b133a43a1ecd55d4086bd5b4dc6c1751c68b1bfbeba7a5040442022c7e7c02e" 441 | "checksum serde_json 1.0.41 (registry+https://github.com/rust-lang/crates.io-index)" = "2f72eb2a68a7dc3f9a691bfda9305a1c017a6215e5a4545c258500d2099a37c2" 442 | "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" 443 | "checksum spectral 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ae3c15181f4b14e52eeaac3efaeec4d2764716ce9c86da0c934c3e318649c5ba" 444 | "checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 445 | "checksum syn 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "66850e97125af79138385e9b88339cbcd037e3f28ceab8c5ad98e64f0f1f80bf" 446 | "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 447 | "checksum unicode-width 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7007dbd421b92cc6e28410fe7362e2e0a2503394908f417b68ec8d1c364c4e20" 448 | "checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" 449 | "checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" 450 | "checksum wasm-bindgen 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "cd34c5ba0d228317ce388e87724633c57edca3e7531feb4e25e35aaa07a656af" 451 | "checksum wasm-bindgen-backend 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "927196b315c23eed2748442ba675a4c54a1a079d90d9bdc5ad16ce31cf90b15b" 452 | "checksum wasm-bindgen-macro 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "92c2442bf04d89792816650820c3fb407af8da987a9f10028d5317f5b04c2b4a" 453 | "checksum wasm-bindgen-macro-support 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "9c075d27b7991c68ca0f77fe628c3513e64f8c477d422b859e03f28751b46fc5" 454 | "checksum wasm-bindgen-shared 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "83d61fe986a7af038dd8b5ec660e5849cbd9f38e7492b9404cc48b2b4df731d1" 455 | "checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" 456 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 457 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 458 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ "native", ] 4 | 5 | [profile.release] 6 | lto = true 7 | opt-level = 's' 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # glTFVariantMeld 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![CircleCI](https://circleci.com/gh/facebookincubator/glTFVariantMeld/tree/master.svg?style=svg&circle-token=444333da241c0fc99a7ac8f786129f3bce774b43)](https://circleci.com/gh/facebookincubator/glTFVariantMeld/tree/master) 5 | [![Actions Status](https://github.com/facebookincubator/glTFVariantMeld/workflows/Rust/badge.svg)](https://github.com/facebookincubator/glTFVariantMeld/actions) 6 | 7 | ## Description 8 | 9 | This tool melds multiple glTF assets, each representing a different _variant_ of a model, into a single, compact format, implemented as a glTF extension. 10 | 11 | A canonical use case is a retail product that's available in a range of colour combinations, with an 12 | application that lets a prospective customer switch between these different variants with minimal 13 | latency. 14 | 15 | We're making this internal tool publicly available with the hope of helping the glTF 16 | ecosystem come together around a common, open format. 17 | 18 | In this prerelease version, the tool produces files with the Khronos extension [`KHR_materials_variants`](https://github.com/KhronosGroup/glTF/blob/07c109becc3153d0d982d6c2086da7da979ab439/extensions/2.0/Khronos//KHR_materials_variants/README.md). We are hopeful that the glTF community will find speedy consensus around a Khronos extension. 19 | 20 | Our aspirational roadmap includes the development of a web app which would leverage 21 | WebAssembly to run entirely in the browser. There will also be a native CLI. 22 | 23 | **Assistance is always welcome!** Pull requests are encouraged. 24 | 25 | ## Installation 26 | 27 | We've yet to actually publish a release. Until we do, please [build the bleeding edge code yourself.](BUILDING.md) 28 | 29 | ## Usage 30 | 31 | The tool depends on glTF source files that are **identical** except for which materials the various 32 | meshes reference. The proposed work flow is to export the same asset from the same digital content 33 | creation app repeatedly, taking care to make no changes to geometry or structure between each 34 | exported file. 35 | 36 | Then, using the (quite primitive, as yet) command-line interface might look like: 37 | 38 | ```shell 39 | > dist/app.js black:GizmoBlack.glb blue:GizmoBlue.glb clear:GizmoClear.glb GizmoVariational.glb 40 | Parsing source asset: 'GizmoBlack.glb'... 41 | Initial asset: 42 | Total file size: 2.4 MB 43 | Total texture data: 1.8 MB 44 | Of which is depends on tag: 0.0 kB 45 | 46 | Parsing source asset: 'GizmoBlue.glb'... 47 | New melded result: 48 | Total file size: 3.9 MB 49 | Total texture data: 3.3 MB 50 | Of which is depends on tag: 3.3 MB 51 | 52 | Parsing source asset: 'GizmoClear.glb'... 53 | New melded result: 54 | Total file size: 4.6 MB 55 | Total texture data: 4.0 MB 56 | Of which is depends on tag: 4.0 MB 57 | Success! 4594404 bytes written to 'GizmoVariational.glb'. 58 | ``` 59 | 60 | The first source file contains 1.8 MB of textures and 0.6 MB of geometry. Subsequent source files 61 | contribute first another 1.5 MB of textures, and then for the third variant, 0.7 MB. The geometry 62 | of the asset remains constant. 63 | 64 | 65 | ### Asset Requirements 66 | 67 | For assets to be meldable, they must be logically identical: contain the same meshes, with 68 | the same mesh primitives. They may vary meaningfully only in what _materials_ are assigned 69 | to each mesh primitive. The tool will complain if it finds disrepancies between the source 70 | assets that are too confusing for it to work around. 71 | 72 | During the melding process, all common data is shared, whereas varying material definitions and 73 | textures are copied as needed. Parts of the assets that don't vary are left untouched. 74 | 75 | Each source asset brought into the tool is identified by a _tag_, a short string, and it's 76 | these same tags that are later used to trigger different runtime apperances. 77 | 78 | ## Building 79 | 80 | Please see separate [BUILDING](BUILDING.md) instructions. 81 | 82 | ## Contributing 83 | 84 | See the [CONTRIBUTING](CONTRIBUTING.md) file for how to help out. 85 | 86 | ## Credits 87 | 88 | This tool was written by Pär Winzel and Renee Rashid with help from Susie Su and Jeremy Cytryn, 89 | and ultimately made possible only through the hard work of others: 90 | 91 | - The [Rust](https://www.rust-lang.org/) language & community, 92 | - The authors of [`wasm-bindgen`](https://rustwasm.github.io/docs/wasm-bindgen/), for WebAssembly support, 93 | - The Rust crates [`gltf`](https://github.com/gltf-rs/gltf) and 94 | [`serde`](https://github.com/serde-rs/serde). 95 | - and many others... 96 | 97 | ## License 98 | 99 | glTFVariantMeld is NIT licensed, as found in the [LICENSE](LICENSE.txt) file. 100 | -------------------------------------------------------------------------------- /assets/pinecones/matte/Pinecone.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "FBX2glTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "buffers": [ 8 | { 9 | "uri": "Pinecone_data.bin", 10 | "byteLength": 69360 11 | } 12 | ], 13 | "bufferViews": [ 14 | { 15 | "buffer": 0, 16 | "byteLength": 5760, 17 | "byteOffset": 0, 18 | "target": 34963 19 | }, 20 | { 21 | "buffer": 0, 22 | "byteLength": 15900, 23 | "byteOffset": 5760, 24 | "target": 34962 25 | }, 26 | { 27 | "buffer": 0, 28 | "byteLength": 15900, 29 | "byteOffset": 21660, 30 | "target": 34962 31 | }, 32 | { 33 | "buffer": 0, 34 | "byteLength": 21200, 35 | "byteOffset": 37560, 36 | "target": 34962 37 | }, 38 | { 39 | "buffer": 0, 40 | "byteLength": 10600, 41 | "byteOffset": 58760, 42 | "target": 34962 43 | } 44 | ], 45 | "scenes": [ 46 | { 47 | "name": "Root Scene", 48 | "nodes": [ 49 | 0 50 | ] 51 | } 52 | ], 53 | "accessors": [ 54 | { 55 | "componentType": 5123, 56 | "type": "SCALAR", 57 | "count": 2880, 58 | "bufferView": 0, 59 | "byteOffset": 0 60 | }, 61 | { 62 | "componentType": 5126, 63 | "type": "VEC3", 64 | "count": 1325, 65 | "bufferView": 1, 66 | "byteOffset": 0, 67 | "min": [ 68 | -45.0671310424805, 69 | 0.0485343933105469, 70 | -45.0665054321289 71 | ], 72 | "max": [ 73 | 45.0674324035645, 74 | 99.8729095458984, 75 | 45.0677680969238 76 | ] 77 | }, 78 | { 79 | "componentType": 5126, 80 | "type": "VEC3", 81 | "count": 1325, 82 | "bufferView": 2, 83 | "byteOffset": 0 84 | }, 85 | { 86 | "componentType": 5126, 87 | "type": "VEC4", 88 | "count": 1325, 89 | "bufferView": 3, 90 | "byteOffset": 0 91 | }, 92 | { 93 | "componentType": 5126, 94 | "type": "VEC2", 95 | "count": 1325, 96 | "bufferView": 4, 97 | "byteOffset": 0 98 | } 99 | ], 100 | "samplers": [ 101 | {} 102 | ], 103 | "materials": [ 104 | { 105 | "name": "lambert1", 106 | "alphaMode": "OPAQUE", 107 | "pbrMetallicRoughness": { 108 | "metallicFactor": 0.2, 109 | "roughnessFactor": 0.8 110 | } 111 | } 112 | ], 113 | "meshes": [ 114 | { 115 | "name": "Pinecone", 116 | "primitives": [ 117 | { 118 | "material": 0, 119 | "mode": 4, 120 | "attributes": { 121 | "COLOR_0": 3, 122 | "NORMAL": 2, 123 | "POSITION": 1, 124 | "TEXCOORD_0": 4 125 | }, 126 | "indices": 0 127 | } 128 | ] 129 | } 130 | ], 131 | "nodes": [ 132 | { 133 | "name": "RootNode", 134 | "translation": [ 135 | 0, 136 | 0, 137 | 0 138 | ], 139 | "rotation": [ 140 | 0, 141 | 0, 142 | 0, 143 | 1 144 | ], 145 | "scale": [ 146 | 1, 147 | 1, 148 | 1 149 | ], 150 | "children": [ 151 | 1 152 | ] 153 | }, 154 | { 155 | "name": "Pinecone", 156 | "translation": [ 157 | 0, 158 | 1.73472347597681e-15, 159 | 1.89326617253043e-27 160 | ], 161 | "rotation": [ 162 | 0, 163 | 0, 164 | 0, 165 | 1 166 | ], 167 | "scale": [ 168 | 1, 169 | 1, 170 | 1 171 | ], 172 | "mesh": 0 173 | } 174 | ] 175 | } -------------------------------------------------------------------------------- /assets/pinecones/matte/Pinecone_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/pinecones/matte/Pinecone_data.bin -------------------------------------------------------------------------------- /assets/pinecones/shiny/Pinecone.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "FBX2glTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "buffers": [ 8 | { 9 | "uri": "Pinecone_data.bin", 10 | "byteLength": 69360 11 | } 12 | ], 13 | "bufferViews": [ 14 | { 15 | "buffer": 0, 16 | "byteLength": 5760, 17 | "byteOffset": 0, 18 | "target": 34963 19 | }, 20 | { 21 | "buffer": 0, 22 | "byteLength": 15900, 23 | "byteOffset": 5760, 24 | "target": 34962 25 | }, 26 | { 27 | "buffer": 0, 28 | "byteLength": 15900, 29 | "byteOffset": 21660, 30 | "target": 34962 31 | }, 32 | { 33 | "buffer": 0, 34 | "byteLength": 21200, 35 | "byteOffset": 37560, 36 | "target": 34962 37 | }, 38 | { 39 | "buffer": 0, 40 | "byteLength": 10600, 41 | "byteOffset": 58760, 42 | "target": 34962 43 | } 44 | ], 45 | "scenes": [ 46 | { 47 | "name": "Root Scene", 48 | "nodes": [ 49 | 0 50 | ] 51 | } 52 | ], 53 | "accessors": [ 54 | { 55 | "componentType": 5123, 56 | "type": "SCALAR", 57 | "count": 2880, 58 | "bufferView": 0, 59 | "byteOffset": 0 60 | }, 61 | { 62 | "componentType": 5126, 63 | "type": "VEC3", 64 | "count": 1325, 65 | "bufferView": 1, 66 | "byteOffset": 0, 67 | "min": [ 68 | -45.0671310424805, 69 | 0.0485343933105469, 70 | -45.0665054321289 71 | ], 72 | "max": [ 73 | 45.0674324035645, 74 | 99.8729095458984, 75 | 45.0677680969238 76 | ] 77 | }, 78 | { 79 | "componentType": 5126, 80 | "type": "VEC3", 81 | "count": 1325, 82 | "bufferView": 2, 83 | "byteOffset": 0 84 | }, 85 | { 86 | "componentType": 5126, 87 | "type": "VEC4", 88 | "count": 1325, 89 | "bufferView": 3, 90 | "byteOffset": 0 91 | }, 92 | { 93 | "componentType": 5126, 94 | "type": "VEC2", 95 | "count": 1325, 96 | "bufferView": 4, 97 | "byteOffset": 0 98 | } 99 | ], 100 | "samplers": [ 101 | {} 102 | ], 103 | "materials": [ 104 | { 105 | "name": "lambert1", 106 | "alphaMode": "OPAQUE", 107 | "pbrMetallicRoughness": { 108 | "metallicFactor": 0.8, 109 | "roughnessFactor": 0.2 110 | } 111 | } 112 | ], 113 | "meshes": [ 114 | { 115 | "name": "Pinecone", 116 | "primitives": [ 117 | { 118 | "material": 0, 119 | "mode": 4, 120 | "attributes": { 121 | "COLOR_0": 3, 122 | "NORMAL": 2, 123 | "POSITION": 1, 124 | "TEXCOORD_0": 4 125 | }, 126 | "indices": 0 127 | } 128 | ] 129 | } 130 | ], 131 | "nodes": [ 132 | { 133 | "name": "RootNode", 134 | "translation": [ 135 | 0, 136 | 0, 137 | 0 138 | ], 139 | "rotation": [ 140 | 0, 141 | 0, 142 | 0, 143 | 1 144 | ], 145 | "scale": [ 146 | 1, 147 | 1, 148 | 1 149 | ], 150 | "children": [ 151 | 1 152 | ] 153 | }, 154 | { 155 | "name": "Pinecone", 156 | "translation": [ 157 | 0, 158 | 1.73472347597681e-15, 159 | 1.89326617253043e-27 160 | ], 161 | "rotation": [ 162 | 0, 163 | 0, 164 | 0, 165 | 1 166 | ], 167 | "scale": [ 168 | 1, 169 | 1, 170 | 1 171 | ], 172 | "mesh": 0 173 | } 174 | ] 175 | } -------------------------------------------------------------------------------- /assets/pinecones/shiny/Pinecone_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/pinecones/shiny/Pinecone_data.bin -------------------------------------------------------------------------------- /assets/pinecones/tinted/Pinecone.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "FBX2glTF", 4 | "version": "2.0" 5 | }, 6 | "scene": 0, 7 | "buffers": [ 8 | { 9 | "uri": "Pinecone_data.bin", 10 | "byteLength": 69360 11 | } 12 | ], 13 | "bufferViews": [ 14 | { 15 | "buffer": 0, 16 | "byteLength": 5760, 17 | "byteOffset": 0, 18 | "target": 34963 19 | }, 20 | { 21 | "buffer": 0, 22 | "byteLength": 15900, 23 | "byteOffset": 5760, 24 | "target": 34962 25 | }, 26 | { 27 | "buffer": 0, 28 | "byteLength": 15900, 29 | "byteOffset": 21660, 30 | "target": 34962 31 | }, 32 | { 33 | "buffer": 0, 34 | "byteLength": 21200, 35 | "byteOffset": 37560, 36 | "target": 34962 37 | }, 38 | { 39 | "buffer": 0, 40 | "byteLength": 10600, 41 | "byteOffset": 58760, 42 | "target": 34962 43 | } 44 | ], 45 | "scenes": [ 46 | { 47 | "name": "Root Scene", 48 | "nodes": [ 49 | 0 50 | ] 51 | } 52 | ], 53 | "accessors": [ 54 | { 55 | "componentType": 5123, 56 | "type": "SCALAR", 57 | "count": 2880, 58 | "bufferView": 0, 59 | "byteOffset": 0 60 | }, 61 | { 62 | "componentType": 5126, 63 | "type": "VEC3", 64 | "count": 1325, 65 | "bufferView": 1, 66 | "byteOffset": 0, 67 | "min": [ 68 | -45.0671310424805, 69 | 0.0485343933105469, 70 | -45.0665054321289 71 | ], 72 | "max": [ 73 | 45.0674324035645, 74 | 99.8729095458984, 75 | 45.0677680969238 76 | ] 77 | }, 78 | { 79 | "componentType": 5126, 80 | "type": "VEC3", 81 | "count": 1325, 82 | "bufferView": 2, 83 | "byteOffset": 0 84 | }, 85 | { 86 | "componentType": 5126, 87 | "type": "VEC4", 88 | "count": 1325, 89 | "bufferView": 3, 90 | "byteOffset": 0 91 | }, 92 | { 93 | "componentType": 5126, 94 | "type": "VEC2", 95 | "count": 1325, 96 | "bufferView": 4, 97 | "byteOffset": 0 98 | } 99 | ], 100 | "samplers": [ 101 | {} 102 | ], 103 | "materials": [ 104 | { 105 | "name": "lambert1", 106 | "alphaMode": "OPAQUE", 107 | "pbrMetallicRoughness": { 108 | "baseColorFactor": [ 109 | 1, 110 | 0.2, 111 | 0.2, 112 | 1 113 | ], 114 | "metallicFactor": 0.8, 115 | "roughnessFactor": 0.2 116 | } 117 | } 118 | ], 119 | "meshes": [ 120 | { 121 | "name": "Pinecone", 122 | "primitives": [ 123 | { 124 | "material": 0, 125 | "mode": 4, 126 | "attributes": { 127 | "COLOR_0": 3, 128 | "NORMAL": 2, 129 | "POSITION": 1, 130 | "TEXCOORD_0": 4 131 | }, 132 | "indices": 0 133 | } 134 | ] 135 | } 136 | ], 137 | "nodes": [ 138 | { 139 | "name": "RootNode", 140 | "translation": [ 141 | 0, 142 | 0, 143 | 0 144 | ], 145 | "rotation": [ 146 | 0, 147 | 0, 148 | 0, 149 | 1 150 | ], 151 | "scale": [ 152 | 1, 153 | 1, 154 | 1 155 | ], 156 | "children": [ 157 | 1 158 | ] 159 | }, 160 | { 161 | "name": "Pinecone", 162 | "translation": [ 163 | 0, 164 | 1.73472347597681e-15, 165 | 1.89326617253043e-27 166 | ], 167 | "rotation": [ 168 | 0, 169 | 0, 170 | 0, 171 | 1 172 | ], 173 | "scale": [ 174 | 1, 175 | 1, 176 | 1 177 | ], 178 | "mesh": 0 179 | } 180 | ] 181 | } -------------------------------------------------------------------------------- /assets/pinecones/tinted/Pinecone_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/pinecones/tinted/Pinecone_data.bin -------------------------------------------------------------------------------- /assets/pinecones/variational/Pinecone.gltf: -------------------------------------------------------------------------------- 1 | { 2 | "asset": { 3 | "generator": "FBX2glTF", 4 | "version": "2.0" 5 | }, 6 | "extensions": { 7 | "KHR_materials_variants": { 8 | "variants": [ 9 | { 10 | "name": "tag_1" 11 | }, 12 | { 13 | "name": "tag_2" 14 | } 15 | ] 16 | } 17 | }, 18 | "extensionsUsed": [ 19 | "KHR_materials_variants" 20 | ], 21 | "scene": 0, 22 | "buffers": [ 23 | { 24 | "uri": "Pinecone_data.bin", 25 | "byteLength": 69360 26 | } 27 | ], 28 | "bufferViews": [ 29 | { 30 | "buffer": 0, 31 | "byteLength": 5760, 32 | "byteOffset": 0, 33 | "target": 34963 34 | }, 35 | { 36 | "buffer": 0, 37 | "byteLength": 15900, 38 | "byteOffset": 5760, 39 | "target": 34962 40 | }, 41 | { 42 | "buffer": 0, 43 | "byteLength": 15900, 44 | "byteOffset": 21660, 45 | "target": 34962 46 | }, 47 | { 48 | "buffer": 0, 49 | "byteLength": 21200, 50 | "byteOffset": 37560, 51 | "target": 34962 52 | }, 53 | { 54 | "buffer": 0, 55 | "byteLength": 10600, 56 | "byteOffset": 58760, 57 | "target": 34962 58 | } 59 | ], 60 | "scenes": [ 61 | { 62 | "name": "Root Scene", 63 | "nodes": [ 64 | 0 65 | ] 66 | } 67 | ], 68 | "accessors": [ 69 | { 70 | "componentType": 5123, 71 | "type": "SCALAR", 72 | "count": 2880, 73 | "bufferView": 0, 74 | "byteOffset": 0 75 | }, 76 | { 77 | "componentType": 5126, 78 | "type": "VEC3", 79 | "count": 1325, 80 | "bufferView": 1, 81 | "byteOffset": 0, 82 | "min": [ 83 | -45.0671310424805, 84 | 0.0485343933105469, 85 | -45.0665054321289 86 | ], 87 | "max": [ 88 | 45.0674324035645, 89 | 99.8729095458984, 90 | 45.0677680969238 91 | ] 92 | }, 93 | { 94 | "componentType": 5126, 95 | "type": "VEC3", 96 | "count": 1325, 97 | "bufferView": 2, 98 | "byteOffset": 0 99 | }, 100 | { 101 | "componentType": 5126, 102 | "type": "VEC4", 103 | "count": 1325, 104 | "bufferView": 3, 105 | "byteOffset": 0 106 | }, 107 | { 108 | "componentType": 5126, 109 | "type": "VEC2", 110 | "count": 1325, 111 | "bufferView": 4, 112 | "byteOffset": 0 113 | } 114 | ], 115 | "samplers": [ 116 | {} 117 | ], 118 | "materials": [ 119 | { 120 | "name": "lambert1", 121 | "alphaMode": "OPAQUE", 122 | "pbrMetallicRoughness": { 123 | "metallicFactor": 0.8, 124 | "roughnessFactor": 0.2 125 | } 126 | } 127 | ], 128 | "meshes": [ 129 | { 130 | "name": "Pinecone", 131 | "primitives": [ 132 | { 133 | "material": 0, 134 | "mode": 4, 135 | "attributes": { 136 | "COLOR_0": 3, 137 | "NORMAL": 2, 138 | "POSITION": 1, 139 | "TEXCOORD_0": 4 140 | }, 141 | "indices": 0, 142 | "extensions": { 143 | "KHR_materials_variants": { 144 | "mappings": [ 145 | { 146 | "variants": [ 147 | 0, 148 | 1 149 | ], 150 | "material": 0 151 | } 152 | ] 153 | } 154 | } 155 | } 156 | ] 157 | } 158 | ], 159 | "nodes": [ 160 | { 161 | "name": "RootNode", 162 | "translation": [ 163 | 0, 164 | 0, 165 | 0 166 | ], 167 | "rotation": [ 168 | 0, 169 | 0, 170 | 0, 171 | 1 172 | ], 173 | "scale": [ 174 | 1, 175 | 1, 176 | 1 177 | ], 178 | "children": [ 179 | 1 180 | ] 181 | }, 182 | { 183 | "name": "Pinecone", 184 | "translation": [ 185 | 0, 186 | 1.73472347597681e-15, 187 | 1.89326617253043e-27 188 | ], 189 | "rotation": [ 190 | 0, 191 | 0, 192 | 0, 193 | 1 194 | ], 195 | "scale": [ 196 | 1, 197 | 1, 198 | 1 199 | ], 200 | "mesh": 0 201 | } 202 | ] 203 | } 204 | -------------------------------------------------------------------------------- /assets/pinecones/variational/Pinecone_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/pinecones/variational/Pinecone_data.bin -------------------------------------------------------------------------------- /assets/tank_teapots/camouflage-pattern.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/tank_teapots/camouflage-pattern.jpg -------------------------------------------------------------------------------- /assets/tank_teapots/green-brushed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/tank_teapots/green-brushed.jpg -------------------------------------------------------------------------------- /assets/tank_teapots/teapot_data.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/glTFVariantMeld/165df2c7225db442546a8aa8060a63fa007ee0d1/assets/tank_teapots/teapot_data.bin -------------------------------------------------------------------------------- /build-wasm-pkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 4 | # 5 | 6 | SCRIPT_DIR="$(cd `dirname $BASH_SOURCE`; pwd)" 7 | 8 | cd "${SCRIPT_DIR}/native" 9 | wasm-pack build -d ${SCRIPT_DIR}/web/wasmpkg/ 10 | -------------------------------------------------------------------------------- /native/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "cargo", 8 | "subcommand": "build", 9 | "problemMatcher": [ 10 | "$rustc" 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | } 16 | }, 17 | { 18 | "type": "cargo", 19 | "subcommand": "test", 20 | "problemMatcher": [ 21 | "$rustc" 22 | ], 23 | "group": { 24 | "kind": "test", 25 | "isDefault": true 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /native/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gltf_variant_meld" 3 | version = "0.1.0" 4 | description = "Meld multiple asset variants into one, using extended glTF." 5 | authors = [ 6 | "Pär Winzell ", 7 | "Renee Rashid", 8 | ] 9 | repository = "https://github.com/facebookincubator/glTFVariantMeld/" 10 | license = "MIT" 11 | edition = "2018" 12 | publish = false 13 | 14 | [lib] 15 | name="gltf_variant_meld" 16 | crate-type = ["lib", "cdylib"] 17 | 18 | [dependencies.assets] 19 | path = "./assets" 20 | 21 | [dependencies.spectral] 22 | version = "^0.6" 23 | default-features=false 24 | 25 | [dependencies.wasm-bindgen] 26 | version = "^0.2" 27 | 28 | [dependencies.gltf] 29 | version = "^0.13" 30 | git = "https://github.com/zellski/gltf-rs" 31 | branch = "for-gltf-variant-meld" 32 | features = ["extras", "names"] 33 | 34 | [dependencies.serde] 35 | version = "^1.0" 36 | 37 | [dependencies.serde_derive] 38 | version = "^1.0" 39 | 40 | [dependencies.serde_json] 41 | version = "^1.0" 42 | features = ["raw_value"] 43 | 44 | [dependencies.sha1] 45 | version = "^0.6" 46 | 47 | [dependencies.clap] 48 | version = "^2.33.0" 49 | 50 | [[bin]] 51 | name = "meldtool" 52 | path = "src/bin/meldtool/mod.rs" 53 | -------------------------------------------------------------------------------- /native/assets/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "assets" 3 | version = "0.1.0" 4 | authors = [ 5 | "Jeremy Cytryn", 6 | "Renee Rashid", 7 | "Susie Su", 8 | "Pär Winzell", 9 | ] 10 | edition = "2018" 11 | -------------------------------------------------------------------------------- /native/assets/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | #![allow(dead_code, non_snake_case)] 5 | 6 | use std::path::Path; 7 | 8 | pub fn ASSET_PINECONE_MATTE() -> &'static Path { 9 | Path::new("../assets/pinecones/matte/Pinecone.gltf") 10 | } 11 | pub fn ASSET_PINECONE_SHINY() -> &'static Path { 12 | Path::new("../assets/pinecones/shiny/Pinecone.gltf") 13 | } 14 | pub fn ASSET_PINECONE_TINTED() -> &'static Path { 15 | Path::new("../assets/pinecones/tinted/Pinecone.gltf") 16 | } 17 | pub fn ASSET_PINECONE_VARIATIONAL() -> &'static Path { 18 | Path::new("../assets/pinecones/variational/Pinecone.gltf") 19 | } 20 | 21 | pub fn ASSET_TEAPOT_CAMO_PINK_BRONZE() -> &'static Path { 22 | Path::new("../assets/tank_teapots/teapot-camo-pink-bronze.gltf") 23 | } 24 | pub fn ASSET_TEAPOT_CAMO_PINK_SILVER() -> &'static Path { 25 | Path::new("../assets/tank_teapots/teapot-camo-pink-silver.gltf") 26 | } 27 | pub fn ASSET_TEAPOT_GREEN_PINK_BRONZE() -> &'static Path { 28 | Path::new("../assets/tank_teapots/teapot-green-pink-bronze.gltf") 29 | } 30 | pub fn ASSET_TEAPOT_GREEN_PINK_SILVER() -> &'static Path { 31 | Path::new("../assets/tank_teapots/teapot-green-pink-silver.gltf") 32 | } 33 | -------------------------------------------------------------------------------- /native/src/bin/meldtool/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use std::collections::HashMap; 5 | use std::fs; 6 | use std::path::PathBuf; 7 | 8 | use clap::{crate_authors, crate_version, App, Arg}; 9 | 10 | #[derive(Debug, PartialEq)] 11 | pub enum Verbosity { 12 | Quiet, 13 | Normal, 14 | Verbose, 15 | } 16 | 17 | #[derive(Debug)] 18 | pub struct WorkOrder { 19 | pub source_assets: SourceAssets, 20 | pub output_path: PathBuf, 21 | pub verbosity: Verbosity, 22 | } 23 | 24 | impl WorkOrder { 25 | pub fn verbose(&self) -> bool { 26 | self.verbosity == Verbosity::Verbose 27 | } 28 | pub fn quiet(&self) -> bool { 29 | self.verbosity == Verbosity::Quiet 30 | } 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct SourceAssets { 35 | pub base: SourceAsset, 36 | pub melds: Vec, 37 | } 38 | 39 | #[derive(Debug)] 40 | pub struct SourceAsset { 41 | pub path: PathBuf, 42 | pub tag: Option, 43 | } 44 | 45 | pub fn parse_args() -> WorkOrder { 46 | let matches = App::new("glTFVariantMeld") 47 | .author(crate_authors!()) 48 | .version(crate_version!()) 49 | .arg( 50 | Arg::with_name("base") 51 | .short("b") 52 | .long("base") 53 | .required(true) 54 | .takes_value(true) 55 | .value_name("FILE") 56 | .help("the base source asset into which to meld"), 57 | ) 58 | .arg( 59 | Arg::with_name("tag") 60 | .short("t") 61 | .long("tagged-as") 62 | .takes_value(true) 63 | .multiple(true) 64 | .value_name("TAG") 65 | .help("a variant tag representing the preceding source asset"), 66 | ) 67 | .arg( 68 | Arg::with_name("meld") 69 | .short("m") 70 | .long("meld") 71 | .takes_value(true) 72 | .multiple(true) 73 | .value_name("FILE") 74 | .help("a source asset to meld into the base"), 75 | ) 76 | .arg( 77 | Arg::with_name("output") 78 | .short("o") 79 | .long("output") 80 | .required(true) 81 | .takes_value(true) 82 | .value_name("FILE") 83 | .help("the name of the output file"), 84 | ) 85 | .arg( 86 | Arg::with_name("force") 87 | .short("f") 88 | .long("force") 89 | .takes_value(false) 90 | .help("overwrite output file if it exists"), 91 | ) 92 | .arg( 93 | Arg::with_name("verbose") 94 | .short("v") 95 | .long("verbose") 96 | .takes_value(false) 97 | .help("output more detailed progress"), 98 | ) 99 | .arg( 100 | Arg::with_name("quiet") 101 | .short("q") 102 | .long("quiet") 103 | .takes_value(false) 104 | .help("output nothing"), 105 | ) 106 | .get_matches(); 107 | 108 | let source_assets = parse_source_assets(&matches); 109 | 110 | let force = matches.occurrences_of("force") > 0; 111 | let output_path = &matches.value_of("output").unwrap(); 112 | if let Ok(metadata) = fs::metadata(output_path) { 113 | if metadata.is_dir() { 114 | eprintln!("Error: Output path is a directory: {}", output_path); 115 | std::process::exit(1); 116 | } else if metadata.is_file() && !force { 117 | eprintln!( 118 | "Error: Output path exists (use -f to overwrite): {}", 119 | output_path 120 | ); 121 | std::process::exit(1); 122 | } 123 | } 124 | let output_path = PathBuf::from(output_path); 125 | 126 | let verbosity = if matches.occurrences_of("verbose") > 0 { 127 | Verbosity::Verbose 128 | } else if matches.occurrences_of("quiet") > 0 { 129 | Verbosity::Quiet 130 | } else { 131 | Verbosity::Normal 132 | }; 133 | 134 | WorkOrder { 135 | source_assets, 136 | output_path, 137 | verbosity, 138 | } 139 | } 140 | 141 | fn parse_source_assets(matches: &clap::ArgMatches) -> SourceAssets { 142 | let base = matches.value_of("base").unwrap(); 143 | let base_ix = matches.index_of("base").unwrap(); 144 | 145 | let tag_map = if let Some(tags) = matches.values_of("tag") { 146 | let ix = matches.indices_of("tag").unwrap(); 147 | ix.zip(tags).collect() 148 | } else { 149 | HashMap::new() 150 | }; 151 | 152 | let mk_asset = |pathstr, ix| { 153 | let path = PathBuf::from(pathstr); 154 | if path.exists() { 155 | let tag = tag_map.get(&(ix + 2)).map(|t| (*t).to_owned()); 156 | SourceAsset { path, tag } 157 | } else { 158 | eprintln!("Error: Couldn't open file: {}", pathstr); 159 | std::process::exit(1); 160 | } 161 | }; 162 | 163 | let base = mk_asset(base, base_ix); 164 | 165 | let melds = if let Some(melds) = matches.values_of("meld") { 166 | let ix = matches.indices_of("meld").unwrap(); 167 | melds 168 | .zip(ix) 169 | .map(|(meld, meld_ix)| mk_asset(meld, meld_ix)) 170 | .collect() 171 | } else { 172 | vec![] 173 | }; 174 | 175 | SourceAssets { base, melds } 176 | } 177 | -------------------------------------------------------------------------------- /native/src/bin/meldtool/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | extern crate clap; 5 | 6 | extern crate gltf_variant_meld; 7 | 8 | use std::fs; 9 | 10 | use gltf_variant_meld::{Result, VariationalAsset}; 11 | 12 | mod args; 13 | use args::parse_args; 14 | pub use args::{SourceAsset, SourceAssets, WorkOrder}; 15 | 16 | fn main() { 17 | let work_order = parse_args(); 18 | 19 | if let Err(err) = process(work_order) { 20 | eprintln!("Error: {}", err); 21 | } 22 | } 23 | 24 | fn process(work_order: WorkOrder) -> Result<()> { 25 | let base = read_asset(&work_order.source_assets.base)?; 26 | if work_order.verbose() { 27 | println!("Base asset:"); 28 | describe_asset(&base); 29 | } 30 | 31 | let mut result = base; 32 | for meld in &work_order.source_assets.melds { 33 | let meld = read_asset(meld)?; 34 | result = VariationalAsset::meld(&result, &meld)?; 35 | if work_order.verbose() { 36 | println!("New melded result:"); 37 | describe_asset(&result); 38 | } 39 | } 40 | 41 | fs::write(&work_order.output_path, result.glb()) 42 | .map_err(|e| format!("Couldn't write output file: {}", e))?; 43 | 44 | if !work_order.quiet() { 45 | println!( 46 | "Success! {} bytes written to '{}'.", 47 | result.glb().len(), 48 | work_order.output_path.to_str().unwrap_or(""), 49 | ); 50 | } 51 | Ok(()) 52 | } 53 | 54 | fn read_asset(asset: &SourceAsset) -> Result { 55 | Ok(VariationalAsset::from_file( 56 | &asset.path, 57 | asset.tag.as_ref(), 58 | )?) 59 | } 60 | 61 | fn describe_asset(asset: &VariationalAsset) { 62 | println!(" Total file size: {}", size(asset.glb().len())); 63 | let total = asset.metadata().total_sizes().texture_bytes; 64 | let variational = asset.metadata().variational_sizes().texture_bytes; 65 | println!(" Total texture data: {}", size(total)); 66 | println!(" Of which is depends on tag: {}", size(variational)); 67 | } 68 | 69 | fn size(byte_count: usize) -> String { 70 | if byte_count < 1000000 { 71 | format!("{:.01} kB", byte_count / 1000) 72 | } else { 73 | format!("{:.01} MB", byte_count / 1000000) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /native/src/extension/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Implementation of our 5 | //! [`FB_variant_mapping`](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md) 6 | //! extension. 7 | //! 8 | //! We're specifically concerned with reading and writing values that are meaningful from 9 | //! the point of view of i.e. `WorkAsset` into a glTF format, and especially the abstraction 10 | //! we get from the `gltf` crates. 11 | 12 | use gltf::json::Root; 13 | 14 | const KHR_MATERIALS_VARIANTS: &str = "KHR_materials_variants"; 15 | 16 | mod on_root; 17 | pub use on_root::{write_root_variant_lookup_map, get_variant_lookup}; 18 | 19 | mod on_primitive; 20 | pub use on_primitive::{extract_variant_map, write_variant_map}; 21 | 22 | /// Updates the `extensions_used` glTF property with the name of our extension. 23 | /// 24 | pub fn install(root: &mut Root) { 25 | let used = &mut root.extensions_used; 26 | if !used.contains(&String::from(KHR_MATERIALS_VARIANTS)) { 27 | used.push(String::from(KHR_MATERIALS_VARIANTS)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /native/src/extension/on_primitive.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use std::collections::{HashMap, HashSet}; 5 | 6 | use serde_derive::{Deserialize, Serialize}; 7 | 8 | use gltf::json::mesh::Primitive; 9 | 10 | use super::KHR_MATERIALS_VARIANTS; 11 | use crate::{Result, Tag}; 12 | 13 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 14 | pub struct FBMaterialVariantPrimitiveExtension { 15 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 16 | pub mappings: Vec, 17 | } 18 | 19 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Deserialize, Serialize)] 20 | pub struct FBMaterialVariantPrimitiveEntry { 21 | #[serde(default)] 22 | pub material: u32, 23 | 24 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 25 | pub variants: Vec, 26 | } 27 | 28 | /// Write the `tag_to_ix` mapping to the `Primitive' in `KHR_materials_variants` form. 29 | /// 30 | /// This method guarantees a deterministic ordering of the output. 31 | /// 32 | /// Please see [the `KHR_materials_variants` 33 | /// spec](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md) 34 | /// for further details. 35 | pub fn write_variant_map(primitive: &mut Primitive, tag_to_ix: &HashMap, variant_ix_lookup: &HashMap) -> Result<()> { 36 | if tag_to_ix.is_empty() { 37 | if let Some(extensions) = &mut primitive.extensions { 38 | extensions.others.remove(KHR_MATERIALS_VARIANTS); 39 | } 40 | return Ok(()); 41 | } 42 | // invert the mapping tag->ix to a ix->set-of-tags one 43 | let mut ix_to_tags = HashMap::new(); 44 | for (tag, &ix) in tag_to_ix { 45 | ix_to_tags 46 | .entry(ix) 47 | .or_insert(HashSet::new()) 48 | .insert(tag.to_owned()); 49 | } 50 | let mut mapping_entries: Vec = ix_to_tags 51 | .iter() 52 | .map(|(&ix, tags)| { 53 | let mut tag_vec: Vec = tags.iter().cloned().collect(); 54 | // order tags deterministically 55 | tag_vec.sort_unstable(); 56 | 57 | let mut variants: Vec = tag_vec 58 | .iter() 59 | .map(|tag| { 60 | let (&variant_ix, _) = variant_ix_lookup.iter().find(|(_k, v)| v == &tag).unwrap(); 61 | variant_ix as u32 62 | }) 63 | .collect(); 64 | variants.sort_unstable(); 65 | 66 | FBMaterialVariantPrimitiveEntry { 67 | material: ix as u32, 68 | variants, 69 | } 70 | }) 71 | .collect(); 72 | // order entries deterministically 73 | mapping_entries.sort_unstable(); 74 | // build structured extension data 75 | let new_extension = FBMaterialVariantPrimitiveExtension { 76 | mappings: mapping_entries, 77 | }; 78 | // serialise to JSON string 79 | let value = serde_json::to_string(&new_extension) 80 | .and_then(|s| serde_json::from_str(&s)) 81 | .map_err(|e| { 82 | format!( 83 | "Failed to transform primitive extension {:#?}, with error: {}", 84 | new_extension, e, 85 | ) 86 | })?; 87 | 88 | // and done 89 | primitive 90 | .extensions 91 | .get_or_insert(Default::default()) 92 | .others 93 | .insert(KHR_MATERIALS_VARIANTS.to_owned(), value); 94 | Ok(()) 95 | } 96 | 97 | /// Parses and returns the `KHR_materials_variants` data on a primitive, if any. 98 | /// 99 | /// Please see [the `KHR_materials_variants` 100 | /// spec](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md) 101 | /// for further details 102 | pub fn extract_variant_map(primitive: &Primitive, variant_ix_lookup: &HashMap) -> Result> { 103 | if let Some(extensions) = &primitive.extensions { 104 | if let Some(boxed) = extensions.others.get(KHR_MATERIALS_VARIANTS) { 105 | let json_string = &boxed.to_string(); 106 | let parse: serde_json::Result = 107 | serde_json::from_str(json_string); 108 | return match parse { 109 | Ok(parse) => { 110 | let mut result = HashMap::new(); 111 | for entry in parse.mappings { 112 | for variant_ix in entry.variants { 113 | let key = variant_ix as usize; 114 | let variant_tag = &variant_ix_lookup[&key]; 115 | result.insert(variant_tag.to_owned(), entry.material as usize); 116 | } 117 | } 118 | Ok(result) 119 | } 120 | Err(e) => Err(format!( 121 | "Bad JSON in KHR_materials_variants extension: {}; json = {}", 122 | e.to_string(), 123 | json_string, 124 | )), 125 | }; 126 | } 127 | } 128 | Ok(HashMap::new()) 129 | } 130 | -------------------------------------------------------------------------------- /native/src/extension/on_root.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | use std::collections::{HashMap}; 4 | use serde_derive::{Deserialize, Serialize}; 5 | 6 | use gltf::json::Root; 7 | 8 | use super::KHR_MATERIALS_VARIANTS; 9 | use crate::{Result, Tag}; 10 | 11 | #[derive(Clone, Debug, Default, Deserialize, Serialize)] 12 | pub struct FBMaterialVariantRootExtension { 13 | pub variants: Vec, 14 | } 15 | 16 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Deserialize, Serialize)] 17 | pub struct FBMaterialVariantVariantEntry { 18 | #[serde(default)] 19 | pub name: String, 20 | } 21 | 22 | /// Writes the root level variant lookup table containing object entries with each variant's 23 | /// associated tag. 24 | /// 25 | /// Please see [the `KHR_materials_variants` 26 | /// spec](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md) 27 | /// for further details. 28 | pub fn write_root_variant_lookup_map(root: &mut Root, tags_in_use: &Vec) -> Result<()> { 29 | // Transform list of tags into a list of object with object property name and tag 30 | let variant_entries: Vec = tags_in_use 31 | .into_iter() 32 | .map(|tag| { 33 | FBMaterialVariantVariantEntry { 34 | name: tag.clone(), 35 | } 36 | }) 37 | .collect(); 38 | 39 | let root_extension = FBMaterialVariantRootExtension { 40 | variants: variant_entries, 41 | }; 42 | 43 | let value = serde_json::to_string(&root_extension) 44 | .and_then(|s| serde_json::from_str(&s)) 45 | .map_err(|e| { 46 | format!( 47 | "Failed to transform root extension {:#?}, with error: {}", 48 | root_extension, e, 49 | ) 50 | })?; 51 | 52 | root 53 | .extensions 54 | .get_or_insert(Default::default()) 55 | .others 56 | .insert(KHR_MATERIALS_VARIANTS.to_owned(), value); 57 | Ok(()) 58 | } 59 | 60 | /// Extracts the variant lookup object from the root of the glTF file. This lookup is used to 61 | /// translate Tags with indicies located on mesh primitives. 62 | /// 63 | /// Please see [the `KHR_materials_variants` 64 | /// spec](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md) 65 | /// for further details. 66 | pub fn get_variant_lookup(root: &Root) -> Result> { 67 | match get_root_extension(&root)? { 68 | Some(extension) => { 69 | let mut lookup = HashMap::new(); 70 | for (ix, variant) in extension.variants.iter().enumerate() { 71 | lookup.insert(ix, variant.name.to_owned()); 72 | } 73 | Ok(lookup) 74 | } 75 | None => { 76 | Ok(HashMap::new()) 77 | } 78 | } 79 | } 80 | 81 | fn get_root_extension(root: &Root) -> Result> { 82 | if let Some(extensions) = &root.extensions { 83 | if let Some(ref boxed) = extensions.others.get(KHR_MATERIALS_VARIANTS) { 84 | let json_string = boxed.to_string(); 85 | let parse: serde_json::Result = 86 | serde_json::from_str(&json_string); 87 | return match parse { 88 | Ok(parse) => { 89 | Ok(Some(parse)) 90 | } 91 | Err(e) => Err(format!( 92 | "Bad JSON in KHR_materials_variants extension: {}; json = {}", 93 | e.to_string(), 94 | json_string, 95 | )), 96 | }; 97 | } 98 | } 99 | Ok(None) 100 | } 101 | -------------------------------------------------------------------------------- /native/src/glb.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Utilities for building binary glTF (GLB) files. 5 | 6 | use crate::Result; 7 | 8 | use GlbChunk::{BIN, JSON}; 9 | 10 | const GLB_VERSION: u32 = 2; 11 | const GLB_MAGIC: [u8; 4] = [b'g', b'l', b'T', b'F']; 12 | 13 | /// GLB 2.0 holds one JSON chunk followed by an optional BIN chunk. 14 | pub enum GlbChunk<'a> { 15 | /// A byte slice of valid JSON conforming to the [glTF schema]. 16 | /// 17 | /// [glTF schema]: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0 18 | JSON(&'a [u8]), 19 | /// An binary blob, destined to become the data underlying a glTF buffer. 20 | BIN(&'a [u8]), 21 | } 22 | 23 | impl<'a> GlbChunk<'a> { 24 | fn magic(&self) -> u32 { 25 | match *self { 26 | JSON(_) => 0x4E4F534A, 27 | BIN(_) => 0x004E4942, 28 | } 29 | } 30 | fn bytes(&self) -> &[u8] { 31 | match *self { 32 | JSON(bytes) => bytes, 33 | BIN(bytes) => bytes, 34 | } 35 | } 36 | 37 | /// Serialised JSON & optional BIN chunks binary glTF, i.e. GLB 2.0. 38 | pub fn to_bytes(json_chunk: Self, bin_chunk: Option) -> Result> { 39 | // create the initial header 40 | let mut glb_bytes = vec![]; 41 | glb_bytes.extend_from_slice(&GLB_MAGIC); 42 | glb_bytes.extend_from_slice(&(GLB_VERSION as u32).to_le_bytes()); 43 | glb_bytes.extend_from_slice(&(0 as u32).to_le_bytes()); // fill in later 44 | 45 | let mut append_chunk = |chunk: Self| { 46 | let mut chunk_bytes = chunk.bytes().to_vec(); 47 | if chunk_bytes.len() > 0 { 48 | while (chunk_bytes.len() % 4) != 0 { 49 | chunk_bytes.push(if let JSON(_) = chunk { b' ' } else { 0x00 }); 50 | } 51 | glb_bytes.extend_from_slice(&(chunk_bytes.len() as u32).to_le_bytes()); 52 | glb_bytes.extend_from_slice(&(chunk.magic() as u32).to_le_bytes()); 53 | glb_bytes.extend_from_slice(&chunk_bytes); 54 | } 55 | }; 56 | 57 | if let JSON(_) = json_chunk { 58 | append_chunk(json_chunk); 59 | } else { 60 | return Err(format!("First GLB chunk must be of type JSON.")); 61 | } 62 | if let Some(bin_chunk) = bin_chunk { 63 | if let BIN(_) = bin_chunk { 64 | append_chunk(bin_chunk); 65 | } else { 66 | return Err(format!("Second GLB chunk must be of type BIN, or None.")); 67 | } 68 | } 69 | 70 | let glb_len_bytes = &(glb_bytes.len() as u32).to_le_bytes(); 71 | for i in 0..3 { 72 | glb_bytes[0x08 + i] = glb_len_bytes[i]; 73 | } 74 | Ok(glb_bytes) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /native/src/gltfext.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Utility functions that extend the functionality of the `gltf` crate(s) for our needs. 5 | 6 | use gltf::json::{buffer::View, Buffer, Index}; 7 | 8 | use crate::Result; 9 | 10 | /// Returns the underlying byte slice of the given buffer view. 11 | pub fn get_slice_from_buffer_view<'a>(view: &'a View, blob: &'a Vec) -> Result<&'a [u8]> { 12 | let start = view.byte_offset.unwrap_or(0) as usize; 13 | let end = start + view.byte_length as usize; 14 | (&blob.get(start..end)).ok_or_else(|| { 15 | format!( 16 | "Slice [{}..{}] out of range for buffer view of length {}.", 17 | start, end, view.byte_length 18 | ) 19 | }) 20 | } 21 | 22 | /// Adds a byte slice to the given blob, creates & pushes a buffer view onto the given vector. 23 | /// 24 | /// This method ensures the byte slice ends up at a 4-byte-aligned position in the blob. 25 | pub fn add_buffer_view_from_slice( 26 | bytes: &[u8], 27 | buffer_views: &mut Vec, 28 | blob: &mut Vec, 29 | ) -> Index { 30 | while (blob.len() % 4) != 0 { 31 | blob.push(0x00); 32 | } 33 | 34 | let view_ix = buffer_views.len(); 35 | let view = View { 36 | buffer: Index::new(0), 37 | byte_length: bytes.len() as u32, 38 | byte_offset: Some(blob.len() as u32), 39 | byte_stride: None, 40 | name: None, 41 | target: None, 42 | extensions: None, 43 | extras: None, 44 | }; 45 | buffer_views.push(view); 46 | 47 | blob.extend_from_slice(bytes); 48 | 49 | Index::new(view_ix as u32) 50 | } 51 | 52 | /// Replaces any contents of the provided buffer vector with a single one, holding the given blob. 53 | pub fn set_root_buffer(blob: &[u8], buffers: &mut Vec) { 54 | buffers.clear(); 55 | if !blob.is_empty() { 56 | buffers.push(Buffer { 57 | byte_length: blob.len() as u32, 58 | uri: None, 59 | name: None, 60 | extensions: None, 61 | extras: None, 62 | }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /native/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | #![warn(missing_docs)] 5 | 6 | //! # glTFVariantMeld 7 | //! 8 | //! ### Introduction 9 | //! 10 | //! This library exists to do one single thing: meld multiple glTF assets, each representing a 11 | //! different *variant* of some basic model, into a single, compact format, implemented as a formal 12 | //! glTF extension. 13 | //! 14 | //! For a practical canonical use case, take the common case of a retail product that's available in 15 | //! a range of colours, and an application that lets a prospective customer switch between these 16 | //! different variants without latency or stutters. 17 | //! 18 | //! We're making this internal tool publicly available with the hope of bringing the ecosystem 19 | //! together around a common, open format, the lingua franca of variational 3D assets. One day, 20 | //! perhaps digital content creation tools will include ways to export variational assets natively. 21 | //! Until that day, this is how we're doing it. 22 | //! 23 | //! In this prerelease version, the tool produces files with the Khronos extension 24 | //! [`KHR_materials_variants`](https://github.com/zellski/glTF/blob/ext/zell-fb-asset-variants/extensions/2.0/Khronos/KHR_materials_variants/README.md). 25 | //! We are hopeful that the glTF community will find speedy consensus around a fully 26 | //! ratified extension, e.g. `KHR_material_variants`. 27 | //! 28 | //! At present, we offer a simple command line interface. Our aspirational roadmap includes the 29 | //! development of a web app which would leverage WebAssembly to run entirely in the browser. 30 | //! 31 | //! ### Technical Requirements 32 | //! 33 | //! For assets to be meldable, they must be logically identical, i.e. contain the same meshes – and 34 | //! vary only in what materials are assigned to those meshes. The tool will complain if it finds 35 | //! disrepancies between the source assets that are too confusing for it to work around. 36 | //! 37 | //! During the melding process, all common data is shared, whereas varying material definitions and 38 | //! textures are copied as needed. Parts of the assets that don't vary are left untouched. 39 | //! 40 | //! Each source asset brought into the tool is identified by a *tag*, a short string, and it's 41 | //! these same tags that are later used to trigger different runtime apperances. 42 | 43 | extern crate gltf; 44 | extern crate serde; 45 | extern crate serde_derive; 46 | extern crate serde_json; 47 | extern crate sha1; 48 | extern crate spectral; 49 | 50 | /// Tags are short identifiers used to switch between different mesh primitive materials. 51 | pub type Tag = String; 52 | 53 | /// Our library-wide error type is (as yet) a simple string. 54 | pub type Error = String; 55 | /// Convenience type for a Result using our Error. 56 | pub type Result = ::std::result::Result; 57 | 58 | /// The JSON/Serde implementation of `KHR_materials_variants`. 59 | pub mod extension; 60 | 61 | /// The VarationalAsset struct and associated functionality. 62 | pub mod variational_asset; 63 | pub use variational_asset::{AssetSizes, Metadata, VariationalAsset}; 64 | 65 | /// The internal workhorse WorkAsset struct & functionality. 66 | pub mod work_asset; 67 | pub use work_asset::WorkAsset; 68 | 69 | pub mod glb; 70 | pub use glb::GlbChunk; 71 | 72 | pub mod gltfext; 73 | pub use gltfext::*; 74 | 75 | /// Mapping glTF objects to unique keys for melding purposes. 76 | pub mod meld_keys; 77 | pub use meld_keys::{Fingerprint, MeldKey}; 78 | -------------------------------------------------------------------------------- /native/src/meld_keys/fingerprints.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use gltf::{mesh::Primitive, Buffer}; 5 | 6 | use spectral::prelude::*; 7 | 8 | use crate::{Fingerprint, Result}; 9 | 10 | /// Computes a `Fingerprint` from a `Primitive`. 11 | /// 12 | /// A fingerprint needs to be independent of triangle order and vertex order, and obviously it 13 | /// should be non-trivially different from the fingerprint of some other `Primitive`. This isn't 14 | /// as obvious as it seems: for example, if we simply took the geometric average of positions, 15 | /// all shapes that are symmetric around origin, regardless of scale, would be identical. 16 | /// 17 | /// We look at vertex positions and vertex colours, and simply add them up, with an added 18 | /// skew to the Y and Z dimensions, to break symmetries. 19 | /// 20 | /// More complexity could be added here, if warranted. 21 | pub fn build_fingerprint(primitive: &Primitive, blob: &[u8]) -> Result { 22 | let buf_to_blob = |buf: Buffer| { 23 | assert_that(&buf.index()).is_equal_to(0); 24 | if blob.is_empty() { 25 | None 26 | } else { 27 | Some(blob) 28 | } 29 | }; 30 | 31 | let reader = primitive.reader(buf_to_blob); 32 | 33 | let positions: Vec<[f32; 3]> = reader 34 | .read_positions() 35 | .ok_or(format!("Primitive lacks position data!"))? 36 | .collect(); 37 | 38 | let indices: Vec = reader 39 | .read_indices() 40 | .ok_or(format!("Primitive lacks indices!"))? 41 | .into_u32() 42 | .collect(); 43 | 44 | let count = indices.len() as f64; 45 | 46 | let mut cumulative_fingerprint = { 47 | let mut print: f64 = 0.0; 48 | for &ix in &indices { 49 | print += vec3_to_print(positions[ix as usize]) / count; 50 | } 51 | print 52 | }; 53 | 54 | if let Some(colors) = reader.read_colors(0) { 55 | let colors: Vec<[f32; 4]> = colors.into_rgba_f32().collect(); 56 | 57 | cumulative_fingerprint += { 58 | let mut print: f64 = 0.0; 59 | for &ix in &indices { 60 | print += vec4_to_print(colors[ix as usize]) / count; 61 | } 62 | print 63 | } 64 | } 65 | 66 | Ok(cumulative_fingerprint) 67 | } 68 | 69 | fn vec3_to_print(vec: [f32; 3]) -> f64 { 70 | // arbitrary symmetry-breaking shear 71 | (vec[0] + 1.3 * vec[1] + 1.7 * vec[2]) as f64 72 | } 73 | 74 | fn vec4_to_print(vec: [f32; 4]) -> f64 { 75 | // arbitrary symmetry-breaking shear 76 | (vec[0] + 1.1 * vec[1] + 1.3 * vec[2] + 1.5 * vec[3]) as f64 77 | } 78 | -------------------------------------------------------------------------------- /native/src/meld_keys/key_trait.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use sha1::Sha1; 5 | 6 | use gltf::json::texture; 7 | use gltf::json::Mesh; 8 | use gltf::json::{material::NormalTexture, material::OcclusionTexture}; 9 | use gltf::json::{texture::Sampler, Image, Index, Material, Texture}; 10 | 11 | use crate::{MeldKey, Result, WorkAsset}; 12 | 13 | /// A trait implemented on glTF objects for which we need a `MeldKey`. 14 | /// 15 | /// Please consult [the `MeldKey` documentation](../meld_keys/type.MeldKey.html) for an overview. 16 | pub trait HasKeyForVariants { 17 | /// Computes and returns the `MeldKey` for this object. 18 | /// 19 | /// See individual implementations for details. 20 | fn build_meld_key(&self, work_asset: &WorkAsset) -> Result; 21 | } 22 | 23 | impl HasKeyForVariants for Image { 24 | /// The `MeldKey` of an `Image` is a stringified SHA1-hash of the underlying bytes. 25 | /// 26 | /// Example: "`daf12297c5c549fa199b85adbe77d626edc93184`" 27 | fn build_meld_key(&self, work_asset: &WorkAsset) -> Result { 28 | let image_bytes = work_asset.read_image_bytes(self)?; 29 | Ok(Sha1::from(image_bytes).digest().to_string()) 30 | } 31 | } 32 | 33 | impl HasKeyForVariants for Texture { 34 | /// The `MeldKey` of an `Texture` combines a `Sampler` and an `Image` `MeldKey`. 35 | /// 36 | /// Example: "`[sampler=,source=daf12297c5c549fa199b85adbe77d626edc93184]`" 37 | fn build_meld_key(&self, work_asset: &WorkAsset) -> Result { 38 | Ok(format!( 39 | "[sampler={},source={}]", 40 | key_or_empty(work_asset.sampler_keys(), self.sampler), 41 | key(work_asset.image_keys(), self.source), 42 | )) 43 | } 44 | } 45 | 46 | impl HasKeyForVariants for Sampler { 47 | /// The `MeldKey` of a `Sampler` is a stringification of simple JSON attributes. 48 | /// 49 | /// Example: "`[mag_filter=None,min_filter=None,wrap_s=Repeat,wrap_t=Repeat]`" 50 | fn build_meld_key(&self, _work_asset: &WorkAsset) -> Result { 51 | Ok(format!( 52 | "[mag_filter={:?},min_filter={:?},wrap_s={:?},wrap_t={:?}]", 53 | self.mag_filter, self.min_filter, self.wrap_s, self.wrap_t 54 | )) 55 | } 56 | } 57 | 58 | impl HasKeyForVariants for Material { 59 | /// The `MeldKey` of a `Material` combines `Texture` keys with its own many JSON attributes. 60 | /// 61 | /// Example: "`[[pbr=[bcf=[1.0, 1.0, 1.0, 1.0], bct=[tc=0,src=[sampler=,source=49ff16b74ed7beabc95d49ef8a0f7615db949851]], mf=0.4, rf=0.6, mrt=[]], nt=[], ot=[], et=[], ef=[0.0, 0.0, 0.0], am=Opaque, ac=0.5, ds=false]`" 62 | fn build_meld_key(&self, work_asset: &WorkAsset) -> Result { 63 | let pbr = &self.pbr_metallic_roughness; 64 | Ok(format!( 65 | "[[pbr=[bcf={:?}, bct={}, mf={:?}, rf={:?}, mrt={}], nt={}, ot={}, et={}, ef={:?}, am={:?}, ac={:?}, ds={}]", 66 | pbr.base_color_factor, 67 | key_for_texinfo(work_asset, &pbr.base_color_texture), 68 | pbr.metallic_factor, 69 | pbr.roughness_factor, 70 | key_for_texinfo(work_asset, &pbr.metallic_roughness_texture), 71 | key_for_normal_texinfo(work_asset, &self.normal_texture), 72 | key_for_occlusion_texinfo(work_asset, &self.occlusion_texture), 73 | key_for_texinfo(work_asset, &self.emissive_texture), 74 | self.emissive_factor, 75 | self.alpha_mode, 76 | self.alpha_cutoff, 77 | self.double_sided, 78 | )) 79 | } 80 | } 81 | 82 | impl HasKeyForVariants for Mesh { 83 | /// The `MeldKey` of a `Mesh` is simply its name. This is probably a temporary solution. 84 | /// 85 | /// Example: "`polySurface12`" 86 | /// 87 | /// Note: It'd be very, very convenient if we can match up meshes by name, because comparing 88 | /// them numerically is kind of a nightmare of fuzzy computational geometry. The question is if 89 | /// the tool can require users to control the glTF level name to the extend necessary. 90 | fn build_meld_key(&self, _work_asset: &WorkAsset) -> Result { 91 | self.name 92 | .as_ref() 93 | .map(String::from) 94 | .ok_or_else(|| format!("Mesh with no name! Eee.")) 95 | } 96 | } 97 | 98 | fn key_for_texinfo(work_asset: &WorkAsset, texinfo: &Option) -> MeldKey { 99 | if let Some(texinfo) = &texinfo { 100 | format!( 101 | "[tc={},src={}]", 102 | texinfo.tex_coord, 103 | key(work_asset.texture_keys(), texinfo.index), 104 | ) 105 | } else { 106 | String::from("[]") 107 | } 108 | } 109 | 110 | fn key_for_normal_texinfo(work_asset: &WorkAsset, texinfo: &Option) -> MeldKey { 111 | if let Some(texinfo) = &texinfo { 112 | format!( 113 | "[s={},tc={},src={}]", 114 | texinfo.scale, 115 | texinfo.tex_coord, 116 | key(work_asset.texture_keys(), texinfo.index), 117 | ) 118 | } else { 119 | String::from("[]") 120 | } 121 | } 122 | 123 | fn key_for_occlusion_texinfo( 124 | work_asset: &WorkAsset, 125 | texinfo: &Option, 126 | ) -> MeldKey { 127 | if let Some(texinfo) = &texinfo { 128 | format!( 129 | "[s={:?},tc={},src={}]", 130 | texinfo.strength, 131 | texinfo.tex_coord, 132 | key(work_asset.texture_keys(), texinfo.index), 133 | ) 134 | } else { 135 | String::from("[]") 136 | } 137 | } 138 | 139 | fn key_or_empty(keys: &Vec, ix: Option>) -> MeldKey { 140 | match ix { 141 | Some(ix) => key(keys, ix), 142 | None => String::new(), 143 | } 144 | } 145 | 146 | fn key(keys: &Vec, ix: Index) -> MeldKey { 147 | keys[ix.value()].to_owned() 148 | } 149 | -------------------------------------------------------------------------------- /native/src/meld_keys/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | mod key_trait; 5 | pub use key_trait::HasKeyForVariants; 6 | 7 | mod fingerprints; 8 | pub use fingerprints::build_fingerprint; 9 | 10 | /// A short string that uniquely identifies all glTF objects other than `Mesh` `Primitives`. 11 | pub type MeldKey = String; 12 | 13 | /// A floating-point number that identifies logically identical `Mesh` `Primitives`. 14 | /// 15 | /// Most glTF objects are given a simple, unique key as part of the `MeldKey` mechanism. 16 | /// For geometry, things are trickier. To begin with, neither the order of triangles (indices) 17 | /// nor vectors are important, so any comparison must be order-agnostic. Worse, floating-point 18 | /// calculations are inexact, and so identity there must be of the ||x - x'|| < ε type. 19 | pub type Fingerprint = f64; 20 | -------------------------------------------------------------------------------- /native/src/variational_asset/metadata.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use std::collections::{HashMap, HashSet}; 5 | 6 | extern crate wasm_bindgen; 7 | use wasm_bindgen::prelude::*; 8 | 9 | use serde_derive::{Deserialize, Serialize}; 10 | use serde_json::json; 11 | 12 | use crate::{AssetSizes, Tag}; 13 | 14 | /// All the metadata generated for a variational asset. 15 | #[wasm_bindgen] 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct Metadata { 18 | /// The set of variational tags in this asset. 19 | pub(crate) tags: HashSet, 20 | /// The sum byte size of **every** referenced texture in this asset. 21 | pub(crate) total_sizes: AssetSizes, 22 | /// The sum byte size of textures that are referenced depending on active variant tag. 23 | pub(crate) variational_sizes: AssetSizes, 24 | // The sum byte size of textures active under each variant tag specifically. 25 | pub(crate) per_tag_sizes: HashMap, 26 | } 27 | 28 | // methods that are already happily wasm_bind compliant 29 | #[wasm_bindgen] 30 | impl Metadata { 31 | /// The sum byte size of **every** referenced texture in this asset. 32 | pub fn total_sizes(&self) -> AssetSizes { 33 | self.total_sizes 34 | } 35 | 36 | /// The sum byte size of textures that are referenced depending on active variant tag. 37 | pub fn variational_sizes(&self) -> AssetSizes { 38 | self.variational_sizes 39 | } 40 | } 41 | 42 | // methods that wasm_bindgen can't cope with in their preferred form 43 | impl Metadata { 44 | /// The set of variational tags in this asset. 45 | pub fn tags(&self) -> &HashSet { 46 | &self.tags 47 | } 48 | 49 | /// The asset sizes associated with the given tag, if any. 50 | pub fn tag_sizes(&self, tag: &Tag) -> Option<&AssetSizes> { 51 | self.per_tag_sizes.get(tag) 52 | } 53 | } 54 | 55 | #[wasm_bindgen] 56 | impl Metadata { 57 | /// WASM-friendly version of `tags()`; returns a JSON-encoded array of strings. 58 | pub fn wasm_tags(&self) -> String { 59 | json!(self.tags).to_string() 60 | } 61 | 62 | /// WASM-friendly version of `tags()`; returns a JSON-encoded map of tags to sizes. 63 | pub fn wasm_tag_sizes(&self) -> String { 64 | json!(self.per_tag_sizes).to_string() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /native/src/variational_asset/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use std::path::Path; 5 | 6 | extern crate wasm_bindgen; 7 | use wasm_bindgen::prelude::*; 8 | 9 | use serde_derive::{Deserialize, Serialize}; 10 | 11 | use crate::{Error, Tag, WorkAsset}; 12 | 13 | /// The Metadata struct & accessor methods 14 | pub mod metadata; 15 | pub use metadata::Metadata; 16 | 17 | /// Compatibility methods for the WebAssembly build 18 | pub mod wasm; 19 | 20 | /// The primary API data structure. 21 | /// 22 | /// The key property is a glTF asset in binary form (which will always implement 23 | /// the `KHR_materials_variants` extension), along with various useful metadata for 24 | /// the benefit of clients. 25 | /// 26 | /// The key method melds one variational asset into another: 27 | /// ``` 28 | /// extern crate assets; 29 | /// use std::path::Path; 30 | /// use gltf_variant_meld::{Tag, VariationalAsset}; 31 | /// 32 | /// let (matte_tag, shiny_tag) = (Tag::from("matte"), Tag::from("shiny")); 33 | /// let pinecone_matte = VariationalAsset::from_file( 34 | /// &Path::new(assets::ASSET_PINECONE_MATTE()), 35 | /// Some(&matte_tag), 36 | /// ).expect("Eek! Couldn't create matte pinecone VariationalAsset."); 37 | /// 38 | /// let pinecone_shiny = VariationalAsset::from_file( 39 | /// &Path::new(assets::ASSET_PINECONE_SHINY()), 40 | /// Some(&shiny_tag), 41 | /// ).expect("Eek! Couldn't create shiny pinecone VariationalAsset."); 42 | /// 43 | /// let result = VariationalAsset::meld( 44 | /// &pinecone_matte, 45 | /// &pinecone_shiny 46 | /// ).expect("Erk. Failed to meld two pinecones."); 47 | /// 48 | /// assert!(result.metadata().tags().contains(&matte_tag)); 49 | /// assert!(result.metadata().tags().contains(&shiny_tag)); 50 | /// assert_eq!(result.metadata().tags().len(), 2); 51 | ///``` 52 | #[wasm_bindgen] 53 | #[derive(Debug, Clone)] 54 | pub struct VariationalAsset { 55 | /// The generated glTF for this asset. Will always implement `KHR_materials_variants` 56 | /// and is always in binary (GLB) form. 57 | pub(crate) glb: Vec, 58 | 59 | /// The tag that stands in for any default material references in the asset glTF. 60 | pub(crate) default_tag: Tag, 61 | 62 | /// All the metadata generated for this asset. 63 | pub(crate) metadata: Metadata, 64 | } 65 | 66 | /// A summary of a mesh primitive's byte size requirements; currently textures only. 67 | #[wasm_bindgen] 68 | #[derive(Debug, Copy, Clone, Serialize, Deserialize)] 69 | pub struct AssetSizes { 70 | /// Byte count for texture image data, in its raw encoded form. 71 | pub texture_bytes: usize, 72 | } 73 | 74 | // methods that wasm_bindgen can't cope with in their preferred form 75 | impl VariationalAsset { 76 | /// Generates a new `VariationalAsset` from a glTF file. 77 | /// 78 | /// If the provided asset implements `KHR_materials_variants`, then `default_tag` must 79 | /// either be empty or match the default tag within the asset. 80 | /// 81 | /// If the asset doesn't implement `KHR_materials_variants`, then the argument 82 | /// `default_tag` must be non-empty. If it does, then `default_tag` must either match 83 | /// what's in the asset, or else be empty. 84 | pub fn from_file(file: &Path, default_tag: Option<&Tag>) -> Result { 85 | let loaded = WorkAsset::from_file(file, default_tag)?; 86 | loaded.export() 87 | } 88 | 89 | /// Generates a new `VariationalAsset` from a byte slice of glTF. 90 | /// 91 | /// If the provided asset implements `KHR_materials_variants`, then `default_tag` must 92 | /// either be empty or match the default tag within the asset. 93 | /// 94 | /// If the asset doesn't implement `KHR_materials_variants`, then the argument 95 | /// `default_tag` must be non-empty. If it does, then `default_tag` must either match 96 | /// what's in the asset, or else be empty. 97 | pub fn from_slice( 98 | gltf: &[u8], 99 | default_tag: Option<&Tag>, 100 | base_dir: Option<&Path>, 101 | ) -> Result { 102 | let loaded = WorkAsset::from_slice(gltf, default_tag, base_dir)?; 103 | loaded.export() 104 | } 105 | 106 | /// The generated glTF for this asset. Will always implement `KHR_materials_variants` 107 | /// and is always in binary (GLB) form. 108 | pub fn glb(&self) -> &[u8] { 109 | self.glb.as_slice() 110 | } 111 | 112 | /// The tag that stands in for any default material references in the asset glTF. 113 | pub fn default_tag(&self) -> &Tag { 114 | &self.default_tag 115 | } 116 | 117 | /// All the metadata generated for this asset. 118 | pub fn metadata(&self) -> &Metadata { 119 | &self.metadata 120 | } 121 | 122 | /// Melds one variational asset into another, combining material-switching tags 123 | /// on a per-mesh, per-primitive basis. 124 | /// 125 | /// Note that logically identical glTF objects may be bitwise quite different, e.g. 126 | /// because glTF array order is undefined, floating-point values are only equal to 127 | /// within some epsilon, the final global position of a vector can be the end 128 | /// result of very different transformations, and so on. 129 | /// 130 | /// Further, the whole point of this tool is to identify shared pieces of data 131 | /// between the two assets, keep only one, and redirect all references to it. 132 | /// 133 | pub fn meld<'a>( 134 | base: &'a VariationalAsset, 135 | other: &'a VariationalAsset, 136 | ) -> Result { 137 | let base = &WorkAsset::from_slice(base.glb(), Some(base.default_tag()), None)?; 138 | let other = &WorkAsset::from_slice(other.glb(), Some(other.default_tag()), None)?; 139 | 140 | let meld = WorkAsset::meld(base, other)?; 141 | meld.export() 142 | } 143 | } 144 | 145 | impl AssetSizes { 146 | /// Instantiate a new `AssetSizes` with the given texture byte count. 147 | pub fn new(texture_bytes: usize) -> AssetSizes { 148 | AssetSizes { texture_bytes } 149 | } 150 | 151 | /// Byte count for texture image data, in its raw encoded form. 152 | pub fn texture_bytes(&self) -> usize { 153 | self.texture_bytes 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /native/src/variational_asset/wasm.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | extern crate wasm_bindgen; 5 | use wasm_bindgen::prelude::*; 6 | 7 | use crate::{Metadata, Tag, VariationalAsset}; 8 | 9 | // simplified versions of methods for the benefit only of wasm_bind 10 | #[wasm_bindgen] 11 | impl VariationalAsset { 12 | /// WASM-friendly version of `from_slice`; remaps its errors as `JsValue`. 13 | pub fn wasm_from_slice(glb: &[u8], tag: Option) -> Result { 14 | VariationalAsset::from_slice(glb, tag.as_ref(), None).map_err(JsValue::from) 15 | } 16 | 17 | /// WASM-friendly version of `meld``; remaps its errors as `JsValue`. 18 | pub fn wasm_meld( 19 | base: &VariationalAsset, 20 | melded: &VariationalAsset, 21 | ) -> Result { 22 | VariationalAsset::meld(base, melded).map_err(JsValue::from) 23 | } 24 | 25 | /// WASM-friendly version of `glb()`; returns an ownable `Vec` instead of a `&[u8]` slice. 26 | pub fn wasm_glb(&self) -> Vec { 27 | self.glb.to_owned() 28 | } 29 | 30 | /// WASM-friendly version of `default_tag()`; returns a clone of the tag 31 | pub fn wasm_default_tag(&self) -> Tag { 32 | self.default_tag.clone() 33 | } 34 | 35 | /// WASM-friendly version of `metadata()`; returns a clone of our metadata 36 | pub fn wasm_metadata(&self) -> Metadata { 37 | self.metadata.clone() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /native/src/work_asset/construct.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Code to parse & index a glTF asset into `WorkAsset` format. 5 | 6 | use std::collections::{HashMap, HashSet}; 7 | use std::fs; 8 | use std::path::{Path, PathBuf}; 9 | 10 | use spectral::prelude::*; 11 | 12 | use gltf::json::{image::MimeType, mesh::Primitive, Mesh, Root}; 13 | use gltf::Gltf; 14 | 15 | use crate::extension; 16 | use crate::gltfext::{add_buffer_view_from_slice, set_root_buffer}; 17 | use crate::meld_keys::{build_fingerprint, HasKeyForVariants}; 18 | use crate::{Fingerprint, MeldKey, Result, Tag, WorkAsset}; 19 | 20 | impl WorkAsset { 21 | /// Constructs a `WorkAsset` from a file `Path` using `::from_slice`. 22 | pub fn from_file(file: &Path, default_tag: Option<&Tag>) -> Result { 23 | let slice = fs::read(file).map_err(|e| { 24 | format!( 25 | "Couldn't read asset file {}: {}", 26 | file.to_str().unwrap(), 27 | e.to_string() 28 | ) 29 | })?; 30 | Self::from_slice(&slice, default_tag, file.parent()) 31 | } 32 | 33 | /// Constructs a `WorkAsset` from a glTF byte slice, which can be text (JSON) or binary (GLB). 34 | /// 35 | /// We lean on `Gltf::from_slice()` to parse the contents, yielding a `Document` 36 | /// (which wraps the JSON component we're really after) and a byte blob, which we will 37 | /// read from and may add to during other operations on this asset. 38 | /// 39 | /// See constructor `new()` for details on how the rest of `WorkAsset` is built. 40 | pub fn from_slice( 41 | gltf: &[u8], 42 | default_tag: Option<&Tag>, 43 | file_base: Option<&Path>, 44 | ) -> Result { 45 | let result = Gltf::from_slice(gltf).or_else(|e| { 46 | Err(format!( 47 | "Parse error in VariationalAsset glTF: {}", 48 | e.to_string() 49 | )) 50 | })?; 51 | 52 | // break the `Gltf` object into a `Root` and a byte blob here 53 | let parse = result.document.into_json(); 54 | let blob = if let Some(blob) = result.blob { 55 | assert_that!(parse.buffers.len()).is_equal_to(1); 56 | assert_that!(parse.buffers[0].byte_length as usize) 57 | .is_less_than_or_equal_to(blob.len()); 58 | blob 59 | } else { 60 | vec![] 61 | }; 62 | 63 | Self::new(parse, blob, default_tag, file_base) 64 | } 65 | 66 | /// Constructs a `WorkAsset` given a JSON `Root`, a byte blob, default tag & file base. 67 | /// 68 | /// First, any filesystem references within the glTF are converted to binary references, by 69 | /// resolving paths, reading files, and appending them to the blob & as `BufferView` objects in 70 | /// the JSON. After this step, the asset is entirely self-contained, and the `file_base` 71 | /// argument is no longer used. 72 | /// 73 | /// Next, we validate/retrieve any default tag embedded using the `KHR_materials_variants` 74 | /// extension. This tag must match the `default_tag` provided as argument, if any, and if none 75 | /// was provided it must exist in the asset. This ensure `WorkAsset.default_tag` always exists 76 | /// and makes sense. 77 | /// 78 | /// Then, we construct `MeldKey` strings for every glTF object we track – `Image`, `Sampler`, 79 | /// `Texture`, `Material` and `Mesh`. Please consult the `::meld_keys` module for details on 80 | /// meld keys. 81 | /// 82 | /// We require that `Mesh` keys are unique, and protest if they're not. 83 | /// 84 | /// Next, every `Primitives` of every `Mesh` is given a `Fingerprint`, which is essentially a 85 | /// floating-point `MeldKey` that can be used to match logically identical objects that have 86 | /// numerically drifted apart to some microscopic degree. 87 | /// 88 | /// We require that `Primitive` fingerprints are unique, to within a tolerance. 89 | /// 90 | /// Finally, each mesh and mesh primitive is inspected, and any `KHR_materials_variants` data is 91 | /// parsed and converted to a Tag->MeldKey mapping, filling in `mesh_primitive_variants` and 92 | /// completing the `WorkAsset` construction. 93 | pub fn new( 94 | mut parse: Root, 95 | mut blob: Vec, 96 | default_tag: Option<&Tag>, 97 | file_base: Option<&Path>, 98 | ) -> Result { 99 | Self::transform_parse(&mut parse, &mut blob, file_base)?; 100 | 101 | let default = Tag::from("default"); 102 | let tag = default_tag.unwrap_or(&default); 103 | 104 | let mut asset = WorkAsset { 105 | parse, 106 | blob, 107 | default_tag: tag.to_owned(), 108 | mesh_primitive_variants: vec![], 109 | 110 | image_keys: vec![], 111 | material_keys: vec![], 112 | mesh_keys: vec![], 113 | sampler_keys: vec![], 114 | texture_keys: vec![], 115 | 116 | mesh_primitive_fingerprints: vec![], 117 | }; 118 | 119 | // there is a strict dependency order here which must be observed 120 | asset.image_keys = asset.build_meld_keys(&asset.parse.images)?; 121 | asset.sampler_keys = asset.build_meld_keys(&asset.parse.samplers)?; 122 | asset.texture_keys = asset.build_meld_keys(&asset.parse.textures)?; 123 | asset.material_keys = asset.build_meld_keys(&asset.parse.materials)?; 124 | asset.mesh_keys = asset.build_meld_keys(&asset.parse.meshes)?; 125 | asset.mesh_primitive_fingerprints = asset.build_fingerprints()?; 126 | 127 | asset.ensure_unique_mesh_keys()?; 128 | asset.ensure_uniqueish_fingerprints()?; 129 | 130 | let variant_lookup = extension::get_variant_lookup(&asset.parse)?; 131 | let mesh_primitive_variants = asset.map_variants(variant_lookup)?; 132 | asset.mesh_primitive_variants = mesh_primitive_variants; 133 | 134 | Ok(asset) 135 | } 136 | 137 | fn build_meld_keys(&self, objects: &Vec) -> Result> { 138 | let vec_of_results: Vec> = objects 139 | .iter() 140 | .map(|o| o.build_meld_key(self)) 141 | .to_owned() 142 | .collect(); 143 | vec_of_results.into_iter().collect() 144 | } 145 | 146 | fn build_fingerprints(&self) -> Result>> { 147 | let gltf = self.to_owned_gltf(); 148 | 149 | let mut result = vec![]; 150 | for mesh in gltf.meshes() { 151 | let mut fingerprints = vec![]; 152 | for primitive in mesh.primitives() { 153 | fingerprints.push(build_fingerprint(&primitive, &self.blob)?); 154 | } 155 | result.push(fingerprints); 156 | } 157 | Ok(result) 158 | } 159 | 160 | fn ensure_unique_mesh_keys(&self) -> Result<()> { 161 | let mut seen = HashSet::new(); 162 | let mut dups = HashSet::new(); 163 | for mesh_key in &self.mesh_keys { 164 | if seen.contains(mesh_key) { 165 | dups.insert(mesh_key); 166 | } 167 | seen.insert(mesh_key); 168 | } 169 | if !dups.is_empty() { 170 | Err(format!("Aii, non-unique meld keys: {:#?}", dups)) 171 | } else { 172 | Ok(()) 173 | } 174 | } 175 | 176 | fn ensure_uniqueish_fingerprints(&self) -> Result<()> { 177 | for (mesh_ix, fingerprints) in self.mesh_primitive_fingerprints.iter().enumerate() { 178 | for (primitive_ix, fingerprint) in fingerprints.iter().enumerate() { 179 | if let Some(other_print) = 180 | self.find_almost_equal_fingerprint(mesh_ix, fingerprint, Some(primitive_ix)) 181 | { 182 | return Err(format!( 183 | "Can't cope with primitives {} and {} of mesh {} being identical.", 184 | primitive_ix, other_print, mesh_ix 185 | )); 186 | } 187 | } 188 | } 189 | Ok(()) 190 | } 191 | 192 | fn map_variants(&self, variant_ix_lookup: HashMap) -> Result>>> { 193 | let map_material = |(tag, ix): (&MeldKey, &usize)| -> Result<(Tag, MeldKey)> { 194 | Ok((tag.to_string(), self.material_keys[*ix].to_owned())) 195 | }; 196 | let map_primitive = |p: &Primitive| -> Result> { 197 | let variant_map = extension::extract_variant_map(p, &variant_ix_lookup)?; 198 | variant_map.iter().map(map_material).collect() 199 | }; 200 | let map_mesh = |m: &Mesh| -> Result>> { 201 | m.primitives.iter().map(map_primitive).collect() 202 | }; 203 | self.parse.meshes.iter().map(map_mesh).collect() 204 | } 205 | 206 | // ensure the glTF is in the state that WorkAsset expects 207 | fn transform_parse( 208 | root: &mut Root, 209 | blob: &mut Vec, 210 | file_base: Option<&Path>, 211 | ) -> Result<()> { 212 | // load from URI any non-GLB buffers 213 | Self::transform_buffers(root, blob, file_base)?; 214 | // load from URI any images not already embedded 215 | Self::transform_images(root, blob, file_base)?; 216 | Ok(()) 217 | } 218 | 219 | // resolve any buffers in the asset that reference URIs, read those files 220 | // and append them to the binary blob, and finally replace the entire buffer 221 | // vector with our single BIN buffer entry 222 | fn transform_buffers( 223 | root: &mut Root, 224 | blob: &mut Vec, 225 | file_base: Option<&Path>, 226 | ) -> Result<()> { 227 | assert_that!(blob.len() % 4).is_equal_to(0); 228 | 229 | for buffer in &mut root.buffers { 230 | if let Some(uri) = &buffer.uri { 231 | let mut buffer_bytes = Self::read_from_uri(uri, file_base)?; 232 | blob.append(&mut buffer_bytes); 233 | while (blob.len() % 4) != 0 { 234 | blob.push(0x00); 235 | } 236 | } 237 | } 238 | 239 | set_root_buffer(blob, &mut root.buffers); 240 | 241 | Ok(()) 242 | } 243 | 244 | // resolve any images in the asset that reference URIs, read those files and create 245 | // buffer_views for them and add them + buffer views to the asset. 246 | fn transform_images( 247 | root: &mut Root, 248 | blob: &mut Vec, 249 | file_base: Option<&Path>, 250 | ) -> Result<()> { 251 | let images = &mut root.images; 252 | let buffer_views = &mut root.buffer_views; 253 | 254 | for img in images { 255 | if img.buffer_view.is_none() { 256 | if let Some(uri) = &img.uri { 257 | let image_bytes = Self::read_from_uri(uri, file_base)?; 258 | let view_ix = 259 | add_buffer_view_from_slice(image_bytes.as_slice(), buffer_views, blob); 260 | 261 | img.buffer_view = Some(view_ix); 262 | img.mime_type = Some(Self::guess_mime_type(uri)?); 263 | img.uri = None; 264 | } 265 | } 266 | } 267 | Ok(()) 268 | } 269 | 270 | fn guess_mime_type(uri: &String) -> Result { 271 | if let Some(extension) = Path::new(uri).extension() { 272 | match &extension.to_str().unwrap().to_ascii_lowercase()[..] { 273 | "jpg" | "jpeg" => { 274 | return Ok(MimeType("image/jpeg".to_string())); 275 | } 276 | "png" => { 277 | return Ok(MimeType("image/png".to_string())); 278 | } 279 | _ => {} 280 | } 281 | }; 282 | Err(format!("Can't guess mime type of URI: {}", uri)) 283 | } 284 | 285 | fn read_from_uri(uri: &str, file_base: Option<&Path>) -> Result> { 286 | // this is very temporary, lifted lifted from gltf::import.rs 287 | let path = if uri.contains(":") { 288 | if uri.starts_with("file://") { 289 | &uri["file://".len()..] 290 | } else if uri.starts_with("file:") { 291 | &uri["file:".len()..] 292 | } else { 293 | panic!("Can only handle file:// URIs yet."); 294 | } 295 | } else { 296 | &uri[..] 297 | }; 298 | let mut path = PathBuf::from(path); 299 | if path.is_relative() { 300 | if let Some(file_base) = file_base { 301 | path = file_base.join(path); 302 | } 303 | } 304 | Ok(fs::read(path.as_path()) 305 | .map_err(|e| format!("Error reading file {}: {}", path.display(), e.to_string()))?) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /native/src/work_asset/export.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Code to generate a glTF asset from a `WorkAsset` instance. 5 | 6 | use std::collections::{HashMap, HashSet}; 7 | 8 | use gltf::json::{Material, Root}; 9 | 10 | use crate::extension; 11 | use crate::{AssetSizes, Metadata, Result, Tag, VariationalAsset}; 12 | 13 | use crate::glb::GlbChunk; 14 | 15 | use super::*; 16 | 17 | impl<'a> WorkAsset { 18 | /// Builds fully standalone variational glTF from this `WorkAsset`'s state. 19 | /// 20 | /// First, we put together the finished structured asset: 21 | /// - Clone the JSON in `WorkAsset.parse` as well as the `WorkAsset.blob` byte vector. 22 | /// - Write `WorkAsset.default_tag` into the root of the new JSON, using the 23 | /// `KHR_materials_variants` extension. 24 | /// - Iterate over every mesh and mesh primitive in `WorkAsset.mesh_primitive_variants`, 25 | /// and wherever there is a non-empty variational Tag->Material mapping, convert that to 26 | /// `KHR_materials_variants` form and write it into the mesh primitive's JSON. 27 | /// 28 | /// Next, we count up all the metadata. 29 | /// 30 | /// Finally, the binary glTF (GLB) blob is generated, by serialising the glTF JSON into 31 | /// text form, and merging it with the binary blob (see `::crate::glb` for details.) 32 | pub fn export(&self) -> Result { 33 | let (parse, blob, metadata) = self.prepare_for_export()?; 34 | let default_tag = self.default_tag.clone(); 35 | let glb = self.build_glb_for_export(parse, blob.as_slice())?; 36 | 37 | Ok(VariationalAsset { 38 | glb, 39 | default_tag, 40 | metadata, 41 | }) 42 | } 43 | 44 | fn prepare_for_export(&self) -> Result<(Root, Vec, Metadata)> { 45 | // clone our Root & blob for new export 46 | let mut root = self.parse.clone(); 47 | let blob = self.blob.clone(); 48 | 49 | // make note of the use of our glTF extension 50 | extension::install(&mut root); 51 | 52 | // then mutate the clone with our variational state 53 | self.export_variant_root_lookup(&mut root)?; 54 | 55 | let variant_ix_lookup = extension::get_variant_lookup(&root)?; 56 | 57 | // finally write out the tag->material_ix mapping to glTF JSON 58 | let metadata = self.export_variant_mapping(&mut root, &variant_ix_lookup)?; 59 | 60 | Ok((root, blob, metadata)) 61 | } 62 | 63 | fn export_variant_root_lookup(&self, root: &mut Root) -> Result<()> { 64 | let tags_in_use = self.get_tags_in_use()?; 65 | extension::write_root_variant_lookup_map(root, &tags_in_use) 66 | } 67 | 68 | // export our `mesh_primitive_variants` member into glTF form, by transforming the 69 | // tag->material_key mapping of each mesh/primitive to a tag->material_ix one, then 70 | // finally calling the glTF extension code to actually convert it to JSON. 71 | fn export_variant_mapping(&self, root: &mut Root, variant_ix_lookup: &HashMap) -> Result { 72 | let mut image_sizer = ImageSizes::new(&self); 73 | 74 | // for each mesh... 75 | for (m_ix, mesh) in root.meshes.iter_mut().enumerate() { 76 | // and for each of that mesh's primitives... 77 | for (p_ix, primitive) in mesh.primitives.iter_mut().enumerate() { 78 | // retrieve the mapping of tag->material_key 79 | let variant_mapping = self.variant_mapping(m_ix, p_ix); 80 | 81 | // prepare to build the mapping of tag->material_ix 82 | let mut tag_to_ix = HashMap::new(); 83 | 84 | // loop over the (tag, key) entries in that mapping... 85 | for (tag, material_key) in variant_mapping.iter() { 86 | // map the material key to a glTF material index... 87 | if let Some(material_ix) = self.material_ix(material_key) { 88 | if *tag == self.default_tag { 89 | // there may be a mapping for the default tag, but if so the primitive 90 | // must have a default material too, and they must match, and we do 91 | // not keep or count it – it's treated elsewhere further down 92 | if let Some(default_material_ix) = primitive.material { 93 | if default_material_ix.value() == material_ix { 94 | continue; 95 | } 96 | return Err(format!( 97 | "Huh? Default material {} != variant map entry {} of default tag {}.", 98 | default_material_ix, 99 | material_ix, self.default_tag 100 | )); 101 | } 102 | return Err(format!( 103 | "Huh? No default material, but variant map entry {} of default tag {} ", 104 | material_ix, self.default_tag 105 | )); 106 | } 107 | 108 | // place it into the tag->material_ix mapping 109 | tag_to_ix.insert(tag.to_owned(), material_ix); 110 | 111 | // and update metadata 112 | image_sizer.accumulate_material(material_ix, true); 113 | image_sizer.accumulate_tagged_material(material_ix, tag); 114 | } else { 115 | return Err(format!("Huh? Non-existent meld key: {}", material_key)); 116 | } 117 | } 118 | 119 | // now handle the primitive's default material, if any 120 | if let Some(default_material_ix) = primitive.material { 121 | let default_material_ix = default_material_ix.value(); 122 | let is_variational = !tag_to_ix.is_empty(); 123 | 124 | image_sizer.accumulate_material(default_material_ix, is_variational); 125 | image_sizer.accumulate_tagged_material(default_material_ix, &self.default_tag); 126 | 127 | if is_variational { 128 | // only map the default tag if there's other tags already in the mapping 129 | tag_to_ix.insert(self.default_tag.clone(), default_material_ix); 130 | } 131 | }; 132 | 133 | extension::write_variant_map(primitive, &tag_to_ix, &variant_ix_lookup)?; 134 | } 135 | } 136 | 137 | // ask metadata sizer to count up all the totals 138 | let (total_image_size, variational_image_size, per_tag_image_size) = image_sizer.count()?; 139 | // use it to create an authoritative set of all variational tags 140 | let tags: HashSet = per_tag_image_size.keys().cloned().collect(); 141 | 142 | // use it also to create the Tag->AssetSize mapping 143 | let per_tag_sizes: HashMap = tags 144 | .iter() 145 | .map(|tag| (tag.to_owned(), AssetSizes::new(per_tag_image_size[tag]))) 146 | .collect(); 147 | 148 | // finally construct & return the Metadata structure 149 | Ok(Metadata { 150 | tags, 151 | total_sizes: AssetSizes { 152 | texture_bytes: total_image_size, 153 | }, 154 | variational_sizes: AssetSizes { 155 | texture_bytes: variational_image_size, 156 | }, 157 | per_tag_sizes, 158 | }) 159 | } 160 | 161 | // given a `Root` and a binary blob, create an actual GLB file 162 | fn build_glb_for_export(&self, export_parse: Root, export_blob: &[u8]) -> Result> { 163 | let json = export_parse.to_string_pretty(); 164 | let json = json 165 | .map(|s| s.into_bytes()) 166 | .map_err(|e| format!("JSON deserialisation error: {}", e))?; 167 | 168 | let json_chunk = GlbChunk::JSON(&json); 169 | let bin_chunk = if !export_blob.is_empty() { 170 | Some(GlbChunk::BIN(export_blob)) 171 | } else { 172 | None 173 | }; 174 | 175 | Ok(GlbChunk::to_bytes(json_chunk, bin_chunk)?) 176 | } 177 | } 178 | 179 | struct ImageSizes<'a> { 180 | asset: &'a WorkAsset, 181 | all_images: HashSet, 182 | variational_images: HashSet, 183 | per_tag_images: HashMap>, 184 | } 185 | 186 | impl<'a> ImageSizes<'a> { 187 | fn new(asset: &'a WorkAsset) -> ImageSizes { 188 | ImageSizes { 189 | asset: asset, 190 | all_images: HashSet::new(), 191 | variational_images: HashSet::new(), 192 | per_tag_images: HashMap::new(), 193 | } 194 | } 195 | fn accumulate_material(&mut self, ix: usize, is_variational: bool) { 196 | let materials = self.asset.materials(); 197 | accumulate_material_into_set(&materials[ix], &mut self.all_images); 198 | if is_variational { 199 | accumulate_material_into_set(&materials[ix], &mut self.variational_images); 200 | } 201 | } 202 | 203 | fn accumulate_tagged_material(&mut self, ix: usize, tag: &Tag) { 204 | let materials = self.asset.materials(); 205 | let image_set = self 206 | .per_tag_images 207 | .entry(tag.to_owned()) 208 | .or_insert(HashSet::new()); 209 | accumulate_material_into_set(&materials[ix], image_set); 210 | } 211 | 212 | fn count(&self) -> Result<(usize, usize, HashMap)> { 213 | let mut all = 0; 214 | let mut variational = 0; 215 | let mut size_map = HashMap::new(); 216 | 217 | for image_ix in &self.all_images { 218 | let size = image_size(&self.asset, *image_ix)?; 219 | size_map.insert(image_ix, size); 220 | 221 | all += size; 222 | if self.variational_images.contains(&image_ix) { 223 | variational += size; 224 | } 225 | } 226 | 227 | let tagged = { 228 | let mut result = HashMap::new(); 229 | for (tag, image_ix_set) in &self.per_tag_images { 230 | result.insert(tag.clone(), { 231 | let mut sum = 0; 232 | for image_ix in image_ix_set { 233 | sum += size_map.get(image_ix).ok_or_else(|| { 234 | format!("Tag {} references unknown image ix {}!?", tag, image_ix) 235 | })?; 236 | } 237 | sum 238 | }); 239 | } 240 | result 241 | }; 242 | 243 | Ok((all, variational, tagged)) 244 | } 245 | } 246 | 247 | fn image_size(asset: &WorkAsset, image_ix: usize) -> Result { 248 | Ok(asset.read_image_bytes(&asset.images()[image_ix])?.len()) 249 | } 250 | 251 | fn accumulate_material_into_set(material: &Material, image_set: &mut HashSet) { 252 | let pbr = &material.pbr_metallic_roughness; 253 | if let Some(ref tex_info) = pbr.base_color_texture { 254 | image_set.insert(tex_info.index.value()); 255 | } 256 | if let Some(ref tex_info) = pbr.metallic_roughness_texture { 257 | image_set.insert(tex_info.index.value()); 258 | } 259 | if let Some(ref tex_info) = material.normal_texture { 260 | image_set.insert(tex_info.index.value()); 261 | } 262 | if let Some(ref tex_info) = material.occlusion_texture { 263 | image_set.insert(tex_info.index.value()); 264 | } 265 | if let Some(ref tex_info) = material.emissive_texture { 266 | image_set.insert(tex_info.index.value()); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /native/src/work_asset/meld.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | //! Core asset meld functionality. 5 | //! 6 | //! This module implements the core functionality of the whole library: iterating over the meshes of 7 | //! one asset, locating its equivalent in another asset, and melding together the tagged material 8 | //! uses of the two. 9 | 10 | use spectral::prelude::*; 11 | 12 | use gltf::json::{buffer::View, texture::Sampler, Image, Index, Material, Texture}; 13 | 14 | use crate::{Result, WorkAsset}; 15 | 16 | impl<'a> WorkAsset { 17 | /// Meld `WorkAsset` *other* into `WorkAsset` *base*, returning the result. 18 | /// 19 | /// We begin by cloning *base* then we selectively meld in glTF objects from *other*. Because 20 | /// glTF objects depend on one another recursively, melding a material will typically require 21 | /// melding textures, which requires melding images sources, and so on. For each such meld, the 22 | /// object may already exist in *base*, in which case we return its existing index reference, or 23 | /// it may be new, in which case we copy it over and return the newly created index. 24 | pub fn meld(base: &'a WorkAsset, other: &'a WorkAsset) -> Result { 25 | let mut result = base.clone(); 26 | for (other_mesh_ix, other_mesh_key) in other.mesh_keys.iter().enumerate() { 27 | if let Some(base_mesh_ix) = base.mesh_ix(&other_mesh_key) { 28 | let base_primitives = &base.meshes()[base_mesh_ix].primitives; 29 | let other_primitives = &other.meshes()[other_mesh_ix].primitives; 30 | assert_that!(base_primitives.len()).is_equal_to(other_primitives.len()); 31 | 32 | for primitive_ix in 0..other_primitives.len() { 33 | let mut base_map = base.variant_mapping(base_mesh_ix, primitive_ix).clone(); 34 | let base_primitive = &base_primitives[primitive_ix]; 35 | if let Some(base_material) = base_primitive.material { 36 | if !base_map.contains_key(&base.default_tag) { 37 | base_map.insert( 38 | base.default_tag.clone(), 39 | base.material_keys[base_material.value()].to_owned(), 40 | ); 41 | } 42 | } 43 | 44 | let mut other_map = other.variant_mapping(other_mesh_ix, primitive_ix).clone(); 45 | 46 | let base_print = base.mesh_primitive_fingerprints[base_mesh_ix][primitive_ix]; 47 | let other_primitive_ix = other 48 | .find_almost_equal_fingerprint(other_mesh_ix, &base_print, None) 49 | .ok_or(format!( 50 | "Melded asset has no equivalent to base mesh {}, primitive {}.", 51 | base_mesh_ix, primitive_ix 52 | ))?; 53 | if let Some(other_material) = other_primitives[other_primitive_ix].material { 54 | if !other_map.contains_key(&other.default_tag) { 55 | other_map.insert( 56 | other.default_tag.clone(), 57 | other.material_keys[other_material.value()].to_owned(), 58 | ); 59 | } 60 | } 61 | 62 | let mut result_map = base_map.clone(); 63 | 64 | for other_tag in other_map.keys() { 65 | if base_map.contains_key(other_tag) { 66 | if base_map[other_tag] != other_map[other_tag] { 67 | return Err(format!( 68 | "Base[{}/{}] vs Foreign[{}/{}]: Tag {} material mismatch!", 69 | base_mesh_ix, 70 | primitive_ix, 71 | other_mesh_ix, 72 | primitive_ix, 73 | other_tag, 74 | )); 75 | } 76 | continue; 77 | } 78 | let other_material_key = &other_map[other_tag]; 79 | 80 | if let Some(other_material_ix) = other.material_ix(&other_material_key) { 81 | let _new_material_ix = meld_in_material( 82 | &mut result, 83 | other, 84 | Index::new(other_material_ix as u32), 85 | ); 86 | result_map.insert(other_tag.clone(), other_material_key.clone()); 87 | } else { 88 | return Err(format!( 89 | "Other[{}/{}]: Material key {} not found!", 90 | other_mesh_ix, primitive_ix, other_material_key 91 | )); 92 | } 93 | } 94 | result.mesh_primitive_variants[base_mesh_ix][primitive_ix] = result_map; 95 | } 96 | } else { 97 | return Err(format!( 98 | "meldd mesh #{} has no corresponding mesh in base!", 99 | other_mesh_ix 100 | )); 101 | } 102 | } 103 | Ok(result) 104 | } 105 | } 106 | 107 | // Note: the methods below are all on a very similar structure, and could be abstracted using e.g. 108 | // macros, but in our experiments we didn't get much more readability, and the complexity increases 109 | // quite a bit. We'll stick with a bit of copy-and-paste boilerplate for now. 110 | 111 | /// Meld a glTF `image` (i.e. texture source) from from *other* into *base*. 112 | fn meld_in_image(base: &mut WorkAsset, other: &WorkAsset, other_ix: Index) -> Index { 113 | let other_ix = other_ix.value(); 114 | let key = &other.image_keys[other_ix]; 115 | if let Some(ix) = base.image_ix(key) { 116 | return Index::new(ix as u32); 117 | } 118 | let mut new_object = other.images()[other_ix].clone(); 119 | 120 | // meld logic 121 | assert_that!(new_object.buffer_view).is_some(); 122 | new_object.buffer_view = Some(copy_byte_view(base, other, new_object.buffer_view.unwrap())); 123 | // end meld logic 124 | 125 | Index::new(base.push_image(new_object, key) as u32) 126 | } 127 | 128 | /// Meld a glTF `sampler` (texture filter/wrap configuration) from *other* into *base*. 129 | fn meld_in_sampler( 130 | base: &mut WorkAsset, 131 | other: &WorkAsset, 132 | other_ix: Index, 133 | ) -> Index { 134 | let other_ix = other_ix.value(); 135 | let key = &other.sampler_keys()[other_ix]; 136 | if let Some(ix) = base.sampler_ix(key) { 137 | return Index::new(ix as u32); 138 | } 139 | let new_object = other.samplers()[other_ix].clone(); 140 | 141 | // no current meld logic needed 142 | 143 | Index::new(base.push_sampler(new_object, key) as u32) 144 | } 145 | 146 | /// Meld a glTF `texture` (consisting of a `sampler` and an `image`) from *other* into *base*. 147 | fn meld_in_texture( 148 | base: &mut WorkAsset, 149 | other: &WorkAsset, 150 | other_ix: Index, 151 | ) -> Index { 152 | let other_ix = other_ix.value(); 153 | let key = &other.texture_keys()[other_ix]; 154 | if let Some(ix) = base.texture_ix(key) { 155 | return Index::new(ix as u32); 156 | } 157 | let mut new_object = other.textures()[other_ix].clone(); 158 | 159 | // meld logic 160 | new_object.source = meld_in_image(base, other, new_object.source); 161 | new_object.sampler = new_object.sampler.map(|s| meld_in_sampler(base, other, s)); 162 | // end meld logic 163 | 164 | Index::new(base.push_texture(new_object, key) as u32) 165 | } 166 | 167 | /// Meld a glTF `material` from *other* into *base*. 168 | /// 169 | /// For non-trivial materials, this typically requires melding in textures as well. 170 | fn meld_in_material( 171 | base: &mut WorkAsset, 172 | other: &WorkAsset, 173 | other_ix: Index, 174 | ) -> Index { 175 | let other_ix = other_ix.value(); 176 | let key = &other.material_keys[other_ix]; 177 | if let Some(ix) = base.material_ix(key) { 178 | return Index::new(ix as u32); 179 | } 180 | let mut new_object = other.materials()[other_ix].clone(); 181 | 182 | // laboriously hand-meld the five relevant textures 183 | if let Some(mut info) = new_object.normal_texture { 184 | info.index = meld_in_texture(base, other, info.index); 185 | new_object.normal_texture = Some(info); 186 | } 187 | if let Some(mut info) = new_object.occlusion_texture { 188 | info.index = meld_in_texture(base, other, info.index); 189 | new_object.occlusion_texture = Some(info); 190 | } 191 | if let Some(mut info) = new_object.emissive_texture { 192 | info.index = meld_in_texture(base, other, info.index); 193 | new_object.emissive_texture = Some(info); 194 | } 195 | if let Some(mut info) = new_object.pbr_metallic_roughness.base_color_texture { 196 | info.index = meld_in_texture(base, other, info.index); 197 | new_object.pbr_metallic_roughness.base_color_texture = Some(info); 198 | } 199 | if let Some(mut info) = new_object.pbr_metallic_roughness.metallic_roughness_texture { 200 | info.index = meld_in_texture(base, other, info.index); 201 | new_object.pbr_metallic_roughness.metallic_roughness_texture = Some(info); 202 | } 203 | // end meld logic 204 | 205 | Index::new(base.push_material(new_object, key) as u32) 206 | } 207 | 208 | fn copy_byte_view( 209 | base: &mut WorkAsset, 210 | foreign: &WorkAsset, 211 | foreign_ix: Index, 212 | ) -> Index { 213 | let view = foreign.buffer_view(foreign_ix.value()); 214 | let slice = foreign.buffer_view_as_slice(&view); 215 | let new_ix = base.push_buffer_view_from_slice(slice) as u32; 216 | Index::new(new_ix) 217 | } 218 | -------------------------------------------------------------------------------- /native/src/work_asset/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | use std::collections::HashMap; 5 | 6 | use gltf::json::{buffer::View, Image, Material, Mesh, Root}; 7 | use gltf::json::{texture::Sampler, Texture}; 8 | 9 | use crate::{Fingerprint, MeldKey, Result, Tag}; 10 | 11 | use crate::gltfext::add_buffer_view_from_slice; 12 | 13 | pub mod construct; 14 | 15 | pub mod export; 16 | 17 | pub mod meld; 18 | 19 | const EPS_FINGERPRINT: f64 = 1e-6; 20 | 21 | /// The primary internal data structure, which enables and accelerates the melding operation. 22 | /// 23 | /// The first half of the asset constitutes all the data needed to export fully variational glTF: 24 | /// the source document and blob, a default tag, and the per-mesh, per-primitive mapping of tags to 25 | /// materials. 26 | /// 27 | /// The second half are meld keys for various glTF objects, which are used heavily in the melding 28 | /// process. 29 | /// 30 | #[derive(Clone, Debug)] 31 | pub struct WorkAsset { 32 | /// The parsed JSON of the underlying asset. 33 | parse: Root, 34 | 35 | /// The binary content of the asset; textures, geometry, animation data, ... 36 | blob: Vec, 37 | 38 | /// The tag used to represent vanilla glTF's material references during a meld operation. 39 | /// 40 | /// See crate-level documentation for more exhaustive information on how this happens. 41 | default_tag: Tag, 42 | 43 | /// A glTF asset's geometry is laid out in a vector of meshes, each of which consists of a 44 | /// vector of mesh primitives. For each mesh primitive, the variational extension adds a 45 | /// mapping of variant tag -> material references. That data is stored in this field, and 46 | /// gets used during melding & during export. 47 | mesh_primitive_variants: Vec>>, 48 | 49 | /// A `MeldKey` for each `Image`; essentially a hash of the binary contents. 50 | image_keys: Vec, 51 | /// A `MeldKey` for each `Material`; a straight-forward string expansion of its state. 52 | material_keys: Vec, 53 | /// A `MeldKey` for each `Mesh`; a straight-forward string expansion of its state. 54 | mesh_keys: Vec, 55 | /// A `MeldKey` for each `Sampler`; a straight-forward string expansion of its state. 56 | sampler_keys: Vec, 57 | /// A `MeldKey` for each `Texture`; a straight-forward string expansion of its state. 58 | texture_keys: Vec, 59 | 60 | /// Each `Primitive` of each `Mesh` has a `Fingerprint` computed for it, and they are 61 | /// stored herein. 62 | mesh_primitive_fingerprints: Vec>, 63 | } 64 | 65 | impl WorkAsset { 66 | /// A slice view of the entire binary blob. 67 | pub fn blob_slice(&self) -> &[u8] { 68 | &self.blob.as_slice() 69 | } 70 | 71 | /// Returns a vector of tags being used throughout the entire asset. 72 | pub fn get_tags_in_use(&self) -> Result> { 73 | let mut tags_in_use: Vec = Vec::new(); 74 | for vec_of_prims in &self.mesh_primitive_variants { 75 | for tag_meld_entry in vec_of_prims { 76 | for tag in tag_meld_entry.keys() { 77 | if !tags_in_use.contains(tag) { 78 | tags_in_use.push(tag.clone()); 79 | } 80 | } 81 | } 82 | }; 83 | Ok(tags_in_use) 84 | } 85 | 86 | /// The mapping of `Tag` to material `MeldKey` for a given primitive of a given mesh. 87 | pub fn variant_mapping(&self, m_ix: usize, p_ix: usize) -> &HashMap { 88 | let mesh_mappings = &self.mesh_primitive_variants[m_ix]; 89 | let primitive_mapping = &mesh_mappings[p_ix]; 90 | primitive_mapping 91 | } 92 | 93 | /// The slice of bytes that constitute the raw data of a given `Image`. 94 | pub fn read_image_bytes(&self, image: &Image) -> Result<&[u8]> { 95 | if let Some(view) = image.buffer_view { 96 | if let Some(view) = self.parse.get(view) { 97 | let offset = view.byte_offset.unwrap_or(0) as usize; 98 | let length = view.byte_length as usize; 99 | return Ok(&self.blob_slice()[offset..offset + length]); 100 | } 101 | } 102 | Err(format!("Internal error: Image with a URI field?!")) 103 | } 104 | 105 | /// A `View` representing the ix:th buffer view of the underlying asset. 106 | pub fn buffer_view(&self, ix: usize) -> &View { 107 | &self.parse.buffer_views[ix] 108 | } 109 | 110 | /// The slice of bytes underlying the given buffer view. 111 | pub fn buffer_view_as_slice(&self, view: &View) -> &[u8] { 112 | let start = view.byte_offset.unwrap_or(0) as usize; 113 | let end = start + view.byte_length as usize; 114 | &self.blob[start..end] 115 | } 116 | 117 | /// Clone our JSON data and blob, and create a `Gltf` wrapper around it. 118 | pub fn to_owned_gltf(&self) -> gltf::Gltf { 119 | gltf::Gltf { 120 | document: gltf::Document::from_json_without_validation(self.parse.clone()), 121 | blob: if self.blob.is_empty() { 122 | None 123 | } else { 124 | Some(self.blob.clone()) 125 | }, 126 | } 127 | } 128 | 129 | /// Search the `Primitives` of a `Mesh` non-exactly for a specific `Fingerprint`. 130 | pub fn find_almost_equal_fingerprint( 131 | &self, 132 | mesh_ix: usize, 133 | print: &Fingerprint, 134 | exclude_ix: Option, 135 | ) -> Option { 136 | let prints = &self.mesh_primitive_fingerprints[mesh_ix]; 137 | for (primitive_ix, primitive_print) in prints.iter().enumerate() { 138 | if let Some(exclude_ix) = exclude_ix { 139 | if exclude_ix == primitive_ix { 140 | continue; 141 | } 142 | } 143 | if (primitive_print - print).abs() < EPS_FINGERPRINT { 144 | return Some(primitive_ix); 145 | } 146 | } 147 | return None; 148 | } 149 | 150 | /// Adds a new buffer view to the asset, returning its index. 151 | pub fn push_buffer_view_from_slice(&mut self, bytes: &[u8]) -> usize { 152 | add_buffer_view_from_slice(bytes, &mut self.parse.buffer_views, &mut self.blob).value() 153 | } 154 | } 155 | 156 | /// Provide accessors and mutators for images, materials, meshes, samplers and textures: 157 | macro_rules! impl_accessors_and_mutators { 158 | ($ty:ty, $name:expr, $objects:ident, $keys:ident, $index_of_keys:ident, $push:ident) => { 159 | #[doc = " This asset's `"] 160 | #[doc = $name] 161 | #[doc = "` glTF objects."] 162 | pub fn $objects(&self) -> &Vec<$ty> { 163 | &self.parse.$objects 164 | } 165 | #[doc = " This asset's `"] 166 | #[doc = $name] 167 | #[doc = "` keys."] 168 | pub fn $keys(&self) -> &Vec { 169 | &self.$keys 170 | } 171 | #[doc = " The index of the given `"] 172 | #[doc = $name] 173 | #[doc = "` key, if any."] 174 | pub fn $index_of_keys(&self, key: &MeldKey) -> Option { 175 | self.$keys().iter().position(|k| k == key) 176 | } 177 | #[doc = " Add a new `"] 178 | #[doc = $name] 179 | #[doc = "` glTF object, along with its computed key."] 180 | pub fn $push(&mut self, item: $ty, key: &MeldKey) -> usize 181 | where 182 | $ty: Clone, 183 | { 184 | let new_ix = self.parse.$objects.len(); 185 | self.parse.$objects.push(item.clone()); 186 | self.$keys.push(key.clone()); 187 | new_ix 188 | } 189 | }; 190 | 191 | ($ty:ty, $objects:ident, $keys:ident, $index_of_keys:ident, $push:ident) => { 192 | impl_accessors_and_mutators!($ty, stringify!($ty), $objects, $keys, $index_of_keys, $push); 193 | }; 194 | } 195 | 196 | impl WorkAsset { 197 | impl_accessors_and_mutators!(Image, images, image_keys, image_ix, push_image); 198 | impl_accessors_and_mutators!( 199 | Material, 200 | materials, 201 | material_keys, 202 | material_ix, 203 | push_material 204 | ); 205 | impl_accessors_and_mutators!(Mesh, meshes, mesh_keys, mesh_ix, push_mesh); 206 | impl_accessors_and_mutators!(Sampler, samplers, sampler_keys, sampler_ix, push_sampler); 207 | impl_accessors_and_mutators!(Texture, textures, texture_keys, texture_ix, push_texture); 208 | } 209 | -------------------------------------------------------------------------------- /native/tests/basic_parse_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | extern crate assets; 5 | extern crate gltf_variant_meld; 6 | 7 | use std::iter::FromIterator; 8 | 9 | use spectral::prelude::*; 10 | 11 | use assets::*; 12 | 13 | use gltf::Gltf; 14 | 15 | use gltf_variant_meld::{Tag, VariationalAsset}; 16 | 17 | #[test] 18 | fn test_tiny_parse() { 19 | let json = r#" 20 | { 21 | "asset": { 22 | "version": "2.0" 23 | } 24 | } 25 | "#; 26 | let asset = VariationalAsset::from_slice(json.as_bytes(), Some(&Tag::from("tag")), None) 27 | .expect("glTF parse failure"); 28 | 29 | let asset = Gltf::from_slice(asset.glb()) 30 | .or_else(|e| Err(e.to_string())) 31 | .expect("glTF re-parse failure"); 32 | 33 | assert_that!(Vec::from_iter(asset.accessors())).has_length(0); 34 | assert_that!(Vec::from_iter(asset.extensions_used())).has_length(1); 35 | 36 | println!("JSON as parsed: {:#?}", asset.document.clone().into_json()); 37 | } 38 | 39 | #[test] 40 | fn test_larger_parse() { 41 | let asset = VariationalAsset::from_file(ASSET_PINECONE_SHINY(), Some(&Tag::from("tag"))) 42 | .expect("glTF import failure"); 43 | let asset = &Gltf::from_slice(asset.glb()) 44 | .or_else(|e| Err(e.to_string())) 45 | .expect("glTF re-parse failure"); 46 | 47 | assert_that!(Vec::from_iter(asset.accessors())).has_length(5); 48 | } 49 | 50 | #[test] 51 | fn test_pinecone_comparison() { 52 | let tests = vec![ 53 | (ASSET_PINECONE_MATTE(), Tag::from("matte"), 0.2, 0.8), 54 | (ASSET_PINECONE_SHINY(), Tag::from("shiny"), 0.8, 0.2), 55 | ]; 56 | for test in tests { 57 | let pinecone = 58 | VariationalAsset::from_file(test.0, Some(&test.1)).expect("glTF import failure"); 59 | 60 | let pinecone = &Gltf::from_slice(pinecone.glb()) 61 | .or_else(|e| Err(e.to_string())) 62 | .expect("glTF re-parse failure"); 63 | 64 | assert_that!(Vec::from_iter(pinecone.accessors())).has_length(5); 65 | 66 | let pbr = pinecone 67 | .materials() 68 | .nth(0) 69 | .unwrap() 70 | .pbr_metallic_roughness(); 71 | assert_that!(pbr.metallic_factor()).is_equal_to(test.2); 72 | assert_that!(pbr.roughness_factor()).is_equal_to(test.3); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /native/tests/meld_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | extern crate assets; 5 | extern crate gltf_variant_meld; 6 | 7 | use spectral::prelude::*; 8 | 9 | use assets::*; 10 | 11 | use gltf_variant_meld::{Tag, VariationalAsset}; 12 | 13 | #[test] 14 | fn test_pinecone_meld() { 15 | let (shiny, matte, tinted) = (Tag::from("shiny"), Tag::from("matte"), Tag::from("tinted")); 16 | 17 | let load_asset = |path, tag| { 18 | VariationalAsset::from_file(path, Some(tag)).expect("VariationalAsset::from_file() failure") 19 | }; 20 | let meld_assets = |base, other| { 21 | VariationalAsset::meld(base, other).expect("VariationalAsset::meld() failure") 22 | }; 23 | 24 | // helper lambdas 25 | let test = |asset: &VariationalAsset, default_tag, tags: Vec<&Tag>| { 26 | assert_that!(asset.default_tag()).is_equal_to(default_tag); 27 | assert_that!(asset.metadata().tags().iter()).contains_all_of(&tags); 28 | assert_that!(asset.metadata().tags().iter().count()).is_equal_to(tags.len()); 29 | }; 30 | 31 | let matte_pinecone = load_asset(ASSET_PINECONE_MATTE(), &matte); 32 | let shiny_pinecone = load_asset(ASSET_PINECONE_SHINY(), &shiny); 33 | let matte_shiny_pinecone = meld_assets(&matte_pinecone, &shiny_pinecone); 34 | test(&matte_shiny_pinecone, &matte, vec![&matte, &shiny]); 35 | 36 | let tinted_pinecone = load_asset(ASSET_PINECONE_TINTED(), &tinted); 37 | let matte_shiny_tinted = meld_assets(&matte_shiny_pinecone, &tinted_pinecone); 38 | test(&matte_shiny_tinted, &matte, vec![&matte, &shiny, &tinted]); 39 | 40 | let tinted_matte_shiny = meld_assets(&tinted_pinecone, &matte_shiny_pinecone); 41 | test(&tinted_matte_shiny, &tinted, vec![&matte, &shiny, &tinted]); 42 | } 43 | 44 | #[test] 45 | fn test_teapot_meld() { 46 | let (camo_pink_bronze, camo_pink_silver, green_pink_bronze, green_pink_silver) = ( 47 | Tag::from("camo_pink_bronze"), 48 | Tag::from("camo_pink_silver"), 49 | Tag::from("green_pink_bronze"), 50 | Tag::from("green_pink_silver"), 51 | ); 52 | 53 | // helper lambdas 54 | let load_and_test = |path, tag, ts| { 55 | let asset = VariationalAsset::from_file(path, Some(tag)).expect("glTF import failure"); 56 | 57 | assert_that!(asset.metadata().total_sizes().texture_bytes()).is_equal_to(ts); 58 | assert_that!(asset.metadata().variational_sizes().texture_bytes()).is_equal_to(0); 59 | return asset; 60 | }; 61 | 62 | let meld_and_test = |base, meld, ts| { 63 | let melded = VariationalAsset::meld(base, meld).expect("VariationalAsset::meld() failure"); 64 | let metadata = melded.metadata(); 65 | assert_that!(metadata.total_sizes().texture_bytes()).is_equal_to(ts); 66 | assert_that!(metadata.variational_sizes().texture_bytes()).is_equal_to(ts); 67 | return melded; 68 | }; 69 | 70 | let test_tag = |asset: &VariationalAsset, tag, ts| { 71 | assert_that!(asset.metadata().tag_sizes(tag).unwrap().texture_bytes()).is_equal_to(ts) 72 | }; 73 | 74 | // actual test logic begins here 75 | let base_pot = load_and_test(ASSET_TEAPOT_CAMO_PINK_BRONZE(), &camo_pink_bronze, 227318); 76 | let meld_pot = load_and_test(ASSET_TEAPOT_CAMO_PINK_SILVER(), &camo_pink_silver, 227318); 77 | 78 | let melded = meld_and_test(&base_pot, &meld_pot, 227318); 79 | test_tag(&melded, &camo_pink_bronze, 227318); 80 | test_tag(&melded, &camo_pink_silver, 227318); 81 | 82 | // add in a pot with the green texture 83 | let base_pot = melded; 84 | let meld_pot = load_and_test(ASSET_TEAPOT_GREEN_PINK_SILVER(), &green_pink_silver, 337020); 85 | let melded = meld_and_test(&base_pot, &meld_pot, 564338); 86 | test_tag(&melded, &camo_pink_bronze, 227318); 87 | test_tag(&melded, &camo_pink_silver, 227318); 88 | test_tag(&melded, &green_pink_silver, 337020); 89 | 90 | // finally a fourth variant that should add no new texture 91 | let base_pot = melded; 92 | let meld_pot = load_and_test(ASSET_TEAPOT_GREEN_PINK_BRONZE(), &green_pink_bronze, 337020); 93 | let melded = meld_and_test(&base_pot, &meld_pot, 564338); 94 | test_tag(&melded, &camo_pink_bronze, 227318); 95 | test_tag(&melded, &camo_pink_silver, 227318); 96 | test_tag(&melded, &green_pink_silver, 337020); 97 | test_tag(&melded, &green_pink_bronze, 337020); 98 | } 99 | -------------------------------------------------------------------------------- /native/tests/variational_asset_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | extern crate assets; 5 | extern crate gltf_variant_meld; 6 | 7 | use std::collections::{HashMap}; 8 | 9 | use spectral::prelude::*; 10 | 11 | use gltf_variant_meld::{Tag, VariationalAsset, WorkAsset}; 12 | 13 | use assets::*; 14 | 15 | #[test] 16 | fn test_parse_simple_variational() { 17 | let (tag_1, tag_2) = ( 18 | Tag::from("tag_1"), 19 | Tag::from("tag_2"), 20 | ); 21 | 22 | let mut variant_ix_lookup = HashMap::new(); 23 | variant_ix_lookup.insert(0, tag_1.to_owned()); 24 | variant_ix_lookup.insert(1, tag_2.to_owned()); 25 | 26 | let asset_result = 27 | VariationalAsset::from_file(ASSET_PINECONE_VARIATIONAL(), Some(&tag_1)); 28 | assert_that!(asset_result).is_ok(); 29 | let asset = asset_result.unwrap(); 30 | let asset = WorkAsset::from_slice(asset.glb(), Some(&tag_2), None) 31 | .or_else(|e| Err(e.to_string())) 32 | .expect("glTF re-parse failure"); 33 | 34 | let mesh = asset.meshes().get(0).expect("No meshes!?"); 35 | let primitive = mesh 36 | .primitives 37 | .get(0) 38 | .expect("No primitives in first mesh!"); 39 | let extracted_map = gltf_variant_meld::extension::extract_variant_map(&primitive, &variant_ix_lookup) 40 | .expect("Failed to extract variant map from mesh primitive."); 41 | 42 | assert_that!(extracted_map).has_length(2); 43 | assert_that!(extracted_map.keys()).contains_all_of(&vec![&tag_1, &tag_2]); 44 | } 45 | -------------------------------------------------------------------------------- /variationator.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "editor.formatOnSave": true, 9 | "git.ignoreLimitWarning": true, 10 | "cSpell.enabled": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /web/cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /web/cli/build-node-app.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 4 | # 5 | 6 | 7 | NODE_VERSION="v11.15.0" 8 | NVMSH="${HOME}/.nvm/nvm.sh" 9 | 10 | test -f "${NVMSH}" || { 11 | printf "Please install NVM, the Node Version Manager. See README.md". 12 | exit 1 13 | } 14 | 15 | . "${NVMSH}" 16 | 17 | nvm use "${NODE_VERSION}" || { 18 | printf "Please run: nvm install \"${NODE_VERSION}\" --latest-npm" 19 | printf "(If you don't have nvm install, see README.md.)" 20 | exit 1 21 | } 22 | 23 | printf "\n" 24 | printf "\033[1;91m(Re)building with WebPack & TypeScript:\033[0m\n" 25 | printf "\033[1;91m---------------------------------------\033[0m\n" 26 | node node_modules/webpack-cli/bin/cli.js --target node 27 | chmod 755 dist/app.js 28 | 29 | printf "\n" 30 | printf "\033[1;91mRun the application with: dist/app.js\033[0m\n" 31 | printf "\033[1;91m---------------------------------------------------------------------------\033[0m\n" 32 | -------------------------------------------------------------------------------- /web/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "serve": "webpack-dev-server" 4 | }, 5 | "dependencies": { 6 | "@types/node": "^12.7.7", 7 | "glTFVariantMeld": "file:../wasmpkg" 8 | }, 9 | "devDependencies": { 10 | "ts-loader": "^6.0.4", 11 | "typescript": "^3.6.2", 12 | "webpack": "^4.42.1", 13 | "webpack-cli": "^3.1.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /web/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | import { glTFVariantMeld, runWithVariantMeld } from "./wasmpkg"; 5 | import { readFileSync, writeFileSync } from "fs"; 6 | import { basename } from "path"; 7 | import { VariationalAsset } from "../node_modules/glTFVariantMeld/gltf_variant_meld"; 8 | 9 | const GLB_MAGIC: number = 0x46546c67; 10 | 11 | runWithVariantMeld(start) 12 | .then(() => { 13 | // cool 14 | }) 15 | .catch(err => { 16 | console.error("Aborting due to error: " + err); 17 | }); 18 | 19 | function start(wasmpkg: glTFVariantMeld) { 20 | let args = process.argv.slice(2); 21 | if (args.length < 2) { 22 | console.error( 23 | "Usage: %s [:] [:] ", 24 | basename(process.argv[1]) 25 | ); 26 | process.exit(1); 27 | } 28 | 29 | let inputs: [string, string][] = []; 30 | for (let ix = 0; ix < args.length - 1; ix++) { 31 | inputs.push(parseInputArg(args[ix])); 32 | } 33 | let output = args[args.length - 1]; 34 | 35 | function parse_input(ix: number): VariationalAsset { 36 | let tag = inputs[ix][0]; 37 | let file = inputs[ix][1]; 38 | 39 | console.log("Parsing source asset: '" + file + "'..."); 40 | let bytes = readAndValidate(file) as Uint8Array; 41 | return wasmpkg.VariationalAsset.wasm_from_slice(bytes, tag); 42 | } 43 | 44 | let result = parse_input(0); 45 | console.log("Initial asset:"); 46 | describe_asset(result); 47 | 48 | for (let ix = 1; ix < inputs.length; ix++) { 49 | console.log(); 50 | result = wasmpkg.VariationalAsset.wasm_meld(result, parse_input(ix)); 51 | console.log("New melded result:"); 52 | describe_asset(result); 53 | } 54 | 55 | let output_glb = result.wasm_glb(); 56 | writeFileSync(output, output_glb); 57 | 58 | console.log("Success! %d bytes written to '%s'.", output_glb.length, output); 59 | } 60 | 61 | function describe_asset(asset: VariationalAsset) { 62 | console.log(" Total file size: " + size(asset.wasm_glb().length)); 63 | let total = asset.wasm_metadata().total_sizes().texture_bytes; 64 | let variational = asset.wasm_metadata().variational_sizes().texture_bytes; 65 | console.log(" Total texture data: " + size(total)); 66 | console.log(" Of which is depends on tag: " + size(variational)); 67 | } 68 | 69 | function size(byte_count: number): string { 70 | if (byte_count < 1000000) { 71 | return (byte_count / 1000).toFixed(1) + " kB"; 72 | } 73 | return (byte_count / 1000000).toFixed(1) + " MB"; 74 | } 75 | 76 | // A file input argument can either be : or plain , e.g. 77 | // matte:./models/matte.glb 78 | // ./models/matte.gplb 79 | function parseInputArg(arg: string): [string, string] { 80 | let ix = arg.indexOf(":"); 81 | if (ix < 0) { 82 | return [undefined, arg]; 83 | } 84 | return [arg.substring(0, ix), arg.substring(ix + 1)]; 85 | } 86 | 87 | // Synchronously read the contents of a file, and ascertain that it's a GLB file. 88 | function readAndValidate(file: string): Buffer { 89 | let contents = readFileSync(file); 90 | let first_word = contents.readUIntLE(0, 4); 91 | if (first_word === GLB_MAGIC) { 92 | return contents; 93 | } 94 | console.error("File %s is not a GLB file: starts with 0x%s.", file, first_word.toString(16)); 95 | process.exit(1); 96 | } 97 | -------------------------------------------------------------------------------- /web/cli/src/wasmpkg.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | import { VariationalAsset } from "../node_modules/glTFVariantMeld/gltf_variant_meld"; 5 | 6 | export type glTFVariantMeld = typeof import("../node_modules/glTFVariantMeld/gltf_variant_meld.js"); 7 | 8 | export async function runWithVariantMeld(callback: (v: glTFVariantMeld) => void) { 9 | let module = await import("../node_modules/glTFVariantMeld/gltf_variant_meld.js"); 10 | callback(module); 11 | } 12 | -------------------------------------------------------------------------------- /web/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "sourceMap": true, 5 | "noImplicitAny": true, 6 | "module": "esNext", 7 | "target": "es5", 8 | "allowJs": true, 9 | "lib": [ 10 | "es5", 11 | "es2015", 12 | "dom" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /web/cli/webpack.config.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved 2 | // 3 | 4 | const path = require("path"); 5 | const webpack = require("webpack"); 6 | 7 | module.exports = { 8 | entry: "./src/index.ts", 9 | devtool: "inline-source-map", 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.tsx?$/, 14 | use: "ts-loader", 15 | exclude: /node_modules/ 16 | } 17 | ] 18 | }, 19 | plugins: [ 20 | new webpack.BannerPlugin({ 21 | banner: "#!/usr/bin/env node", 22 | entryOnly: true, 23 | raw: true 24 | }) 25 | ], 26 | resolve: { 27 | extensions: [".tsx", ".ts", ".js"] 28 | }, 29 | output: { 30 | filename: "app.js", 31 | path: path.resolve(__dirname, "dist") 32 | }, 33 | mode: "development" 34 | }; 35 | --------------------------------------------------------------------------------