commit 2759560936a70899b53182279312fae525802f7f Author: 4hyhzi <2082529121@qq.com> Date: Wed Apr 29 10:20:59 2026 +0800 初始化 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..c3f7e9c --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +registry=https://npm.blendercg.art/ +always-auth=true +//npm.blendercg.art/:_authToken=MjVhMDVlN2ZiODUxMjFkNjhmYzVhZWFmOTZhNTA3Mzc6ZjkyNTYxN2RhM2QwOTc5NTFiMTU3MGEyM2U3Zjc0NzQ4YmUzYTJiOTc1Yzg0OWUwMzEzNjgyZDAwMjdlNDA2ZmZkYjJmODFhYWQ2MjVjYmFlZGJj \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3140c09 --- /dev/null +++ b/README.md @@ -0,0 +1,435 @@ +# compress-gltf-tools + +这是从当前仓库里提取出来的一个 Node 20+ 模型处理子项目,用来在 Node 环境里直接处理 glTF / GLB 资源。 + +目前支持: + +- glTF 模型压缩 +- 压缩纹理转换:ASTC / PVRTC / ETC / S3TC / fallback +- 资源转 Base64 +- glTF 打包成 GLB +- 一次输出多个纹理变体 + +## 安装 + +```bash +cd node-model-tools +npm install +``` + +## 构建 + +```bash +npm run build +``` + +## 基本用法 + +```ts +import { processModel } from './dist'; + +const result = await processModel('../demo/assets/building/task_building_6.gltf', { + rootDir: '..', + outDir: './output', + compress: { + enabled: true + }, + compressTextures: { + enabled: true, + quality: 'medium', + astc: { enabled: true }, + pvrtc: { enabled: true }, + fallback: { useRGBA4444: true, useRGB565: true } + }, + glb: { + enabled: false + }, + base64: { + enabled: false, + includeModel: false, + threshold: 1000 + } +}); + +console.log(result.entries); +console.log(result.assets); +console.log(result.uploadFiles); +``` + +## `processModel(inputPath, options)` 参数说明 + +### `inputPath` + +要处理的模型文件路径。 + +- 支持 `.gltf` +- 支持 `.glb` + +示例: + +```ts +'../demo/assets/building/task_building_6.gltf' +``` + +### `options` + +处理选项对象。 + +#### `rootDir` + +项目根目录,主要给内部处理中间目录和相对路径计算使用。 + +注意: + +- 它现在不会再影响最终输出目录结构 +- 它更像“当前项目根路径”的参考值 + +常见写法: + +```ts +rootDir: process.cwd() +``` + +#### `outDir` + +最终输出目录。 + +注意: + +- 这里就是最终输出位置 +- 不会再自动拼上输入模型原本的目录层级 + +例如输入: + +```txt +../demo/assets/building/task_building_6.gltf +``` + +如果设置: + +```ts +outDir: './output' +``` + +那么输出会直接写到: + +```txt +./output +``` + +## `compress` 模型压缩配置 + +用于控制模型几何压缩。 + +```ts +compress: { + enabled: true, + excludes: [], + quantization: {} +} +``` + +字段说明: + +- `enabled`:是否开启模型压缩 +- `excludes`:排除规则,命中的文件不做模型压缩 +- `quantization`:量化参数,传给模型压缩器 + +`quantization` 示例: + +```ts +quantization: { + POSITION: 13, + NORMAL: 8, + TEXCOORD: 10 +} +``` + +## `compressTextures` 纹理压缩配置 + +用于把纹理转换成压缩纹理格式,或者输出 fallback 版本。 + +```ts +compressTextures: { + enabled: true, + quality: 'medium', + excludes: [], + astc: { enabled: true }, + pvrtc: { enabled: true }, + etc: { enabled: false }, + s3tc: { enabled: false }, + fallback: { + useRGBA4444: true, + useRGB565: true + } +} +``` + +字段说明: + +- `enabled`:是否开启纹理压缩链路 +- `quality`:整体质量档位,可选 `high` / `medium` / `low` +- `excludes`:排除规则,命中的纹理不参与压缩 +- `astc`:ASTC 配置 +- `pvrtc`:PVRTC 配置 +- `etc`:ETC 配置 +- `s3tc`:S3TC 配置 +- `fallback`:fallback 配置 + +### `astc / pvrtc / etc / s3tc` 子字段 + +这些编码器配置结构基本一致: + +```ts +astc: { + enabled: true, + formatOpaque: 'ASTC_8x5', + formatTransparent: 'ASTC_6x6', + quality: 'astcmedium', + excludes: [] +} +``` + +字段说明: + +- `enabled`:是否启用该纹理格式输出 +- `formatOpaque`:不透明纹理使用的压缩格式 +- `formatTransparent`:透明纹理使用的压缩格式 +- `quality`:该编码器自己的质量参数 +- `excludes`:排除规则 + +### `fallback` 子字段 + +```ts +fallback: { + useRGBA4444: true, + useRGB565: true, + excludes: [] +} +``` + +字段说明: + +- `useRGBA4444`:透明图走 fallback 时是否标记为 `RGBA4444` +- `useRGB565`:不透明图走 fallback 时是否标记为 `RGB565` +- `excludes`:排除规则 + +补充说明: + +- 当前 fallback 分支沿用老项目逻辑 +- 它会输出 fallback 版本并写入 `Sein_textureImprove` 标记 +- 但不会真正把 PNG 重新编码成低位格式图片 + +## `glb` 配置 + +用于控制是否把 glTF 打包成 GLB。 + +```ts +glb: { + enabled: true, + excludes: [] +} +``` + +字段说明: + +- `enabled`:是否输出 GLB +- `excludes`:排除规则,命中的资源保持分离 + +## `base64` 配置 + +用于把小资源直接转成 Base64 内联。 + +```ts +base64: { + enabled: true, + threshold: 1000, + includeModel: false, + excludes: [] +} +``` + +字段说明: + +- `enabled`:是否开启 Base64 内联 +- `threshold`:小于这个字节数的资源才会转 Base64 +- `includeModel`:是否允许把 glTF / GLB 本体也转成 Base64 +- `excludes`:排除规则 + +## `processors` 配置 + +用于在写出资源前,自定义处理资源内容。 + +```ts +processors: [ + { + test: /\\.bin$/, + async process({ data, filePath }) { + return data; + } + } +] +``` + +字段说明: + +- `test`:匹配规则,命中的文件会进入这个处理器 +- `process`:处理函数,接收 `{ data, filePath }` + +其中: + +- `data`:当前资源的二进制内容,类型是 `Buffer` +- `filePath`:当前资源路径 + +返回值必须是: + +- `Promise` + +## 排除规则 `excludes` + +很多地方都支持 `excludes`。 + +它可以是: + +- `RegExp` +- `(filePath: string) => boolean` + +示例: + +```ts +excludes: [ + /normal/i, + filePath => filePath.endsWith('.mp3') +] +``` + +## 返回值说明 + +`processModel` 返回: + +```ts +{ + inputPath, + outDir, + entries, + assets, + uploadFiles +} +``` + +### `inputPath` + +本次处理的输入模型绝对路径。 + +### `outDir` + +本次实际使用的输出目录绝对路径。 + +### `entries` + +模型主输出列表。每个元素代表一个模型变体。 + +单个元素结构: + +```ts +{ + name, + variant, + type, + required, + url, + outputPath +} +``` + +字段说明: + +- `name`:模型名,不带扩展名 +- `variant`:变体名,例如 `normal` / `astc` / `fallback` +- `type`:输出类型,`gltf` 或 `glb` +- `required`:该变体依赖的纹理扩展 +- `url`:输出文件名或相对路径 +- `outputPath`:实际输出文件绝对路径 + +### `assets` + +附属资源输出列表,比如: + +- `.bin` +- `.png` +- `.ktx` +- 被内联成 Base64 的资源 + +单个元素结构: + +```ts +{ + sourcePath, + distPath, + outputPath, + url, + inlined +} +``` + +字段说明: + +- `sourcePath`:原始资源路径 +- `distPath`:相对输出路径 +- `outputPath`:实际输出文件绝对路径 +- `url`:相对输出路径,或者 Base64 字符串 +- `inlined`:是否被内联成 Base64 + +### `uploadFiles` + +专门整理好的“待上传文件数组”。 + +特点: + +- 只包含真实写到磁盘的文件 +- 不包含被 Base64 内联的资源 +- 适合你后续直接遍历上传 OSS + +单个元素结构: + +```ts +{ + sourcePath, + relativePath, + outputPath, + fileName, + mimeType, + size +} +``` + +字段说明: + +- `sourcePath`:原始来源文件路径 +- `relativePath`:输出相对路径,可直接作为上传 key 的基础值 +- `outputPath`:本地输出文件绝对路径 +- `fileName`:输出文件名 +- `mimeType`:文件 MIME 类型 +- `size`:文件大小,单位字节 + +示例: + +```ts +for (const file of result.uploadFiles) { + console.log(file.relativePath, file.outputPath); +} +``` + +## 处理后的 generator + +处理后的 glTF 资产会统一写入: + +```json +"generator": "XOSMO Toolkit" +``` + +## 额外说明 + +- 如果输入是 `.glb`,不会走 glTF 贴图引用改写流程 +- 如果同时开启纹理压缩,会按配置输出多个变体 +- 某些压缩纹理格式在磁盘上不一定比原图更小,但它们的目标主要是运行时显存和采样性能 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..47c943f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,289 @@ +{ + "name": "seinjs-node-model-tools", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "seinjs-node-model-tools", + "version": "0.1.0", + "dependencies": { + "amc": "^1.0.5", + "bluebird": "^3.7.2", + "cesium": "1.59.0", + "mime": "^2.6.0", + "seinjs-texture-compressor": "^1.0.3" + }, + "devDependencies": { + "@types/node": "^20.19.39", + "typescript": "^5.6.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/amc": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/amc/-/amc-1.0.6.tgz", + "integrity": "sha512-5FcQelou8+sTQDj54QfJPSVX4i6IlFrGQv+izQFUE0dOryxM6kK1vbZgYSZYwk/FumFhp+zGxD3mXfiAr6TmGg==", + "license": "MIT", + "dependencies": { + "args": "^5.0.1", + "chalk": "^2.4.2" + }, + "bin": { + "amc-glTF": "bin.js" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/args": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.3.tgz", + "integrity": "sha512-h6k/zfFgusnv3i5TU08KQkVKuCPBtL/PWQbWkHUxvJrZ2nAyeaUupneemcrgn1xmqxPQsPIzwkUhOpoqPDRZuA==", + "license": "MIT", + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cesium": { + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/cesium/-/cesium-1.59.0.tgz", + "integrity": "sha512-pW1X3d7ZsL8X++M6V+wwVoL+OPMp66SlRWYIZaZHKPkubkbnTFFgQktyxW061Vp5T+hxGtzVtE7ALmmO+1MC/Q==", + "license": "Apache-2.0", + "dependencies": { + "requirejs": "^2.3.2" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/image-size": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.7.5.tgz", + "integrity": "sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==", + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/requirejs": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.8.tgz", + "integrity": "sha512-7/cTSLOdYkNBNJcDMWf+luFvMriVm7eYxp4BcFCsAX0wF421Vyce5SXP17c+Jd5otXKGNehIonFlyQXSowL6Mw==", + "license": "MIT", + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/seinjs-texture-compressor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/seinjs-texture-compressor/-/seinjs-texture-compressor-1.0.3.tgz", + "integrity": "sha512-qPgi/aRkTjydc/e9dOezZJAAcC/5FCQTmyfqsUvFEDCnYu5OmLnLgsort/L6ldqiHO+P0tSJVGzoxI9LwNqJIA==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.10", + "image-size": "^0.7.4", + "systeminformation": "^4.16.0" + }, + "bin": { + "seinjs-texture-compressor": "bin/texture-compressor.js" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/systeminformation": { + "version": "4.34.23", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.34.23.tgz", + "integrity": "sha512-33+lQwlLxXoxy0o9WLOgw8OjbXeS3Jv+pSl+nxKc2AOClBI28HsdRPpH0u9Xa9OVjHLT9vonnOMw1ug7YXI0dA==", + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..65c3ee2 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "compress-gltf-tools", + "version": "0.1.0", + "private": true, + "description": "Node 20+ model processing toolkit extracted from seinjs-gltf-loader", + "main": "dist/index.js", + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsc -p tsconfig.json" + }, + "dependencies": { + "amc": "^1.0.5", + "bluebird": "^3.7.2", + "cesium": "1.59.0", + "mime": "^2.6.0", + "seinjs-texture-compressor": "^1.0.3" + }, + "devDependencies": { + "@types/node": "^20.19.39", + "typescript": "^5.6.3" + } +} diff --git a/src/compressTextures.ts b/src/compressTextures.ts new file mode 100644 index 0000000..dc46516 --- /dev/null +++ b/src/compressTextures.ts @@ -0,0 +1,151 @@ +import path from 'path'; + +import {pack} from 'seinjs-texture-compressor'; + +import type {TextureCompressionOptions, TextureVariant} from './options'; +import {checkFileWithRules, getAssetType} from './utils'; + +type GltfImage = { + uri: string; + extras?: { + type?: string; + format?: string; + isNormalMap?: boolean; + useMipmaps?: boolean; + }; +}; + +export async function compressTextures( + gltf: any, + baseDir: string, + variant: TextureVariant, + options: TextureCompressionOptions +) { + if (!gltf.images || gltf.images.length === 0) { + return gltf; + } + + const images = gltf.images as GltfImage[]; + + for (let index = 0; index < images.length; index += 1) { + const image = images[index]; + if (checkFileWithRules(image.uri, options.excludes)) { + continue; + } + + if (getAssetType(image.uri) !== 'relative') { + continue; + } + + const ext = path.extname(image.uri); + let isTransparent = ext === '.png'; + let isNormalMap = false; + let useMipmaps = true; + + if (image.extras) { + isNormalMap = image.extras.isNormalMap || false; + useMipmaps = image.extras.useMipmaps === false ? false : true; + + if (image.extras.type === 'HDR') { + if (image.extras.format !== 'RGBD' && ext === '.exr') { + isTransparent = true; + } + } else if (image.extras.format !== 'RGB' && ext === '.png') { + isTransparent = true; + } + } + + if (variant.name === 'fallback') { + const fallback = options.fallback; + if (checkFileWithRules(image.uri, fallback.excludes || [])) { + continue; + } + + if (isNormalMap) { + continue; + } + + const fallbackType = isTransparent + ? (fallback.useRGBA4444 ? 'RGBA4444' : null) + : (fallback.useRGB565 ? 'RGB565' : null); + + if (!fallbackType) { + continue; + } + + (gltf.textures || []).forEach((texture: any) => { + if (texture.source === index) { + texture.extensions = texture.extensions || {}; + texture.extensions.Sein_textureImprove = texture.extensions.Sein_textureImprove || {}; + texture.extensions.Sein_textureImprove.textureType = fallbackType === 'RGBA4444' ? 32819 : 33635; + } + }); + + continue; + } + + const codecOptions = options[variant.name]; + if (!codecOptions || !codecOptions.enabled) { + continue; + } + + if (checkFileWithRules(image.uri, codecOptions.excludes || [])) { + continue; + } + + let compression: string | null = null; + let quality: string | null = null; + + if (variant.name === 'astc') { + if (options.quality === 'high') { + compression = (isTransparent || isNormalMap) ? codecOptions.formatTransparent || 'ASTC_4x4' : codecOptions.formatOpaque || 'ASTC_6x6'; + quality = codecOptions.quality || 'astcthorough'; + } else if (options.quality === 'medium') { + compression = (isTransparent || isNormalMap) ? codecOptions.formatTransparent || 'ASTC_6x6' : codecOptions.formatOpaque || 'ASTC_8x5'; + quality = codecOptions.quality || 'astcmedium'; + } else { + compression = (isTransparent || isNormalMap) ? codecOptions.formatTransparent || 'ASTC_8x5' : codecOptions.formatOpaque || 'ASTC_8x6'; + quality = codecOptions.quality || 'astcfast'; + } + } else if (variant.name === 'pvrtc') { + if (options.quality === 'low') { + compression = (isTransparent && !isNormalMap) ? codecOptions.formatTransparent || 'PVRTC1_2' : codecOptions.formatOpaque || 'PVRTC1_2_RGB'; + quality = codecOptions.quality || 'pvrtcbest'; + } else { + compression = (isTransparent && !isNormalMap) ? codecOptions.formatTransparent || 'PVRTC1_4' : codecOptions.formatOpaque || 'PVRTC1_4_RGB'; + quality = codecOptions.quality || 'pvrtcnormal'; + } + } else if (variant.name === 'etc') { + compression = (isTransparent && !isNormalMap) ? codecOptions.formatTransparent || 'ETC2_RGBA' : codecOptions.formatOpaque || 'ETC2_RGB'; + quality = codecOptions.quality || (options.quality === 'low' ? 'etcslow' : 'etcfast'); + } else if (variant.name === 's3tc') { + if (options.quality === 'low') { + compression = (isTransparent && !isNormalMap) ? codecOptions.formatTransparent || 'DXT1A' : codecOptions.formatOpaque || 'DXT1'; + quality = codecOptions.quality || 'better'; + } else { + compression = (isTransparent && !isNormalMap) ? codecOptions.formatTransparent || 'DXT3' : codecOptions.formatOpaque || 'DXT3'; + quality = codecOptions.quality || 'fast'; + } + } + + if (!compression || !quality) { + continue; + } + + const outputUri = image.uri.replace(ext, `-${variant.name}.ktx`); + await pack({ + type: variant.name, + input: path.join(baseDir, image.uri), + output: path.join(baseDir, outputUri), + compression, + quality, + verbose: false, + mipmap: useMipmaps, + square: variant.name === 'pvrtc' ? '+' : 'no' + }); + + image.uri = outputUri; + } + + return gltf; +} diff --git a/src/gltf2glb/FileUrl.js b/src/gltf2glb/FileUrl.js new file mode 100644 index 0000000..b7d32d9 --- /dev/null +++ b/src/gltf2glb/FileUrl.js @@ -0,0 +1,121 @@ +'use strict'; + +const { Check, RuntimeError } = require('cesium'); +const os = require('os'); +const path = require('path'); +const { domainToUnicode, URL } = require('url'); + +module.exports = { + fileURLToPath: fileURLToPath, + pathToFileURL: pathToFileURL +}; + +const isWindows = os.platform() === 'win32'; +const forwardSlashRegEx = /\//g; + +const CHAR_LOWERCASE_A = 97; +const CHAR_LOWERCASE_Z = 122; +const CHAR_FORWARD_SLASH = 47; +const CHAR_BACKWARD_SLASH = 92; + +const percentRegEx = /%/g; +const backslashRegEx = /\\/g; +const newlineRegEx = /\n/g; +const carriageReturnRegEx = /\r/g; +const tabRegEx = /\t/g; + +// The following function is copied from Node.js implementation of url module +// https://github.com/nodejs/node/blob/7237eaa3353aacf284289c8b59b0a5e0fa5744bb/lib/internal/url.js#L1345-L1383 +// pathToFileURL & fileURLToPath were added in 10.12 so we want to maintain ability run under older versions. +function fileURLToPath(path) { + Check.defined('path', path); + + if (typeof path === 'string') { + path = new URL(path); + } + + if (path.protocol !== 'file:') { + throw new RuntimeError('Expected path.protocol to start with file:'); + } + return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path); +} + +function pathToFileURL(filepath) { + let resolved = path.resolve(filepath); + // path.resolve strips trailing slashes so we must add them back + const filePathLast = filepath.charCodeAt(filepath.length - 1); + if ((filePathLast === CHAR_FORWARD_SLASH || + isWindows && filePathLast === CHAR_BACKWARD_SLASH) && + resolved[resolved.length - 1] !== path.sep) { + resolved += '/'; + } + const outURL = new URL('file://'); + if (resolved.includes('%')) { + resolved = resolved.replace(percentRegEx, '%25'); + } + // in posix, "/" is a valid character in paths + if (!isWindows && resolved.includes('\\')) { + resolved = resolved.replace(backslashRegEx, '%5C'); + } + if (resolved.includes('\n')) { + resolved = resolved.replace(newlineRegEx, '%0A'); + } + if (resolved.includes('\r')) { + resolved = resolved.replace(carriageReturnRegEx, '%0D'); + } + if (resolved.includes('\t')) { + resolved = resolved.replace(tabRegEx, '%09'); + } + outURL.pathname = resolved; + return outURL; +} + +function getPathFromURLWin32(url) { + const hostname = url.hostname; + let pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === '%') { + const third = pathname.codePointAt(n + 2) | 0x20; + if ((pathname[n + 1] === '2' && third === 102) || // 2f 2F / + (pathname[n + 1] === '5' && third === 99)) { // 5c 5C \ + throw new RuntimeError('file URL must not include encoded \\ or / characters'); + } + } + } + pathname = pathname.replace(forwardSlashRegEx, '\\'); + pathname = decodeURIComponent(pathname); + if (hostname !== '') { + // If hostname is set, then we have a UNC path + // Pass the hostname through domainToUnicode just in case + // it is an IDN using punycode encoding. We do not need to worry + // about percent encoding because the URL parser will have + // already taken care of that for us. Note that this only + // causes IDNs with an appropriate `xn--` prefix to be decoded. + return `\\\\${domainToUnicode(hostname)}${pathname}`; + } + + // Otherwise, it's a local path that requires a drive letter + const letter = pathname.codePointAt(1) | 0x20; + const sep = pathname[2]; + if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z || // a..z A..Z + (sep !== ':')) { + throw new RuntimeError('file URL must be absolute'); + } + return pathname.slice(1); +} + +function getPathFromURLPosix(url) { + if (url.hostname !== '') { + throw new RuntimeError('Invalid platform'); + } + const pathname = url.pathname; + for (let n = 0; n < pathname.length; n++) { + if (pathname[n] === '%') { + const third = pathname.codePointAt(n + 2) | 0x20; + if (pathname[n + 1] === '2' && third === 102) { + throw new RuntimeError('file URL must not include encoded \\ or / characters'); + } + } + } + return decodeURIComponent(pathname); +} diff --git a/src/gltf2glb/ForEach.js b/src/gltf2glb/ForEach.js new file mode 100644 index 0000000..bac6fd9 --- /dev/null +++ b/src/gltf2glb/ForEach.js @@ -0,0 +1,493 @@ +const Cesium = require('cesium'); +const hasExtension = require('./hasExtension'); + +const defined = Cesium.defined; +const isArray = Cesium.isArray; + +module.exports = ForEach; + +/** + * Contains traversal functions for processing elements of the glTF hierarchy. + * @constructor + * + * @private + */ +function ForEach() { +} + +/** + * Fallback for glTF 1.0 + * @private + */ +ForEach.objectLegacy = function(objects, handler) { + if (defined(objects)) { + for (const objectId in objects) { + if (objects.hasOwnProperty(objectId)) { + const object = objects[objectId]; + const value = handler(object, objectId); + + if (defined(value)) { + + return value; + } + } + } + } +}; + +/** + * @private + */ +ForEach.objectLegacyAsync = async function(objects, handler) { + if (defined(objects)) { + for (const objectId in objects) { + if (objects.hasOwnProperty(objectId)) { + const object = objects[objectId]; + const value = await handler(object, objectId); + + if (defined(value)) { + return value; + } + } + } + } +}; + +/** + * @private + */ +ForEach.object = function(arrayOfObjects, handler) { + if (defined(arrayOfObjects)) { + const length = arrayOfObjects.length; + for (let i = 0; i < length; i++) { + const object = arrayOfObjects[i]; + const value = handler(object, i); + + if (defined(value)) { + + return value; + } + } + } +}; + +/** + * @private + */ +ForEach.objectAsync = async function(arrayOfObjects, handler) { + if (defined(arrayOfObjects)) { + const length = arrayOfObjects.length; + for (let i = 0; i < length; i++) { + const object = arrayOfObjects[i]; + const value = await handler(object, i); + + if (defined(value)) { + return value; + } + } + } +}; + +/** + * Supports glTF 1.0 and 2.0 + * @private + */ +ForEach.topLevel = function(gltf, name, handler) { + const gltfProperty = gltf[name]; + if (defined(gltfProperty) && !isArray(gltfProperty)) { + return ForEach.objectLegacy(gltfProperty, handler); + } + + return ForEach.object(gltfProperty, handler); +}; + +/** + * Supports glTF 1.0 and 2.0 + * @private + */ +ForEach.topLevelAsync = function(gltf, name, handler) { + const gltfProperty = gltf[name]; + if (defined(gltfProperty) && !isArray(gltfProperty)) { + return ForEach.objectLegacyAsync(gltfProperty, handler); + } + + return ForEach.objectAsync(gltfProperty, handler); +}; + +ForEach.accessor = function(gltf, handler) { + return ForEach.topLevel(gltf, 'accessors', handler); +}; + +ForEach.accessorWithSemantic = function(gltf, semantic, handler) { + const visited = {}; + return ForEach.mesh(gltf, function(mesh) { + return ForEach.meshPrimitive(mesh, function(primitive) { + const valueForEach = ForEach.meshPrimitiveAttribute(primitive, function(accessorId, attributeSemantic) { + if (attributeSemantic.indexOf(semantic) === 0 && !defined(visited[accessorId])) { + visited[accessorId] = true; + const value = handler(accessorId); + + if (defined(value)) { + return value; + } + } + }); + + if (defined(valueForEach)) { + return valueForEach; + } + + return ForEach.meshPrimitiveTarget(primitive, function(target) { + return ForEach.meshPrimitiveTargetAttribute(target, function(accessorId, attributeSemantic) { + if (attributeSemantic.indexOf(semantic) === 0 && !defined(visited[accessorId])) { + visited[accessorId] = true; + const value = handler(accessorId); + + if (defined(value)) { + return value; + } + } + }); + }); + }); + }); +}; + +ForEach.accessorContainingVertexAttributeData = function(gltf, handler) { + const visited = {}; + return ForEach.mesh(gltf, function(mesh) { + return ForEach.meshPrimitive(mesh, function(primitive) { + const valueForEach = ForEach.meshPrimitiveAttribute(primitive, function(accessorId) { + if (!defined(visited[accessorId])) { + visited[accessorId] = true; + const value = handler(accessorId); + + if (defined(value)) { + return value; + } + } + }); + + if (defined(valueForEach)) { + return valueForEach; + } + + return ForEach.meshPrimitiveTarget(primitive, function(target) { + return ForEach.meshPrimitiveTargetAttribute(target, function(accessorId) { + if (!defined(visited[accessorId])) { + visited[accessorId] = true; + const value = handler(accessorId); + + if (defined(value)) { + return value; + } + } + }); + }); + }); + }); +}; + +ForEach.accessorContainingIndexData = function(gltf, handler) { + const visited = {}; + return ForEach.mesh(gltf, function(mesh) { + return ForEach.meshPrimitive(mesh, function(primitive) { + const indices = primitive.indices; + if (defined(indices) && !defined(visited[indices])) { + visited[indices] = true; + const value = handler(indices); + + if (defined(value)) { + return value; + } + } + }); + }); +}; + +ForEach.animation = function(gltf, handler) { + return ForEach.topLevel(gltf, 'animations', handler); +}; + +ForEach.animationChannel = function(animation, handler) { + const channels = animation.channels; + return ForEach.object(channels, handler); +}; + +ForEach.animationSampler = function(animation, handler) { + const samplers = animation.samplers; + return ForEach.object(samplers, handler); +}; + +ForEach.buffer = function(gltf, handler) { + return ForEach.topLevel(gltf, 'buffers', handler); +}; + +ForEach.bufferAsync = function(gltf, handler) { + return ForEach.topLevelAsync(gltf, 'buffers', handler); +}; + +ForEach.bufferView = function(gltf, handler) { + return ForEach.topLevel(gltf, 'bufferViews', handler); +}; + +ForEach.camera = function(gltf, handler) { + return ForEach.topLevel(gltf, 'cameras', handler); +}; + +ForEach.image = function(gltf, handler) { + return ForEach.topLevel(gltf, 'images', handler); +}; + +ForEach.imageAsync = function(gltf, handler) { + return ForEach.topLevelAsync(gltf, 'images', handler); +}; + +ForEach.compressedImage = function(image, handler) { + if (defined(image.extras)) { + const compressedImages = image.extras.compressedImage3DTiles; + for (const type in compressedImages) { + if (compressedImages.hasOwnProperty(type)) { + const compressedImage = compressedImages[type]; + const value = handler(compressedImage, type); + + if (defined(value)) { + return value; + } + } + } + } +}; + +ForEach.compressedImageAsync = async function(image, handler) { + if (defined(image.extras)) { + const compressedImages = image.extras.compressedImage3DTiles; + for (const type in compressedImages) { + if (compressedImages.hasOwnProperty(type)) { + const compressedImage = compressedImages[type]; + const value = await handler(compressedImage, type); + + if (defined(value)) { + return value; + } + } + } + } +}; + +ForEach.material = function(gltf, handler) { + return ForEach.topLevel(gltf, 'materials', handler); +}; + +ForEach.materialValue = function(material, handler) { + let values = material.values; + if (defined(material.extensions) && defined(material.extensions.KHR_techniques_webgl)) { + values = material.extensions.KHR_techniques_webgl.values; + } + + for (const name in values) { + if (values.hasOwnProperty(name)) { + const value = handler(values[name], name); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.mesh = function(gltf, handler) { + return ForEach.topLevel(gltf, 'meshes', handler); +}; + +ForEach.meshPrimitive = function(mesh, handler) { + const primitives = mesh.primitives; + if (defined(primitives)) { + const primitivesLength = primitives.length; + for (let i = 0; i < primitivesLength; i++) { + const primitive = primitives[i]; + const value = handler(primitive, i); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.meshPrimitiveAttribute = function(primitive, handler) { + const attributes = primitive.attributes; + for (const semantic in attributes) { + if (attributes.hasOwnProperty(semantic)) { + const value = handler(attributes[semantic], semantic); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.meshPrimitiveTarget = function(primitive, handler) { + const targets = primitive.targets; + if (defined(targets)) { + const length = targets.length; + for (let i = 0; i < length; ++i) { + const value = handler(targets[i], i); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.meshPrimitiveTargetAttribute = function(target, handler) { + for (const semantic in target) { + if (target.hasOwnProperty(semantic)) { + const accessorId = target[semantic]; + const value = handler(accessorId, semantic); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.node = function(gltf, handler) { + return ForEach.topLevel(gltf, 'nodes', handler); +}; + +ForEach.nodeInTree = function(gltf, nodeIds, handler) { + const nodes = gltf.nodes; + if (defined(nodes)) { + const length = nodeIds.length; + for (let i = 0; i < length; i++) { + const nodeId = nodeIds[i]; + const node = nodes[nodeId]; + if (defined(node)) { + let value = handler(node, nodeId); + + if (defined(value)) { + return value; + } + + const children = node.children; + if (defined(children)) { + value = ForEach.nodeInTree(gltf, children, handler); + + if (defined(value)) { + return value; + } + } + } + } + } +}; + +ForEach.nodeInScene = function(gltf, scene, handler) { + const sceneNodeIds = scene.nodes; + if (defined(sceneNodeIds)) { + return ForEach.nodeInTree(gltf, sceneNodeIds, handler); + } +}; + +ForEach.program = function(gltf, handler) { + if (hasExtension(gltf, 'KHR_techniques_webgl')) { + return ForEach.object(gltf.extensions.KHR_techniques_webgl.programs, handler); + } + + return ForEach.topLevel(gltf, 'programs', handler); +}; + +ForEach.sampler = function(gltf, handler) { + return ForEach.topLevel(gltf, 'samplers', handler); +}; + +ForEach.scene = function(gltf, handler) { + return ForEach.topLevel(gltf, 'scenes', handler); +}; + +ForEach.shader = function(gltf, handler) { + if (hasExtension(gltf, 'KHR_techniques_webgl')) { + return ForEach.object(gltf.extensions.KHR_techniques_webgl.shaders, handler); + } + + return ForEach.topLevel(gltf, 'shaders', handler); +}; + +ForEach.shaderAsync = function(gltf, handler) { + if (hasExtension(gltf, 'KHR_techniques_webgl')) { + return ForEach.objectAsync(gltf.extensions.KHR_techniques_webgl.shaders, handler); + } + + return ForEach.topLevelAsync(gltf, 'shaders', handler); +}; + +ForEach.skin = function(gltf, handler) { + return ForEach.topLevel(gltf, 'skins', handler); +}; + +ForEach.techniqueAttribute = function(technique, handler) { + const attributes = technique.attributes; + for (const attributeName in attributes) { + if (attributes.hasOwnProperty(attributeName)) { + const value = handler(attributes[attributeName], attributeName); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.techniqueUniform = function(technique, handler) { + const uniforms = technique.uniforms; + for (const uniformName in uniforms) { + if (uniforms.hasOwnProperty(uniformName)) { + const value = handler(uniforms[uniformName], uniformName); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.techniqueParameter = function(technique, handler) { + const parameters = technique.parameters; + for (const parameterName in parameters) { + if (parameters.hasOwnProperty(parameterName)) { + const value = handler(parameters[parameterName], parameterName); + + if (defined(value)) { + return value; + } + } + } +}; + +ForEach.technique = function(gltf, handler) { + if (hasExtension(gltf, 'KHR_techniques_webgl')) { + return ForEach.object(gltf.extensions.KHR_techniques_webgl.techniques, handler); + } + + return ForEach.topLevel(gltf, 'techniques', handler); +}; + +ForEach.texture = function(gltf, handler) { + return ForEach.topLevel(gltf, 'textures', handler); +}; + +ForEach.audioClip = function(gltf, handler) { + const audios = ((gltf.extensions || {}).Sein_audioClips || {}).clips || []; + + return ForEach.object(audios, handler); +}; + +ForEach.audioClipAsync = function(gltf, handler) { + const audios = ((gltf.extensions || {}).Sein_audioClips || {}).clips || []; + + return ForEach.objectAsync(audios, handler); +}; diff --git a/src/gltf2glb/LICENCE b/src/gltf2glb/LICENCE new file mode 100644 index 0000000..2791a8a --- /dev/null +++ b/src/gltf2glb/LICENCE @@ -0,0 +1,73 @@ +Copyright 2015-2017 Richard Lee, Analytical Graphics, Inc., and Contributors + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + +(b) You must cause any modified files to carry prominent notices stating that You changed the files; and + +(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + +(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. +Copyright 2015-2017 Richard Lee, Analytical Graphics, Inc., and Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. \ No newline at end of file diff --git a/src/gltf2glb/addBuffer.js b/src/gltf2glb/addBuffer.js new file mode 100644 index 0000000..a16066b --- /dev/null +++ b/src/gltf2glb/addBuffer.js @@ -0,0 +1,36 @@ +'use strict'; +const addToArray = require('./addToArray'); + +module.exports = addBuffer; + +/** + * Adds buffer to gltf. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Buffer} buffer A Buffer object which will be added to gltf.buffers. + * @returns {Number} The bufferView id of the newly added bufferView. + * + * @private + */ +function addBuffer(gltf, buffer) { + const newBuffer = { + byteLength: buffer.length, + extras: { + _pipeline: { + source: buffer + } + } + }; + const bufferId = addToArray(gltf.buffers, newBuffer); + const bufferView = { + buffer: bufferId, + byteOffset: 0, + byteLength: buffer.length + }; + + if (!gltf.bufferViews) { + gltf.bufferViews = []; + } + + return addToArray(gltf.bufferViews, bufferView); +} diff --git a/src/gltf2glb/addDefaults.js b/src/gltf2glb/addDefaults.js new file mode 100644 index 0000000..8810b4a --- /dev/null +++ b/src/gltf2glb/addDefaults.js @@ -0,0 +1,184 @@ +'use strict'; +const Cesium = require('cesium'); +const addToArray = require('./addToArray'); +const ForEach = require('./ForEach'); +const getAccessorByteStride = require('./getAccessorByteStride'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const WebGLConstants = Cesium.WebGLConstants; + +module.exports = addDefaults; + +/** + * Adds default glTF values if they don't exist. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The modified glTF. + * + * @private + */ +function addDefaults(gltf) { + ForEach.accessor(gltf, function(accessor) { + if (defined(accessor.bufferView)) { + accessor.byteOffset = defaultValue(accessor.byteOffset, 0); + } + }); + + ForEach.bufferView(gltf, function(bufferView) { + if (defined(bufferView.buffer)) { + bufferView.byteOffset = defaultValue(bufferView.byteOffset, 0); + } + }); + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + primitive.mode = defaultValue(primitive.mode, WebGLConstants.TRIANGLES); + if (!defined(primitive.material)) { + if (!defined(gltf.materials)) { + gltf.materials = []; + } + const defaultMaterial = { + name: 'default' + }; + primitive.material = addToArray(gltf.materials, defaultMaterial); + } + }); + }); + + ForEach.accessorContainingVertexAttributeData(gltf, function(accessorId) { + const accessor = gltf.accessors[accessorId]; + const bufferViewId = accessor.bufferView; + accessor.normalized = defaultValue(accessor.normalized, false); + if (defined(bufferViewId)) { + const bufferView = gltf.bufferViews[bufferViewId]; + bufferView.byteStride = getAccessorByteStride(gltf, accessor); + bufferView.target = WebGLConstants.ARRAY_BUFFER; + } + }); + + ForEach.accessorContainingIndexData(gltf, function(accessorId) { + const accessor = gltf.accessors[accessorId]; + const bufferViewId = accessor.bufferView; + if (defined(bufferViewId)) { + const bufferView = gltf.bufferViews[bufferViewId]; + bufferView.target = WebGLConstants.ELEMENT_ARRAY_BUFFER; + } + }); + + ForEach.material(gltf, function(material) { + const extensions = defaultValue(material.extensions, defaultValue.EMPTY_OBJECT); + const materialsCommon = extensions.KHR_materials_common; + if (defined(materialsCommon)) { + const technique = materialsCommon.technique; + const values = defined(materialsCommon.values) ? materialsCommon.values : {}; + materialsCommon.values = values; + + values.ambient = defined(values.ambient) ? values.ambient : [0.0, 0.0, 0.0, 1.0]; + values.emission = defined(values.emission) ? values.emission : [0.0, 0.0, 0.0, 1.0]; + + values.transparency = defaultValue(values.transparency, 1.0); + values.transparent = defaultValue(values.transparent, false); + values.doubleSided = defaultValue(values.doubleSided, false); + + if (technique !== 'CONSTANT') { + values.diffuse = defined(values.diffuse) ? values.diffuse : [0.0, 0.0, 0.0, 1.0]; + if (technique !== 'LAMBERT') { + values.specular = defined(values.specular) ? values.specular : [0.0, 0.0, 0.0, 1.0]; + values.shininess = defaultValue(values.shininess, 0.0); + } + } + return; + } + + material.emissiveFactor = defaultValue(material.emissiveFactor, [0.0, 0.0, 0.0]); + material.alphaMode = defaultValue(material.alphaMode, 'OPAQUE'); + material.doubleSided = defaultValue(material.doubleSided, false); + + if (material.alphaMode === 'MASK') { + material.alphaCutoff = defaultValue(material.alphaCutoff, 0.5); + } + + const techniquesExtension = extensions.KHR_techniques_webgl; + if (defined(techniquesExtension)) { + ForEach.materialValue(material, function (materialValue) { + // Check if material value is a TextureInfo object + if (defined(materialValue.index)) { + addTextureDefaults(materialValue); + } + }); + } + + addTextureDefaults(material.emissiveTexture); + addTextureDefaults(material.normalTexture); + addTextureDefaults(material.occlusionTexture); + + const pbrMetallicRoughness = material.pbrMetallicRoughness; + if (defined(pbrMetallicRoughness)) { + pbrMetallicRoughness.baseColorFactor = defaultValue(pbrMetallicRoughness.baseColorFactor, [1.0, 1.0, 1.0, 1.0]); + pbrMetallicRoughness.metallicFactor = defaultValue(pbrMetallicRoughness.metallicFactor, 1.0); + pbrMetallicRoughness.roughnessFactor = defaultValue(pbrMetallicRoughness.roughnessFactor, 1.0); + addTextureDefaults(pbrMetallicRoughness.baseColorTexture); + addTextureDefaults(pbrMetallicRoughness.metallicRoughnessTexture); + } + + const pbrSpecularGlossiness = extensions.pbrSpecularGlossiness; + if (defined(pbrSpecularGlossiness)) { + pbrSpecularGlossiness.diffuseFactor = defaultValue(pbrSpecularGlossiness.diffuseFactor, [1.0, 1.0, 1.0, 1.0]); + pbrSpecularGlossiness.specularFactor = defaultValue(pbrSpecularGlossiness.specularFactor, [1.0, 1.0, 1.0]); + pbrSpecularGlossiness.glossinessFactor = defaultValue(pbrSpecularGlossiness.glossinessFactor, 1.0); + addTextureDefaults(pbrSpecularGlossiness.specularGlossinessTexture); + } + }); + + ForEach.animation(gltf, function(animation) { + ForEach.animationSampler(animation, function(sampler) { + sampler.interpolation = defaultValue(sampler.interpolation, 'LINEAR'); + }); + }); + + const animatedNodes = getAnimatedNodes(gltf); + ForEach.node(gltf, function(node, id) { + const animated = defined(animatedNodes[id]); + if (animated || defined(node.translation) || defined(node.rotation) || defined(node.scale)) { + node.translation = defaultValue(node.translation, [0.0, 0.0, 0.0]); + node.rotation = defaultValue(node.rotation, [0.0, 0.0, 0.0, 1.0]); + node.scale = defaultValue(node.scale, [1.0, 1.0, 1.0]); + } else { + node.matrix = defaultValue(node.matrix, [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]); + } + }); + + ForEach.sampler(gltf, function(sampler) { + sampler.wrapS = defaultValue(sampler.wrapS, WebGLConstants.REPEAT); + sampler.wrapT = defaultValue(sampler.wrapT, WebGLConstants.REPEAT); + }); + + if (defined(gltf.scenes) && !defined(gltf.scene)) { + gltf.scene = 0; + } + + return gltf; +} + +function getAnimatedNodes(gltf) { + const nodes = {}; + ForEach.animation(gltf, function(animation) { + ForEach.animationChannel(animation, function(channel) { + const target = channel.target; + const nodeId = target.node; + const path = target.path; + // Ignore animations that target 'weights' + if (path === 'translation' || path === 'rotation' || path === 'scale') { + nodes[nodeId] = true; + } + }); + }); + return nodes; +} + +function addTextureDefaults(texture) { + if (defined(texture)) { + texture.texCoord = defaultValue(texture.texCoord, 0); + } +} diff --git a/src/gltf2glb/addExtensionsRequired.js b/src/gltf2glb/addExtensionsRequired.js new file mode 100644 index 0000000..987b429 --- /dev/null +++ b/src/gltf2glb/addExtensionsRequired.js @@ -0,0 +1,27 @@ +'use strict'; +const Cesium = require('cesium'); +const addExtensionsUsed = require('./addExtensionsUsed'); +const addToArray = require('./addToArray'); + +const defined = Cesium.defined; + +module.exports = addExtensionsRequired; + +/** + * Adds an extension to gltf.extensionsRequired if it does not already exist. + * Initializes extensionsRequired if it is not defined. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The extension to add. + * + * @private + */ +function addExtensionsRequired(gltf, extension) { + let extensionsRequired = gltf.extensionsRequired; + if (!defined(extensionsRequired)) { + extensionsRequired = []; + gltf.extensionsRequired = extensionsRequired; + } + addToArray(extensionsRequired, extension, true); + addExtensionsUsed(gltf, extension); +} diff --git a/src/gltf2glb/addExtensionsUsed.js b/src/gltf2glb/addExtensionsUsed.js new file mode 100644 index 0000000..13b57a3 --- /dev/null +++ b/src/gltf2glb/addExtensionsUsed.js @@ -0,0 +1,25 @@ +'use strict'; +const Cesium = require('cesium'); +const addToArray = require('./addToArray'); + +const defined = Cesium.defined; + +module.exports = addExtensionsUsed; + +/** + * Adds an extension to gltf.extensionsUsed if it does not already exist. + * Initializes extensionsUsed if it is not defined. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The extension to add. + * + * @private + */ +function addExtensionsUsed(gltf, extension) { + let extensionsUsed = gltf.extensionsUsed; + if (!defined(extensionsUsed)) { + extensionsUsed = []; + gltf.extensionsUsed = extensionsUsed; + } + addToArray(extensionsUsed, extension, true); +} diff --git a/src/gltf2glb/addPipelineExtras.js b/src/gltf2glb/addPipelineExtras.js new file mode 100644 index 0000000..2f5fcaf --- /dev/null +++ b/src/gltf2glb/addPipelineExtras.js @@ -0,0 +1,40 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; + +module.exports = addPipelineExtras; + +/** + * Adds extras._pipeline to each object that can have extras in the glTF asset. + * This stage runs before updateVersion and handles both glTF 1.0 and glTF 2.0 assets. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The glTF asset with the added pipeline extras. + * + * @private + */ +function addPipelineExtras(gltf) { + ForEach.shader(gltf, function(shader) { + addExtras(shader); + }); + ForEach.buffer(gltf, function(buffer) { + addExtras(buffer); + }); + ForEach.image(gltf, function (image) { + addExtras(image); + ForEach.compressedImage(image, function(compressedImage) { + addExtras(compressedImage); + }); + }); + + addExtras(gltf); + + return gltf; +} + +function addExtras(object) { + object.extras = defined(object.extras) ? object.extras : {}; + object.extras._pipeline = defined(object.extras._pipeline) ? object.extras._pipeline : {}; +} diff --git a/src/gltf2glb/addToArray.js b/src/gltf2glb/addToArray.js new file mode 100644 index 0000000..d9e829d --- /dev/null +++ b/src/gltf2glb/addToArray.js @@ -0,0 +1,28 @@ +'use strict'; +const Cesium = require('cesium'); + +const defaultValue = Cesium.defaultValue; + +module.exports = addToArray; + +/** + * Adds an element to an array and returns the element's index. + * + * @param {Array} array The array to add to. + * @param {Object} element The element to add. + * @param {Boolean} [checkDuplicates=false] When true, if a duplicate element is found its index is returned and element is not added to the array. + * + * @private + */ +function addToArray(array, element, checkDuplicates) { + checkDuplicates = defaultValue(checkDuplicates, false); + if (checkDuplicates) { + const index = array.indexOf(element); + if (index > -1) { + return index; + } + } + + array.push(element); + return array.length - 1; +} diff --git a/src/gltf2glb/compressDracoMeshes.js b/src/gltf2glb/compressDracoMeshes.js new file mode 100644 index 0000000..61b398a --- /dev/null +++ b/src/gltf2glb/compressDracoMeshes.js @@ -0,0 +1,368 @@ +'use strict'; +const Cesium = require('cesium'); +const draco3d = require('draco3d'); +const hashObject = require('object-hash'); +const addBuffer = require('./addBuffer'); +const addExtensionsRequired = require('./addExtensionsRequired'); +const addExtensionsUsed = require('./addExtensionsUsed'); +const addToArray = require('./addToArray'); +const ForEach = require('./ForEach'); +const numberOfComponentsForType = require('./numberOfComponentsForType'); +const readAccessorPacked = require('./readAccessorPacked'); +const removeUnusedElements = require('./removeUnusedElements'); +const replaceWithDecompressedPrimitive = require('./replaceWithDecompressedPrimitive'); +const splitPrimitives = require('./splitPrimitives'); + +const arrayFill = Cesium.arrayFill; +const Cartesian3 = Cesium.Cartesian3; +const Check = Cesium.Check; +const clone = Cesium.clone; +const ComponentDatatype = Cesium.ComponentDatatype; +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const RuntimeError = Cesium.RuntimeError; +const WebGLConstants = Cesium.WebGLConstants; + +// Prepare encoder for compressing meshes. +const encoderModule = draco3d.createEncoderModule({}); + +module.exports = compressDracoMeshes; + +/** + * Compresses meshes using Draco compression in the glTF model. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} options The same options object as {@link processGltf} + * @param {Object} options.dracoOptions Options defining Draco compression settings. + * @param {Number} [options.dracoOptions.compressionLevel=7] A value between 0 and 10 specifying the quality of the Draco compression. Higher values produce better quality compression but may take longer to decompress. A value of 0 will apply sequential encoding and preserve face order. + * @param {Number} [options.dracoOptions.quantizePositionBits=14] A value between 0 and 30 specifying the number of bits used for positions. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization. + * @param {Number} [options.dracoOptions.quantizeNormalBits=10] A value between 0 and 30 specifying the number of bits used for normals. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization. + * @param {Number} [options.dracoOptions.quantizeTexcoordBits=12] A value between 0 and 30 specifying the number of bits used for texture coordinates. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization. + * @param {Number} [options.dracoOptions.quantizeColorBits=8] A value between 0 and 30 specifying the number of bits used for color attributes. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization. + * @param {Number} [options.dracoOptions.quantizeGenericBits=12] A value between 0 and 30 specifying the number of bits used for skinning attributes (joint indices and joint weights) and custom attributes. Lower values produce better compression, but will lose precision. A value of 0 does not set quantization. + * @param {Boolean} [options.dracoOptions.uncompressedFallback=false] If set, add uncompressed fallback versions of the compressed meshes. + * @param {Boolean} [options.dracoOptions.unifiedQuantization=false] Quantize positions, defined by the unified bounding box of all primitives. If not set, quantization is applied separately. + * @param {Object} [options.dracoOptions.quantizationVolume] An AxisAlignedBoundingBox defining the explicit quantization volume. + * @returns {Object} The glTF asset with compressed meshes. + * + * @private + */ +function compressDracoMeshes(gltf, options) { + options = defaultValue(options, {}); + const dracoOptions = defaultValue(options.dracoOptions, {}); + const defaults = compressDracoMeshes.defaults; + const compressionLevel = defaultValue(dracoOptions.compressionLevel, defaults.compressionLevel); + const quantizePositionBits = defaultValue(dracoOptions.quantizePositionBits, defaults.quantizePositionBits); + const quantizeNormalBits = defaultValue(dracoOptions.quantizeNormalBits, defaults.quantizeNormalBits); + const quantizeTexcoordBits = defaultValue(dracoOptions.quantizeTexcoordBits, defaults.quantizeTexcoordBits); + const quantizeColorBits = defaultValue(dracoOptions.quantizeColorBits, defaults.quantizeColorBits); + const quantizeGenericBits = defaultValue(dracoOptions.quantizeGenericBits, defaults.quantizeGenericBits); + const uncompressedFallback = defaultValue(dracoOptions.uncompressedFallback, defaults.uncompressedFallback); + const unifiedQuantization = defaultValue(dracoOptions.unifiedQuantization, defaults.unifiedQuantization); + const quantizationVolume = dracoOptions.quantizationVolume; + const explicitQuantization = unifiedQuantization || defined(quantizationVolume); + checkRange('compressionLevel', compressionLevel, 0, 10); + checkRange('quantizePositionBits', quantizePositionBits, 0, 30); + checkRange('quantizeNormalBits', quantizeNormalBits, 0, 30); + checkRange('quantizeTexcoordBits', quantizeTexcoordBits, 0, 30); + checkRange('quantizeColorBits', quantizeColorBits, 0, 30); + checkRange('quantizeGenericBits', quantizeGenericBits, 0, 30); + + splitPrimitives(gltf); + + const hashPrimitives = {}; + let positionOrigin; + let positionRange; + + if (defined(quantizationVolume)) { + positionOrigin = Cartesian3.pack(quantizationVolume.minimum, new Array(3)); + positionRange = Cartesian3.maximumComponent(Cartesian3.subtract(quantizationVolume.maximum, quantizationVolume.minimum, new Cartesian3())); + } else if (unifiedQuantization) { + // Collect bounding box from all primitives. Currently works only for vec3 positions (XYZ). + const accessors = gltf.accessors; + const min = arrayFill(new Array(3), Number.POSITIVE_INFINITY); + const max = arrayFill(new Array(3), Number.NEGATIVE_INFINITY); + ForEach.accessorWithSemantic(gltf, 'POSITION', function(accessorId) { + const accessor = accessors[accessorId]; + if (accessor.type !== 'VEC3') { + throw new RuntimeError('Could not perform unified quantization. Input contains position accessor with an unsupported number of components.'); + } + const accessorMin = accessor.min; + const accessorMax = accessor.max; + for (let j = 0; j < 3; ++j) { + min[j] = Math.min(min[j], accessorMin[j]); + max[j] = Math.max(max[j], accessorMax[j]); + } + }); + positionOrigin = min; + positionRange = Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]); + } + + let addedExtension = false; + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.mode) && primitive.mode !== WebGLConstants.TRIANGLES) { + return; + } + if (!defined(primitive.indices)) { + addIndices(gltf, primitive); + } + + addedExtension = true; + + const primitiveGeometry = { + attributes: primitive.attributes, + indices: primitive.indices, + mode: primitive.mode + }; + const hashValue = hashObject(primitiveGeometry); + if (defined(hashPrimitives[hashValue])) { + // Copy compressed primitive. + copyCompressedExtensionToPrimitive(primitive, hashPrimitives[hashValue]); + return; + } + hashPrimitives[hashValue] = primitive; + + const encoder = new encoderModule.Encoder(); + const meshBuilder = new encoderModule.MeshBuilder(); + const mesh = new encoderModule.Mesh(); + + // First get the faces and add to geometry. + const indicesData = readAccessorPacked(gltf, gltf.accessors[primitive.indices]); + const indices = new Uint32Array(indicesData); + const numberOfFaces = indices.length / 3; + meshBuilder.AddFacesToMesh(mesh, numberOfFaces, indices); + + // Add attributes to mesh. + const attributeToId = {}; + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + const accessor = gltf.accessors[accessorId]; + const componentType = accessor.componentType; + const numberOfPoints = accessor.count; + const numberOfComponents = numberOfComponentsForType(accessor.type); + const packed = readAccessorPacked(gltf, accessor); + const addAttributeFunctionName = getAddAttributeFunctionName(componentType); + const data = ComponentDatatype.createTypedArray(componentType, packed); + + let attributeName = semantic; + if (semantic.indexOf('_') > 0) { // Skip user-defined semantics prefixed with underscore + attributeName = attributeName.substring(0, semantic.indexOf('_')); + } + + let attributeEnum; + if (attributeName === 'POSITION' || attributeName === 'NORMAL' || attributeName === 'COLOR') { + attributeEnum = encoderModule[attributeName]; + } else if (attributeName === 'TEXCOORD') { + attributeEnum = encoderModule.TEX_COORD; + } else { + attributeEnum = encoderModule.GENERIC; + } + + const attributeId = meshBuilder[addAttributeFunctionName](mesh, attributeEnum, numberOfPoints, numberOfComponents, data); + + if (attributeId === -1) { + throw new RuntimeError('Error: Failed adding attribute ' + semantic); + } else { + attributeToId[semantic] = attributeId; + } + }); + + const encodedDracoDataArray = new encoderModule.DracoInt8Array(); + encoder.SetSpeedOptions(10 - compressionLevel, 10 - compressionLevel); // Compression level is 10 - speed. + if (quantizePositionBits > 0) { + if (explicitQuantization) { + encoder.SetAttributeExplicitQuantization(encoderModule.POSITION, quantizePositionBits, 3, positionOrigin, positionRange); + } else { + encoder.SetAttributeQuantization(encoderModule.POSITION, quantizePositionBits); + } + } + if (quantizeNormalBits > 0) { + encoder.SetAttributeQuantization(encoderModule.NORMAL, quantizeNormalBits); + } + if (quantizeTexcoordBits > 0) { + encoder.SetAttributeQuantization(encoderModule.TEX_COORD, quantizeTexcoordBits); + } + if (quantizeColorBits > 0) { + encoder.SetAttributeQuantization(encoderModule.COLOR, quantizeColorBits); + } + if (quantizeGenericBits > 0) { + encoder.SetAttributeQuantization(encoderModule.GENERIC, quantizeGenericBits); + } + + if (defined(primitive.targets)) { + // Set sequential encoding to preserve order of vertices. + encoder.SetEncodingMethod(encoderModule.MESH_SEQUENTIAL_ENCODING); + } + + encoder.SetTrackEncodedProperties(true); + const encodedLength = encoder.EncodeMeshToDracoBuffer(mesh, encodedDracoDataArray); + if (encodedLength <= 0) { + throw new RuntimeError('Error: Draco encoding failed.'); + } + + const encodedData = Buffer.alloc(encodedLength); + for (let i = 0; i < encodedLength; ++i) { + encodedData[i] = encodedDracoDataArray.GetValue(i); + } + + const dracoEncodedBuffer = { + buffer: encodedData, + numberOfPoints: encoder.GetNumberOfEncodedPoints(), + numberOfFaces: encoder.GetNumberOfEncodedFaces() + }; + addCompressionExtensionToPrimitive(gltf, primitive, attributeToId, dracoEncodedBuffer, uncompressedFallback); + + encoderModule.destroy(encodedDracoDataArray); + encoderModule.destroy(mesh); + encoderModule.destroy(meshBuilder); + encoderModule.destroy(encoder); + }); + }); + + if (addedExtension) { + if (uncompressedFallback) { + addExtensionsUsed(gltf, 'KHR_draco_mesh_compression'); + } else { + addExtensionsRequired(gltf, 'KHR_draco_mesh_compression'); + } + removeUnusedElements(gltf); + + if (uncompressedFallback) { + assignMergedBufferNames(gltf); + } + } + + return gltf; +} + +function addIndices(gltf, primitive) { + // Reserve the 65535 index for primitive restart + const length = gltf.accessors[primitive.attributes.POSITION].count; + const componentType = length < 65535 ? WebGLConstants.UNSIGNED_SHORT : WebGLConstants.UNSIGNED_INT; + const array = ComponentDatatype.createTypedArray(componentType, length); + for (let i = 0; i < length; ++i) { + array[i] = i; + } + const buffer = Buffer.from(array.buffer); + const bufferView = addBuffer(gltf, buffer); + const accessor = { + bufferView: bufferView, + byteOffset: 0, + componentType: componentType, + count: length, + type: 'SCALAR', + min: [0], + max: [length - 1] + }; + primitive.indices = addToArray(gltf.accessors, accessor); +} + +function addCompressionExtensionToPrimitive(gltf, primitive, attributeToId, dracoEncodedBuffer, uncompressedFallback) { + if (!uncompressedFallback) { + // Remove properties from accessors. + // Remove indices bufferView. + const indicesAccessor = clone(gltf.accessors[primitive.indices]); + delete indicesAccessor.bufferView; + delete indicesAccessor.byteOffset; + primitive.indices = addToArray(gltf.accessors, indicesAccessor); + + // Remove attributes bufferViews. + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + const attributeAccessor = clone(gltf.accessors[accessorId]); + delete attributeAccessor.bufferView; + delete attributeAccessor.byteOffset; + primitive.attributes[semantic] = addToArray(gltf.accessors, attributeAccessor); + }); + } + + const bufferViewId = addBuffer(gltf, dracoEncodedBuffer.buffer); + + primitive.extensions = defaultValue(primitive.extensions, {}); + primitive.extensions.KHR_draco_mesh_compression = { + bufferView: bufferViewId, + attributes: attributeToId + }; + + gltf = replaceWithDecompressedPrimitive(gltf, primitive, dracoEncodedBuffer, uncompressedFallback); +} + +function copyCompressedExtensionToPrimitive(primitive, compressedPrimitive) { + ForEach.meshPrimitiveAttribute(compressedPrimitive, function(accessorId, semantic) { + primitive.attributes[semantic] = accessorId; + }); + primitive.indices = compressedPrimitive.indices; + + const dracoExtension = compressedPrimitive.extensions.KHR_draco_mesh_compression; + primitive.extensions = defaultValue(primitive.extensions, {}); + primitive.extensions.KHR_draco_mesh_compression = { + bufferView: dracoExtension.bufferView, + attributes: dracoExtension.attributes + }; +} + +function assignBufferViewName(gltf, bufferViewId, name) { + const bufferView = gltf.bufferViews[bufferViewId]; + const buffer = gltf.buffers[bufferView.buffer]; + buffer.extras._pipeline.mergedBufferName = name; +} + +function assignAccessorName(gltf, accessorId, name) { + const bufferViewId = gltf.accessors[accessorId].bufferView; + if (defined(bufferViewId)) { + assignBufferViewName(gltf, bufferViewId, name); + } +} + +function assignMergedBufferNames(gltf) { + ForEach.accessorContainingVertexAttributeData(gltf, function(accessorId) { + assignAccessorName(gltf, accessorId, 'uncompressed'); + }); + ForEach.accessorContainingIndexData(gltf, function(accessorId) { + assignAccessorName(gltf, accessorId, 'uncompressed'); + }); + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.extensions) && + defined(primitive.extensions.KHR_draco_mesh_compression)) { + assignBufferViewName(gltf, primitive.extensions.KHR_draco_mesh_compression.bufferView, 'draco'); + } + }); + }); +} + +function getAddAttributeFunctionName(componentType) { + switch (componentType) { + case WebGLConstants.UNSIGNED_BYTE: + return 'AddUInt8Attribute'; + case WebGLConstants.BYTE: + return 'AddInt8Attribute'; + case WebGLConstants.UNSIGNED_SHORT: + return 'AddUInt16Attribute'; + case WebGLConstants.SHORT: + return 'AddInt16Attribute'; + case WebGLConstants.UNSIGNED_INT: + return 'AddUInt32Attribute'; + case WebGLConstants.INT: + return 'AddInt32Attribute'; + case WebGLConstants.FLOAT: + return 'AddFloatAttribute'; + } +} + +function checkRange(name, value, minimum, maximum) { + Check.typeOf.number.greaterThanOrEquals(name, value, minimum); + Check.typeOf.number.lessThanOrEquals(name, value, maximum); +} + +compressDracoMeshes.defaults = { + compressionLevel: 7, + quantizePositionBits: 14, + quantizeNormalBits: 10, + quantizeTexcoordBits: 12, + quantizeColorBits: 8, + quantizeSkinBits: 12, + quantizeGenericBits: 12, + uncompressedFallback: false, + unifiedQuantization: false +}; diff --git a/src/gltf2glb/dataUriToBuffer.js b/src/gltf2glb/dataUriToBuffer.js new file mode 100644 index 0000000..56f58ce --- /dev/null +++ b/src/gltf2glb/dataUriToBuffer.js @@ -0,0 +1,18 @@ +'use strict'; +module.exports = dataUriToBuffer; + +/** + * Read a data uri string into a buffer. + * + * @param {String} dataUri The data uri. + * @returns {Buffer} + * + * @private + */ +function dataUriToBuffer(dataUri) { + const data = dataUri.slice(dataUri.indexOf(',') + 1); + if (dataUri.indexOf('base64') >= 0) { + return Buffer.from(data, 'base64'); + } + return Buffer.from(data, 'utf8'); +} diff --git a/src/gltf2glb/findAccessorMinMax.js b/src/gltf2glb/findAccessorMinMax.js new file mode 100644 index 0000000..9c534db --- /dev/null +++ b/src/gltf2glb/findAccessorMinMax.js @@ -0,0 +1,68 @@ +'use strict'; +const Cesium = require('cesium'); + +const arrayFill = Cesium.arrayFill; +const ComponentDatatype = Cesium.ComponentDatatype; +const defined = Cesium.defined; + +const getAccessorByteStride = require('./getAccessorByteStride'); +const getComponentReader = require('./getComponentReader'); +const numberOfComponentsForType = require('./numberOfComponentsForType'); + +module.exports = findAccessorMinMax; + +/** + * Finds the min and max values of the accessor. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} accessor The accessor object from the glTF asset to read. + * @returns {{min: Array, max: Array}} min holding the array of minimum values and max holding the array of maximum values. + * + * @private + */ +function findAccessorMinMax(gltf, accessor) { + const bufferViews = gltf.bufferViews; + const buffers = gltf.buffers; + const bufferViewId = accessor.bufferView; + const numberOfComponents = numberOfComponentsForType(accessor.type); + + // According to the spec, when bufferView is not defined, accessor must be initialized with zeros + if (!defined(accessor.bufferView)) { + return { + min: arrayFill(new Array(numberOfComponents), 0.0), + max: arrayFill(new Array(numberOfComponents), 0.0) + }; + } + + const min = arrayFill(new Array(numberOfComponents), Number.POSITIVE_INFINITY); + const max = arrayFill(new Array(numberOfComponents), Number.NEGATIVE_INFINITY); + + const bufferView = bufferViews[bufferViewId]; + const bufferId = bufferView.buffer; + const buffer = buffers[bufferId]; + const source = buffer.extras._pipeline.source; + + const count = accessor.count; + const byteStride = getAccessorByteStride(gltf, accessor); + let byteOffset = accessor.byteOffset + bufferView.byteOffset + source.byteOffset; + const componentType = accessor.componentType; + const componentTypeByteLength = ComponentDatatype.getSizeInBytes(componentType); + const dataView = new DataView(source.buffer); + const components = new Array(numberOfComponents); + const componentReader = getComponentReader(componentType); + + for (let i = 0; i < count; i++) { + componentReader(dataView, byteOffset, numberOfComponents, componentTypeByteLength, components); + for (let j = 0; j < numberOfComponents; j++) { + const value = components[j]; + min[j] = Math.min(min[j], value); + max[j] = Math.max(max[j], value); + } + byteOffset += byteStride; + } + + return { + min: min, + max: max + }; +} diff --git a/src/gltf2glb/getAccessorByteStride.js b/src/gltf2glb/getAccessorByteStride.js new file mode 100644 index 0000000..43b8af0 --- /dev/null +++ b/src/gltf2glb/getAccessorByteStride.js @@ -0,0 +1,29 @@ +'use strict'; +const Cesium = require('cesium'); +const numberOfComponentsForType = require('./numberOfComponentsForType'); + +const ComponentDatatype = Cesium.ComponentDatatype; +const defined = Cesium.defined; + +module.exports = getAccessorByteStride; + +/** + * Returns the byte stride of the provided accessor. + * If the byteStride is 0, it is calculated based on type and componentType + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} accessor The accessor. + * @returns {Number} The byte stride of the accessor. + * + * @private + */ +function getAccessorByteStride(gltf, accessor) { + const bufferViewId = accessor.bufferView; + if (defined(bufferViewId)) { + const bufferView = gltf.bufferViews[bufferViewId]; + if (defined(bufferView.byteStride) && bufferView.byteStride > 0) { + return bufferView.byteStride; + } + } + return ComponentDatatype.getSizeInBytes(accessor.componentType) * numberOfComponentsForType(accessor.type); +} diff --git a/src/gltf2glb/getBufferPadded.js b/src/gltf2glb/getBufferPadded.js new file mode 100644 index 0000000..be824e3 --- /dev/null +++ b/src/gltf2glb/getBufferPadded.js @@ -0,0 +1,22 @@ +'use strict'; +module.exports = getBufferPadded; + +/** + * Pad the buffer to the next 4-byte boundary to ensure proper alignment for the section that follows. + * + * @param {Buffer} buffer The buffer. + * @returns {Buffer} The padded buffer. + * + * @private + */ +function getBufferPadded(buffer) { + const boundary = 4; + const byteLength = buffer.length; + const remainder = byteLength % boundary; + if (remainder === 0) { + return buffer; + } + const padding = (remainder === 0) ? 0 : boundary - remainder; + const emptyBuffer = Buffer.alloc(padding); + return Buffer.concat([buffer, emptyBuffer]); +} diff --git a/src/gltf2glb/getComponentReader.js b/src/gltf2glb/getComponentReader.js new file mode 100644 index 0000000..6d84a43 --- /dev/null +++ b/src/gltf2glb/getComponentReader.js @@ -0,0 +1,78 @@ +'use strict'; +const Cesium = require('cesium'); + +const ComponentDatatype = Cesium.ComponentDatatype; + +module.exports = getComponentReader; + +/** + * Returns a function to read and convert data from a DataView into an array. + * + * @param {Number} componentType Type to convert the data to. + * @returns {ComponentReader} Function that reads and converts data. + * + * @private + */ +function getComponentReader(componentType) { + switch (componentType) { + case ComponentDatatype.BYTE: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getInt8(byteOffset + i * componentTypeByteLength); + } + }; + case ComponentDatatype.UNSIGNED_BYTE: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getUint8(byteOffset + i * componentTypeByteLength); + } + }; + case ComponentDatatype.SHORT: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getInt16(byteOffset + i * componentTypeByteLength, true); + } + }; + case ComponentDatatype.UNSIGNED_SHORT: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getUint16(byteOffset + i * componentTypeByteLength, true); + } + }; + case ComponentDatatype.INT: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getInt32(byteOffset + i * componentTypeByteLength, true); + } + }; + case ComponentDatatype.UNSIGNED_INT: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getUint32(byteOffset + i * componentTypeByteLength, true); + } + }; + case ComponentDatatype.FLOAT: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getFloat32(byteOffset + i * componentTypeByteLength, true); + } + }; + case ComponentDatatype.DOUBLE: + return function (dataView, byteOffset, numberOfComponents, componentTypeByteLength, result) { + for (let i = 0; i < numberOfComponents; ++i) { + result[i] = dataView.getFloat64(byteOffset + i * componentTypeByteLength, true); + } + }; + } +} + +/** + * A callback function that logs messages. + * @callback ComponentReader + * + * @param {DataView} dataView The data view to read from. + * @param {Number} byteOffset The byte offset applied when reading from the data view. + * @param {Number} numberOfComponents The number of components to read. + * @param {Number} componentTypeByteLength The byte length of each component. + * @param {Number} result An array storing the components that are read. + */ diff --git a/src/gltf2glb/getImageExtension.js b/src/gltf2glb/getImageExtension.js new file mode 100644 index 0000000..8b41527 --- /dev/null +++ b/src/gltf2glb/getImageExtension.js @@ -0,0 +1,39 @@ +'use strict'; +const Cesium = require('cesium'); + +const RuntimeError = Cesium.RuntimeError; + +module.exports = getImageExtension; + +/** + * Get the image extension from a Buffer containing image data. + * + * @param {Buffer} data The image data. + * @returns {String} The image extension. + * + * @private + */ +function getImageExtension(data) { + const header = data.slice(0, 2); + const webpHeaderRIFFChars = data.slice(0, 4); + const webpHeaderWEBPChars = data.slice(8, 12); + + if (header.equals(Buffer.from([0x42, 0x4D]))) { + return '.bmp'; + } else if (header.equals(Buffer.from([0x47, 0x49]))) { + return '.gif'; + } else if (header.equals(Buffer.from([0xFF, 0xD8]))) { + return '.jpg'; + } else if (header.equals(Buffer.from([0x89, 0x50]))) { + return '.png'; + } else if (header.equals(Buffer.from([0xAB, 0x4B]))) { + return '.ktx'; + } else if (header.equals(Buffer.from([0x48, 0x78]))) { + return '.crn'; + } else if (webpHeaderRIFFChars.equals(Buffer.from([0x52, 0x49, 0x46, 0x46])) && webpHeaderWEBPChars.equals(Buffer.from([0x57, 0x45, 0x42, 0x50]))) { + // See https://developers.google.com/speed/webp/docs/riff_container#webp_file_header + return '.webp'; + } + + throw new RuntimeError('Image data does not have valid header'); +} diff --git a/src/gltf2glb/getJsonBufferPadded.js b/src/gltf2glb/getJsonBufferPadded.js new file mode 100644 index 0000000..6cb6fc9 --- /dev/null +++ b/src/gltf2glb/getJsonBufferPadded.js @@ -0,0 +1,29 @@ +'use strict'; +module.exports = getJsonBufferPadded; + +/** + * Convert the JSON object to a padded buffer. + * + * Pad the JSON with extra whitespace to fit the next 4-byte boundary. This ensures proper alignment + * for the section that follows. + * + * @param {Object} [json] The JSON object. + * @returns {Buffer} The padded JSON buffer. + * + * @private + */ +function getJsonBufferPadded(json) { + let string = JSON.stringify(json); + + const boundary = 4; + const byteLength = Buffer.byteLength(string); + const remainder = byteLength % boundary; + const padding = (remainder === 0) ? 0 : boundary - remainder; + let whitespace = ''; + for (let i = 0; i < padding; ++i) { + whitespace += ' '; + } + string += whitespace; + + return Buffer.from(string); +} diff --git a/src/gltf2glb/getStatistics.js b/src/gltf2glb/getStatistics.js new file mode 100644 index 0000000..208b435 --- /dev/null +++ b/src/gltf2glb/getStatistics.js @@ -0,0 +1,180 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; +const isDataUri = Cesium.isDataUri; +const WebGLConstants = Cesium.WebGLConstants; + +module.exports = getStatistics; + +/** + * Returns an object containing the statistics for the glTF asset. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Number} [nodeId] If defined, statistics will only process number of draw calls and rendered primitives for the specified node. + * @returns {Statistics} Object containing the statistics of the glTF asset. + * + * @see Statistics + */ +function getStatistics(gltf, nodeId) { + const statistics = new Statistics(); + + if (defined(nodeId)) { + const nodeDrawStats = getDrawCallStatisticsForNode(gltf, nodeId); + statistics.numberOfDrawCalls = nodeDrawStats.numberOfDrawCalls; + statistics.numberOfRenderedPrimitives = nodeDrawStats.numberOfRenderedPrimitives; + return statistics; + } + + const drawStats = getDrawCallStatistics(gltf); + + statistics.buffersByteLength = getBuffersByteLength(gltf); + statistics.numberOfImages = defined(gltf.images) ? gltf.images.length : 0; + statistics.numberOfExternalRequests = getNumberOfExternalRequests(gltf); + statistics.numberOfDrawCalls = drawStats.numberOfDrawCalls; + statistics.numberOfRenderedPrimitives = drawStats.numberOfRenderedPrimitives; + statistics.numberOfNodes = defined(gltf.nodes) ? gltf.nodes.length : 0; + statistics.numberOfMeshes = defined(gltf.meshes) ? gltf.meshes.length : 0; + statistics.numberOfMaterials = defined(gltf.materials) ? gltf.materials.length : 0; + statistics.numberOfAnimations = defined(gltf.animations) ? gltf.animations.length : 0; + + return statistics; +} + +function getBuffersByteLength(gltf) { + let byteLength = 0; + ForEach.buffer(gltf, function(buffer) { + byteLength += buffer.byteLength; + }); + return byteLength; +} + +function getNumberOfExternalRequests(gltf) { + let count = 0; + ForEach.buffer(gltf, function(buffer) { + if (defined(buffer.uri) && !isDataUri(buffer.uri)) { + count++; + } + }); + ForEach.image(gltf, function(image) { + if (defined(image.uri) && !isDataUri(image.uri)) { + count++; + } + }); + ForEach.shader(gltf, function(shader) { + if (defined(shader.uri) && !isDataUri(shader.uri)) { + count++; + } + }); + return count; +} + +function getNumberOfRenderedPrimitives(gltf, primitive) { + let count = 0; + if (defined(primitive.indices)) { + count = gltf.accessors[primitive.indices].count; + } else if (defined(primitive.attributes.POSITION)) { + count = gltf.accessors[primitive.attributes.POSITION].count; + } + switch (primitive.mode) { + case WebGLConstants.POINTS: + return count; + case WebGLConstants.LINES: + return (count / 2); + case WebGLConstants.LINE_LOOP: + return count; + case WebGLConstants.LINE_STRIP: + return Math.max(count - 1, 0); + case WebGLConstants.TRIANGLES: + return (count / 3); + case WebGLConstants.TRIANGLE_STRIP: + case WebGLConstants.TRIANGLE_FAN: + return Math.max(count - 2, 0); + default: // TRIANGLES + return (count / 3); + } +} + +function getDrawCallStatisticsForNode(gltf, nodeId) { + let numberOfDrawCalls = 0; + let numberOfRenderedPrimitives = 0; + + ForEach.nodeInTree(gltf, [nodeId], function(node) { + const mesh = gltf.meshes[node.mesh]; + if (defined(mesh)) { + ForEach.meshPrimitive(mesh, function(primitive) { + numberOfDrawCalls++; + numberOfRenderedPrimitives += getNumberOfRenderedPrimitives(gltf, primitive); + }); + } + }); + + return { + numberOfDrawCalls: numberOfDrawCalls, + numberOfRenderedPrimitives: numberOfRenderedPrimitives + }; +} + +function getDrawCallStatistics(gltf) { + let numberOfDrawCalls = 0; + let numberOfRenderedPrimitives = 0; + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + numberOfDrawCalls++; + numberOfRenderedPrimitives += getNumberOfRenderedPrimitives(gltf, primitive); + }); + }); + + return { + numberOfDrawCalls: numberOfDrawCalls, + numberOfRenderedPrimitives: numberOfRenderedPrimitives + }; +} + +/** + * Contains statistics for a glTF asset. + * + * @property {Number} buffersByteLength The total byte length of all buffers. + * @property {Number} numberOfImages The number of images in the asset. + * @property {Number} numberOfExternalRequests The number of external requests required to fetch the asset data. + * @property {Number} numberOfDrawCalls The number of draw calls required to render the asset. + * @property {Number} numberOfRenderedPrimitives The total number of rendered primitives in the asset (e.g. triangles). + * @property {Number} numberOfNodes The total number of nodes in the asset. + * @property {Number} numberOfMeshes The total number of meshes in the asset. + * @property {Number} numberOfMaterials The total number of materials in the asset. + * @property {Number} numberOfAnimations The total number of animations in the asset. + * + * @constructor + * + * @see getStatistics + */ +function Statistics() { + this.buffersByteLength = 0; + this.numberOfImages = 0; + this.numberOfExternalRequests = 0; + this.numberOfDrawCalls = 0; + this.numberOfRenderedPrimitives = 0; + this.numberOfNodes = 0; + this.numberOfMeshes = 0; + this.numberOfMaterials = 0; + this.numberOfAnimations = 0; +} + +/** + * Creates a string listing the statistics along with their descriptions. + * + * @returns {String} A string describing the statistics for the glTF asset. + */ +Statistics.prototype.toString = function() { + return 'Total byte length of all buffers: ' + this.buffersByteLength + ' bytes' + + '\nImages: ' + this.numberOfImages + + '\nDraw calls: ' + this.numberOfDrawCalls + + '\nRendered primitives (e.g., triangles): ' + this.numberOfRenderedPrimitives + + '\nNodes: ' + this.numberOfNodes + + '\nMeshes: ' + this.numberOfMeshes + + '\nMaterials: ' + this.numberOfMaterials + + '\nAnimations: ' + this.numberOfAnimations + + '\nExternal requests (not data uris): ' + this.numberOfExternalRequests; +}; diff --git a/src/gltf2glb/glbToGltf.js b/src/gltf2glb/glbToGltf.js new file mode 100644 index 0000000..b6775e7 --- /dev/null +++ b/src/gltf2glb/glbToGltf.js @@ -0,0 +1,17 @@ +'use strict'; +const parseGlb = require('./parseGlb'); +const processGltf = require('./processGltf'); + +module.exports = glbToGltf; + +/** + * Convert a glb to glTF. + * + * @param {Buffer} glb A buffer containing the glb contents. + * @param {Object} [options] The same options object as {@link processGltf} + * @returns {Promise} A promise that resolves to an object containing a glTF asset. + */ +function glbToGltf(glb, options) { + const gltf = parseGlb(glb); + return processGltf(gltf, options); +} diff --git a/src/gltf2glb/gltfToGlb.js b/src/gltf2glb/gltfToGlb.js new file mode 100644 index 0000000..bd98f4d --- /dev/null +++ b/src/gltf2glb/gltfToGlb.js @@ -0,0 +1,76 @@ +'use strict'; +const Cesium = require('cesium'); +const getJsonBufferPadded = require('./getJsonBufferPadded'); +const processGltf = require('./processGltf'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; + +module.exports = gltfToGlb; + +/** + * Convert a glTF to glb. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} [options] The same options object as {@link processGltf} + * @returns {Promise} A promise that resolves to a buffer containing the glb contents. + */ +async function gltfToGlb(gltf, options) { + options = defaultValue(options, {}); + options.bufferStorage = { + buffer: undefined + }; + + const results = await processGltf(gltf, options); + const separateResources = results.separateResources; + + return { + glb: getGlb(results.gltf, options.bufferStorage.buffer), + separateResources + } +} + +function getGlb(gltf, binaryBuffer) { + const jsonBuffer = getJsonBufferPadded(gltf); + + // Compute glb length: (Global header) + (JSON chunk header) + (JSON chunk) + [(Binary chunk header) + (Binary chunk)] + let glbLength = 12 + 8 + jsonBuffer.length; + + if (defined(binaryBuffer)) { + glbLength += 8 + binaryBuffer.length; + } + + const glb = Buffer.alloc(glbLength); + + // Write binary glTF header (magic, version, length) + let byteOffset = 0; + glb.writeUInt32LE(0x46546C67, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(2, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(glbLength, byteOffset); + byteOffset += 4; + + // Write JSON Chunk header (length, type) + glb.writeUInt32LE(jsonBuffer.length, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(0x4E4F534A, byteOffset); // JSON + byteOffset += 4; + + // Write JSON Chunk + jsonBuffer.copy(glb, byteOffset); + byteOffset += jsonBuffer.length; + + if (defined(binaryBuffer)) { + // Write Binary Chunk header (length, type) + glb.writeUInt32LE(binaryBuffer.length, byteOffset); + byteOffset += 4; + glb.writeUInt32LE(0x004E4942, byteOffset); // BIN + byteOffset += 4; + + // Write Binary Chunk + binaryBuffer.copy(glb, byteOffset); + } + + return glb; +} diff --git a/src/gltf2glb/hasExtension.js b/src/gltf2glb/hasExtension.js new file mode 100644 index 0000000..f8ec6d6 --- /dev/null +++ b/src/gltf2glb/hasExtension.js @@ -0,0 +1,19 @@ +'use strict'; +const Cesium = require('cesium'); + +const defined = Cesium.defined; + +module.exports = hasExtension; + +/** + * Checks whether the glTF has the given extension. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The name of the extension. + * @returns {Boolean} Whether the glTF has the given extension. + * + * @private + */ +function hasExtension(gltf, extension) { + return defined(gltf.extensionsUsed) && (gltf.extensionsUsed.indexOf(extension) >= 0); +} diff --git a/src/gltf2glb/mergeBuffers.js b/src/gltf2glb/mergeBuffers.js new file mode 100644 index 0000000..7b739f9 --- /dev/null +++ b/src/gltf2glb/mergeBuffers.js @@ -0,0 +1,117 @@ +'use strict'; +const BUFFER_MAX_BYTE_LENGTH = require('buffer').constants.MAX_LENGTH; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; + +module.exports = mergeBuffers; + +/** + * Merge all buffers. Buffers with the same extras._pipeline.mergedBufferName will be merged together. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} [defaultName] The default name of the buffer data files. + * @returns {Object} The glTF asset with its buffers merged. + * + * @private + */ +function mergeBuffers(gltf, defaultName) { + let baseBufferName = defaultName; + ForEach.buffer(gltf, function(buffer) { + baseBufferName = defaultValue(buffer.name, baseBufferName); + }); + baseBufferName = defaultValue(baseBufferName, 'buffer'); + + let buffersByteLength = 0; + ForEach.buffer(gltf, function(buffer) { + buffersByteLength += buffer.extras._pipeline.source.length; + }); + + // Don't merge buffers if the merged buffer will exceed the Node limit. + const splitBuffers = (buffersByteLength > mergeBuffers._getBufferMaxByteLength()); + + const buffersToMerge = {}; + const mergedNameCount = {}; + + ForEach.bufferView(gltf, function(bufferView) { + const buffer = gltf.buffers[bufferView.buffer]; + let mergedName = buffer.extras._pipeline.mergedBufferName; + mergedName = defined(mergedName) ? baseBufferName + '-' + mergedName : baseBufferName; + + if (splitBuffers) { + if (!defined(mergedNameCount[mergedName])) { + mergedNameCount[mergedName] = 0; + } + mergedName += '-' + mergedNameCount[mergedName]++; + } + + if (!defined(buffersToMerge[mergedName])) { + buffersToMerge[mergedName] = { + buffers: [], + byteLength: 0, + index: Object.keys(buffersToMerge).length + }; + } + const buffers = buffersToMerge[mergedName].buffers; + let byteLength = buffersToMerge[mergedName].byteLength; + const index = buffersToMerge[mergedName].index; + + const sourceBufferViewData = Buffer.from(buffer.extras._pipeline.source.slice(bufferView.byteOffset, bufferView.byteOffset + bufferView.byteLength)); + const bufferViewPadding = allocateBufferPadding(byteLength); + if (defined(bufferViewPadding)) { + buffers.push(bufferViewPadding); + byteLength += bufferViewPadding.byteLength; + } + + bufferView.byteOffset = byteLength; + bufferView.buffer = index; + + buffers.push(sourceBufferViewData); + byteLength += sourceBufferViewData.byteLength; + + buffersToMerge[mergedName].byteLength = byteLength; + }); + + const buffersLength = Object.keys(buffersToMerge).length; + gltf.buffers = new Array(buffersLength); + + for (const mergedName in buffersToMerge) { + if (buffersToMerge.hasOwnProperty(mergedName)) { + const buffers = buffersToMerge[mergedName].buffers; + const byteLength = buffersToMerge[mergedName].byteLength; + const index = buffersToMerge[mergedName].index; + const bufferPadding = allocateBufferPadding(byteLength); + if (defined(bufferPadding)) { + buffers.push(bufferPadding); + } + const mergedSource = (buffers.length > 1) ? Buffer.concat(buffers) : buffers[0]; + gltf.buffers[index] = { + name: mergedName, + byteLength: mergedSource.byteLength, + extras: { + _pipeline: { + source: mergedSource + } + } + }; + } + } + + return gltf; +} + +function allocateBufferPadding(byteLength) { + const alignment = byteLength & 3; + if (alignment > 0) { + const bytesToPad = 4 - alignment; + return Buffer.alloc(bytesToPad); + } + return undefined; +} + +// Exposed for testing +mergeBuffers._getBufferMaxByteLength = function() { + return BUFFER_MAX_BYTE_LENGTH; +}; diff --git a/src/gltf2glb/moveTechniqueRenderStates.js b/src/gltf2glb/moveTechniqueRenderStates.js new file mode 100644 index 0000000..9573a1f --- /dev/null +++ b/src/gltf2glb/moveTechniqueRenderStates.js @@ -0,0 +1,133 @@ +'use strict'; + +const Cesium = require('cesium'); +const addExtensionsUsed = require('./addExtensionsUsed'); +const ForEach = require('./ForEach'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const WebGLConstants = Cesium.WebGLConstants; + +module.exports = moveTechniqueRenderStates; + +const defaultBlendEquation = [ + WebGLConstants.FUNC_ADD, + WebGLConstants.FUNC_ADD +]; + +const defaultBlendFactors = [ + WebGLConstants.ONE, + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.ZERO +]; + +function isStateEnabled(renderStates, state) { + const enabled = renderStates.enable; + if (!defined(enabled)) { + return false; + } + + return (enabled.indexOf(state) > -1); +} + +const supportedBlendFactors = [ + WebGLConstants.ZERO, + WebGLConstants.ONE, + WebGLConstants.SRC_COLOR, + WebGLConstants.ONE_MINUS_SRC_COLOR, + WebGLConstants.SRC_ALPHA, + WebGLConstants.ONE_MINUS_SRC_ALPHA, + WebGLConstants.DST_ALPHA, + WebGLConstants.ONE_MINUS_DST_ALPHA, + WebGLConstants.DST_COLOR, + WebGLConstants.ONE_MINUS_DST_COLOR +]; + +// If any of the blend factors are not supported, return the default +function getSupportedBlendFactors(value, defaultValue) { + if (!defined(value)) { + return defaultValue; + } + + for (let i = 0; i < 4; i++) { + if (supportedBlendFactors.indexOf(value[i]) === -1) { + return defaultValue; + } + } + + return value; +} + +/** + * Move glTF 1.0 technique render states to glTF 2.0 materials properties and KHR_blend extension. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The updated glTF asset. + * + * @private + */ +function moveTechniqueRenderStates(gltf) { + const blendingForTechnique = {}; + const materialPropertiesForTechnique = {}; + const techniquesLegacy = gltf.techniques; + if (!defined(techniquesLegacy)) { + return gltf; + } + + ForEach.technique(gltf, function (techniqueLegacy, techniqueIndex) { + const renderStates = techniqueLegacy.states; + if (defined(renderStates)) { + const materialProperties = materialPropertiesForTechnique[techniqueIndex] = {}; + + // If BLEND is enabled, the material should have alpha mode BLEND + if (isStateEnabled(renderStates, WebGLConstants.BLEND)) { + materialProperties.alphaMode = 'BLEND'; + + const blendFunctions = renderStates.functions; + if (defined(blendFunctions) && (defined(blendFunctions.blendEquationSeparate) + || defined(blendFunctions.blendFuncSeparate))) { + blendingForTechnique[techniqueIndex] = { + blendEquation: defaultValue(blendFunctions.blendEquationSeparate, defaultBlendEquation), + blendFactors: getSupportedBlendFactors(blendFunctions.blendFuncSeparate, defaultBlendFactors) + }; + } + } + + // If CULL_FACE is not enabled, the material should be doubleSided + if (!isStateEnabled(renderStates, WebGLConstants.CULL_FACE)) { + materialProperties.doubleSided = true; + } + + delete techniqueLegacy.states; + } + }); + + if (Object.keys(blendingForTechnique).length > 0) { + if (!defined(gltf.extensions)) { + gltf.extensions = {}; + } + + addExtensionsUsed(gltf, 'KHR_blend'); + } + + ForEach.material(gltf, function (material) { + if (defined(material.technique)) { + const materialProperties = materialPropertiesForTechnique[material.technique]; + ForEach.objectLegacy(materialProperties, function (value, property) { + material[property] = value; + }); + + const blending = blendingForTechnique[material.technique]; + if (defined(blending)) { + if (!defined(material.extensions)) { + material.extensions = {}; + } + + material.extensions.KHR_blend = blending; + } + } + }); + + return gltf; +} diff --git a/src/gltf2glb/moveTechniquesToExtension.js b/src/gltf2glb/moveTechniquesToExtension.js new file mode 100644 index 0000000..df6d82e --- /dev/null +++ b/src/gltf2glb/moveTechniquesToExtension.js @@ -0,0 +1,128 @@ +'use strict'; + +const Cesium = require('cesium'); +const addExtensionsUsed = require('./addExtensionsUsed'); +const addExtensionsRequired = require('./addExtensionsRequired'); +const addToArray = require('./addToArray'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; + +module.exports = moveTechniquesToExtension; + +/** + * Move glTF 1.0 material techniques to glTF 2.0 KHR_techniques_webgl extension. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The updated glTF asset. + * + * @private + */ +function moveTechniquesToExtension(gltf) { + const techniquesLegacy = gltf.techniques; + const mappedUniforms = {}; + const updatedTechniqueIndices = {}; + if (defined(techniquesLegacy)) { + const extension = { + programs: [], + shaders: [], + techniques: [] + }; + + // Some 1.1 models have a glExtensionsUsed property that can be transferred to program.glExtensions + const glExtensions = gltf.glExtensionsUsed; + delete gltf.glExtensionsUsed; + + ForEach.technique(gltf, function (techniqueLegacy, techniqueIndex) { + const technique = { + name: techniqueLegacy.name, + program: undefined, + attributes: {}, + uniforms: {} + }; + + let parameterLegacy; + ForEach.techniqueAttribute(techniqueLegacy, function (parameterName, attributeName) { + parameterLegacy = techniqueLegacy.parameters[parameterName]; + technique.attributes[attributeName] = { + semantic: parameterLegacy.semantic + }; + }); + + ForEach.techniqueUniform(techniqueLegacy, function (parameterName, uniformName) { + parameterLegacy = techniqueLegacy.parameters[parameterName]; + technique.uniforms[uniformName] = { + count: parameterLegacy.count, + node: parameterLegacy.node, + type: parameterLegacy.type, + semantic: parameterLegacy.semantic, + value: parameterLegacy.value + }; + + // Store the name of the uniform to update material values. + mappedUniforms[parameterName] = uniformName; + }); + + const programLegacy = gltf.programs[techniqueLegacy.program]; + const program = { + name: programLegacy.name, + fragmentShader: undefined, + vertexShader: undefined, + glExtensions: glExtensions + }; + + const fs = gltf.shaders[programLegacy.fragmentShader]; + program.fragmentShader = addToArray(extension.shaders, fs, true); + + const vs = gltf.shaders[programLegacy.vertexShader]; + program.vertexShader = addToArray(extension.shaders, vs, true); + + technique.program = addToArray(extension.programs, program); + + // Store the index of the new technique to reference instead. + updatedTechniqueIndices[techniqueIndex] = addToArray(extension.techniques, technique); + }); + + if (extension.techniques.length > 0) { + if (!defined(gltf.extensions)) { + gltf.extensions = {}; + } + + gltf.extensions.KHR_techniques_webgl = extension; + addExtensionsUsed(gltf, 'KHR_techniques_webgl'); + addExtensionsRequired(gltf, 'KHR_techniques_webgl'); + } + } + + ForEach.material(gltf, function (material) { + if (defined(material.technique)) { + const materialExtension = { + technique: updatedTechniqueIndices[material.technique] + }; + + ForEach.objectLegacy(material.values, function (value, parameterName) { + if (!defined(materialExtension.values)) { + materialExtension.values = {}; + } + + const uniformName = mappedUniforms[parameterName]; + materialExtension.values[uniformName] = value; + }); + + if (!defined(material.extensions)) { + material.extensions = {}; + } + + material.extensions.KHR_techniques_webgl = materialExtension; + } + + delete material.technique; + delete material.values; + }); + + delete gltf.techniques; + delete gltf.programs; + delete gltf.shaders; + + return gltf; +} diff --git a/src/gltf2glb/numberOfComponentsForType.js b/src/gltf2glb/numberOfComponentsForType.js new file mode 100644 index 0000000..e5bd63a --- /dev/null +++ b/src/gltf2glb/numberOfComponentsForType.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = numberOfComponentsForType; + +/** + * Utility function for retrieving the number of components in a given type. + * + * @param {String} type glTF type + * @returns {Number} The number of components in that type. + * + * @private + */ +function numberOfComponentsForType(type) { + switch (type) { + case 'SCALAR': + return 1; + case 'VEC2': + return 2; + case 'VEC3': + return 3; + case 'VEC4': + case 'MAT2': + return 4; + case 'MAT3': + return 9; + case 'MAT4': + return 16; + } +} diff --git a/src/gltf2glb/parseGlb.js b/src/gltf2glb/parseGlb.js new file mode 100644 index 0000000..d11119d --- /dev/null +++ b/src/gltf2glb/parseGlb.js @@ -0,0 +1,118 @@ +'use strict'; +const Cesium = require('cesium'); +const addPipelineExtras = require('./addPipelineExtras'); +const removeExtensionsUsed = require('./removeExtensionsUsed'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const getMagic = Cesium.getMagic; +const getStringFromTypedArray = Cesium.getStringFromTypedArray; +const RuntimeError = Cesium.RuntimeError; + +const sizeOfUint32 = 4; + +module.exports = parseGlb; + +/** + * Convert a binary glTF to glTF. + * + * The returned glTF has pipeline extras included. The embedded binary data is stored in gltf.buffers[0].extras._pipeline.source. + * + * @param {Buffer} glb The glb data to parse. + * @returns {Object} A javascript object containing a glTF asset with pipeline extras included. + * + * @private + */ +function parseGlb(glb) { + // Check that the magic string is present + const magic = getMagic(glb); + if (magic !== 'glTF') { + throw new RuntimeError('File is not valid binary glTF'); + } + + const header = readHeader(glb, 0, 5); + const version = header[1]; + if (version !== 1 && version !== 2) { + throw new RuntimeError('Binary glTF version is not 1 or 2'); + } + + if (version === 1) { + return parseGlbVersion1(glb, header); + } + + return parseGlbVersion2(glb, header); +} + +function readHeader(glb, byteOffset, count) { + const dataView = new DataView(glb.buffer); + const header = new Array(count); + for (let i = 0; i < count; ++i) { + header[i] = dataView.getUint32(glb.byteOffset + byteOffset + i * sizeOfUint32, true); + } + return header; +} + +function parseGlbVersion1(glb, header) { + const length = header[2]; + const contentLength = header[3]; + const contentFormat = header[4]; + + // Check that the content format is 0, indicating that it is JSON + if (contentFormat !== 0) { + throw new RuntimeError('Binary glTF scene format is not JSON'); + } + + const jsonStart = 20; + const binaryStart = jsonStart + contentLength; + + const contentString = getStringFromTypedArray(glb, jsonStart, contentLength); + const gltf = JSON.parse(contentString); + addPipelineExtras(gltf); + + const binaryBuffer = glb.subarray(binaryStart, length); + + const buffers = gltf.buffers; + if (defined(buffers) && Object.keys(buffers).length > 0) { + // In some older models, the binary glTF buffer is named KHR_binary_glTF + const binaryGltfBuffer = defaultValue(buffers.binary_glTF, buffers.KHR_binary_glTF); + if (defined(binaryGltfBuffer)) { + binaryGltfBuffer.extras._pipeline.source = binaryBuffer; + } + } + // Remove the KHR_binary_glTF extension + removeExtensionsUsed(gltf, 'KHR_binary_glTF'); + return gltf; +} + +function parseGlbVersion2(glb, header) { + const length = header[2]; + let byteOffset = 12; + let gltf; + let binaryBuffer; + while (byteOffset < length) { + const chunkHeader = readHeader(glb, byteOffset, 2); + const chunkLength = chunkHeader[0]; + const chunkType = chunkHeader[1]; + byteOffset += 8; + const chunkBuffer = glb.subarray(byteOffset, byteOffset + chunkLength); + byteOffset += chunkLength; + // Load JSON chunk + if (chunkType === 0x4E4F534A) { + const jsonString = getStringFromTypedArray(chunkBuffer); + gltf = JSON.parse(jsonString); + addPipelineExtras(gltf); + } + // Load Binary chunk + else if (chunkType === 0x004E4942) { + binaryBuffer = chunkBuffer; + } + } + if (defined(gltf) && defined(binaryBuffer)) { + const buffers = gltf.buffers; + if (defined(buffers) && buffers.length > 0) { + const buffer = buffers[0]; + buffer.extras._pipeline.source = binaryBuffer; + } + } + return gltf; +} diff --git a/src/gltf2glb/processGlb.js b/src/gltf2glb/processGlb.js new file mode 100644 index 0000000..67a5f54 --- /dev/null +++ b/src/gltf2glb/processGlb.js @@ -0,0 +1,17 @@ +'use strict'; +const parseGlb = require('./parseGlb'); +const gltfToGlb = require('./gltfToGlb'); + +module.exports = processGlb; + +/** + * Run a glb through the gltf-pipeline. + * + * @param {Buffer} glb A buffer containing the glb contents. + * @param {Object} [options] The same options object as {@link processGltf} + * @returns {Promise} A promise that resolves to a buffer containing the glb contents. + */ +function processGlb(glb, options) { + const gltf = parseGlb(glb); + return gltfToGlb(gltf, options); +} diff --git a/src/gltf2glb/processGltf.js b/src/gltf2glb/processGltf.js new file mode 100644 index 0000000..95fb403 --- /dev/null +++ b/src/gltf2glb/processGltf.js @@ -0,0 +1,143 @@ +'use strict'; +const Cesium = require('cesium'); +const Promise = require('bluebird'); +const addDefaults = require('./addDefaults'); +const addPipelineExtras = require('./addPipelineExtras'); +const getStatistics = require('./getStatistics'); +const readResources = require('./readResources'); +const removeDefaults = require('./removeDefaults'); +const removePipelineExtras = require('./removePipelineExtras'); +const updateVersion = require('./updateVersion'); +const writeResources = require('./writeResources'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; + +module.exports = processGltf; + +/** + * Run a glTF through the gltf-pipeline. + * + * @param {Object} gltf A javascript object containing a glTF asset. The glTF is modified in place. + * @param {Object} [options] An object with the following properties: + * @param {String} [options.resourceDirectory] The path for reading separate resources. + * @param {String} [options.name] The name of the glTF asset, for writing separate resources. + * @param {Boolean} [options.separate = false] Write separate buffers, shaders, and textures instead of embedding them in the glTF. + * @param {Boolean} [options.separateTextures = false] Write out separate textures only. + * @param {String => Boolean} [options.separateCustom = null] A callback to decide if this file should be separated. + * @param {Boolean} [options.stats = false] Print statistics to console for input and output glTF files. + * @param {Object} [options.dracoOptions] Options to pass to the compressDracoMeshes stage. If undefined, stage is not run. + * @param {Stage[]} [options.customStages] Custom stages to run on the glTF asset. + * @param {Logger} [options.logger] A callback function for handling logged messages. Defaults to console.log. + * + * @returns {Promise} A promise that resolves to the processed glTF and a dictionary containing separate resources. + */ +function processGltf(gltf, options) { + const defaults = processGltf.defaults; + options = defaultValue(options, {}); + options.separateBuffers = defaultValue(options.separate, defaults.separate); + options.separateShaders = defaultValue(options.separate, defaults.separate); + options.separateTextures = defaultValue(options.separateTextures, defaults.separateTextures) || options.separate; + options.stats = defaultValue(options.stats, defaults.stats); + options.logger = defaultValue(options.logger, getDefaultLogger()); + options.separateResources = {}; + options.separateCustom = defaultValue(options.separateCustom, defaults.separateCustom); + options.prepareNonSeparateResources = defaultValue(options.prepareNonSeparateResources, defaults.prepareNonSeparateResources); + options.processSeparateResource = defaultValue(options.processSeparateResource, defaults.processSeparateResource); + options.customStages = defaultValue(options.customStages, []); + + const preStages = [ + addPipelineExtras, + readResources, + updateVersion, + addDefaults + ]; + + const postStages = [ + writeResources, + removePipelineExtras, + removeDefaults + ]; + + const pipelineStages = getStages(options); + const stages = preStages.concat(options.customStages, pipelineStages, postStages); + + return Promise.each(stages, function(stage) { + return stage(gltf, options); + }).then(function() { + printStats(gltf, options, true); + return { + gltf: gltf, + separateResources: options.separateResources + }; + }); +} + +function printStats(gltf, options, processed) { + if (options.stats) { + options.logger(processed ? 'Statistics after:' : 'Statistics before:'); + options.logger(getStatistics(gltf).toString()); + } +} + +function getStages(options) { + const stages = []; + // if (defined(options.dracoOptions)) { + // stages.push(compressDracoMeshes); + // } + return stages; +} + +function getDefaultLogger() { + return function(message) { + console.log(message); + }; +} + +/** + * Default values that will be used when calling processGltf(options) unless specified in the options object. + */ +processGltf.defaults = { + /** + * Gets or sets whether to write out separate buffers, shaders, and textures instead of embedding them in the glTF + * @type Boolean + * @default false + */ + separate: false, + /** + * Gets or sets whether to write out separate textures only. + * @type Boolean + * @default false + */ + separateTextures: false, + /** + * Gets or sets whether to print statistics to console for input and output glTF files. + * @type Boolean + * @default false + */ + stats: false, + /** + * Gets or sets whether to compress the meshes using Draco. Adds the KHR_draco_mesh_compression extension. + * @type Boolean + * @default false + */ + // compressDracoMeshes: false, + separateCustom: null, + prepareNonSeparateResources: null, + processSeparateResource: null +}; + +/** + * A callback function that logs messages. + * @callback Logger + * + * @param {String} message The message to log. + */ + +/** + * A stage that processes a glTF asset. + * @callback Stage + * + * @param {Object} gltf The glTF asset. + * @returns {Promise|Object} The glTF asset or a promise that resolves to the glTF asset. + */ diff --git a/src/gltf2glb/readAccessorPacked.js b/src/gltf2glb/readAccessorPacked.js new file mode 100644 index 0000000..a18529d --- /dev/null +++ b/src/gltf2glb/readAccessorPacked.js @@ -0,0 +1,50 @@ +'use strict'; +const Cesium = require('cesium'); +const getAccessorByteStride = require('./getAccessorByteStride'); +const getComponentReader = require('./getComponentReader'); +const numberOfComponentsForType = require('./numberOfComponentsForType'); + +const arrayFill = Cesium.arrayFill; +const ComponentDatatype = Cesium.ComponentDatatype; +const defined = Cesium.defined; + +module.exports = readAccessorPacked; + +/** + * Returns the accessor data in a contiguous array. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} accessor The accessor. + * @returns {Array} The accessor values in a contiguous array. + * + * @private + */ +function readAccessorPacked(gltf, accessor) { + const byteStride = getAccessorByteStride(gltf, accessor); + const componentTypeByteLength = ComponentDatatype.getSizeInBytes(accessor.componentType); + const numberOfComponents = numberOfComponentsForType(accessor.type); + const count = accessor.count; + const values = new Array(numberOfComponents * count); + + if (!defined(accessor.bufferView)) { + arrayFill(values, 0); + return values; + } + + const bufferView = gltf.bufferViews[accessor.bufferView]; + const source = gltf.buffers[bufferView.buffer].extras._pipeline.source; + let byteOffset = accessor.byteOffset + bufferView.byteOffset + source.byteOffset; + + const dataView = new DataView(source.buffer); + const components = new Array(numberOfComponents); + const componentReader = getComponentReader(accessor.componentType); + + for (let i = 0; i < count; ++i) { + componentReader(dataView, byteOffset, numberOfComponents, componentTypeByteLength, components); + for (let j = 0; j < numberOfComponents; ++j) { + values[i * numberOfComponents + j] = components[j]; + } + byteOffset += byteStride; + } + return values; +} diff --git a/src/gltf2glb/readResources.js b/src/gltf2glb/readResources.js new file mode 100644 index 0000000..bc6e77c --- /dev/null +++ b/src/gltf2glb/readResources.js @@ -0,0 +1,168 @@ +'use strict'; +const Cesium = require('cesium'); +const fs = require('fs'); +const path = require('path'); +const Promise = require('bluebird'); +const { URL } = require('url'); + +const addPipelineExtras = require('./addPipelineExtras'); +const dataUriToBuffer = require('./dataUriToBuffer'); +const { fileURLToPath, pathToFileURL } = require('./FileUrl'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; +const defaultValue = Cesium.defaultValue; +const isDataUri = Cesium.isDataUri; +const RuntimeError = Cesium.RuntimeError; + +module.exports = readResources; + +/** + * Read data uris, buffer views, or files referenced by the glTF into buffers. + * The buffer data is placed into extras._pipeline.source for the corresponding object. + * This stage runs before updateVersion and handles both glTF 1.0 and glTF 2.0 assets. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} [options] Object with the following properties: + * @param {String} [options.resourceDirectory] The path to look in when reading separate files. + * @returns {Promise} A promise that resolves to the glTF asset when all resources are read. + * + * @private + */ +function readResources(gltf, options) { + addPipelineExtras(gltf); + options = defaultValue(options, {}); + + // Make sure its an absolute path with a trailing separator + options.resourceDirectory = defined(options.resourceDirectory) ? + (path.resolve(options.resourceDirectory) + path.sep) : undefined; + + const bufferPromises = []; + const resourcePromises = []; + + ForEach.buffer(gltf, function(buffer) { + bufferPromises.push(readBuffer(gltf, buffer, options)); + }); + + // Buffers need to be read first because images and shader may resolve to bufferViews + return Promise.all(bufferPromises) + .then(function() { + ForEach.shader(gltf, function (shader) { + resourcePromises.push(readShader(gltf, shader, options)); + }); + ForEach.image(gltf, function (image) { + resourcePromises.push(readImage(gltf, image, options)); + ForEach.compressedImage(image, function(compressedImage) { + resourcePromises.push(readImage(gltf, compressedImage, options)); + }); + }); + ForEach.audioClip(gltf, function (clip) { + resourcePromises.push(readAudioClip(gltf, clip, options)); + }); + return Promise.all(resourcePromises); + }) + .then(function() { + return gltf; + }); +} + +function readBuffer(gltf, buffer, options) { + return readResource(gltf, buffer, options) + .then(function(data) { + buffer.extras._pipeline.source = data; + }); +} + +function readImage(gltf, image, options) { + return readResource(gltf, image, options) + .then(function(data) { + image.extras._pipeline.source = data; + }); +} + +function readShader(gltf, shader, options) { + return readResource(gltf, shader, options) + .then(function(data) { + shader.extras._pipeline.source = data.toString(); + }); +} + +function readAudioClip(gltf, audioClip, options) { + audioClip.extras = {_pipeline: {}}; + const ext = path.extname(audioClip.uri); + return readResource(gltf, audioClip, options) + .then(function(data) { + audioClip.extras._pipeline.source = data; + audioClip.extras._pipeline.ext = ext; + }); +} + +function readResource(gltf, object, options) { + const uri = object.uri; + delete object.uri; // Don't hold onto the uri, its contents will be stored in extras._pipeline.source + + // Source already exists if the gltf was converted from a glb + const source = object.extras._pipeline.source; + if (defined(source)) { + return Promise.resolve(Buffer.from(source)); + } + // Handle reading buffer view from 1.0 glb model + const extensions = object.extensions; + if (defined(extensions)) { + const khrBinaryGltf = extensions.KHR_binary_glTF; + if (defined(khrBinaryGltf)) { + return Promise.resolve(readBufferView(gltf, khrBinaryGltf.bufferView)); + } + } + if (defined(object.bufferView)) { + return Promise.resolve(readBufferView(gltf, object.bufferView)); + } + if (isDataUri(uri)) { + return Promise.resolve(dataUriToBuffer(uri)); + } + return readFile(object, uri, options); +} + +function readBufferView(gltf, bufferViewId) { + const bufferView = gltf.bufferViews[bufferViewId]; + const buffer = gltf.buffers[bufferView.buffer]; + const source = buffer.extras._pipeline.source; + const byteOffset = defaultValue(bufferView.byteOffset, 0); + return source.slice(byteOffset, byteOffset + bufferView.byteLength); +} + +function readFile(object, uri, options) { + const resourceDirectory = options.resourceDirectory; + const hasResourceDirectory = defined(resourceDirectory); + + // Resolve the URL + let absoluteUrl; + try { + absoluteUrl = new URL(uri, + hasResourceDirectory ? pathToFileURL(resourceDirectory) : undefined); + } catch(error) { + return Promise.reject(new RuntimeError('glTF model references separate files but no resourceDirectory is supplied')); + } + + // Generate file paths for the resource + const absolutePath = fileURLToPath(absoluteUrl); + const relativePath = hasResourceDirectory ? path.relative(resourceDirectory, absolutePath) : path.basename(absolutePath); + + if (!defined(object.name)) { + const extension = path.extname(relativePath); + object.name = path.basename(relativePath, extension); + } + + object.extras._pipeline.absolutePath = absolutePath; + object.extras._pipeline.relativePath = relativePath; + + return new Promise((resolve, reject) => { + fs.readFile(absolutePath, (err, data) => { + if (err) { + return reject(err); + } + + resolve(data); + }) + }); +} diff --git a/src/gltf2glb/removeDefaults.js b/src/gltf2glb/removeDefaults.js new file mode 100644 index 0000000..4c5ae05 --- /dev/null +++ b/src/gltf2glb/removeDefaults.js @@ -0,0 +1,30 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; +const Matrix4 = Cesium.Matrix4; + +module.exports = removeDefaults; + +/** + * Remove default values from the glTF. Not exhaustive. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} glTF with default values removed. + * + * @private + */ +function removeDefaults(gltf) { + ForEach.node(gltf, function(node) { + if (defined(node.matrix) && Matrix4.equals(Matrix4.fromArray(node.matrix), Matrix4.IDENTITY)) { + delete node.matrix; + } + }); + ForEach.accessor(gltf, function(accessor) { + if (accessor.normalized === false) { + delete accessor.normalized; + } + }); + return gltf; +} diff --git a/src/gltf2glb/removeExtension.js b/src/gltf2glb/removeExtension.js new file mode 100644 index 0000000..750f54c --- /dev/null +++ b/src/gltf2glb/removeExtension.js @@ -0,0 +1,64 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); +const removeExtensionsUsed = require('./removeExtensionsUsed'); + +const defined = Cesium.defined; +const isArray = Cesium.isArray; + +module.exports = removeExtension; + +/** + * Removes an extension from gltf.extensions, gltf.extensionsUsed, gltf.extensionsRequired, and any other objects in the glTF if it is present. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The extension to remove. + * + * @returns {*} The extension data removed from gltf.extensions. + */ +function removeExtension(gltf, extension) { + removeExtensionsUsed(gltf, extension); // Also removes from extensionsRequired + + if (extension === 'CESIUM_RTC') { + removeCesiumRTC(gltf); + } + + return removeExtensionAndTraverse(gltf, extension); +} + +function removeCesiumRTC(gltf) { + ForEach.technique(gltf, function(technique) { + ForEach.techniqueUniform(technique, function(uniform) { + if (uniform.semantic === 'CESIUM_RTC_MODELVIEW') { + uniform.semantic = 'MODELVIEW'; + } + }); + }); +} + +function removeExtensionAndTraverse(object, extension) { + if (isArray(object)) { + const length = object.length; + for (let i = 0; i < length; ++i) { + removeExtensionAndTraverse(object[i], extension); + } + } else if (object !== null && typeof object === 'object' && object.constructor === Object) { + const extensions = object.extensions; + let extensionData; + if (defined(extensions)) { + extensionData = extensions[extension]; + if (defined(extensionData)) { + delete extensions[extension]; + if (Object.keys(extensions).length === 0) { + delete object.extensions; + } + } + } + for (const key in object) { + if (object.hasOwnProperty(key)) { + removeExtensionAndTraverse(object[key], extension); + } + } + return extensionData; + } +} diff --git a/src/gltf2glb/removeExtensionsRequired.js b/src/gltf2glb/removeExtensionsRequired.js new file mode 100644 index 0000000..38b3d8b --- /dev/null +++ b/src/gltf2glb/removeExtensionsRequired.js @@ -0,0 +1,27 @@ +'use strict'; +const Cesium = require('cesium'); + +const defined = Cesium.defined; + +module.exports = removeExtensionsRequired; + +/** + * Removes an extension from gltf.extensionsRequired if it is present. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The extension to remove. + * + * @private + */ +function removeExtensionsRequired(gltf, extension) { + const extensionsRequired = gltf.extensionsRequired; + if (defined(extensionsRequired)) { + const index = extensionsRequired.indexOf(extension); + if (index >= 0) { + extensionsRequired.splice(index, 1); + } + if (extensionsRequired.length === 0) { + delete gltf.extensionsRequired; + } + } +} diff --git a/src/gltf2glb/removeExtensionsUsed.js b/src/gltf2glb/removeExtensionsUsed.js new file mode 100644 index 0000000..a0e3d17 --- /dev/null +++ b/src/gltf2glb/removeExtensionsUsed.js @@ -0,0 +1,29 @@ +'use strict'; +const Cesium = require('cesium'); +const removeExtensionsRequired = require('./removeExtensionsRequired'); + +const defined = Cesium.defined; + +module.exports = removeExtensionsUsed; + +/** + * Removes an extension from gltf.extensionsUsed and gltf.extensionsRequired if it is present. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {String} extension The extension to remove. + * + * @private + */ +function removeExtensionsUsed(gltf, extension) { + const extensionsUsed = gltf.extensionsUsed; + if (defined(extensionsUsed)) { + const index = extensionsUsed.indexOf(extension); + if (index >= 0) { + extensionsUsed.splice(index, 1); + } + removeExtensionsRequired(gltf, extension); + if (extensionsUsed.length === 0) { + delete gltf.extensionsUsed; + } + } +} diff --git a/src/gltf2glb/removePipelineExtras.js b/src/gltf2glb/removePipelineExtras.js new file mode 100644 index 0000000..bebe806 --- /dev/null +++ b/src/gltf2glb/removePipelineExtras.js @@ -0,0 +1,51 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); + +const defined = Cesium.defined; + +module.exports = removePipelineExtras; + +/** + * Iterate through the objects within the glTF and delete their pipeline extras object. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} glTF with no pipeline extras. + * + * @private + */ +function removePipelineExtras(gltf) { + ForEach.shader(gltf, function(shader) { + removeExtras(shader); + }); + ForEach.buffer(gltf, function(buffer) { + removeExtras(buffer); + }); + ForEach.audioClip(gltf, function(buffer) { + removeExtras(buffer); + }); + ForEach.image(gltf, function (image) { + removeExtras(image); + ForEach.compressedImage(image, function(compressedImage) { + removeExtras(compressedImage); + }); + }); + + removeExtras(gltf); + + return gltf; +} + +function removeExtras(object) { + if (!defined(object.extras)) { + return; + } + + if (defined(object.extras._pipeline)) { + delete object.extras._pipeline; + } + + if (Object.keys(object.extras).length === 0) { + delete object.extras; + } +} diff --git a/src/gltf2glb/removeUnusedElements.js b/src/gltf2glb/removeUnusedElements.js new file mode 100644 index 0000000..bca2433 --- /dev/null +++ b/src/gltf2glb/removeUnusedElements.js @@ -0,0 +1,291 @@ +'use strict'; +const Cesium = require('cesium'); +const ForEach = require('./ForEach'); +const hasExtension = require('./hasExtension'); + +const defined = Cesium.defined; + +module.exports = removeUnusedElements; + +/** + * Removes unused elements from gltf. + * This function currently only works for accessors, buffers, and bufferViews. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * + * @private + */ +function removeUnusedElements(gltf) { + removeUnusedElementsByType(gltf, 'accessor'); + removeUnusedElementsByType(gltf, 'bufferView'); + removeUnusedElementsByType(gltf, 'buffer'); + return gltf; +} + +const TypeToGltfElementName = { + accessor: 'accessors', + buffer: 'buffers', + bufferView: 'bufferViews' +}; + +function removeUnusedElementsByType(gltf, type) { + const name = TypeToGltfElementName[type]; + const arrayOfObjects = gltf[name]; + + if (defined(arrayOfObjects)) { + let removed = 0; + const usedIds = getListOfElementsIdsInUse[type](gltf); + const length = arrayOfObjects.length; + + for (let i = 0; i < length; ++i) { + if (!usedIds[i]) { + Remove[type](gltf, i - removed); + removed++; + } + } + } +} + +/** + * Contains functions for removing elements from a glTF hierarchy. + * Since top-level glTF elements are arrays, when something is removed, referring + * indices need to be updated. + * @constructor + * + * @private + */ +function Remove() {} + +Remove.accessor = function(gltf, accessorId) { + const accessors = gltf.accessors; + + accessors.splice(accessorId, 1); + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + // Update accessor ids for the primitives. + ForEach.meshPrimitiveAttribute(primitive, function(attributeAccessorId, semantic) { + if (attributeAccessorId > accessorId) { + primitive.attributes[semantic]--; + } + }); + + // Update accessor ids for the targets. + ForEach.meshPrimitiveTarget(primitive, function(target) { + ForEach.meshPrimitiveTargetAttribute(target, function(attributeAccessorId, semantic) { + if (attributeAccessorId > accessorId) { + target[semantic]--; + } + }); + }); + const indices = primitive.indices; + if (defined(indices) && indices > accessorId) { + primitive.indices--; + } + }); + }); + + ForEach.skin(gltf, function(skin) { + if (defined(skin.inverseBindMatrices) && skin.inverseBindMatrices > accessorId) { + skin.inverseBindMatrices--; + } + }); + + ForEach.animation(gltf, function(animation) { + ForEach.animationSampler(animation, function(sampler) { + if (defined(sampler.input) && sampler.input > accessorId) { + sampler.input--; + } + if (defined(sampler.output) && sampler.output > accessorId) { + sampler.output--; + } + }); + }); +}; + +Remove.buffer = function(gltf, bufferId) { + const buffers = gltf.buffers; + + buffers.splice(bufferId, 1); + + ForEach.bufferView(gltf, function(bufferView) { + if (defined(bufferView.buffer) && bufferView.buffer > bufferId) { + bufferView.buffer--; + } + }); +}; + +Remove.bufferView = function(gltf, bufferViewId) { + const bufferViews = gltf.bufferViews; + + bufferViews.splice(bufferViewId, 1); + + ForEach.accessor(gltf, function(accessor) { + if (defined(accessor.bufferView) && accessor.bufferView > bufferViewId) { + accessor.bufferView--; + } + }); + + ForEach.shader(gltf, function(shader) { + if (defined(shader.bufferView) && shader.bufferView > bufferViewId) { + shader.bufferView--; + } + }); + + ForEach.image(gltf, function(image) { + if (defined(image.bufferView) && image.bufferView > bufferViewId) { + image.bufferView--; + } + ForEach.compressedImage(image, function(compressedImage) { + const compressedImageBufferView = compressedImage.bufferView; + if (defined(compressedImageBufferView) && compressedImageBufferView > bufferViewId) { + compressedImage.bufferView--; + } + }); + }); + + if (hasExtension(gltf, 'KHR_draco_mesh_compression')) { + ForEach.mesh(gltf, function (mesh) { + ForEach.meshPrimitive(mesh, function (primitive) { + if (defined(primitive.extensions) && + defined(primitive.extensions.KHR_draco_mesh_compression)) { + if (primitive.extensions.KHR_draco_mesh_compression.bufferView > bufferViewId) { + primitive.extensions.KHR_draco_mesh_compression.bufferView--; + } + } + }); + }); + } + + if (hasExtension(gltf, 'ALI_amc_mesh_compression')) { + ForEach.mesh(gltf, function (mesh) { + ForEach.meshPrimitive(mesh, function (primitive) { + if (defined(primitive.extensions) && + defined(primitive.extensions.ALI_amc_mesh_compression)) { + if (primitive.extensions.ALI_amc_mesh_compression.bufferView > bufferViewId) { + primitive.extensions.ALI_amc_mesh_compression.bufferView--; + } + } + }); + }); + } +}; + +/** + * Contains functions for getting a list of element ids in use by the glTF asset. + * @constructor + * + * @private + */ +function getListOfElementsIdsInUse() {} + +getListOfElementsIdsInUse.accessor = function(gltf) { + // Calculate accessor's that are currently in use. + const usedAccessorIds = {}; + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + ForEach.meshPrimitiveAttribute(primitive, function(accessorId) { + usedAccessorIds[accessorId] = true; + }); + ForEach.meshPrimitiveTarget(primitive, function(target) { + ForEach.meshPrimitiveTargetAttribute(target, function(accessorId) { + usedAccessorIds[accessorId] = true; + }); + }); + const indices = primitive.indices; + if (defined(indices)) { + usedAccessorIds[indices] = true; + } + }); + }); + + ForEach.skin(gltf, function(skin) { + if (defined(skin.inverseBindMatrices)) { + usedAccessorIds[skin.inverseBindMatrices] = true; + } + }); + + ForEach.animation(gltf, function(animation) { + ForEach.animationSampler(animation, function(sampler) { + if (defined(sampler.input)) { + usedAccessorIds[sampler.input] = true; + } + if (defined(sampler.output)) { + usedAccessorIds[sampler.output] = true; + } + }); + }); + + return usedAccessorIds; +}; + +getListOfElementsIdsInUse.buffer = function(gltf) { + // Calculate buffer's that are currently in use. + const usedBufferIds = {}; + + ForEach.bufferView(gltf, function(bufferView) { + if (defined(bufferView.buffer)) { + usedBufferIds[bufferView.buffer] = true; + } + }); + + return usedBufferIds; +}; + +getListOfElementsIdsInUse.bufferView = function(gltf) { + // Calculate bufferView's that are currently in use. + const usedBufferViewIds = {}; + + ForEach.accessor(gltf, function(accessor) { + if (defined(accessor.bufferView)) { + usedBufferViewIds[accessor.bufferView] = true; + } + }); + + ForEach.shader(gltf, function(shader) { + if (defined(shader.bufferView)) { + usedBufferViewIds[shader.bufferView] = true; + } + }); + + ForEach.image(gltf, function(image) { + if (defined(image.bufferView)) { + usedBufferViewIds[image.bufferView] = true; + } + ForEach.compressedImage(image, function(compressedImage) { + if (defined(compressedImage.bufferView)) { + usedBufferViewIds[compressedImage.bufferView] = true; + } + }); + }); + + ForEach.audioClip(gltf, function(audioClip) { + if (defined(audioClip.bufferView)) { + usedBufferViewIds[audioClip.bufferView] = true; + } + }); + + if (hasExtension(gltf, 'KHR_draco_mesh_compression')) { + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.extensions) && + defined(primitive.extensions.KHR_draco_mesh_compression)) { + usedBufferViewIds[primitive.extensions.KHR_draco_mesh_compression.bufferView] = true; + } + }); + }); + } + + if (hasExtension(gltf, 'ALI_amc_mesh_compression')) { + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.extensions) && defined(primitive.extensions.ALI_amc_mesh_compression)) { + usedBufferViewIds[primitive.extensions.ALI_amc_mesh_compression.bufferView] = true; + } + }); + }); + } + + return usedBufferViewIds; +}; diff --git a/src/gltf2glb/replaceWithDecompressedPrimitive.js b/src/gltf2glb/replaceWithDecompressedPrimitive.js new file mode 100644 index 0000000..66c9dda --- /dev/null +++ b/src/gltf2glb/replaceWithDecompressedPrimitive.js @@ -0,0 +1,179 @@ +'use strict'; +const Cesium = require('cesium'); +const draco3d = require('draco3d'); +const addBuffer = require('./addBuffer'); + +const RuntimeError = Cesium.RuntimeError; +const WebGLConstants = Cesium.WebGLConstants; + +const decoderModule = draco3d.createDecoderModule({}); + +module.exports = replaceWithDecompressedPrimitive; + +/** + * Replace the accessor properties of the original primitive with the values from the decompressed primitive. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} primitive A javascript object containing a glTF primitive. + * @param {Object} dracoEncodedBuffer A javascript object containing a Draco encoded mesh. + * @param {Number} dracoEncodedBuffer.numberOfPoints Number of points after the mesh is decompressed. + * @param {Number} dracoEncodedBuffer.numberOfFaces Number of faces after the mesh is decompressed. + * @param {Buffer} dracoEncodedBuffer.buffer A Buffer object containing a Draco compressed mesh. + * @param {Boolean} uncompressedFallback If set, replaces the original with the decompressed data. + * @returns {Object} The glTF asset with the decompressed primitive. + * + * @private + */ +function replaceWithDecompressedPrimitive(gltf, primitive, dracoEncodedBuffer, uncompressedFallback) { + let decoder; + let dracoGeometry; + + // Add decompressed indices data to indices accessor. + const indicesAccessor = gltf.accessors[primitive.indices]; + indicesAccessor.count = dracoEncodedBuffer.numberOfFaces * 3; + + if (uncompressedFallback) { + decoder = new decoderModule.Decoder(); + dracoGeometry = decompressDracoBuffer(decoder, dracoEncodedBuffer.buffer); + + const indicesBuffer = getIndicesBuffer(indicesAccessor, decoderModule, decoder, dracoGeometry, dracoEncodedBuffer.numberOfFaces); + + indicesAccessor.bufferView = addBuffer(gltf, indicesBuffer); + indicesAccessor.byteOffset = 0; + } + + const dracoAttributes = primitive.extensions.KHR_draco_mesh_compression.attributes; + for (const semantic in dracoAttributes) { + if (dracoAttributes.hasOwnProperty(semantic)) { + const attributeAccessor = gltf.accessors[primitive.attributes[semantic]]; + attributeAccessor.count = dracoEncodedBuffer.numberOfPoints; + + if (uncompressedFallback) { + const attributeId = decoder.GetAttributeByUniqueId(dracoGeometry, dracoAttributes[semantic]); + const attributeBuffer = getAttributeBuffer(decoderModule, decoder, dracoGeometry, attributeId); + + attributeAccessor.bufferView = addBuffer(gltf, attributeBuffer); + attributeAccessor.byteOffset = 0; + } + } + } + + if (uncompressedFallback) { + decoderModule.destroy(dracoGeometry); + decoderModule.destroy(decoder); + } + + return gltf; +} + +function decompressDracoBuffer(decoder, compressedData) { + const source = new Uint8Array(compressedData.buffer); + const dracoBuffer = new decoderModule.DecoderBuffer(); + dracoBuffer.Init(source, source.length); + + const geometryType = decoder.GetEncodedGeometryType(dracoBuffer); + if (geometryType !== decoderModule.TRIANGULAR_MESH) { + decoderModule.destroy(decoder); + throw new RuntimeError('Compressed data is not a mesh.'); + } + const dracoGeometry = new decoderModule.Mesh(); + const decodingStatus = decoder.DecodeBufferToMesh(dracoBuffer, dracoGeometry); + if (!decodingStatus.ok() || dracoGeometry.ptr === 0) { + decoderModule.destroy(decoder); + throw new RuntimeError('Draco decoding failed: ' + decodingStatus.error_msg()); + } + + decoderModule.destroy(dracoBuffer); + return dracoGeometry; +} + +function getIndicesBuffer(accessor, decoderModule, decoder, dracoGeometry, numFaces) { + // Convert indices + const numIndices = numFaces * 3; + let indices; + if (accessor.componentType === WebGLConstants.BYTE) { + indices = new Int8Array(numIndices); + } else if (accessor.componentType === WebGLConstants.UNSIGNED_BYTE) { + indices = new Uint8Array(numIndices); + } else if (accessor.componentType === WebGLConstants.SHORT) { + indices = new Int16Array(numIndices); + } else if (accessor.componentType === WebGLConstants.UNSIGNED_SHORT) { + indices = new Uint16Array(numIndices); + } else { + indices = new Uint32Array(numIndices); + } + + const ia = new decoderModule.DracoInt32Array(); + for (let i = 0; i < numFaces; ++i) { + decoder.GetFaceFromMesh(dracoGeometry, i, ia); + const index = i * 3; + indices[index] = ia.GetValue(0); + indices[index + 1] = ia.GetValue(1); + indices[index + 2] = ia.GetValue(2); + } + + decoderModule.destroy(ia); + + const indicesUint8Data = Buffer.from(indices.buffer); + return indicesUint8Data; +} + +function getAttributeBuffer(decoderModule, decoder, dracoGeometry, attribute) { + const numComponents = attribute.num_components(); + const numPoints = dracoGeometry.num_points(); + const numValues = numPoints * numComponents; + + let attributeData; + let attributeArray; + if (attribute.data_type() === 1) { + attributeData = new decoderModule.DracoInt8Array(); + if (!decoder.GetAttributeInt8ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Int8Array(attributeData.size()); + } else if (attribute.data_type() === 2) { + attributeData = new decoderModule.DracoUInt8Array(); + if (!decoder.GetAttributeUInt8ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Uint8Array(attributeData.size()); + } else if (attribute.data_type() === 3) { + attributeData = new decoderModule.DracoInt16Array(); + if (!decoder.GetAttributeInt16ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Int16Array(attributeData.size()); + } else if (attribute.data_type() === 4) { + attributeData = new decoderModule.DracoUInt16Array(); + if (!decoder.GetAttributeUInt16ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Uint16Array(attributeData.size()); + } else if (attribute.data_type() === 6) { + attributeData = new decoderModule.DracoUInt32Array(); + if (!decoder.GetAttributeUInt32ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Uint32Array(attributeData.size()); + } else if (attribute.data_type() === 5) { + attributeData = new decoderModule.DracoInt32Array(); + if (!decoder.GetAttributeInt32ForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Int32Array(attributeData.size()); + } else { + attributeData = new decoderModule.DracoFloat32Array(); + if (!decoder.GetAttributeFloatForAllPoints(dracoGeometry, attribute, attributeData)) { + throw new RuntimeError('Could not get attribute data for id:' + attribute.unique_id().toString()); + } + attributeArray = new Float32Array(attributeData.size()); + } + + for (let i = 0; i < numValues; i++) { + attributeArray[i] = attributeData.GetValue(i); + } + + decoderModule.destroy(attributeData); + const attributeBuffer = Buffer.from(attributeArray.buffer); + return attributeBuffer; +} diff --git a/src/gltf2glb/splitPrimitives.js b/src/gltf2glb/splitPrimitives.js new file mode 100644 index 0000000..b68ed29 --- /dev/null +++ b/src/gltf2glb/splitPrimitives.js @@ -0,0 +1,200 @@ +'use strict'; +const Cesium = require('cesium'); +const hashObject = require('object-hash'); +const addBuffer = require('./addBuffer'); +const addToArray = require('./addToArray'); +const findAccessorMinMax = require('./findAccessorMinMax'); +const ForEach = require('./ForEach'); +const readAccessorPacked = require('./readAccessorPacked'); +const removeUnusedElements = require('./removeUnusedElements'); + +const clone = Cesium.clone; +const ComponentDatatype = Cesium.ComponentDatatype; +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const numberOfComponentsForType = Cesium.numberOfComponentsForType; + +module.exports = splitPrimitives; + +/** + * Splits primitives that reference different indices within the same mesh. + * This stage is used internally by compressDracoMeshes. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} glTF with primitives split. + * + * @private + */ +function splitPrimitives(gltf) { + let i; + let hash; + let primitives; + let primitivesLength; + + const hashPrimitives = {}; + const duplicatePrimitives = {}; + const primitivesWithSharedAttributes = {}; + let primitivesSplit = false; + + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + const hashPrimitive = hashObject({ + indices: primitive.indices, + attributes: primitive.attributes, + targets: primitive.targets, + mode: primitive.mode + }); + if (defined(hashPrimitives[hashPrimitive])) { + const duplicates = defaultValue(duplicatePrimitives[hashPrimitive], []); + duplicatePrimitives[hashPrimitive] = duplicates; + duplicates.push(primitive); + return; + } + hashPrimitives[hashPrimitive] = primitive; + + const hashAttributes = hashObject({ + attributes: primitive.attributes, + targets: primitive.targets, + mode: primitive.mode + }); + const primitivesShared = defaultValue(primitivesWithSharedAttributes[hashAttributes], []); + primitivesWithSharedAttributes[hashAttributes] = primitivesShared; + primitivesShared.push(primitive); + }); + }); + + for (hash in primitivesWithSharedAttributes) { + if (primitivesWithSharedAttributes.hasOwnProperty(hash)) { + primitives = primitivesWithSharedAttributes[hash]; + primitivesLength = primitives.length; + if (primitivesLength === 1) { + continue; + } + primitivesSplit = true; + const attributeData = readAttributes(gltf, primitives[0]); + const targetData = readTargets(gltf, primitives[0]); + for (i = 0; i < primitivesLength; ++i) { + splitPrimitive(gltf, primitives[i], attributeData, targetData); + } + } + } + + if (primitivesSplit) { + for (hash in duplicatePrimitives) { + if (duplicatePrimitives.hasOwnProperty(hash)) { + const primitiveToCopy = hashPrimitives[hash]; + primitives = duplicatePrimitives[hash]; + primitivesLength = primitives.length; + for (i = 0; i < primitivesLength; ++i) { + copyPrimitive(primitiveToCopy, primitives[i]); + } + } + } + removeUnusedElements(gltf); + } + + return gltf; +} + +function copyPrimitive(primitiveToCopy, primitive) { + primitive.indices = primitiveToCopy.indices; + ForEach.meshPrimitiveAttribute(primitiveToCopy, function(accessorId, semantic) { + primitive.attributes[semantic] = accessorId; + }); + ForEach.meshPrimitiveTarget(primitiveToCopy, function(target, targetIndex) { + ForEach.meshPrimitiveTargetAttribute(target, function(accessorId, semantic) { + primitive.targets[targetIndex][semantic] = accessorId; + }); + }); +} + +function splitPrimitive(gltf, primitive, attributeData, targetData) { + const indicesAccessor = gltf.accessors[primitive.indices]; + const indices = readAccessorPacked(gltf, indicesAccessor); + const mappedIndices = {}; + const newIndices = []; + let uniqueIndicesLength = 0; + const indicesLength = indices.length; + for (let i = 0; i < indicesLength; ++i) { + const index = indices[i]; + let mappedIndex = mappedIndices[index]; + if (!defined(mappedIndex)) { + mappedIndex = uniqueIndicesLength++; + mappedIndices[index] = mappedIndex; + } + newIndices.push(mappedIndex); + } + primitive.indices = createNewAccessor(gltf, indicesAccessor, newIndices); + + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + primitive.attributes[semantic] = createNewAttribute(gltf, accessorId, semantic, attributeData, mappedIndices, uniqueIndicesLength); + }); + + ForEach.meshPrimitiveTarget(primitive, function(target, targetIndex) { + ForEach.meshPrimitiveTargetAttribute(target, function(accessorId, semantic) { + target[semantic] = createNewAttribute(gltf, accessorId, semantic, targetData[targetIndex], mappedIndices, uniqueIndicesLength); + }); + }); +} + +function createNewAttribute(gltf, accessorId, semantic, attributeData, mappedIndices, uniqueIndicesLength) { + const accessor = gltf.accessors[accessorId]; + const numberOfComponents = numberOfComponentsForType(accessor.type); + const dataArray = attributeData[semantic]; + const newDataArray = new Array(uniqueIndicesLength * numberOfComponents); + remapData(dataArray, newDataArray, mappedIndices, numberOfComponents); + return createNewAccessor(gltf, accessor, newDataArray); +} + +function remapData(dataArray, newDataArray, mappedIndices, numberOfComponents) { + for (const index in mappedIndices) { + if (mappedIndices.hasOwnProperty(index)) { + const mappedIndex = mappedIndices[index]; + for (let i = 0; i < numberOfComponents; ++i) { + newDataArray[mappedIndex * numberOfComponents + i] = dataArray[index * numberOfComponents + i]; + } + } + } +} + +function createNewAccessor(gltf, oldAccessor, dataArray) { + const componentType = oldAccessor.componentType; + const type = oldAccessor.type; + const numberOfComponents = numberOfComponentsForType(type); + const count = dataArray.length / numberOfComponents; + const newBuffer = Buffer.from(ComponentDatatype.createTypedArray(componentType, dataArray).buffer); + const newBufferViewId = addBuffer(gltf, newBuffer); + + const accessor = clone(oldAccessor, true); + const accessorId = addToArray(gltf.accessors, accessor); + + accessor.bufferView = newBufferViewId; + accessor.byteOffset = 0; + accessor.count = count; + + const minMax = findAccessorMinMax(gltf, accessor); + accessor.min = minMax.min; + accessor.max = minMax.max; + + return accessorId; +} + +function readAttributes(gltf, primitive) { + const attributeData = {}; + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + attributeData[semantic] = readAccessorPacked(gltf, gltf.accessors[accessorId]); + }); + return attributeData; +} + +function readTargets(gltf, primitive) { + const targetData = []; + ForEach.meshPrimitiveTarget(primitive, function(target) { + const attributeData = {}; + targetData.push(attributeData); + ForEach.meshPrimitiveTargetAttribute(target, function(accessorId, semantic) { + attributeData[semantic] = readAccessorPacked(gltf, gltf.accessors[accessorId]); + }); + }); + return targetData; +} diff --git a/src/gltf2glb/updateAccessorComponentTypes.js b/src/gltf2glb/updateAccessorComponentTypes.js new file mode 100644 index 0000000..eb40fdf --- /dev/null +++ b/src/gltf2glb/updateAccessorComponentTypes.js @@ -0,0 +1,51 @@ +'use strict'; +const Cesium = require('cesium'); +const addBuffer = require('./addBuffer'); +const ForEach = require('./ForEach'); +const readAccessorPacked = require('./readAccessorPacked'); + +const ComponentDatatype = Cesium.ComponentDatatype; +const WebGLConstants = Cesium.WebGLConstants; + +module.exports = updateAccessorComponentTypes; + +/** + * Update accessors referenced by JOINTS_0 and WEIGHTS_0 attributes to use correct component types. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @returns {Object} The glTF asset with compressed meshes. + * + * @private + */ +function updateAccessorComponentTypes(gltf) { + let componentType; + ForEach.accessorWithSemantic(gltf, 'JOINTS_0', function(accessorId) { + const accessor = gltf.accessors[accessorId]; + componentType = accessor.componentType; + if (componentType === WebGLConstants.BYTE) { + convertType(gltf, accessor, ComponentDatatype.UNSIGNED_BYTE); + } else if (componentType !== WebGLConstants.UNSIGNED_BYTE + && componentType !== WebGLConstants.UNSIGNED_SHORT) { + convertType(gltf, accessor, ComponentDatatype.UNSIGNED_SHORT); + } + }); + ForEach.accessorWithSemantic(gltf, 'WEIGHTS_0', function(accessorId) { + const accessor = gltf.accessors[accessorId]; + componentType = accessor.componentType; + if (componentType === WebGLConstants.BYTE) { + convertType(gltf, accessor, ComponentDatatype.UNSIGNED_BYTE); + } else if (componentType === WebGLConstants.SHORT) { + convertType(gltf, accessor, ComponentDatatype.UNSIGNED_SHORT); + } + }); + + return gltf; +} + +function convertType(gltf, accessor, updatedComponentType) { + const typedArray = ComponentDatatype.createTypedArray(updatedComponentType, readAccessorPacked(gltf, accessor)); + const newBuffer = new Uint8Array(typedArray.buffer); + accessor.bufferView = addBuffer(gltf, newBuffer); + accessor.componentType = updatedComponentType; + accessor.byteOffset = 0; +} diff --git a/src/gltf2glb/updateVersion.js b/src/gltf2glb/updateVersion.js new file mode 100644 index 0000000..e237230 --- /dev/null +++ b/src/gltf2glb/updateVersion.js @@ -0,0 +1,940 @@ +'use strict'; +const Cesium = require('cesium'); +const addExtensionsUsed = require('./addExtensionsUsed'); +const addToArray = require('./addToArray'); +const findAccessorMinMax = require('./findAccessorMinMax'); +const ForEach = require('./ForEach'); +const getAccessorByteStride = require('./getAccessorByteStride'); +const numberOfComponentsForType = require('./numberOfComponentsForType'); +const moveTechniqueRenderStates = require('./moveTechniqueRenderStates'); +const moveTechniquesToExtension = require('./moveTechniquesToExtension'); +const removeUnusedElements = require('./removeUnusedElements'); +const updateAccessorComponentTypes = require('./updateAccessorComponentTypes'); + +const Cartesian3 = Cesium.Cartesian3; +const Cartesian4 = Cesium.Cartesian4; +const clone = Cesium.clone; +const ComponentDatatype = Cesium.ComponentDatatype; +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const isArray = Cesium.isArray; +const Matrix4 = Cesium.Matrix4; +const Quaternion = Cesium.Quaternion; +const WebGLConstants = Cesium.WebGLConstants; + +module.exports = updateVersion; + +const updateFunctions = { + '0.8': glTF08to10, + '1.0': glTF10to20, + '2.0': undefined +}; + +/** + * Update the glTF version to the latest version (2.0), or targetVersion if specified. + * Applies changes made to the glTF spec between revisions so that the core library + * only has to handle the latest version. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} [options] Options for updating the glTF. + * @param {String} [options.targetVersion] The glTF will be upgraded until it hits the specified version. + * @returns {Object} The updated glTF asset. + * + * @private + */ +function updateVersion(gltf, options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + const targetVersion = options.targetVersion; + let version = gltf.version; + + gltf.asset = defaultValue(gltf.asset, { + version: '1.0' + }); + + gltf.asset.version = defaultValue(gltf.asset.version, '1.0'); + version = defaultValue(version, gltf.asset.version).toString(); + + // Invalid version + if (!updateFunctions.hasOwnProperty(version)) { + // Try truncating trailing version numbers, could be a number as well if it is 0.8 + if (defined(version)) { + version = version.substring(0, 3); + } + // Default to 1.0 if it cannot be determined + if (!updateFunctions.hasOwnProperty(version)) { + version = '1.0'; + } + } + + let updateFunction = updateFunctions[version]; + + while (defined(updateFunction)) { + if (version === targetVersion) { + break; + } + updateFunction(gltf, options); + version = gltf.asset.version; + updateFunction = updateFunctions[version]; + } + return gltf; +} + +function updateInstanceTechniques(gltf) { + const materials = gltf.materials; + for (const materialId in materials) { + if (materials.hasOwnProperty(materialId)) { + const material = materials[materialId]; + const instanceTechnique = material.instanceTechnique; + if (defined(instanceTechnique)) { + material.technique = instanceTechnique.technique; + material.values = instanceTechnique.values; + delete material.instanceTechnique; + } + } + } +} + +function setPrimitiveModes(gltf) { + const meshes = gltf.meshes; + for (const meshId in meshes) { + if (meshes.hasOwnProperty(meshId)) { + const mesh = meshes[meshId]; + const primitives = mesh.primitives; + if (defined(primitives)) { + const primitivesLength = primitives.length; + for (let i = 0; i < primitivesLength; ++i) { + const primitive = primitives[i]; + const defaultMode = defaultValue(primitive.primitive, WebGLConstants.TRIANGLES); + primitive.mode = defaultValue(primitive.mode, defaultMode); + delete primitive.primitive; + } + } + } + } +} + +function updateNodes(gltf) { + const nodes = gltf.nodes; + const axis = new Cartesian3(); + const quat = new Quaternion(); + for (const nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + const node = nodes[nodeId]; + if (defined(node.rotation)) { + const rotation = node.rotation; + Cartesian3.fromArray(rotation, 0, axis); + Quaternion.fromAxisAngle(axis, rotation[3], quat); + node.rotation = [quat.x, quat.y, quat.z, quat.w]; + } + const instanceSkin = node.instanceSkin; + if (defined(instanceSkin)) { + node.skeletons = instanceSkin.skeletons; + node.skin = instanceSkin.skin; + node.meshes = instanceSkin.meshes; + delete node.instanceSkin; + } + } + } +} + +function updateAnimations(gltf) { + const animations = gltf.animations; + const accessors = gltf.accessors; + const bufferViews = gltf.bufferViews; + const buffers = gltf.buffers; + const updatedAccessors = {}; + const axis = new Cartesian3(); + const quat = new Quaternion(); + for (const animationId in animations) { + if (animations.hasOwnProperty(animationId)) { + const animation = animations[animationId]; + const channels = animation.channels; + const parameters = animation.parameters; + const samplers = animation.samplers; + if (defined(channels)) { + const channelsLength = channels.length; + for (let i = 0; i < channelsLength; ++i) { + const channel = channels[i]; + if (channel.target.path === 'rotation') { + const accessorId = parameters[samplers[channel.sampler].output]; + if (defined(updatedAccessors[accessorId])) { + continue; + } + updatedAccessors[accessorId] = true; + const accessor = accessors[accessorId]; + const bufferView = bufferViews[accessor.bufferView]; + const buffer = buffers[bufferView.buffer]; + const source = buffer.extras._pipeline.source; + const byteOffset = source.byteOffset + bufferView.byteOffset + accessor.byteOffset; + const componentType = accessor.componentType; + const count = accessor.count; + const componentsLength = numberOfComponentsForType(accessor.type); + const length = accessor.count * componentsLength; + const typedArray = ComponentDatatype.createArrayBufferView(componentType, source.buffer, byteOffset, length); + + for (let j = 0; j < count; j++) { + const offset = j * componentsLength; + Cartesian3.unpack(typedArray, offset, axis); + const angle = typedArray[offset + 3]; + Quaternion.fromAxisAngle(axis, angle, quat); + Quaternion.pack(quat, typedArray, offset); + } + } + } + } + } + } +} + +function removeTechniquePasses(gltf) { + const techniques = gltf.techniques; + for (const techniqueId in techniques) { + if (techniques.hasOwnProperty(techniqueId)) { + const technique = techniques[techniqueId]; + const passes = technique.passes; + if (defined(passes)) { + const passName = defaultValue(technique.pass, 'defaultPass'); + if (passes.hasOwnProperty(passName)) { + const pass = passes[passName]; + const instanceProgram = pass.instanceProgram; + technique.attributes = defaultValue(technique.attributes, instanceProgram.attributes); + technique.program = defaultValue(technique.program, instanceProgram.program); + technique.uniforms = defaultValue(technique.uniforms, instanceProgram.uniforms); + technique.states = defaultValue(technique.states, pass.states); + } + delete technique.passes; + delete technique.pass; + } + } + } +} + +function glTF08to10(gltf) { + if (!defined(gltf.asset)) { + gltf.asset = {}; + } + const asset = gltf.asset; + asset.version = '1.0'; + // Profile should be an object, not a string + if (typeof asset.profile === 'string') { + const split = asset.profile.split(' '); + asset.profile = { + api: split[0], + version: split[1] + }; + } else { + asset.profile = {}; + } + + // Version property should be in asset, not on the root element + if (defined(gltf.version)) { + delete gltf.version; + } + // material.instanceTechnique properties should be directly on the material + updateInstanceTechniques(gltf); + // primitive.primitive should be primitive.mode + setPrimitiveModes(gltf); + // Node rotation should be quaternion, not axis-angle + // node.instanceSkin is deprecated + updateNodes(gltf); + // Animations that target rotations should be quaternion, not axis-angle + updateAnimations(gltf); + // technique.pass and techniques.passes are deprecated + removeTechniquePasses(gltf); + // gltf.allExtensions -> extensionsUsed + if (defined(gltf.allExtensions)) { + gltf.extensionsUsed = gltf.allExtensions; + delete gltf.allExtensions; + } + // gltf.lights -> khrMaterialsCommon.lights + if (defined(gltf.lights)) { + const extensions = defaultValue(gltf.extensions, {}); + gltf.extensions = extensions; + const materialsCommon = defaultValue(extensions.KHR_materials_common, {}); + extensions.KHR_materials_common = materialsCommon; + materialsCommon.lights = gltf.lights; + delete gltf.lights; + addExtensionsUsed(gltf, 'KHR_materials_common'); + } +} + +function removeAnimationSamplersIndirection(gltf) { + const animations = gltf.animations; + for (const animationId in animations) { + if (animations.hasOwnProperty(animationId)) { + const animation = animations[animationId]; + const parameters = animation.parameters; + if (defined(parameters)) { + const samplers = animation.samplers; + for (const samplerId in samplers) { + if (samplers.hasOwnProperty(samplerId)) { + const sampler = samplers[samplerId]; + sampler.input = parameters[sampler.input]; + sampler.output = parameters[sampler.output]; + } + } + delete animation.parameters; + } + } + } +} + +function objectToArray(object, mapping) { + const array = []; + for (const id in object) { + if (object.hasOwnProperty(id)) { + const value = object[id]; + mapping[id] = array.length; + array.push(value); + if (!defined(value.name)) { + value.name = id; + } + } + } + return array; +} + +function objectsToArrays(gltf) { + let i; + const globalMapping = { + accessors: {}, + animations: {}, + buffers: {}, + bufferViews: {}, + cameras: {}, + images: {}, + materials: {}, + meshes: {}, + nodes: {}, + programs: {}, + samplers: {}, + scenes: {}, + shaders: {}, + skins: {}, + textures: {}, + techniques: {} + }; + + // Map joint names to id names + let jointName; + const jointNameToId = {}; + const nodes = gltf.nodes; + for (const id in nodes) { + if (nodes.hasOwnProperty(id)) { + jointName = nodes[id].jointName; + if (defined(jointName)) { + jointNameToId[jointName] = id; + } + } + } + + // Convert top level objects to arrays + for (const topLevelId in gltf) { + if (gltf.hasOwnProperty(topLevelId) && defined(globalMapping[topLevelId])) { + const objectMapping = {}; + const object = gltf[topLevelId]; + gltf[topLevelId] = objectToArray(object, objectMapping); + globalMapping[topLevelId] = objectMapping; + } + } + + // Remap joint names to array indexes + for (jointName in jointNameToId) { + if (jointNameToId.hasOwnProperty(jointName)) { + jointNameToId[jointName] = globalMapping.nodes[jointNameToId[jointName]]; + } + } + + // Fix references + if (defined(gltf.scene)) { + gltf.scene = globalMapping.scenes[gltf.scene]; + } + ForEach.bufferView(gltf, function(bufferView) { + if (defined(bufferView.buffer)) { + bufferView.buffer = globalMapping.buffers[bufferView.buffer]; + } + }); + ForEach.accessor(gltf, function(accessor) { + if (defined(accessor.bufferView)) { + accessor.bufferView = globalMapping.bufferViews[accessor.bufferView]; + } + }); + ForEach.shader(gltf, function(shader) { + const extensions = shader.extensions; + if (defined(extensions)) { + const binaryGltf = extensions.KHR_binary_glTF; + if (defined(binaryGltf)) { + shader.bufferView = globalMapping.bufferViews[binaryGltf.bufferView]; + delete extensions.KHR_binary_glTF; + } + if (Object.keys(extensions).length === 0) { + delete shader.extensions; + } + } + }); + ForEach.program(gltf, function(program) { + if (defined(program.vertexShader)) { + program.vertexShader = globalMapping.shaders[program.vertexShader]; + } + if (defined(program.fragmentShader)) { + program.fragmentShader = globalMapping.shaders[program.fragmentShader]; + } + }); + ForEach.technique(gltf, function(technique) { + if (defined(technique.program)) { + technique.program = globalMapping.programs[technique.program]; + } + ForEach.techniqueParameter(technique, function(parameter) { + if (defined(parameter.node)) { + parameter.node = globalMapping.nodes[parameter.node]; + } + const value = parameter.value; + if (typeof value === 'string') { + parameter.value = { + index: globalMapping.textures[value] + }; + } + }); + }); + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + if (defined(primitive.indices)) { + primitive.indices = globalMapping.accessors[primitive.indices]; + } + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + primitive.attributes[semantic] = globalMapping.accessors[accessorId]; + }); + if (defined(primitive.material)) { + primitive.material = globalMapping.materials[primitive.material]; + } + }); + }); + ForEach.node(gltf, function(node) { + let children = node.children; + if (defined(children)) { + const childrenLength = children.length; + for (i = 0; i < childrenLength; ++i) { + children[i] = globalMapping.nodes[children[i]]; + } + } + if (defined(node.meshes)) { + // Split out meshes on nodes + const meshes = node.meshes; + const meshesLength = meshes.length; + if (meshesLength > 0) { + node.mesh = globalMapping.meshes[meshes[0]]; + for (i = 1; i < meshesLength; ++i) { + const meshNode = { + mesh: globalMapping.meshes[meshes[i]] + }; + const meshNodeId = addToArray(gltf.nodes, meshNode); + if (!defined(children)) { + children = []; + node.children = children; + } + children.push(meshNodeId); + } + } + delete node.meshes; + } + if (defined(node.camera)) { + node.camera = globalMapping.cameras[node.camera]; + } + if (defined(node.skin)) { + node.skin = globalMapping.skins[node.skin]; + } + if (defined(node.skeletons)) { + // Assign skeletons to skins + const skeletons = node.skeletons; + const skeletonsLength = skeletons.length; + if ((skeletonsLength > 0) && defined(node.skin)) { + const skin = gltf.skins[node.skin]; + skin.skeleton = globalMapping.nodes[skeletons[0]]; + } + delete node.skeletons; + } + if (defined(node.jointName)) { + delete node.jointName; + } + }); + ForEach.skin(gltf, function(skin) { + if (defined(skin.inverseBindMatrices)) { + skin.inverseBindMatrices = globalMapping.accessors[skin.inverseBindMatrices]; + } + const jointNames = skin.jointNames; + if (defined(jointNames)) { + const joints = []; + const jointNamesLength = jointNames.length; + for (i = 0; i < jointNamesLength; ++i) { + joints[i] = jointNameToId[jointNames[i]]; + } + skin.joints = joints; + delete skin.jointNames; + } + }); + ForEach.scene(gltf, function(scene) { + const sceneNodes = scene.nodes; + if (defined(sceneNodes)) { + const sceneNodesLength = sceneNodes.length; + for (i = 0; i < sceneNodesLength; ++i) { + sceneNodes[i] = globalMapping.nodes[sceneNodes[i]]; + } + } + }); + ForEach.animation(gltf, function(animation) { + const samplerMapping = {}; + animation.samplers = objectToArray(animation.samplers, samplerMapping); + ForEach.animationSampler(animation, function(sampler) { + sampler.input = globalMapping.accessors[sampler.input]; + sampler.output = globalMapping.accessors[sampler.output]; + }); + ForEach.animationChannel(animation, function(channel) { + channel.sampler = samplerMapping[channel.sampler]; + const target = channel.target; + if (defined(target)) { + target.node = globalMapping.nodes[target.id]; + delete target.id; + } + }); + }); + ForEach.material(gltf, function(material) { + if (defined(material.technique)) { + material.technique = globalMapping.techniques[material.technique]; + } + ForEach.materialValue(material, function(value, name) { + if (typeof value === 'string') { + material.values[name] = { + index: globalMapping.textures[value] + }; + } + }); + const extensions = material.extensions; + if (defined(extensions)) { + const materialsCommon = extensions.KHR_materials_common; + if (defined(materialsCommon)) { + ForEach.materialValue(materialsCommon, function(value, name) { + if (typeof value === 'string') { + materialsCommon.values[name] = { + index: globalMapping.textures[value] + }; + } + }); + } + } + }); + ForEach.image(gltf, function(image) { + const extensions = image.extensions; + if (defined(extensions)) { + const binaryGltf = extensions.KHR_binary_glTF; + if (defined(binaryGltf)) { + image.bufferView = globalMapping.bufferViews[binaryGltf.bufferView]; + image.mimeType = binaryGltf.mimeType; + delete extensions.KHR_binary_glTF; + } + if (Object.keys(extensions).length === 0) { + delete image.extensions; + } + } + ForEach.compressedImage(image, function(compressedImage) { + const compressedExtensions = compressedImage.extensions; + if (defined(compressedExtensions)) { + const compressedBinaryGltf = compressedExtensions.KHR_binary_glTF; + if (defined(compressedBinaryGltf)) { + compressedImage.bufferView = globalMapping.bufferViews[compressedBinaryGltf.bufferView]; + compressedImage.mimeType = compressedBinaryGltf.mimeType; + delete compressedExtensions.KHR_binary_glTF; + } + if (Object.keys(extensions).length === 0) { + delete compressedImage.extensions; + } + } + }); + }); + ForEach.texture(gltf, function(texture) { + if (defined(texture.sampler)) { + texture.sampler = globalMapping.samplers[texture.sampler]; + } + if (defined(texture.source)) { + texture.source = globalMapping.images[texture.source]; + } + }); +} + +function removeAnimationSamplerNames(gltf) { + ForEach.animation(gltf, function(animation) { + ForEach.animationSampler(animation, function(sampler) { + delete sampler.name; + }); + }); +} + +function removeEmptyArrays(gltf) { + for (const topLevelId in gltf) { + if (gltf.hasOwnProperty(topLevelId)) { + const array = gltf[topLevelId]; + if (isArray(array) && array.length === 0) { + delete gltf[topLevelId]; + } + } + } + ForEach.node(gltf, function(node) { + if (defined(node.children) && node.children.length === 0) { + delete node.children; + } + }); +} + +function stripAsset(gltf) { + const asset = gltf.asset; + delete asset.profile; + delete asset.premultipliedAlpha; +} + +const knownExtensions = { + CESIUM_RTC: true, + KHR_materials_common: true, + WEB3D_quantized_attributes: true +}; +function requireKnownExtensions(gltf) { + const extensionsUsed = gltf.extensionsUsed; + gltf.extensionsRequired = defaultValue(gltf.extensionsRequired, []); + if (defined(extensionsUsed)) { + const extensionsUsedLength = extensionsUsed.length; + for (let i = 0; i < extensionsUsedLength; ++i) { + const extension = extensionsUsed[i]; + if (defined(knownExtensions[extension])) { + gltf.extensionsRequired.push(extension); + } + } + } +} + +function removeBufferType(gltf) { + ForEach.buffer(gltf, function(buffer) { + delete buffer.type; + }); +} + +function removeTextureProperties(gltf) { + ForEach.texture(gltf, function(texture) { + delete texture.format; + delete texture.internalFormat; + delete texture.target; + delete texture.type; + }); +} + +function requireAttributeSetIndex(gltf) { + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + if (semantic === 'TEXCOORD') { + primitive.attributes.TEXCOORD_0 = accessorId; + } else if (semantic === 'COLOR') { + primitive.attributes.COLOR_0 = accessorId; + } + }); + delete primitive.attributes.TEXCOORD; + delete primitive.attributes.COLOR; + }); + }); + ForEach.technique(gltf, function(technique) { + ForEach.techniqueParameter(technique, function(parameter) { + const semantic = parameter.semantic; + if (defined(semantic)) { + if (semantic === 'TEXCOORD') { + parameter.semantic = 'TEXCOORD_0'; + } else if (semantic === 'COLOR') { + parameter.semantic = 'COLOR_0'; + } + } + }); + }); +} + +const knownSemantics = { + POSITION: true, + NORMAL: true, + TANGENT: true +}; +const indexedSemantics = { + COLOR: 'COLOR', + JOINT : 'JOINTS', + JOINTS: 'JOINTS', + TEXCOORD: 'TEXCOORD', + WEIGHT: 'WEIGHTS', + WEIGHTS: 'WEIGHTS' +}; +function underscoreApplicationSpecificSemantics(gltf) { + const mappedSemantics = {}; + ForEach.mesh(gltf, function(mesh) { + ForEach.meshPrimitive(mesh, function(primitive) { + /*eslint-disable no-unused-vars*/ + ForEach.meshPrimitiveAttribute(primitive, function(accessorId, semantic) { + if (semantic.charAt(0) !== '_') { + const setIndex = semantic.search(/_[0-9]+/g); + let strippedSemantic = semantic; + let suffix = '_0'; + if (setIndex >= 0) { + strippedSemantic = semantic.substring(0, setIndex); + suffix = semantic.substring(setIndex); + } + let newSemantic; + const indexedSemantic = indexedSemantics[strippedSemantic]; + if (defined(indexedSemantic)) { + newSemantic = indexedSemantic + suffix; + mappedSemantics[semantic] = newSemantic; + } else if (!defined(knownSemantics[strippedSemantic])) { + newSemantic = '_' + semantic; + mappedSemantics[semantic] = newSemantic; + } + } + }); + for (const semantic in mappedSemantics) { + if (mappedSemantics.hasOwnProperty(semantic)) { + const mappedSemantic = mappedSemantics[semantic]; + const accessorId = primitive.attributes[semantic]; + if (defined(accessorId)) { + delete primitive.attributes[semantic]; + primitive.attributes[mappedSemantic] = accessorId; + } + } + } + }); + }); + ForEach.technique(gltf, function(technique) { + ForEach.techniqueParameter(technique, function(parameter) { + const mappedSemantic = mappedSemantics[parameter.semantic]; + if (defined(mappedSemantic)) { + parameter.semantic = mappedSemantic; + } + }); + }); +} + +function clampCameraParameters(gltf) { + ForEach.camera(gltf, function(camera) { + const perspective = camera.perspective; + if (defined(perspective)) { + const aspectRatio = perspective.aspectRatio; + if (defined(aspectRatio) && aspectRatio === 0.0) { + delete perspective.aspectRatio; + } + const yfov = perspective.yfov; + if (defined(yfov) && yfov === 0.0) { + perspective.yfov = 1.0; + } + } + }); +} + +function computeAccessorByteStride(gltf, accessor) { + return (defined(accessor.byteStride) && accessor.byteStride !== 0) ? accessor.byteStride : getAccessorByteStride(gltf, accessor); +} + +function requireByteLength(gltf) { + ForEach.buffer(gltf, function(buffer) { + if (!defined(buffer.byteLength)) { + buffer.byteLength = buffer.extras._pipeline.source.length; + } + }); + ForEach.accessor(gltf, function(accessor) { + const bufferViewId = accessor.bufferView; + if (defined(bufferViewId)) { + const bufferView = gltf.bufferViews[bufferViewId]; + const accessorByteStride = computeAccessorByteStride(gltf, accessor); + const accessorByteEnd = accessor.byteOffset + accessor.count * accessorByteStride; + bufferView.byteLength = Math.max(defaultValue(bufferView.byteLength, 0), accessorByteEnd); + } + }); +} + +function moveByteStrideToBufferView(gltf) { + let i; + let j; + let bufferView; + const bufferViews = gltf.bufferViews; + + const bufferViewHasVertexAttributes = {}; + ForEach.accessorContainingVertexAttributeData(gltf, function(accessorId) { + const accessor = gltf.accessors[accessorId]; + if (defined(accessor.bufferView)) { + bufferViewHasVertexAttributes[accessor.bufferView] = true; + } + }); + + // Map buffer views to a list of accessors + const bufferViewMap = {}; + ForEach.accessor(gltf, function(accessor) { + if (defined(accessor.bufferView)) { + bufferViewMap[accessor.bufferView] = defaultValue(bufferViewMap[accessor.bufferView], []); + bufferViewMap[accessor.bufferView].push(accessor); + } + }); + + // Split accessors with different byte strides + for (const bufferViewId in bufferViewMap) { + if (bufferViewMap.hasOwnProperty(bufferViewId)) { + bufferView = bufferViews[bufferViewId]; + const accessors = bufferViewMap[bufferViewId]; + accessors.sort(function(a, b) { + return a.byteOffset - b.byteOffset; + }); + let currentByteOffset = 0; + let currentIndex = 0; + const accessorsLength = accessors.length; + for (i = 0; i < accessorsLength; ++i) { + let accessor = accessors[i]; + const accessorByteStride = computeAccessorByteStride(gltf, accessor); + const accessorByteOffset = accessor.byteOffset; + const accessorByteLength = accessor.count * accessorByteStride; + delete accessor.byteStride; + + const hasNextAccessor = (i < accessorsLength - 1); + const nextAccessorByteStride = hasNextAccessor ? computeAccessorByteStride(gltf, accessors[i + 1]) : undefined; + if (accessorByteStride !== nextAccessorByteStride) { + const newBufferView = clone(bufferView, true); + if (bufferViewHasVertexAttributes[bufferViewId]) { + newBufferView.byteStride = accessorByteStride; + } + newBufferView.byteOffset += currentByteOffset; + newBufferView.byteLength = accessorByteOffset + accessorByteLength - currentByteOffset; + const newBufferViewId = addToArray(bufferViews, newBufferView); + for (j = currentIndex; j <= i; ++j) { + accessor = accessors[j]; + accessor.bufferView = newBufferViewId; + accessor.byteOffset = accessor.byteOffset - currentByteOffset; + } + // Set current byte offset to next accessor's byte offset + currentByteOffset = hasNextAccessor ? accessors[i + 1].byteOffset : undefined; + currentIndex = i + 1; + } + } + } + } + + // Remove unused buffer views + removeUnusedElements(gltf); +} + +function requirePositionAccessorMinMax(gltf) { + ForEach.accessorWithSemantic(gltf, 'POSITION', function(accessorId) { + const accessor = gltf.accessors[accessorId]; + if (!defined(accessor.min) || !defined(accessor.max)) { + const minMax = findAccessorMinMax(gltf, accessor); + accessor.min = minMax.min; + accessor.max = minMax.max; + } + }); +} + +function isNodeEmpty(node) { + return (!defined(node.children) || node.children.length === 0) && + (!defined(node.meshes) || node.meshes.length === 0) && + !defined(node.camera) && !defined(node.skin) && !defined(node.skeletons) && !defined(node.jointName) && + (!defined(node.translation) || Cartesian3.fromArray(node.translation).equals(Cartesian3.ZERO)) && + (!defined(node.scale) || Cartesian3.fromArray(node.scale).equals(new Cartesian3(1.0, 1.0, 1.0))) && + (!defined(node.rotation) || Cartesian4.fromArray(node.rotation).equals(new Cartesian4(0.0, 0.0, 0.0, 1.0))) && + (!defined(node.matrix) || Matrix4.fromColumnMajorArray(node.matrix).equals(Matrix4.IDENTITY)) && + !defined(node.extensions) && !defined(node.extras); +} + +function deleteNode(gltf, nodeId) { + // Remove from list of nodes in scene + ForEach.scene(gltf, function(scene) { + const sceneNodes = scene.nodes; + if (defined(sceneNodes)) { + const sceneNodesLength = sceneNodes.length; + for (let i = sceneNodesLength; i >= 0; --i) { + if (sceneNodes[i] === nodeId) { + sceneNodes.splice(i, 1); + return; + } + } + } + }); + + // Remove parent node's reference to this node, and delete the parent if also empty + ForEach.node(gltf, function(parentNode, parentNodeId) { + if (defined(parentNode.children)) { + const index = parentNode.children.indexOf(nodeId); + if (index > -1) { + parentNode.children.splice(index, 1); + + if (isNodeEmpty(parentNode)) { + deleteNode(gltf, parentNodeId); + } + } + } + }); + + delete gltf.nodes[nodeId]; +} + +function removeEmptyNodes(gltf) { + ForEach.node(gltf, function(node, nodeId) { + if (isNodeEmpty(node)) { + deleteNode(gltf, nodeId); + } + }); + + return gltf; +} + +function requireAnimationAccessorMinMax(gltf) { + ForEach.animation(gltf, function(animation) { + ForEach.animationSampler(animation, function(sampler) { + const accessor = gltf.accessors[sampler.input]; + if (!defined(accessor.min) || !defined(accessor.max)) { + const minMax = findAccessorMinMax(gltf, accessor); + accessor.min = minMax.min; + accessor.max = minMax.max; + } + }); + }); +} + +function glTF10to20(gltf) { + gltf.asset = defaultValue(gltf.asset, {}); + gltf.asset.version = '2.0'; + // material.instanceTechnique properties should be directly on the material. instanceTechnique is a gltf 0.8 property but is seen in some 1.0 models. + updateInstanceTechniques(gltf); + // animation.samplers now refers directly to accessors and animation.parameters should be removed + removeAnimationSamplersIndirection(gltf); + // Remove empty nodes and re-assign referencing indices + removeEmptyNodes(gltf); + // Top-level objects are now arrays referenced by index instead of id + objectsToArrays(gltf); + // Animation.sampler objects cannot have names + removeAnimationSamplerNames(gltf); + // asset.profile no longer exists + stripAsset(gltf); + // Move known extensions from extensionsUsed to extensionsRequired + requireKnownExtensions(gltf); + // bufferView.byteLength and buffer.byteLength are required + requireByteLength(gltf); + // byteStride moved from accessor to bufferView + moveByteStrideToBufferView(gltf); + // accessor.min and accessor.max must be defined for accessors containing POSITION attributes + requirePositionAccessorMinMax(gltf); + // An animation sampler's input accessor must have min and max properties defined + requireAnimationAccessorMinMax(gltf); + // buffer.type is unnecessary and should be removed + removeBufferType(gltf); + // Remove format, internalFormat, target, and type + removeTextureProperties(gltf); + // TEXCOORD and COLOR attributes must be written with a set index (TEXCOORD_#) + requireAttributeSetIndex(gltf); + // Add underscores to application-specific parameters + underscoreApplicationSpecificSemantics(gltf); + // Accessors referenced by JOINTS_0 and WEIGHTS_0 attributes must have correct component types + updateAccessorComponentTypes(gltf); + // Clamp camera parameters + clampCameraParameters(gltf); + // Move legacy technique render states to material properties and add KHR_blend extension blending functions + moveTechniqueRenderStates(gltf); + // Add material techniques to KHR_techniques_webgl extension, removing shaders, programs, and techniques + moveTechniquesToExtension(gltf); + // Remove empty arrays + removeEmptyArrays(gltf); +} diff --git a/src/gltf2glb/writeResources.js b/src/gltf2glb/writeResources.js new file mode 100644 index 0000000..9304e55 --- /dev/null +++ b/src/gltf2glb/writeResources.js @@ -0,0 +1,214 @@ +const Cesium = require('cesium'); +const mime = require('mime'); +const addBuffer = require('./addBuffer'); +const ForEach = require('./ForEach'); +const getImageExtension = require('./getImageExtension'); +const mergeBuffers = require('./mergeBuffers'); +const removeUnusedElements = require('./removeUnusedElements'); + +const defaultValue = Cesium.defaultValue; +const defined = Cesium.defined; +const WebGLConstants = Cesium.WebGLConstants; + +// .crn (Crunch) is not a supported mime type, so add it +mime.define({'image/crn': ['crn']}, true); + +// .glsl shaders are text/plain type +mime.define({'text/plain': ['glsl']}, true); + +module.exports = writeResources; + +/** + * Write glTF resources as data uris, buffer views, or files. + * + * @param {Object} gltf A javascript object containing a glTF asset. + * @param {Object} [options] Object with the following properties: + * @param {String} [options.name] The name of the glTF asset, for writing separate resources. + * @param {Boolean} [options.separateBuffers=false] Whether to save buffers as separate files. + * @param {Boolean} [options.separateShaders=false] Whether to save shaders as separate files. + * @param {Boolean} [options.separateTextures=false] Whether to save images as separate files. + * @param {Boolean} [options.dataUris=false] Write embedded resources as data uris instead of buffer views. + * @param {Boolean} [options.dracoOptions.uncompressedFallback=false] If set, add uncompressed fallback versions of the compressed meshes. + * @param {Object} [options.bufferStorage] When defined, the glTF buffer's underlying Buffer object will be saved here instead of encoded as a data uri or saved as a separate resource. + * @param {Object} [options.separateResources] When defined, buffers of separate resources will be saved here. + * @returns {Object} The glTF asset. + * + * @private + */ +async function writeResources(gltf, options) { + options = defaultValue(options, {}); + options.separateBuffers = defaultValue(options.separateBuffers, false); + options.separateTextures = defaultValue(options.separateTextures, false); + options.separateShaders = defaultValue(options.separateShaders, false); + options.dataUris = defaultValue(options.dataUris, false); + + await ForEach.imageAsync(gltf, async function(image, i) { + await writeImage(gltf, image, i, options); + await ForEach.compressedImageAsync(image, async function(compressedImage) { + await writeImage(gltf, compressedImage, i, options); + }); + }); + + await ForEach.shaderAsync(gltf, async function(shader, i) { + await writeShader(gltf, shader, i, options); + }); + + await ForEach.audioClipAsync(gltf, async function(audioClip, i) { + await writeAudio(gltf, audioClip, i, options); + }); + + // Buffers need to be written last because images and shaders may write to new buffers + removeUnusedElements(gltf); + mergeBuffers(gltf, options.name); + + await ForEach.bufferAsync(gltf, async function(buffer, bufferId) { + await writeBuffer(gltf, buffer, bufferId, options, true); + }); + return gltf; +} + +async function writeBuffer(gltf, buffer, i, options) { + if (defined(options.bufferStorage)) { + writeBufferStorage(buffer, options); + } else { + await writeResource(gltf, buffer, i, separate, true, '.bin', options); + } +} + +function writeBufferStorage(buffer, options) { + let combinedBuffer = options.bufferStorage.buffer; + combinedBuffer = defined(combinedBuffer) ? combinedBuffer : Buffer.alloc(0); + combinedBuffer = Buffer.concat([combinedBuffer, buffer.extras._pipeline.source]); + options.bufferStorage.buffer = combinedBuffer; +} + +async function writeImage(gltf, image, i, options) { + const extension = getImageExtension(image.extras._pipeline.source); + await writeResource(gltf, image, i, options.separateTextures, options.dataUris, extension, options); + if (defined(image.bufferView)) { + // Preserve the image mime type when writing to a buffer view + image.mimeType = mime.getType(extension); + } +} + +async function writeAudio(gltf, audioClip, i, options) { + const extension = audioClip.extras._pipeline.ext; + // need not to pack a no-lazy and stream mode audio + const separate = audioClip.isLazy && audioClip.mode === 'Stream'; + + await writeResource(gltf, audioClip, i, separate, options.dataUris, extension, options); + if (defined(audioClip.bufferView)) { + // Preserve the image mime type when writing to a buffer view + audioClip.mimeType = mime.getType(extension); + } +} + +async function writeShader(gltf, shader, i, options) { + await writeResource(gltf, shader, i, options.separateShaders, options.dataUris, '.glsl', options); +} + +async function writeResource(gltf, object, index, separate, dataUris, extension, options) { + separate = separate || options.separateCustom(getRelativePath(gltf, object, index, extension, options)); + + const uri = object.extras._pipeline.relativePath; + const source = object.extras._pipeline.source; + if (!separate) { + object.extras._pipeline.source = await options.prepareNonSeparateResources(uri, source); + } + + if (separate) { + await writeFile(gltf, object, index, extension, options); + } else if (dataUris) { + writeDataUri(object, extension); + } else { + writeBufferView(gltf, object); + } +} + +function writeDataUri(object, extension) { + delete object.bufferView; + const source = object.extras._pipeline.source; + const mimeType = mime.getType(extension); + object.uri = 'data:' + mimeType + ';base64,' + source.toString('base64'); +} + +function writeBufferView(gltf, object) { + delete object.uri; + let source = object.extras._pipeline.source; + if (typeof source === 'string') { + source = Buffer.from(source); + } + object.bufferView = addBuffer(gltf, source); +} + +function getProgram(gltf, shaderIndex) { + return ForEach.program(gltf, function(program, index) { + if (program.fragmentShader === shaderIndex || program.vertexShader === shaderIndex) { + return { + program: program, + index: index + }; + } + }); +} + +function getName(gltf, object, index, extension, options) { + const gltfName = options.name; + const objectName = object.name; + + if (defined(objectName)) { + return objectName; + } else if (extension === '.bin') { + if (defined(gltfName)) { + return gltfName + index; + } + return 'buffer' + index; + } else if (extension === '.glsl') { + const programInfo = getProgram(gltf, index); + const program = programInfo.program; + const programIndex = programInfo.index; + const programName = program.name; + const shaderType = object.type === WebGLConstants.FRAGMENT_SHADER ? 'FS' : 'VS'; + if (defined(programName)) { + return programName + shaderType; + } else if (defined(gltfName)) { + return gltfName + shaderType + programIndex; + } + return shaderType.toLowerCase() + programIndex; + } + + // Otherwise is an image + if (defined(gltfName)) { + return gltfName + index; + } + return 'image' + index; +} + +function getRelativePath(gltf, object, index, extension, options) { + const pipelineExtras = object.extras._pipeline; + let relativePath = pipelineExtras.relativePath; + if (defined(relativePath)) { + return relativePath.replace(/\\/g, '/'); + } + + const name = getName(gltf, object, index, extension, options); + relativePath = name + extension; + + // Check if a file of the same name already exists, and if so, append a number + const number = 1; + while (defined(options.separateResources[relativePath])) { + relativePath = name + '_' + number + extension; + } + return relativePath; +} + +async function writeFile(gltf, object, index, extension, options) { + delete object.bufferView; + const source = object.extras._pipeline.source; + const relativePath = getRelativePath(gltf, object, index, extension, options); + object.uri = await options.prepareSeparateResource(relativePath, source); + + if (defined(options.separateResources)) { + options.separateResources[object.uri] = {object, source}; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e6c73f7 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,19 @@ +export {compressTextures} from './compressTextures'; +export {getTextureVariants, normalizeOptions} from './options'; +export type { + AssetProcessor, + Base64Options, + CompressOptions, + FileRule, + GlbOptions, + ModelPipelineOptions, + NormalizedModelPipelineOptions, + TextureCompressionOptions, + TextureFallbackOptions, + TextureVariant, + TextureVariantName +} from './options'; +export {compressGltf, prepareGltf} from './preprocessGltf'; +export {fileToDataUri, packToGlb, processModel} from './processModel'; +export type {EmittedAsset, ModelEntry, ProcessModelResult, UploadFile} from './processModel'; +export {bufferToDataUri} from './utils'; diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 0000000..53f9bff --- /dev/null +++ b/src/options.ts @@ -0,0 +1,196 @@ +export type FileRule = RegExp | ((filePath: string) => boolean); + +export interface AssetProcessor { + test?: FileRule; + process(options: {data: Buffer; filePath: string}): Promise; +} + +export interface CompressOptions { + enabled: boolean; + excludes: FileRule[]; + quantization: Record; +} + +export interface TextureCodecOptions { + enabled?: boolean; + formatOpaque?: string; + formatTransparent?: string; + quality?: string; + excludes?: FileRule[]; +} + +export interface TextureFallbackOptions { + useRGBA4444?: boolean; + useRGB565?: boolean; + excludes?: FileRule[]; +} + +export interface TextureCompressionOptions { + enabled: boolean; + quality: 'high' | 'medium' | 'low'; + excludes: FileRule[]; + astc: TextureCodecOptions; + pvrtc: TextureCodecOptions; + etc: TextureCodecOptions; + s3tc: TextureCodecOptions; + fallback: TextureFallbackOptions; +} + +export interface Base64Options { + enabled: boolean; + threshold: number; + includeModel: boolean; + excludes: FileRule[]; +} + +export interface GlbOptions { + enabled: boolean; + excludes: FileRule[]; +} + +export interface ModelPipelineOptions { + rootDir?: string; + outDir?: string; + compress?: Partial; + compressTextures?: Partial; + base64?: Partial; + glb?: Partial; + processors?: AssetProcessor[]; +} + +export interface NormalizedModelPipelineOptions { + rootDir: string; + outDir: string; + compress: CompressOptions; + compressTextures: TextureCompressionOptions; + base64: Base64Options; + glb: GlbOptions; + processors: AssetProcessor[]; +} + +export type TextureVariantName = 'normal' | 'astc' | 'pvrtc' | 'etc' | 's3tc' | 'fallback'; + +export interface TextureVariant { + name: TextureVariantName; + required: string[]; +} + +const DEFAULT_OPTIONS: NormalizedModelPipelineOptions = { + rootDir: process.cwd(), + outDir: 'dist', + compress: { + enabled: false, + excludes: [], + quantization: {} + }, + compressTextures: { + enabled: false, + quality: 'medium', + excludes: [], + astc: { + enabled: true, + excludes: [] + }, + pvrtc: { + enabled: true, + excludes: [] + }, + etc: { + enabled: false, + excludes: [] + }, + s3tc: { + enabled: false, + excludes: [] + }, + fallback: { + useRGBA4444: true, + useRGB565: true, + excludes: [] + } + }, + base64: { + enabled: false, + threshold: 1000, + includeModel: false, + excludes: [] + }, + glb: { + enabled: false, + excludes: [] + }, + processors: [] +}; + +export function normalizeOptions(options: ModelPipelineOptions = {}): NormalizedModelPipelineOptions { + return { + rootDir: options.rootDir || DEFAULT_OPTIONS.rootDir, + outDir: options.outDir || DEFAULT_OPTIONS.outDir, + compress: { + ...DEFAULT_OPTIONS.compress, + ...(options.compress || {}) + }, + compressTextures: { + ...DEFAULT_OPTIONS.compressTextures, + ...(options.compressTextures || {}), + astc: { + ...DEFAULT_OPTIONS.compressTextures.astc, + ...((options.compressTextures || {}).astc || {}) + }, + pvrtc: { + ...DEFAULT_OPTIONS.compressTextures.pvrtc, + ...((options.compressTextures || {}).pvrtc || {}) + }, + etc: { + ...DEFAULT_OPTIONS.compressTextures.etc, + ...((options.compressTextures || {}).etc || {}) + }, + s3tc: { + ...DEFAULT_OPTIONS.compressTextures.s3tc, + ...((options.compressTextures || {}).s3tc || {}) + }, + fallback: { + ...DEFAULT_OPTIONS.compressTextures.fallback, + ...((options.compressTextures || {}).fallback || {}) + } + }, + base64: { + ...DEFAULT_OPTIONS.base64, + ...(options.base64 || {}) + }, + glb: { + ...DEFAULT_OPTIONS.glb, + ...(options.glb || {}) + }, + processors: options.processors || [] + }; +} + +export function getTextureVariants(options: NormalizedModelPipelineOptions): TextureVariant[] { + if (!options.compressTextures.enabled) { + return [{name: 'normal', required: []}]; + } + + const variants: TextureVariant[] = []; + const candidates: TextureVariant[] = [ + {name: 'astc', required: ['WEBGL_compressed_texture_astc']}, + {name: 'pvrtc', required: ['WEBGL_compressed_texture_pvrtc']}, + {name: 'etc', required: ['WEBGL_compressed_texture_etc']}, + {name: 's3tc', required: ['WEBGL_compressed_texture_s3tc']}, + {name: 'fallback', required: []} + ]; + + for (const variant of candidates) { + if (variant.required.length === 0) { + variants.push(variant); + continue; + } + + const codecOptions = options.compressTextures[variant.name]; + if (codecOptions && codecOptions.enabled) { + variants.push(variant); + } + } + + return variants; +} diff --git a/src/preprocessGltf.ts b/src/preprocessGltf.ts new file mode 100644 index 0000000..84df67d --- /dev/null +++ b/src/preprocessGltf.ts @@ -0,0 +1,102 @@ +import fs from 'fs/promises'; +import os from 'os'; +import path from 'path'; + +import {copyDir, copyFile, readFileText, removeDir} from './utils'; + +const compressAsync = require('amc/build/compressGLTF'); + +export interface PreparedGltf { + cleanup(): Promise; + filePath: string; + json: any; + dir: string; +} + +export async function prepareGltf( + inputPath: string, + options: { + compress: boolean; + rootDir: string; + quantization?: Record; + } +): Promise { + const filename = path.basename(inputPath); + const sourceDir = path.dirname(inputPath); + const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'node-model-tools-')); + const relativeDir = path.relative(options.rootDir, sourceDir); + const targetDir = path.join(tempRoot, relativeDir.startsWith('..') ? '' : relativeDir); + const targetFilePath = path.join(targetDir, filename); + + const cleanup = async () => { + await removeDir(tempRoot); + }; + + if (!options.compress) { + await copyDir(sourceDir, targetDir); + + return { + cleanup, + filePath: targetFilePath, + json: JSON.parse(await readFileText(inputPath)), + dir: targetDir + }; + } + + try { + await fs.mkdir(targetDir, {recursive: true}); + await compressAsync(inputPath, targetFilePath, { + quantization: { + POSITION: 13, + NORMAL: 8, + TEXCOORD: 10, + TEXCOORD_1: 10, + JOINT: 6, + WEIGHT: 6, + TANGENT: 10, + ...(options.quantization || {}) + } + }); + + const gltfJson = JSON.parse(await readFileText(targetFilePath)); + + await Promise.all((gltfJson.images || []).map((image: {uri: string}) => { + return copyFile(path.join(sourceDir, image.uri), path.join(targetDir, image.uri)); + })); + + await Promise.all((((gltfJson.extensions || {}).Sein_audioClips || {}).clips || []).map((audio: {uri: string}) => { + return copyFile(path.join(sourceDir, audio.uri), path.join(targetDir, audio.uri)); + })); + + return { + cleanup, + filePath: targetFilePath, + json: gltfJson, + dir: targetDir + }; + } catch (error) { + console.warn('Compression failed, fallback to plain copy.', error); + await copyDir(sourceDir, targetDir); + + return { + cleanup, + filePath: targetFilePath, + json: JSON.parse(await readFileText(inputPath)), + dir: targetDir + }; + } +} + +export async function compressGltf( + inputPath: string, + options: { + rootDir?: string; + quantization?: Record; + } = {} +): Promise { + return prepareGltf(inputPath, { + compress: true, + rootDir: path.resolve(options.rootDir || process.cwd()), + quantization: options.quantization + }); +} diff --git a/src/processModel.ts b/src/processModel.ts new file mode 100644 index 0000000..4c47d34 --- /dev/null +++ b/src/processModel.ts @@ -0,0 +1,332 @@ +import mime from 'mime'; +import path from 'path'; + +import {compressTextures} from './compressTextures'; +import {getTextureVariants, normalizeOptions, type ModelPipelineOptions, type NormalizedModelPipelineOptions} from './options'; +import {prepareGltf} from './preprocessGltf'; +import { + appendHashToFilename, + bufferToDataUri, + checkFileWithRules, + cloneJson, + ensureDir, + getAssetType, + getHashedAssetPath, + getMd5, + readFileBuffer, + toPosixPath, + writeFile +} from './utils'; + +const gltfToGlb = require('./gltf2glb/gltfToGlb'); +const OUTPUT_GENERATOR = 'XOSMO Toolkit'; + +export interface EmittedAsset { + sourcePath: string; + distPath?: string; + outputPath?: string; + url: string; + inlined: boolean; +} + +export interface UploadFile { + sourcePath: string; + relativePath: string; + outputPath: string; + fileName: string; + mimeType: string; + size: number; +} + +export interface ModelEntry { + name: string; + variant: string; + type: 'gltf' | 'glb'; + required: string[]; + url: string; + outputPath?: string; +} + +export interface ProcessModelResult { + inputPath: string; + outDir: string; + entries: ModelEntry[]; + assets: EmittedAsset[]; + uploadFiles: UploadFile[]; +} + +export async function fileToDataUri(filePath: string): Promise { + const buffer = await readFileBuffer(filePath); + return bufferToDataUri(buffer, mime.getType(filePath) || ''); +} + +export async function packToGlb(gltf: any, resourceDirectory: string, options: any = {}) { + applyOutputGenerator(gltf); + return gltfToGlb(gltf, { + resourceDirectory, + ...options + }); +} + +export async function processModel(inputPath: string, userOptions: ModelPipelineOptions = {}): Promise { + const options = normalizeOptions(userOptions); + const resourcePath = path.resolve(inputPath); + const rootDir = path.resolve(options.rootDir); + const outDir = path.isAbsolute(options.outDir) ? options.outDir : path.resolve(rootDir, options.outDir); + const source = await readFileBuffer(resourcePath); + const fileName = path.basename(resourcePath, path.extname(resourcePath)); + const assets: EmittedAsset[] = []; + const entries: ModelEntry[] = []; + const uploadFiles: UploadFile[] = []; + const variants = getTextureVariants(options); + + const emitFile = async (params: { + data: Buffer | string; + distPath: string; + sourcePath: string; + }): Promise<{url: string; outputPath: string}> => { + const outputPath = path.resolve(outDir, params.distPath); + await ensureDir(path.dirname(outputPath)); + await writeFile(outputPath, params.data); + const url = toPosixPath(params.distPath); + + assets.push({ + sourcePath: params.sourcePath, + distPath: params.distPath, + outputPath, + url, + inlined: false + }); + uploadFiles.push({ + sourcePath: params.sourcePath, + relativePath: url, + outputPath, + fileName: path.basename(params.distPath), + mimeType: mime.getType(outputPath) || 'application/octet-stream', + size: Buffer.isBuffer(params.data) ? params.data.length : Buffer.byteLength(params.data) + }); + + return {url, outputPath}; + }; + + const processBuffer = async (data: Buffer, filePath: string) => { + let processed = data; + for (const processor of options.processors) { + if (!processor.test || checkFileWithRules(filePath, [processor.test])) { + processed = await processor.process({data: processed, filePath}); + } + } + + return processed; + }; + + if (path.extname(resourcePath) === '.glb') { + const maybeInlined = await maybeInlineModel(resourcePath, source, 'application/octet-stream', options, assets); + if (maybeInlined) { + entries.push({ + name: fileName, + variant: 'normal', + type: 'glb', + required: [], + url: maybeInlined + }); + } else { + const distPath = `${fileName}-${getMd5(source)}.glb`; + const emitted = await emitFile({data: source, distPath, sourcePath: resourcePath}); + entries.push({ + name: fileName, + variant: 'normal', + type: 'glb', + required: [], + url: emitted.url, + outputPath: emitted.outputPath + }); + } + + return { + inputPath: resourcePath, + outDir, + entries, + assets, + uploadFiles + }; + } + + const prepared = await prepareGltf(resourcePath, { + compress: options.compress.enabled && !checkFileWithRules(resourcePath, options.compress.excludes), + rootDir, + quantization: options.compress.quantization + }); + + try { + for (const variant of variants) { + const gltf = cloneJson(prepared.json); + applyOutputGenerator(gltf); + const distDir = ''; + + if (options.compressTextures.enabled && variant.name !== 'normal') { + await compressTextures(gltf, prepared.dir, variant, options.compressTextures); + } + + if (options.glb.enabled && !checkFileWithRules(resourcePath, options.glb.excludes)) { + const {glb} = await gltfToGlb(gltf, { + resourceDirectory: prepared.dir, + separateCustom: (uri: string) => { + return checkFileWithRules(path.resolve(prepared.dir, uri), options.glb.excludes); + }, + prepareNonSeparateResources: async (uri: string, buffer: Buffer) => { + return processBuffer(buffer, path.resolve(prepared.dir, uri)); + }, + prepareSeparateResource: async (uri: string, buffer: Buffer) => { + const sourcePath = path.resolve(prepared.dir, uri); + const processedBuffer = await processBuffer(buffer, sourcePath); + const distPath = path.join(distDir, appendHashToFilename(uri, processedBuffer)); + const emitted = await emitFile({ + data: processedBuffer, + distPath, + sourcePath + }); + + return emitted.url; + } + }); + + const maybeInlined = await maybeInlineModel(resourcePath, glb, 'application/octet-stream', options, assets); + if (maybeInlined) { + entries.push({ + name: fileName, + variant: variant.name, + type: 'glb', + required: variant.required, + url: maybeInlined + }); + } else { + const distPath = path.join(distDir, `${fileName}-${variant.name}-${getMd5(glb)}.glb`); + const emitted = await emitFile({data: glb, distPath, sourcePath: resourcePath}); + entries.push({ + name: fileName, + variant: variant.name, + type: 'glb', + required: variant.required, + url: emitted.url, + outputPath: emitted.outputPath + }); + } + + continue; + } + + await Promise.all([ + ...(gltf.buffers || []).map((item: {uri: string}) => processAssetReference(item, prepared.dir, distDir, options, assets, processBuffer, emitFile)), + ...(gltf.images || []).map((item: {uri: string}) => processAssetReference(item, prepared.dir, distDir, options, assets, processBuffer, emitFile)), + ...((((gltf.extensions || {}).KHR_techniques_webgl || {}).shaders || []).map((item: {uri: string}) => processAssetReference(item, prepared.dir, distDir, options, assets, processBuffer, emitFile))), + ...(((((gltf.extensions || {}).Sein_audioClips || {}).clips || []).map((item: {uri: string}) => processAssetReference(item, prepared.dir, distDir, options, assets, processBuffer, emitFile)))) + ]); + + const content = JSON.stringify(gltf); + const maybeInlined = await maybeInlineModel(resourcePath, Buffer.from(content), 'application/json', options, assets); + if (maybeInlined) { + entries.push({ + name: fileName, + variant: variant.name, + type: 'gltf', + required: variant.required, + url: maybeInlined + }); + } else { + const distPath = path.join(distDir, `${fileName}-${variant.name}-${getMd5(content)}.gltf`); + const emitted = await emitFile({data: content, distPath, sourcePath: resourcePath}); + entries.push({ + name: fileName, + variant: variant.name, + type: 'gltf', + required: variant.required, + url: emitted.url, + outputPath: emitted.outputPath + }); + } + } + } finally { + await prepared.cleanup(); + } + + return { + inputPath: resourcePath, + outDir, + entries, + assets, + uploadFiles + }; +} + +function applyOutputGenerator(gltf: any) { + gltf.asset = gltf.asset || {}; + gltf.asset.generator = OUTPUT_GENERATOR; +} + +async function processAssetReference( + target: {uri: string}, + sourceDir: string, + distDir: string, + options: NormalizedModelPipelineOptions, + assets: EmittedAsset[], + processBuffer: (data: Buffer, filePath: string) => Promise, + emitFile: (params: {data: Buffer | string; distPath: string; sourcePath: string}) => Promise<{url: string; outputPath: string}> +) { + if (getAssetType(target.uri) !== 'relative') { + return; + } + + const sourcePath = path.resolve(sourceDir, target.uri); + let data = await readFileBuffer(sourcePath); + data = await processBuffer(data, sourcePath); + + if ( + options.base64.enabled + && data.length < options.base64.threshold + && !checkFileWithRules(sourcePath, options.base64.excludes) + ) { + const url = bufferToDataUri(data, mime.getType(sourcePath) || ''); + target.uri = url; + assets.push({ + sourcePath, + url, + inlined: true + }); + return; + } + + const distPath = getHashedAssetPath(distDir, target.uri, data); + const emitted = await emitFile({ + data, + distPath, + sourcePath + }); + target.uri = emitted.url; +} + +async function maybeInlineModel( + resourcePath: string, + data: Buffer, + mimeType: string, + options: NormalizedModelPipelineOptions, + assets: EmittedAsset[] +): Promise { + if ( + !options.base64.enabled + || !options.base64.includeModel + || data.length >= options.base64.threshold + || checkFileWithRules(resourcePath, options.base64.excludes) + ) { + return null; + } + + const url = bufferToDataUri(data, mimeType); + assets.push({ + sourcePath: resourcePath, + url, + inlined: true + }); + + return url; +} diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..f674a13 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,17 @@ +declare module 'amc/build/compressGLTF' { + const compressGLTF: (inputPath: string, outputPath: string, options?: any) => Promise; + export = compressGLTF; +} + +declare module 'seinjs-texture-compressor' { + export function pack(options: { + type: string; + input: string; + output: string; + compression: string; + quality: string; + verbose?: boolean; + mipmap?: boolean; + square?: string; + }): Promise; +} diff --git a/src/upng.js b/src/upng.js new file mode 100644 index 0000000..11ebe31 --- /dev/null +++ b/src/upng.js @@ -0,0 +1,1028 @@ +/** + * @File : upng.js + * @source : https://github.com/photopea/UPNG.js + */ +import * as UZIP from 'uzip'; + +var UPNG = {}; + +UPNG.toRGBA8 = function(out) +{ + var w = out.width, h = out.height; + if(out.tabs.acTL==null) return [UPNG.toRGBA8.decodeImage(out.data, w, h, out).buffer]; + + var frms = []; + if(out.frames[0].data==null) out.frames[0].data = out.data; + + var len = w*h*4, img = new Uint8Array(len), empty = new Uint8Array(len), prev=new Uint8Array(len); + for(var i=0; i>3)]>>(7-((i&7)<<0)))& 1), cj=3*j; bf[qi]=p[cj]; bf[qi+1]=p[cj+1]; bf[qi+2]=p[cj+2]; bf[qi+3]=(j>2)]>>(6-((i&3)<<1)))& 3), cj=3*j; bf[qi]=p[cj]; bf[qi+1]=p[cj+1]; bf[qi+2]=p[cj+2]; bf[qi+3]=(j>1)]>>(4-((i&1)<<2)))&15), cj=3*j; bf[qi]=p[cj]; bf[qi+1]=p[cj+1]; bf[qi+2]=p[cj+2]; bf[qi+3]=(j>>3)]>>>(7 -((x&7) )))& 1), al=(gr==tr*255)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; } + else if(depth== 2) for(var x=0; x>>2)]>>>(6 -((x&3)<<1)))& 3), al=(gr==tr* 85)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; } + else if(depth== 4) for(var x=0; x>>1)]>>>(4 -((x&1)<<2)))&15), al=(gr==tr* 17)?0:255; bf32[to+x]=(al<<24)|(gr<<16)|(gr<<8)|gr; } + else if(depth== 8) for(var x=0; x>>2<<3);while(i==0){i=n(N,d,1);m=n(N,d+1,2);d+=3;if(m==0){if((d&7)!=0)d+=8-(d&7); +var D=(d>>>3)+4,q=N[D-4]|N[D-3]<<8;if(Z)W=H.H.W(W,w+q);W.set(new R(N.buffer,N.byteOffset+D,q),w);d=D+q<<3; +w+=q;continue}if(Z)W=H.H.W(W,w+(1<<17));if(m==1){v=b.J;C=b.h;X=(1<<9)-1;u=(1<<5)-1}if(m==2){J=A(N,d,5)+257; +h=A(N,d+5,5)+1;Q=A(N,d+10,4)+4;d+=14;var E=d,j=1;for(var c=0;c<38;c+=2){b.Q[c]=0;b.Q[c+1]=0}for(var c=0; +cj)j=K}d+=3*Q;M(b.Q,j);I(b.Q,j,b.u);v=b.w;C=b.d; +d=l(b.u,(1<>>4;if(p>>>8==0){W[w++]=p}else if(p==256){break}else{var z=w+p-254; +if(p>264){var _=b.q[p-257];z=w+(_>>>3)+A(N,d,_&7);d+=_&7}var $=C[e(N,d)&u];d+=$&15;var s=$>>>4,Y=b.c[s],a=(Y>>>4)+n(N,d,Y&15); +d+=Y&15;while(w>>4; +if(b<=15){A[I]=b;I++}else{var Z=0,m=0;if(b==16){m=3+l(V,n,2);n+=2;Z=A[I-1]}else if(b==17){m=3+l(V,n,3); +n+=3}else if(b==18){m=11+l(V,n,7);n+=7}var J=I+m;while(I>>1; +while(An)n=M;A++}while(A>1,I=N[l+1],e=M<<4|I,b=W-I,Z=N[l]<>>15-W;R[J]=e;Z++}}};H.H.l=function(N,W){var R=H.H.m.r,V=15-W;for(var n=0;n>>V}};H.H.M=function(N,W,R){R=R<<(W&7);var V=W>>>3;N[V]|=R;N[V+1]|=R>>>8}; +H.H.I=function(N,W,R){R=R<<(W&7);var V=W>>>3;N[V]|=R;N[V+1]|=R>>>8;N[V+2]|=R>>>16};H.H.e=function(N,W,R){return(N[W>>>3]|N[(W>>>3)+1]<<8)>>>(W&7)&(1<>>3]|N[(W>>>3)+1]<<8|N[(W>>>3)+2]<<16)>>>(W&7)&(1<>>3]|N[(W>>>3)+1]<<8|N[(W>>>3)+2]<<16)>>>(W&7)}; +H.H.i=function(N,W){return(N[W>>>3]|N[(W>>>3)+1]<<8|N[(W>>>3)+2]<<16|N[(W>>>3)+3]<<24)>>>(W&7)};H.H.m=function(){var N=Uint16Array,W=Uint32Array; +return{K:new N(16),j:new N(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new N(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new W(32),J:new N(512),_:[],h:new N(32),$:[],w:new N(32768),C:[],v:[],d:new N(32768),D:[],u:new N(512),Q:[],r:new N(1<<15),s:new W(286),Y:new W(30),a:new W(19),t:new W(15e3),k:new N(1<<16),g:new N(1<<15)}}(); +(function(){var N=H.H.m,W=1<<15;for(var R=0;R>>1|(V&1431655765)<<1; +V=(V&3435973836)>>>2|(V&858993459)<<2;V=(V&4042322160)>>>4|(V&252645135)<<4;V=(V&4278255360)>>>8|(V&16711935)<<8; +N.r[R]=(V>>>16|V<<16)>>>17}function n(A,l,M){while(l--!=0)A.push(0,M)}for(var R=0;R<32;R++){N.q[R]=N.S[R]<<3|N.T[R]; +N.c[R]=N.p[R]<<4|N.z[R]}n(N._,144,8);n(N._,255-143,9);n(N._,279-255,7);n(N._,287-279,8);H.H.n(N._,9); +H.H.A(N._,9,N.J);H.H.l(N._,9);n(N.$,32,5);H.H.n(N.$,5);H.H.A(N.$,5,N.h);H.H.l(N.$,5);n(N.Q,19,0);n(N.C,286,0); +n(N.D,30,0);n(N.v,320,0)}());return H.H.N}() + + +UPNG.decode._readInterlace = function(data, out) +{ + var w = out.width, h = out.height; + var bpp = UPNG.decode._getBPP(out), cbpp = bpp>>3, bpl = Math.ceil(w*bpp/8); + var img = new Uint8Array( h * bpl ); + var di = 0; + + var starting_row = [ 0, 0, 4, 0, 2, 0, 1 ]; + var starting_col = [ 0, 4, 0, 2, 0, 1, 0 ]; + var row_increment = [ 8, 8, 8, 4, 4, 2, 2 ]; + var col_increment = [ 8, 8, 4, 4, 2, 2, 1 ]; + + var pass=0; + while(pass<7) + { + var ri = row_increment[pass], ci = col_increment[pass]; + var sw = 0, sh = 0; + var cr = starting_row[pass]; while(cr>3]; val = (val>>(7-(cdi&7)))&1; + img[row*bpl + (col>>3)] |= (val << (7-((col&7)<<0))); + } + if(bpp==2) { + var val = data[cdi>>3]; val = (val>>(6-(cdi&7)))&3; + img[row*bpl + (col>>2)] |= (val << (6-((col&3)<<1))); + } + if(bpp==4) { + var val = data[cdi>>3]; val = (val>>(4-(cdi&7)))&15; + img[row*bpl + (col>>1)] |= (val << (4-((col&1)<<2))); + } + if(bpp>=8) { + var ii = row*bpl+col*cbpp; + for(var j=0; j>3)+j]; + } + cdi+=bpp; col+=ci; + } + y++; row += ri; + } + if(sw*sh!=0) di += sh * (1 + bpll); + pass = pass + 1; + } + return img; +} + +UPNG.decode._getBPP = function(out) { + var noc = [1,null,3,1,2,null,4][out.ctype]; + return noc * out.depth; +} + +UPNG.decode._filterZero = function(data, out, off, w, h) +{ + var bpp = UPNG.decode._getBPP(out), bpl = Math.ceil(w*bpp/8), paeth = UPNG.decode._paeth; + bpp = Math.ceil(bpp/8); + + var i=0, di=1, type=data[off], x=0; + + if(type>1) data[off]=[0,0,1][type-2]; + if(type==3) for(x=bpp; x>>1) )&255; + + for(var y=0; y>>1)); + for(; x>>1) ); } + else { for(; x>8)&255; buff[p+1] = n&255; }, + readUint : function(buff,p) { return (buff[p]*(256*256*256)) + ((buff[p+1]<<16) | (buff[p+2]<< 8) | buff[p+3]); }, + writeUint : function(buff,p,n){ buff[p]=(n>>24)&255; buff[p+1]=(n>>16)&255; buff[p+2]=(n>>8)&255; buff[p+3]=n&255; }, + readASCII : function(buff,p,l){ var s = ""; for(var i=0; i=0 && yoff>=0) { si = (y*sw+x)<<2; ti = (( yoff+y)*tw+xoff+x)<<2; } + else { si = ((-yoff+y)*sw-xoff+x)<<2; ti = (y*tw+x)<<2; } + + if (mode==0) { tb[ti] = sb[si]; tb[ti+1] = sb[si+1]; tb[ti+2] = sb[si+2]; tb[ti+3] = sb[si+3]; } + else if(mode==1) { + var fa = sb[si+3]*(1/255), fr=sb[si]*fa, fg=sb[si+1]*fa, fb=sb[si+2]*fa; + var ba = tb[ti+3]*(1/255), br=tb[ti]*ba, bg=tb[ti+1]*ba, bb=tb[ti+2]*ba; + + var ifa=1-fa, oa = fa+ba*ifa, ioa = (oa==0?0:1/oa); + tb[ti+3] = 255*oa; + tb[ti+0] = (fr+br*ifa)*ioa; + tb[ti+1] = (fg+bg*ifa)*ioa; + tb[ti+2] = (fb+bb*ifa)*ioa; + } + else if(mode==2){ // copy only differences, otherwise zero + var fa = sb[si+3], fr=sb[si], fg=sb[si+1], fb=sb[si+2]; + var ba = tb[ti+3], br=tb[ti], bg=tb[ti+1], bb=tb[ti+2]; + if(fa==ba && fr==br && fg==bg && fb==bb) { tb[ti]=0; tb[ti+1]=0; tb[ti+2]=0; tb[ti+3]=0; } + else { tb[ti]=fr; tb[ti+1]=fg; tb[ti+2]=fb; tb[ti+3]=fa; } + } + else if(mode==3){ // check if can be blended + var fa = sb[si+3], fr=sb[si], fg=sb[si+1], fb=sb[si+2]; + var ba = tb[ti+3], br=tb[ti], bg=tb[ti+1], bb=tb[ti+2]; + if(fa==ba && fr==br && fg==bg && fb==bb) continue; + //if(fa!=255 && ba!=0) return false; + if(fa<220 && ba>20) return false; + } + } + return true; +} + + + + +UPNG.encode = function(bufs, w, h, ps, dels, tabs, forbidPlte) +{ + if(ps==null) ps=0; + if(forbidPlte==null) forbidPlte = false; + + var nimg = UPNG.encode.compress(bufs, w, h, ps, [false, false, false, 0, forbidPlte]); + UPNG.encode.compressPNG(nimg, -1); + + return UPNG.encode._main(nimg, w, h, dels, tabs); +} + +UPNG.encodeLL = function(bufs, w, h, cc, ac, depth, dels, tabs) { + var nimg = { ctype: 0 + (cc==1 ? 0 : 2) + (ac==0 ? 0 : 4), depth: depth, frames: [] }; + + var time = Date.now(); + var bipp = (cc+ac)*depth, bipl = bipp * w; + for(var i=0; i1, pltAlpha = false; + + var leng = 8 + (16+5+4) /*+ (9+4)*/ + (anim ? 20 : 0); + if(tabs["sRGB"]!=null) leng += 8+1+4; + if(tabs["pHYs"]!=null) leng += 8+9+4; + if(nimg.ctype==3) { + var dl = nimg.plte.length; + for(var i=0; i>>24)!=255) pltAlpha = true; + leng += (8 + dl*3 + 4) + (pltAlpha ? (8 + dl*1 + 4) : 0); + } + for(var j=0; j>>8)&255, b=(c>>>16)&255; + data[offset+ti+0]=r; data[offset+ti+1]=g; data[offset+ti+2]=b; + } + offset+=dl*3; + wUi(data,offset,crc(data,offset-dl*3-4,dl*3+4)); offset+=4; // crc + + if(pltAlpha) { + wUi(data,offset, dl); offset+=4; + wAs(data,offset,"tRNS"); offset+=4; + for(var i=0; i>>24)&255; + offset+=dl; + wUi(data,offset,crc(data,offset-dl-4,dl+4)); offset+=4; // crc + } + } + + var fi = 0; + for(var j=0; j>2, bln>>2)); + for(var j=0; jnw && c==img32[i-nw]) ind[i]=ind[i-nw]; + else { + var cmc = cmap[c]; + if(cmc==null) { cmap[c]=cmc=plte.length; plte.push(c); if(plte.length>=300) break; } + ind[i]=cmc; + } + } + } + //console.log("make palette", Date.now()-time); time = Date.now(); + } + + var cc=plte.length; //console.log("colors:",cc); + if(cc<=256 && forbidPlte==false) { + if(cc<= 2) depth=1; else if(cc<= 4) depth=2; else if(cc<=16) depth=4; else depth=8; + depth = Math.max(depth, minBits); + } + + for(var j=0; j>1)] |= (inj[ii+x]<<(4-(x&1)*4)); + else if(depth==2) for(var x=0; x>2)] |= (inj[ii+x]<<(6-(x&3)*2)); + else if(depth==1) for(var x=0; x>3)] |= (inj[ii+x]<<(7-(x&7)*1)); + } + cimg=nimg; ctype=3; bpp=1; + } + else if(gotAlpha==false && frms.length==1) { // some next "reduced" frames may contain alpha for blending + var nimg = new Uint8Array(nw*nh*3), area=nw*nh; + for(var i=0; i palette indices", Date.now()-time); time = Date.now(); + + return {ctype:ctype, depth:depth, plte:plte, frames:frms }; +} +UPNG.encode.framize = function(bufs,w,h,alwaysBlend,evenCrd,forbidPrev) { + /* DISPOSE + - 0 : no change + - 1 : clear to transparent + - 2 : retstore to content before rendering (previous frame disposed) + BLEND + - 0 : replace + - 1 : blend + */ + var frms = []; + for(var j=0; jmax) max=x; + if(ymay) may=y; + } + } + if(max==-1) mix=miy=max=may=0; + if(evenCrd) { if((mix&1)==1)mix--; if((miy&1)==1)miy--; } + var sarea = (max-mix+1)*(may-miy+1); + if(sareamax) max=cx; + if(cymay) may=cy; + } + } + if(max==-1) mix=miy=max=may=0; + if(evenCrd) { if((mix&1)==1)mix--; if((miy&1)==1)miy--; } + r = {x:mix, y:miy, width:max-mix+1, height:may-miy+1}; + + var fr = frms[i]; fr.rect = r; fr.blend = 1; fr.img = new Uint8Array(r.width*r.height*4); + if(frms[i-1].dispose==0) { + UPNG._copyTile(pimg,w,h, fr.img,r.width,r.height, -r.x,-r.y, 0); + UPNG.encode._prepareDiff(cimg,w,h,fr.img,r); + //UPNG._copyTile(cimg,w,h, fr.img,r.width,r.height, -r.x,-r.y, 2); + } + else + UPNG._copyTile(cimg,w,h, fr.img,r.width,r.height, -r.x,-r.y, 0); +} +UPNG.encode._prepareDiff = function(cimg, w,h, nimg, rec) { + UPNG._copyTile(cimg,w,h, nimg,rec.width,rec.height, -rec.x,-rec.y, 2); + /* + var n32 = new Uint32Array(nimg.buffer); + var og = new Uint8Array(rec.width*rec.height*4), o32 = new Uint32Array(og.buffer); + UPNG._copyTile(cimg,w,h, og,rec.width,rec.height, -rec.x,-rec.y, 0); + for(var i=4; i>>2]==o32[(i>>>2)-1]) { + n32[i>>>2]=o32[i>>>2]; + //var j = i, c=p32[(i>>>2)-1]; + //while(p32[j>>>2]==c) { n32[j>>>2]=c; j+=4; } + } + } + for(var i=nimg.length-8; i>0; i-=4) { + if(nimg[i+7]!=0 && nimg[i+3]==0 && o32[i>>>2]==o32[(i>>>2)+1]) { + n32[i>>>2]=o32[i>>>2]; + //var j = i, c=p32[(i>>>2)-1]; + //while(p32[j>>>2]==c) { n32[j>>>2]=c; j+=4; } + } + }*/ +} + +UPNG.encode._filterZero = function(img,h,bpp,bpl,data, filter, levelZero) +{ + var fls = [], ftry=[0,1,2,3,4]; + if (filter!=-1) ftry=[filter]; + else if(h*bpl>500000 || bpp==1) ftry=[0]; + var opts; if(levelZero) opts={level:0}; + + var CMPR = (levelZero && UZIP!=null) ? UZIP : pako; + + for(var i=0; i>1) +256)&255; + if(type==4) for(var x=bpp; x>1))&255; + for(var x=bpp; x>1))&255; } + if(type==4) { for(var x= 0; x>> 1); + else c = c >>> 1; + } + tab[n] = c; } + return tab; })(), + update : function(c, buf, off, len) { + for (var i=0; i>> 8); + return c; + }, + crc : function(b,o,l) { return UPNG.crc.update(0xffffffff,b,o,l) ^ 0xffffffff; } +} + + +UPNG.quantize = function(abuf, ps) +{ + var oimg = new Uint8Array(abuf), nimg = oimg.slice(0), nimg32 = new Uint32Array(nimg.buffer); + + var KD = UPNG.quantize.getKDtree(nimg, ps); + var root = KD[0], leafs = KD[1]; + + var planeDst = UPNG.quantize.planeDst; + var sb = oimg, tb = nimg32, len=sb.length; + + var inds = new Uint8Array(oimg.length>>2); + for(var i=0; i>2] = nd.ind; + tb[i>>2] = nd.est.rgba; + } + return { abuf:nimg.buffer, inds:inds, plte:leafs }; +} + +UPNG.quantize.getKDtree = function(nimg, ps, err) { + if(err==null) err = 0.0001; + var nimg32 = new Uint32Array(nimg.buffer); + + var root = {i0:0, i1:nimg.length, bst:null, est:null, tdst:0, left:null, right:null }; // basic statistic, extra statistic + root.bst = UPNG.quantize.stats( nimg,root.i0, root.i1 ); root.est = UPNG.quantize.estats( root.bst ); + var leafs = [root]; + + while(leafs.length maxL) { maxL=leafs[i].est.L; mi=i; } + if(maxL=s0 || node.i1<=s0); + //console.log(maxL, leafs.length, mi); + if(s0wrong) { node.est.L=0; continue; } + + + var ln = {i0:node.i0, i1:s0, bst:null, est:null, tdst:0, left:null, right:null }; ln.bst = UPNG.quantize.stats( nimg, ln.i0, ln.i1 ); + ln.est = UPNG.quantize.estats( ln.bst ); + var rn = {i0:s0, i1:node.i1, bst:null, est:null, tdst:0, left:null, right:null }; rn.bst = {R:[], m:[], N:node.bst.N-ln.bst.N}; + for(var i=0; i<16; i++) rn.bst.R[i] = node.bst.R[i]-ln.bst.R[i]; + for(var i=0; i< 4; i++) rn.bst.m[i] = node.bst.m[i]-ln.bst.m[i]; + rn.est = UPNG.quantize.estats( rn.bst ); + + node.left = ln; node.right = rn; + leafs[mi]=ln; leafs.push(rn); + } + leafs.sort(function(a,b) { return b.bst.N-a.bst.N; }); + for(var i=0; i0) { node0=nd.right; node1=nd.left; } + + var ln = UPNG.quantize.getNearest(node0, r,g,b,a); + if(ln.tdst<=planeDst*planeDst) return ln; + var rn = UPNG.quantize.getNearest(node1, r,g,b,a); + return rn.tdst eMq) i1-=4; + if(i0>=i1) break; + + var t = nimg32[i0>>2]; nimg32[i0>>2] = nimg32[i1>>2]; nimg32[i1>>2]=t; + + i0+=4; i1-=4; + } + while(vecDot(nimg, i0, e)>eMq) i0-=4; + return i0+4; +} +UPNG.quantize.vecDot = function(nimg, i, e) +{ + return nimg[i]*e[0] + nimg[i+1]*e[1] + nimg[i+2]*e[2] + nimg[i+3]*e[3]; +} +UPNG.quantize.stats = function(nimg, i0, i1){ + var R = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; + var m = [0,0,0,0]; + var N = (i1-i0)>>2; + for(var i=i0; i>>0) }; +} +UPNG.M4 = { + multVec : function(m,v) { + return [ + m[ 0]*v[0] + m[ 1]*v[1] + m[ 2]*v[2] + m[ 3]*v[3], + m[ 4]*v[0] + m[ 5]*v[1] + m[ 6]*v[2] + m[ 7]*v[3], + m[ 8]*v[0] + m[ 9]*v[1] + m[10]*v[2] + m[11]*v[3], + m[12]*v[0] + m[13]*v[1] + m[14]*v[2] + m[15]*v[3] + ]; + }, + dot : function(x,y) { return x[0]*y[0]+x[1]*y[1]+x[2]*y[2]+x[3]*y[3]; }, + sml : function(a,y) { return [a*y[0],a*y[1],a*y[2],a*y[3]]; } +} + +UPNG.encode.concatRGBA = function(bufs) { + var tlen = 0; + for(var i=0; i { + return fs.readFile(filePath, 'utf8'); +} + +export async function readFileBuffer(filePath: string): Promise { + return fs.readFile(filePath); +} + +export async function ensureDir(dirPath: string): Promise { + await fs.mkdir(dirPath, {recursive: true}); +} + +export async function writeFile(filePath: string, data: Buffer | string): Promise { + await ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, data); +} + +export async function copyFile(from: string, to: string): Promise { + await ensureDir(path.dirname(to)); + await fs.copyFile(from, to); +} + +export async function copyDir(from: string, to: string): Promise { + await ensureDir(to); + const entries = await fs.readdir(from, {withFileTypes: true}); + + for (const entry of entries) { + const sourcePath = path.join(from, entry.name); + const targetPath = path.join(to, entry.name); + if (entry.isDirectory()) { + await copyDir(sourcePath, targetPath); + } else { + await copyFile(sourcePath, targetPath); + } + } +} + +export async function removeDir(dirPath: string): Promise { + await fs.rm(dirPath, {recursive: true, force: true}); +} + +export function bufferToDataUri(buffer: Buffer, mimeType: string): string { + return `data:${mimeType || ''};base64,${buffer.toString('base64')}`; +} + +export function cloneJson(input: T): T { + return JSON.parse(JSON.stringify(input)); +} + +export function toPosixPath(inputPath: string): string { + return inputPath.replace(/\\/g, '/'); +} + +export function getRelativeOutputDir(rootDir: string, sourceDir: string): string { + const relativeDir = path.relative(rootDir, sourceDir); + if (!relativeDir || relativeDir === '.') { + return ''; + } + + if (relativeDir.startsWith('..')) { + return path.basename(sourceDir); + } + + return relativeDir; +} + +export function getHashedAssetPath(baseDir: string, uri: string, data: Buffer): string { + const parsed = path.posix.parse(toPosixPath(uri)); + const segments = parsed.dir ? parsed.dir.split('/') : []; + segments.push(parsed.name, getMd5(data)); + + return path.join(baseDir, `${segments.join('-')}${parsed.ext}`); +} + +export function appendHashToFilename(relativePath: string, data: Buffer): string { + const parsed = path.parse(relativePath); + return path.join(parsed.dir, `${parsed.name}-${getMd5(data)}${parsed.ext}`); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f8e26ac --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "moduleResolution": "node", + "rootDir": "src", + "outDir": "dist", + "allowJs": true, + "esModuleInterop": true, + "strict": false, + "skipLibCheck": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "dist" + ] +}