| node_modules | |||||
| dist | |||||
| public | |||||
| config | |||||
| libs | |||||
| docs |
| module.exports = { | |||||
| 'root': true, | |||||
| 'extends': [ | |||||
| 'eslint:recommended', | |||||
| ], | |||||
| 'parserOptions': { | |||||
| 'ecmaVersion': 2018, | |||||
| }, | |||||
| 'plugins': [ | |||||
| 'html', | |||||
| ], | |||||
| 'settings': { | |||||
| 'html/indent': 4, | |||||
| }, | |||||
| 'env': { | |||||
| 'es6': true, | |||||
| 'node': true, | |||||
| }, | |||||
| 'rules': { | |||||
| 'array-bracket-spacing': 'error', | |||||
| 'comma-style': 'error', | |||||
| 'space-before-blocks': 'error', | |||||
| 'space-before-function-paren': 'error', | |||||
| 'space-in-parens': 'error', | |||||
| 'space-infix-ops': 'error', | |||||
| 'space-unary-ops': 'error', | |||||
| 'spaced-comment': 'error', | |||||
| 'no-spaced-func': 'error', | |||||
| 'no-multi-spaces': 'error', | |||||
| 'no-regex-spaces': 'error', | |||||
| 'no-trailing-spaces': ['warn', { 'skipBlankLines': true }], | |||||
| 'no-mixed-spaces-and-tabs': 'error', | |||||
| 'no-irregular-whitespace': 'error', | |||||
| 'no-whitespace-before-property': 'error', | |||||
| 'default-case': 'error', | |||||
| 'require-jsdoc': 'warn', | |||||
| 'camelcase': 'error', | |||||
| 'comma-dangle': ['error', 'always-multiline'], | |||||
| 'indent': ['error', 4], | |||||
| 'quotes': ['error', 'single'], | |||||
| 'linebreak-style': ['error', 'unix'], | |||||
| 'no-loss-of-precision': 'error', | |||||
| }, | |||||
| 'overrides': [ | |||||
| { | |||||
| 'files': ['**/*.ts', '**/*.tsx'], | |||||
| 'parser': '@typescript-eslint/parser', // Specifies the ESLint parser | |||||
| 'parserOptions': { | |||||
| 'ecmaVersion': 2021, // Allows for the parsing of modern ECMAScript features | |||||
| 'sourceType': 'module', // Allows for the use of imports | |||||
| 'project': ['./tsconfig.json', './examples/tsconfig.json'], | |||||
| 'tsconfigRootDir': './', | |||||
| }, | |||||
| 'extends': [ | |||||
| 'eslint:recommended', | |||||
| 'plugin:@typescript-eslint/eslint-recommended' , | |||||
| 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin | |||||
| ], | |||||
| 'env': { | |||||
| 'browser': true, | |||||
| 'es6': true, | |||||
| }, | |||||
| 'plugins': [ | |||||
| 'html', | |||||
| '@typescript-eslint', | |||||
| 'import', | |||||
| 'deprecation', | |||||
| ], | |||||
| 'settings': { | |||||
| 'html/indent': 4, | |||||
| 'import/resolver': { | |||||
| 'typescript': {}, | |||||
| }, | |||||
| }, | |||||
| 'rules': { | |||||
| // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs | |||||
| // e.g. "@typescript-eslint/explicit-function-return-type": "off", | |||||
| '@typescript-eslint/no-explicit-any': 'off', | |||||
| 'camelcase': 'off', | |||||
| '@typescript-eslint/naming-convention': [ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/naming-convention.md | |||||
| 'error', | |||||
| { | |||||
| 'selector': 'default', | |||||
| 'format': ['camelCase', 'snake_case', 'PascalCase'], | |||||
| }, | |||||
| { | |||||
| 'selector': 'variable', | |||||
| 'format': ['camelCase', 'UPPER_CASE'], | |||||
| }, | |||||
| { | |||||
| 'selector': 'parameter', | |||||
| 'format': ['camelCase'], | |||||
| 'leadingUnderscore': 'allow', | |||||
| }, | |||||
| { | |||||
| 'selector': 'memberLike', | |||||
| 'modifiers': ['private'], | |||||
| 'format': ['camelCase'], | |||||
| 'leadingUnderscore': 'require', | |||||
| }, | |||||
| { | |||||
| 'selector': 'memberLike', | |||||
| 'modifiers': ['protected'], | |||||
| 'format': ['camelCase'], | |||||
| 'leadingUnderscore': 'require', | |||||
| }, | |||||
| { | |||||
| 'selector': ['typeLike'], | |||||
| 'format': ['PascalCase'], | |||||
| }, | |||||
| { | |||||
| 'selector': ['enumMember'], | |||||
| 'format': ['PascalCase', 'UPPER_CASE'], | |||||
| }, | |||||
| { | |||||
| 'selector': 'memberLike', | |||||
| 'modifiers': ['static'], | |||||
| 'format': ['PascalCase', 'UPPER_CASE'], | |||||
| }, | |||||
| { | |||||
| 'selector': 'objectLiteralProperty', | |||||
| 'format': ['camelCase', 'snake_case', 'PascalCase'], | |||||
| 'leadingUnderscore': 'allowSingleOrDouble', | |||||
| }, | |||||
| { | |||||
| 'selector': 'typeProperty', | |||||
| 'format': ['camelCase', 'snake_case', 'PascalCase', 'UPPER_CASE'], | |||||
| 'leadingUnderscore': 'allowSingleOrDouble', | |||||
| }, | |||||
| ], | |||||
| 'semi': 'off', | |||||
| '@typescript-eslint/semi': ['error','never', { 'beforeStatementContinuationChars': 'always' }], | |||||
| 'no-extra-semi': 'off', | |||||
| '@typescript-eslint/no-extra-semi': ['error'], | |||||
| '@typescript-eslint/adjacent-overload-signatures': 'error', | |||||
| 'comma-spacing': 'off', | |||||
| '@typescript-eslint/comma-spacing': ['error'], | |||||
| 'no-extra-parens': 'off', | |||||
| '@typescript-eslint/no-extra-parens': ['error'], | |||||
| 'brace-style': 'off', | |||||
| '@typescript-eslint/brace-style': ['warn', '1tbs', { 'allowSingleLine': true }], | |||||
| '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], | |||||
| 'default-param-last': 'off', | |||||
| '@typescript-eslint/default-param-last': ['error'], | |||||
| 'func-call-spacing': 'off', | |||||
| '@typescript-eslint/func-call-spacing': ['error'], | |||||
| 'keyword-spacing': 'off', | |||||
| 'object-curly-spacing': 'off', | |||||
| '@typescript-eslint/object-curly-spacing': ['error'], | |||||
| '@typescript-eslint/keyword-spacing': ['error'], | |||||
| 'space-before-function-paren': 'off', | |||||
| '@typescript-eslint/space-before-function-paren': ['error','never'], | |||||
| 'no-shadow': 'off', | |||||
| 'no-magic-numbers': 'off', | |||||
| // '@typescript-eslint/no-magic-numbers': [ | |||||
| // 'warn', { | |||||
| // 'ignoreEnums': true, | |||||
| // 'ignoreNumericLiteralTypes': true, | |||||
| // 'ignoreReadonlyClassProperties': true, | |||||
| // }, | |||||
| // ], | |||||
| '@typescript-eslint/promise-function-async': [ | |||||
| 'error', | |||||
| { | |||||
| 'allowedPromiseNames': ['Thenable'], | |||||
| 'checkArrowFunctions': true, | |||||
| 'checkFunctionDeclarations': true, | |||||
| 'checkFunctionExpressions': true, | |||||
| 'checkMethodDeclarations': true, | |||||
| }, | |||||
| ], | |||||
| 'dot-notation': 'off', // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/dot-notation.md | |||||
| '@typescript-eslint/dot-notation': ['error'], | |||||
| '@typescript-eslint/prefer-string-starts-ends-with': 'error', | |||||
| '@typescript-eslint/prefer-includes': 'error', | |||||
| '@typescript-eslint/prefer-for-of': 'error', | |||||
| '@typescript-eslint/prefer-as-const': 'error', | |||||
| '@typescript-eslint/prefer-function-type': 'error', | |||||
| '@typescript-eslint/no-unsafe-call': 'warn', | |||||
| '@typescript-eslint/no-misused-new': 'error', | |||||
| '@typescript-eslint/no-namespace': 'error', | |||||
| '@typescript-eslint/non-nullable-type-assertion-style': 'error', | |||||
| 'no-invalid-this': 'off', | |||||
| '@typescript-eslint/no-invalid-this': ['error'], | |||||
| '@typescript-eslint/prefer-ts-expect-error': ['error'], | |||||
| 'no-loop-func': 'off', | |||||
| '@typescript-eslint/no-loop-func': ['error'], | |||||
| 'no-loss-of-precision': 'off', | |||||
| '@typescript-eslint/no-loss-of-precision': ['error'], | |||||
| '@typescript-eslint/no-shadow': ['error'], | |||||
| 'no-duplicate-imports': 'off', | |||||
| '@typescript-eslint/no-duplicate-imports': ['error', { 'includeExports': false }], | |||||
| // "@typescript-eslint/prefer-nullish-coalescing": ["error", {ignoreConditionalTests: false, ignoreMixedLogicalExpressions: false}], | |||||
| 'comma-dangle': 'off', | |||||
| '@typescript-eslint/comma-dangle': ['error', { | |||||
| 'arrays': 'always-multiline', | |||||
| 'objects': 'always-multiline', | |||||
| 'imports': 'always-multiline', | |||||
| 'exports': 'always-multiline', | |||||
| 'functions': 'only-multiline', | |||||
| }], | |||||
| 'deprecation/deprecation': 'warn', | |||||
| }, | |||||
| 'globals': { 'Atomics': 'readonly', 'SharedArrayBuffer': 'readonly' }, | |||||
| }, | |||||
| ], | |||||
| } |
| # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node | |||||
| # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs | |||||
| name: Build docs and deploy to github pages. | |||||
| on: | |||||
| # Runs on pushes targeting the default branch | |||||
| push: | |||||
| branches: ["master"] | |||||
| # Allows you to run this workflow manually from the Actions tab | |||||
| workflow_dispatch: | |||||
| # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages | |||||
| permissions: | |||||
| contents: read | |||||
| pages: write | |||||
| id-token: write | |||||
| # Allow one concurrent deployment | |||||
| concurrency: | |||||
| group: "pages" | |||||
| cancel-in-progress: true | |||||
| jobs: | |||||
| build-and-deploy: | |||||
| environment: | |||||
| name: github-pages | |||||
| url: ${{ steps.deployment.outputs.page_url }} | |||||
| runs-on: ubuntu-latest | |||||
| strategy: | |||||
| matrix: | |||||
| node-version: [18.x] | |||||
| # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ | |||||
| steps: | |||||
| - uses: actions/checkout@v3 | |||||
| - name: Use Node.js ${{ matrix.node-version }} | |||||
| uses: actions/setup-node@v3 | |||||
| with: | |||||
| node-version: ${{ matrix.node-version }} | |||||
| cache: 'npm' | |||||
| - run: npm ci | |||||
| - name: Setup Pages | |||||
| uses: actions/configure-pages@v3 | |||||
| - name: Upload artifact | |||||
| uses: actions/upload-pages-artifact@v1 | |||||
| with: | |||||
| path: '.' | |||||
| - name: Deploy to GitHub Pages | |||||
| id: deployment | |||||
| uses: actions/deploy-pages@v1 | |||||
| dist | |||||
| docs | |||||
| # Logs | |||||
| logs | |||||
| *.log | |||||
| npm-debug.log* | |||||
| yarn-debug.log* | |||||
| yarn-error.log* | |||||
| # Runtime data | |||||
| pids | |||||
| *.pid | |||||
| *.seed | |||||
| *.pid.lock | |||||
| # Directory for instrumented libs generated by jscoverage/JSCover | |||||
| lib-cov | |||||
| # Coverage directory used by tools like istanbul | |||||
| coverage | |||||
| # nyc test coverage | |||||
| .nyc_output | |||||
| # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | |||||
| .grunt | |||||
| # Bower dependency directory (https://bower.io/) | |||||
| bower_components | |||||
| # node-waf configuration | |||||
| .lock-wscript | |||||
| # Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
| build/Release | |||||
| # Dependency directories | |||||
| node_modules/ | |||||
| jspm_packages/ | |||||
| # TypeScript v1 declaration files | |||||
| typings/ | |||||
| # Optional npm cache directory | |||||
| .npm | |||||
| # Optional eslint cache | |||||
| .eslintcache | |||||
| # Optional REPL history | |||||
| .node_repl_history | |||||
| # Output of 'npm pack' | |||||
| *.tgz | |||||
| # Yarn Integrity file | |||||
| .yarn-integrity | |||||
| # dotenv environment variables file | |||||
| .env | |||||
| # next.js build output | |||||
| .next |
| # Default ignored files | |||||
| /shelf/ | |||||
| /workspace.xml | |||||
| # Editor-based HTTP Client requests | |||||
| /httpRequests/ | |||||
| # Datasource local storage ignored files | |||||
| /dataSources/ | |||||
| /dataSources.local.xml |
| <component name="InspectionProjectProfileManager"> | |||||
| <profile version="1.0"> | |||||
| <option name="myName" value="Project Default" /> | |||||
| <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" /> | |||||
| </profile> | |||||
| </component> |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="EslintConfiguration"> | |||||
| <option name="fix-on-save" value="true" /> | |||||
| </component> | |||||
| </project> |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="ProjectModuleManager"> | |||||
| <modules> | |||||
| <module fileurl="file://$PROJECT_DIR$/.idea/threepipe.iml" filepath="$PROJECT_DIR$/.idea/threepipe.iml" /> | |||||
| </modules> | |||||
| </component> | |||||
| </project> |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <module type="WEB_MODULE" version="4"> | |||||
| <component name="NewModuleRootManager"> | |||||
| <content url="file://$MODULE_DIR$"> | |||||
| <excludeFolder url="file://$MODULE_DIR$/temp" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/.tmp" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/tmp" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/docs" /> | |||||
| <excludeFolder url="file://$MODULE_DIR$/dist" /> | |||||
| </content> | |||||
| <orderEntry type="inheritedJdk" /> | |||||
| <orderEntry type="sourceFolder" forTests="false" /> | |||||
| </component> | |||||
| </module> |
| <?xml version="1.0" encoding="UTF-8"?> | |||||
| <project version="4"> | |||||
| <component name="VcsDirectoryMappings"> | |||||
| <mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||||
| </component> | |||||
| </project> |
| *.backup |
| MIT License | |||||
| Copyright (c) 2023 repalash <palash@shaders.app> | |||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
| of this software and associated documentation files (the "Software"), to deal | |||||
| in the Software without restriction, including without limitation the rights | |||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
| copies of the Software, and to permit persons to whom the Software is | |||||
| furnished to do so, subject to the following conditions: | |||||
| The above copyright notice and this permission notice shall be included in all | |||||
| copies or substantial portions of the Software. | |||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |||||
| SOFTWARE. |
| # ThreePipe | |||||
| A new way to work with three.js, 3D models and rendering on the web. | |||||
| [ThreePipe](https://threepipe.org/) — | |||||
| [Github](https://github.com/repalash/threepipe) — | |||||
| [Examples](https://threepipe.org/examples/) — | |||||
| [Docs](https://threepipe.org/docs/) — | |||||
| [WebGi](https://webgi.xyz/docs/) | |||||
| [](https://opensource.org/licenses/MIT) | |||||
| [](https://discord.gg/apzU8rUWxY) | |||||
| [](https://www.npmjs.com/package/threepipe) | |||||
| [](https://twitter.com/repalash) | |||||
| ThreePipe is a 3D framework built on top of [three.js](https://threejs.org/) in TypeScript with a focus on quality rendering, modularity and extensibility. | |||||
| Key features include: | |||||
| - Simple, intuitive API for creating 3D model viewers/configurators/editors on web pages, with many built-in presets for common workflows and use-cases. | |||||
| - Companion [editor](TODO) to create, edit and configure 3D scenes in the browser. | |||||
| - Modular architecture that allows you to easily extend the viewer, scene objects, materials, shaders, rendering, post-processing and serialization with custom functionality. | |||||
| - Simple plugin system along with a rich library of built-in plugins that allows you to easily add new features to the viewer. | |||||
| - [uiconfig](https://github.com/repalash/uiconfig.js) compatibility to automatically generate configuration UIs in the browser. | |||||
| - Modular rendering pipeline with built-in deferred rendering, post-processing, RGBM HDR rendering, etc. | |||||
| - Material extension framework to modify/inject/build custom shader code into existing materials at runtime from plugins. | |||||
| - Extendable asset import, export and management pipeline with built-in support for gltf, glb, obj+mtl, fbx, materials(pmat/bmat), json, zip, png, jpeg, svg, webp, ktx2, ply, 3dm and many more. | |||||
| - Automatic serialization of all viewer and plugin settings in GLB(with custom extensions) and JSON formats. | |||||
| - Automatic disposal of all three.js resources with built-in reference management. | |||||
| ## Installation | |||||
| ```bash | |||||
| npm install threepipe | |||||
| ``` | |||||
| ## Getting Started | |||||
| First, create a canvas element in your HTML page: | |||||
| ```html | |||||
| <canvas id="three-canvas" style="width: 800px; height: 600px;"></canvas> | |||||
| ``` | |||||
| Then, import the viewer and create a new instance: | |||||
| ```typescript | |||||
| import {ThreeViewer} from 'threepipe' | |||||
| import {IObject3D} from './IObject' | |||||
| // Create a viewer | |||||
| const viewer = new ThreeViewer({canvas: document.getElementById('three-canvas') as HTMLCanvasElement}) | |||||
| // Load an environment map | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | |||||
| // Load a model | |||||
| const result = await viewer.load<IObject3D>('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| ``` | |||||
| That's it! You should now see a 3D model on your page. | |||||
| The viewer initializes with a Scene, Camera, Camera controls(orbit controls), several importers, exporters and a default rendering pipeline. Additional functionality can be added with plugins. | |||||
| Check out the GLTF Load example to see it in action or to check the JS equivalent code: https://threepipe.org/examples/gltf-load/ | |||||
| ## License | |||||
| The core framework([src](./src), [dist](./dist), [examples](./examples) folders) and any [plugins](./plugins) without a separate license are licensed under the [MIT license](./LICENSE). | |||||
| Some plugins(in the [plugins](./plugins) folder) might have different licenses. Check the individual plugin documentation and the source folder/files for more details. | |||||
| ## Examples | |||||
| Check out all the examples here: https://threepipe.org/examples/ | |||||
| ## Status | |||||
| The project is in `alpha` stage and under active development. Many features will be added but the core API will not change significantly in future releases. | |||||
| Check out [WebGi](https://webgi.xyz/) for a production ready solution for e-commerce and jewelry applications. | |||||
| ## Documentation | |||||
| Check the list of all functions, classes and types in the [API documentation](https://threepipe.org/docs/). | |||||
| ## WebGi | |||||
| Check out WebGi - Premium Photo-realistic 3D rendering framework and tools for web applications and online commerce: [Homepage](https://webgi.xyz/) — [Docs](https://webgi.xyz/docs/) | |||||
| [](https://twitter.com/pixotronics) | |||||
| ## Contributing | |||||
| Contributions to ThreePipe are welcome and encouraged! Feel free to open issues and pull requests on the GitHub repository. |
| { | |||||
| "name": "threepipe", | |||||
| "version": "0.0.1", | |||||
| "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", | |||||
| "main": "src/index.ts", | |||||
| "module": "dist/index.mjs", | |||||
| "types": "src/index.ts", | |||||
| "sources": "src/index.ts", | |||||
| "type": "module", | |||||
| "scripts": { | |||||
| "new:pack": "npm run prepare && clean-package && npm pack && clean-package restore", | |||||
| "new:publish": "npm run prepare && clean-package && npm publish && clean-package restore", | |||||
| "build": "rimraf dist ; NODE_ENV=production rollup -c", | |||||
| "dev": "rollup -c -w", | |||||
| "build-examples": "tsc --project examples/tsconfig.build.json", | |||||
| "dev-examples": "tsc --project examples/tsconfig.build.json -w", | |||||
| "serve-docs": "ws -d docs -p 8080", | |||||
| "serve": "ws -d . -p 8000", | |||||
| "docs": "npx typedoc", | |||||
| "prepare": "npm run build && npm run build-examples && npm run docs" | |||||
| }, | |||||
| "clean-package": { | |||||
| "remove": [ | |||||
| "clean-package", | |||||
| "scripts", | |||||
| "devDependencies", | |||||
| "optionalDependencies", | |||||
| "//" | |||||
| ], | |||||
| "replace": { | |||||
| "main": "dist/index.min.js", | |||||
| "module": "dist/index.mjs", | |||||
| "browser": "dist/index.js", | |||||
| "types": "dist/index.d.ts" | |||||
| } | |||||
| }, | |||||
| "files": [ | |||||
| "dist", | |||||
| "src", | |||||
| "docs", | |||||
| "examples", | |||||
| "plugins", | |||||
| "tsconfig.json" | |||||
| ], | |||||
| "repository": { | |||||
| "type": "git", | |||||
| "url": "git://github.com/repalash/threepipe.git" | |||||
| }, | |||||
| "keywords": [ | |||||
| "3d", | |||||
| "three.js", | |||||
| "typescript", | |||||
| "javascipt", | |||||
| "browser", | |||||
| "esm", | |||||
| "rendering", | |||||
| "viewer", | |||||
| "webgl", | |||||
| "webgi", | |||||
| "canvas" | |||||
| ], | |||||
| "author": "repalash <palash@shaders.app>", | |||||
| "license": "MIT", | |||||
| "bugs": { | |||||
| "url": "https://github.com/repalash/threepipe/issues" | |||||
| }, | |||||
| "homepage": "https://github.com/repalash/threepipe#readme", | |||||
| "devDependencies": { | |||||
| "rimraf": "^5.0.1", | |||||
| "@rollup/plugin-commonjs": "^25.0.0", | |||||
| "@rollup/plugin-json": "^6.0.0", | |||||
| "@rollup/plugin-node-resolve": "^15.0.2", | |||||
| "@rollup/plugin-terser": "^0.4.1", | |||||
| "@rollup/plugin-typescript": "^11.1.1", | |||||
| "@types/stats.js": "^0.17.0", | |||||
| "@typescript-eslint/eslint-plugin": "^5.59.5", | |||||
| "@typescript-eslint/parser": "^5.59.5", | |||||
| "clean-package": "^2.2.0", | |||||
| "eslint": "^8.40.0", | |||||
| "eslint-import-resolver-typescript": "^3.5.5", | |||||
| "eslint-plugin-deprecation": "^1.4.1", | |||||
| "eslint-plugin-html": "^7.1.0", | |||||
| "eslint-plugin-import": "^2.27.5", | |||||
| "local-web-server": "^5.3.0", | |||||
| "rollup": "^3.21.7", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz", | |||||
| "rollup-plugin-license": "^3.0.1", | |||||
| "ts-browser-helpers": "^0.4.0", | |||||
| "stats.js": "^0.17.0", | |||||
| "tslib": "^2.5.0", | |||||
| "typedoc": "^0.24.7", | |||||
| "typescript": "^5.0.4", | |||||
| "uiconfig.js": "^0.0.2", | |||||
| "typescript-plugin-css-modules": "^5.0.1", | |||||
| "rollup-plugin-postcss": "^4.0.2" | |||||
| }, | |||||
| "dependencies": { | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.0004/package.tgz", | |||||
| "@types/webxr": "^0.5.1", | |||||
| "@types/wicg-file-system-access": "^2020.9.5" | |||||
| }, | |||||
| "//": { | |||||
| "dependencies": { | |||||
| "ts-browser-helpers": "^0.4.0", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2005/package.tgz", | |||||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2005.tar.gz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.0004/package.tgz", | |||||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.0004.tar.gz", | |||||
| "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three" | |||||
| }, | |||||
| "local_dependencies": { | |||||
| "ts-browser-helpers": "file:./../ts-browser-helpers", | |||||
| "three": "file:./../three.js", | |||||
| "@types/three": "file:./../three-ts-types/types/three" | |||||
| } | |||||
| }, | |||||
| "optionalDependencies": { | |||||
| "win-node-env": "^0.6.1" | |||||
| }, | |||||
| "browserslist": [ | |||||
| "defaults" | |||||
| ] | |||||
| } |
| // rollup.config.js | |||||
| import commonjs from '@rollup/plugin-commonjs'; | |||||
| import json from '@rollup/plugin-json'; | |||||
| import resolve from '@rollup/plugin-node-resolve'; | |||||
| import typescript from '@rollup/plugin-typescript'; | |||||
| import license from 'rollup-plugin-license' | |||||
| import packageJson from './package.json' assert {type: 'json'}; | |||||
| import path from 'path' | |||||
| import {fileURLToPath} from 'url'; | |||||
| import terser from "@rollup/plugin-terser"; | |||||
| import postcss from 'rollup-plugin-postcss' | |||||
| const __filename = fileURLToPath(import.meta.url); | |||||
| const __dirname = path.dirname(__filename); | |||||
| const {name, version, author} = packageJson | |||||
| const {main, module, browser} = packageJson["clean-package"].replace | |||||
| const isProduction = process.env.NODE_ENV === 'production' | |||||
| const settings = { | |||||
| globals: {}, | |||||
| sourcemap: true | |||||
| } | |||||
| export default { | |||||
| input: './src/index.ts', | |||||
| output: [ | |||||
| // { | |||||
| // file: main, | |||||
| // name: main, | |||||
| // ...settings, | |||||
| // format: 'cjs', | |||||
| // plugins: [ | |||||
| // isProduction && terser() | |||||
| // ] | |||||
| // }, | |||||
| { | |||||
| file: module, | |||||
| ...settings, | |||||
| name: name, | |||||
| format: 'es' | |||||
| }, | |||||
| { | |||||
| file: browser, | |||||
| ...settings, | |||||
| name: name, | |||||
| format: 'umd', | |||||
| plugins: [ | |||||
| isProduction && terser() | |||||
| ] | |||||
| } | |||||
| ], | |||||
| external: [], | |||||
| plugins: [ | |||||
| postcss({ | |||||
| modules: false, | |||||
| autoModules: true, // todo; issues with typescript import css, because inject is false | |||||
| inject: false, | |||||
| minimize: isProduction, | |||||
| // Or with custom options for `postcss-modules` | |||||
| }), | |||||
| json(), | |||||
| resolve({}), | |||||
| typescript({}), | |||||
| commonjs({ | |||||
| include: 'node_modules/**', | |||||
| extensions: ['.js'], | |||||
| ignoreGlobal: false, | |||||
| sourceMap: false | |||||
| }), | |||||
| license({ | |||||
| banner: ` | |||||
| @license | |||||
| ${name} v${version} | |||||
| Copyright 2022<%= moment().format('YYYY') > 2022 ? '-' + moment().format('YYYY') : null %> ${author} | |||||
| ${packageJson.license} License | |||||
| `, | |||||
| thirdParty: { | |||||
| output: path.join(__dirname, 'dist', 'dependencies.txt'), | |||||
| includePrivate: true, // Default is false. | |||||
| }, | |||||
| }) | |||||
| ] | |||||
| } |
| import {BaseEvent, EventDispatcher} from 'three' | |||||
| import {IMaterial, IObject3D, ITexture} from '../core' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {BlobExt, ExportFileOptions, IAssetExporter, IExporter, IExportParser} from './IExporter' | |||||
| import {SimpleTextExporter} from './export/SimpleTextExporter' | |||||
| import {SimpleJSONExporter} from './export/SimpleJSONExporter' | |||||
| export class AssetExporter extends EventDispatcher<BaseEvent, 'exporterCreate' | 'exportFile'> implements IAssetExporter { | |||||
| readonly exporters: IExporter[] = [ | |||||
| {ctor: ()=>new SimpleJSONExporter(), ext: ['json']}, | |||||
| {ctor: ()=>new SimpleTextExporter(), ext: ['txt', 'text']}, | |||||
| // {ctor: ()=>new GLTFDracoExporter(), ext: ['gltf', 'glb']}, | |||||
| ] | |||||
| addExporter(...exporters: IExporter[]) { | |||||
| for (const exporter of exporters) { | |||||
| if (this.exporters.includes(exporter)) { | |||||
| console.warn('Exporter already added', exporter) | |||||
| return | |||||
| } | |||||
| this.exporters.push(exporter) | |||||
| } | |||||
| } | |||||
| removeExporter(...exporters: IExporter[]) { | |||||
| for (const exporter of exporters) { | |||||
| const i = this.exporters.indexOf(exporter) | |||||
| if (i >= 0) this.exporters.splice(i, 1) | |||||
| } | |||||
| } | |||||
| getExporter(...ext: string[]): IExporter|undefined { | |||||
| return this.exporters.find(e=>e.ext.some(e1=>ext.includes(e1))) | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| } | |||||
| public async exportObject(obj?: IObject3D|IMaterial|ITexture, options: ExportFileOptions = {}): Promise<BlobExt|undefined> { | |||||
| if (!obj?.assetType) { | |||||
| console.error('Object has no asset type') | |||||
| return undefined | |||||
| } | |||||
| const excluded: IObject3D[] = [] | |||||
| if (obj.assetType === 'model') { | |||||
| obj.traverse((o)=>{ | |||||
| if (o.userData.excludeFromExport && o.visible) { | |||||
| o.visible = false | |||||
| excluded.push(o) | |||||
| } | |||||
| }) | |||||
| } | |||||
| const blob = await this._exportFile(obj, options) | |||||
| if (obj.assetType === 'model') { | |||||
| excluded.forEach((o: any)=>o.visible = true) | |||||
| } | |||||
| if ((obj as any)?.userData?.rootSceneModelRoot && options.viewerConfig === false) { | |||||
| delete (obj as any)!.userData!.__exportViewerConfig | |||||
| } | |||||
| return blob | |||||
| } | |||||
| // export to blob | |||||
| private async _exportFile(obj: IObject3D|IMaterial|ITexture, options: ExportFileOptions = {}): Promise<BlobExt|undefined> { | |||||
| // if ((file as any)?.__imported) return (file as any).__imported // todo: cache exports? | |||||
| let res: BlobExt | |||||
| try { | |||||
| this.dispatchEvent({type: 'exportFile', obj, state:'processing', exportOptions: options}) | |||||
| const processed = await this.processBeforeExport(obj, options) | |||||
| const ext = options.exportExt ?? processed?.typeExt ?? processed?.ext | |||||
| if (!processed || !ext) throw new Error(`Unable to preprocess before export ${ext}`) | |||||
| const parser = this._getParser(ext) | |||||
| this.dispatchEvent({type: 'exportFile', obj, state:'exporting'}) | |||||
| const blob = await parser.parseAsync(processed.obj, {exportExt: processed.ext ?? ext, ...options}) as BlobExt | |||||
| blob.ext = processed.ext | |||||
| res = blob | |||||
| this.dispatchEvent({type: 'exportFile', obj, state: 'done'}) | |||||
| } catch (e) { | |||||
| console.error('AssetExporter: Unable to Export file', obj) | |||||
| // console.error(e) | |||||
| this.dispatchEvent({type: 'exportFile', obj, state: 'error', error: e}) | |||||
| throw e | |||||
| return undefined | |||||
| } | |||||
| // if (file) (file as any).__imported = res | |||||
| return res | |||||
| } | |||||
| private _createParser(ext: string): IExportParser { | |||||
| const exporter = this.exporters.find(e => e.ext.includes(ext)) | |||||
| if (!exporter) | |||||
| throw new Error(`No exporter found for extension ${ext}`) | |||||
| const parser = exporter?.ctor(this, exporter) | |||||
| if (!parser) throw new Error(`Unable to create parser for extension ${ext}`) | |||||
| this._cachedParsers.push({ext: exporter.ext, parser}) | |||||
| this.dispatchEvent({type: 'exporterCreate', exporter, parser}) | |||||
| return parser | |||||
| } | |||||
| private _cachedParsers: {parser: IExportParser, ext: string[]}[] = [] | |||||
| private _getParser(ext: string): IExportParser { | |||||
| return this._cachedParsers.find(e => e.ext.includes(ext))?.parser ?? this._createParser(ext) | |||||
| } | |||||
| public async processBeforeExport(obj: IObject3D|IMaterial|ITexture, _: AnyOptions = {}): Promise<{obj:any, ext:string, typeExt?:string}|undefined> { | |||||
| // if (obj.assetExporterProcessed && !options.forceExporterReprocess) return obj //todo;;; | |||||
| switch (obj.assetType) { | |||||
| case 'light': | |||||
| console.error('AssetExporter: light export not implemented') | |||||
| return undefined | |||||
| case 'model': | |||||
| return {obj, ext: 'glb'} | |||||
| // return {obj, ext: 'gltf'} | |||||
| case 'material': | |||||
| return {obj: (obj as IMaterial).toJSON(), ext: (obj as IMaterial).constructor?.TypeSlug || 'json', typeExt: 'json'} | |||||
| case 'texture': | |||||
| return {obj: (obj as ITexture).toJSON(), ext: 'json'} | |||||
| default: | |||||
| console.error('AssetExporter: unknown asset type', obj.assetType) | |||||
| } | |||||
| return undefined | |||||
| } | |||||
| dispose(): void { | |||||
| // todo | |||||
| } | |||||
| } |
| import {Event, EventDispatcher, FileLoader, LoaderUtils, LoadingManager} from 'three' | |||||
| import { | |||||
| IAssetImporter, | |||||
| IAssetImporterEventTypes, | |||||
| IImportResultUserData, | |||||
| ImportAssetOptions, | |||||
| ImportFilesOptions, | |||||
| ImportResult, | |||||
| LoadFileOptions, | |||||
| ProcessRawOptions, | |||||
| } from './IAssetImporter' | |||||
| import {IAsset, IFile} from './IAsset' | |||||
| import {IImporter, ILoader} from './IImporter' | |||||
| import {Importer} from './Importer' | |||||
| import {SimpleJSONLoader} from './import' | |||||
| import {parseFileExtension} from 'ts-browser-helpers' | |||||
| import {IObject3D} from '../core' | |||||
| export type IAssetImporterEvent = Event&{ | |||||
| type: IAssetImporterEventTypes, | |||||
| data?: ImportResult, options?: ProcessRawOptions, | |||||
| path?: string, progress?: number, state?: string, error?: any | |||||
| files?: Map<string, IFile> | |||||
| url?: string, loaded?: number, total?: number | |||||
| loader?: ILoader, | |||||
| } | |||||
| export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetImporterEventTypes> implements IAssetImporter { | |||||
| private _loadingManager: LoadingManager | |||||
| private _logger = console.log | |||||
| // Used when loading multiple files at once. | |||||
| protected _rootContext?: {path: string, rootUrl: string, /* baseUrl: string;*/} | |||||
| private _loaderCache: {loader: ILoader, ext: string[], mime: string[]}[] = [] | |||||
| private _fileDatabase: Map<string, IFile> = new Map<string, IFile>() | |||||
| private _cachedAssets: IAsset[] = [] | |||||
| readonly importers: IImporter[] = [ | |||||
| // new Importer(VideoTextureLoader, ['mp4', 'ogg', 'mov', 'data:video'], false), | |||||
| new Importer(SimpleJSONLoader, ['json', 'vjson'], ['application/json'], false), | |||||
| new Importer(FileLoader, ['txt'], ['text/plain'], false), | |||||
| // new Importer(RGBEPNGLoader, ['rgbe.png', 'hdr.png', 'hdrpng'], ['image/png+rgbe'], false), // todo: not working on windows? | |||||
| // new Importer(LUTCubeLoader2, ['cube'], false), | |||||
| ] | |||||
| constructor(logging = false) { | |||||
| super() | |||||
| if (!logging) this._logger = () => {return} | |||||
| // this._viewer = viewer | |||||
| this._onLoad = this._onLoad.bind(this) | |||||
| this._onProgress = this._onProgress.bind(this) | |||||
| this._onError = this._onError.bind(this) | |||||
| this._onStart = this._onStart.bind(this) | |||||
| this._urlModifier = this._urlModifier.bind(this) | |||||
| this._loadingManager = new LoadingManager(this._onLoad, this._onProgress, this._onError) | |||||
| this._loadingManager.onStart = this._onStart | |||||
| this._loadingManager.setURLModifier(this._urlModifier) | |||||
| // addDracoLoader() | |||||
| } | |||||
| get loadingManager(): LoadingManager { | |||||
| return this._loadingManager | |||||
| } | |||||
| get cachedAssets(): IAsset[] { | |||||
| return this._cachedAssets | |||||
| } | |||||
| addImporter(...importers: IImporter[]) { | |||||
| for (const importer of importers) { | |||||
| if (this.importers.includes(importer)) { | |||||
| console.warn('AssetImporter: Importer already added', importer) | |||||
| return | |||||
| } | |||||
| this.importers.push(importer) | |||||
| } | |||||
| } | |||||
| removeImporter(...importers: IImporter[]) { | |||||
| for (const importer of importers) { | |||||
| const index = this.importers.indexOf(importer) | |||||
| if (index >= 0) this.importers.splice(index, 1) | |||||
| } | |||||
| } | |||||
| // region import functions | |||||
| async import<T extends ImportResult|undefined = ImportResult>(assetOrPath?: string | IAsset | IAsset[], options?: ImportAssetOptions): Promise<(T|undefined)[]> { | |||||
| if (!assetOrPath) return [] | |||||
| if (Array.isArray(assetOrPath)) return (await Promise.all(assetOrPath.map(async a => this.import<T>(a, options)))).flat(1) | |||||
| if (typeof assetOrPath === 'object') return await this.importAsset<T>(assetOrPath, options) | |||||
| if (typeof assetOrPath === 'string') return await this.importPath<T>(assetOrPath, options) | |||||
| console.error('AssetImporter: Invalid asset or path', assetOrPath) | |||||
| return [] | |||||
| } | |||||
| async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> { | |||||
| return (await this.import<T>(asset, options))?.[0] | |||||
| } | |||||
| async importPath<T extends ImportResult|undefined = ImportResult|undefined>(path: string, options: ImportAssetOptions = {}): Promise<T[]> { | |||||
| const op = {...options} | |||||
| delete op.pathOverride | |||||
| delete op.forceImport | |||||
| delete op.reimportDisposed | |||||
| delete op.fileHandler | |||||
| delete op.importedFile | |||||
| const opts = JSON.stringify(op) | |||||
| const cached = this._cachedAssets.find(a => a.path === path && a._options === opts) | |||||
| let asset: IAsset | |||||
| if (cached) asset = cached | |||||
| else asset = {path} | |||||
| asset._options = opts | |||||
| if (options.importedFile) asset.file = options.importedFile | |||||
| return await this.importAsset(asset, options) | |||||
| } | |||||
| // import and process an IAsset | |||||
| async importAsset<T extends ImportResult|undefined = ImportResult|undefined>(asset?: IAsset, options: ImportAssetOptions = {}, onDownloadProgress?: (e:ProgressEvent)=>void): Promise<T[]> { | |||||
| if (!asset) return [] | |||||
| if (!asset.path && !asset.file && !options.pathOverride) { | |||||
| return [asset as any] // maybe already imported asset | |||||
| } | |||||
| // Cache the asset reference if it is not already cached | |||||
| if (!this._cachedAssets.includes(asset)) { | |||||
| if (Object.entries(asset).length === 1 && asset.path) { | |||||
| const ca = this._cachedAssets.find(value => value.path === asset.path) | |||||
| if (ca) Object.assign(asset, ca) | |||||
| } | |||||
| const ca = this._cachedAssets.findIndex(value => value.path === asset.path) | |||||
| if (ca >= 0) this._cachedAssets.splice(ca, 1) | |||||
| this._cachedAssets.push(asset) | |||||
| } | |||||
| let result: any = asset?.preImported | |||||
| if (!result && asset?.preImportedRaw) { | |||||
| result = await asset.preImportedRaw | |||||
| } | |||||
| // console.log(result) | |||||
| if (!options.forceImport && result) { | |||||
| const results = await this.processRaw<T>(result, options) // just in case its not processed. Internal check is done to ensure it's not processed twice | |||||
| let isDisposed = false // if any of the objects is disposed | |||||
| for (const r of results) { | |||||
| if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc | |||||
| isDisposed = true | |||||
| break | |||||
| } | |||||
| // todo: should we check if any of it's children is disposed ? | |||||
| if (!isDisposed || options.reimportDisposed === false) return results | |||||
| } | |||||
| // todo: add support to get cloned asset? if we want to import multiple times and everytime return a cloned asset | |||||
| asset.preImportedRaw = this._loadFile(options.pathOverride || asset.path, typeof asset.file?.arrayBuffer === 'function' ? asset.file : undefined, options, onDownloadProgress) | |||||
| result = await asset.preImportedRaw | |||||
| if (result) result = await this.processRaw(result, options) | |||||
| if (result) { | |||||
| if (options.processRaw !== false) asset.preImported = result | |||||
| const arrs: any[] = [] | |||||
| if (Array.isArray(result)) arrs.push(...result) | |||||
| else { | |||||
| if (result.userData?.rootSceneModelRoot) arrs.push(...result.children) | |||||
| else arrs.push(result) | |||||
| } | |||||
| // remove preImportedRaw when any of the assets is disposed. This is to prevent memory leaks | |||||
| arrs.forEach(r=>r.addEventListener?.('dispose', () => { | |||||
| if (asset?.preImportedRaw) asset.preImportedRaw = undefined | |||||
| if (asset?.preImported) asset.preImported = undefined | |||||
| })) | |||||
| } | |||||
| return result | |||||
| } | |||||
| /** | |||||
| * Import multiple local files/blobs from a map of files, like when a local folder is loaded, or when multiple files are dropped. | |||||
| * @param files | |||||
| * @param options | |||||
| */ | |||||
| async importFiles<T extends ImportResult|undefined=ImportResult|undefined>(files: Map<string, IFile>, options: ImportFilesOptions = {}): Promise<Map<string, T[]>> { | |||||
| const loaded = new Map<string, any>() | |||||
| let {allowedExtensions} = options | |||||
| if (allowedExtensions && allowedExtensions.length < 1) allowedExtensions = undefined | |||||
| if (files.size === 0) return loaded | |||||
| this.dispatchEvent({type: 'importFiles', files: files, state: 'start'}) | |||||
| const baseFiles: string[] = [] | |||||
| const altFiles: string[] = [] | |||||
| // Note: mostly path === file.name | |||||
| files.forEach((file, path) => { // todo: handle only one file at the top | |||||
| this.registerFile(path, file) | |||||
| const ext = file.ext | |||||
| const mime = file.mime | |||||
| if ((ext || mime) && // todo: files with no extensions are not supported right now. This also includes __MacOSX | |||||
| (allowedExtensions?.includes((ext || mime || '').toLowerCase()) ?? true)) { | |||||
| if (this._isRootFile(ext)) baseFiles.push(path) | |||||
| else altFiles.push(path) | |||||
| } | |||||
| }) | |||||
| if (baseFiles.length > 0) { | |||||
| for (const value of baseFiles) { | |||||
| let res = await this._loadFile(value, undefined, options) | |||||
| if (res) res = await this.processRaw(res, options) | |||||
| loaded.set(value, res) | |||||
| } | |||||
| } else { | |||||
| for (const value of altFiles) { | |||||
| let res = await this._loadFile(value, undefined, options) | |||||
| if (res) res = await this.processRaw(res, options) | |||||
| loaded.set(value, res) | |||||
| } | |||||
| // todo: handle no baseFiles | |||||
| } | |||||
| this.dispatchEvent({type: 'importFiles', files: files, state: 'end'}) | |||||
| files.forEach((_, path) => this.unregisterFile(path)) | |||||
| return loaded | |||||
| } | |||||
| // load a single file | |||||
| private async _loadFile(path: string, file?: IFile, options: LoadFileOptions = {}, onDownloadProgress?: (e: ProgressEvent)=>void): Promise<ImportResult | ImportResult[] | undefined> { | |||||
| if (file?.__loadedAsset) return file.__loadedAsset | |||||
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | |||||
| let res: ImportResult | ImportResult[] | undefined | |||||
| try { | |||||
| const loader = this.registerFile(path, file) | |||||
| // const url = this.resolveURL(path) // todo: why is this required? maybe for query string? | |||||
| // const path2 = path.replace(/\?.*$/, '') // remove query string to find the handler properly | |||||
| // const loader = (options.fileHandler as ILoader) ?? this._getLoader(path2) ?? | |||||
| // (file ? this._getLoader(file.name, file.ext, file.mime) : undefined) | |||||
| if (!loader) { | |||||
| throw new Error('AssetImporter: Unable to find loader for ' + path) // caught below | |||||
| } | |||||
| this._rootContext = { | |||||
| path, | |||||
| rootUrl: LoaderUtils.extractUrlBase(path), | |||||
| // baseUrl: LoaderUtils.extractUrlBase(url), | |||||
| } | |||||
| res = await loader.loadAsync(path + (options.queryString ? (path.includes('?') ? '&' : '?') + options.queryString : ''), (e)=>{ | |||||
| if (onDownloadProgress) onDownloadProgress(e) | |||||
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: e.loaded / e.total}) | |||||
| }) | |||||
| if (loader.transform) res = await loader.transform(res, options) | |||||
| this._rootContext = undefined | |||||
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 1}) | |||||
| this.dispatchEvent({type: 'importFile', path, state: 'adding'}) | |||||
| if (file) | |||||
| this._logger('AssetImporter: loaded', path) | |||||
| else | |||||
| this._logger('AssetImporter: downloaded', path) | |||||
| if (file) | |||||
| this.unregisterFile(path) | |||||
| } catch (e: any) { | |||||
| console.error('AssetImporter: Unable to import file', path, file) | |||||
| console.error(e) | |||||
| console.error(e?.stack) | |||||
| // throw e | |||||
| this.dispatchEvent({type: 'importFile', path, state: 'error', error: e}) | |||||
| if (file) | |||||
| this.unregisterFile(path) | |||||
| return [] | |||||
| } | |||||
| this.dispatchEvent({type: 'importFile', path, state: 'done'}) // todo: do this after processing? | |||||
| if (file) { | |||||
| file.__loadedAsset = res | |||||
| // Clear the reference __loadedAsset when any one asset is disposed. | |||||
| // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||||
| let ress: any[] = [] | |||||
| if (Array.isArray(res)) ress = res.flat(2) | |||||
| else if ((<IObject3D>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children) | |||||
| else ress.push(res) | |||||
| for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined) | |||||
| } | |||||
| if (res && typeof res === 'object' && !Array.isArray(res)) { | |||||
| res.__rootPath = path | |||||
| if (file) res.__rootBlob = file | |||||
| } | |||||
| return res | |||||
| } | |||||
| // endregion | |||||
| // region file database | |||||
| /** | |||||
| * Register a file in the database and return a loader for it. If the loader does not exist, it will be created. | |||||
| * @param path | |||||
| * @param file | |||||
| */ | |||||
| registerFile(path: string, file?: IFile): ILoader | undefined { | |||||
| const isData = path.startsWith('data:') || false | |||||
| if (!isData) path = path.replace(/\?.*$/, '') // remove query string | |||||
| const ext = isData ? undefined : file?.ext ?? parseFileExtension(file?.name ?? path)?.toLowerCase() | |||||
| const mime = file?.mime ?? isData ? path.slice(0, path.indexOf(';')).split(':')[1] || undefined : undefined | |||||
| if (file) { | |||||
| if (file.name === undefined) (file as any).name = path | |||||
| if (!file.ext) file.ext = ext | |||||
| if (!file.mime) file.mime = mime | |||||
| if (this._fileDatabase.has(path)) { | |||||
| console.warn('AssetImporter: File already registered, replacing', path) | |||||
| this.unregisterFile(path) | |||||
| } | |||||
| this._fileDatabase.set(path, file) | |||||
| } | |||||
| return this._getLoader(path) || this._createLoader(path, ext, mime) | |||||
| } | |||||
| /** | |||||
| * Remove a file from the database and revoke the object url if it exists. | |||||
| * @param path | |||||
| */ | |||||
| unregisterFile(path: string) { | |||||
| path = path.replace(/\?.*$/, '') // remove query string | |||||
| const file = this._fileDatabase.get(path) | |||||
| if (file?.objectUrl) { | |||||
| URL.revokeObjectURL(file.objectUrl) | |||||
| file.objectUrl = undefined | |||||
| } | |||||
| if (file) this._fileDatabase.delete(path) | |||||
| } | |||||
| // endregion | |||||
| // region processRaw | |||||
| public async processRaw<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: ProcessRawOptions): Promise<T[]> { | |||||
| if (!res) return [] | |||||
| // legacy | |||||
| if (options.processImported !== undefined) { | |||||
| console.error('AssetImporter: processImported is deprecated, use processRaw instead') | |||||
| options.processRaw = options.processImported | |||||
| } | |||||
| if (Array.isArray(res)) { | |||||
| const r: any[] = [] | |||||
| for (const re of res) { // todo: can we parallelize? | |||||
| r.push(...await this.processRaw(re, options)) | |||||
| } | |||||
| return r | |||||
| } | |||||
| if (options.processRaw === false) return [res] | |||||
| // todo; | |||||
| // if (res.userData?.rootSceneModelRoot) { | |||||
| // options._rootSceneImported = true // used in Dropzone | |||||
| // } | |||||
| if (res.assetImporterProcessed && !options.forceImporterReprocess) return [res] | |||||
| this.dispatchEvent({type: 'processRawStart', data: res, options}) | |||||
| // for testing only | |||||
| if (res.isTexture && options._testDataTextureComplete) { | |||||
| // if some data textures are not loading correctly, should not ideally be required | |||||
| if (res.isDataTexture && res.image?.data) res.image.complete = true | |||||
| if (res.image?.complete) res.needsUpdate = true | |||||
| } | |||||
| if (res.userData) { | |||||
| const userData: IImportResultUserData = res.userData | |||||
| const rootPath = res.__rootPath | |||||
| if (!userData.rootPath && rootPath && !rootPath.startsWith('blob:') && !rootPath.startsWith('/')) | |||||
| userData.rootPath = rootPath | |||||
| if (res.__rootBlob) { | |||||
| userData.__sourceBlob = res.__rootBlob | |||||
| if (userData.__needsSourceBuffer) { // set __sourceBuffer here if required during serialize later on, __needsSourceBuffer can be set in asset loaders | |||||
| userData.__sourceBuffer = await res.__rootBlob.arrayBuffer() | |||||
| delete userData.__needsSourceBuffer | |||||
| } | |||||
| } | |||||
| } | |||||
| // if (res.assetType) // todo: why if? | |||||
| res.assetImporterProcessed = true // this should not be put in userData | |||||
| this.dispatchEvent({type: 'processRaw', data: res, options}) | |||||
| // special for zip files. ZipLoader gives this | |||||
| if ((<any>res) instanceof Map && options.autoImportZipContents !== false) { | |||||
| // todo: should we pass in onProgress from outside? | |||||
| return [...(await this.importFiles<T>(<any>res, options)).values()].flat() | |||||
| } | |||||
| return [res] | |||||
| } | |||||
| public async processRawSingle<T extends (ImportResult|undefined) = ImportResult>(res: T, options: ProcessRawOptions): Promise<T> { | |||||
| return (await this.processRaw(res, options))[0] | |||||
| } | |||||
| // endregion | |||||
| // region disposal | |||||
| dispose(): void { | |||||
| this.clearCache() | |||||
| // this._processors?.dispose() | |||||
| // this._loadingManager.dispose // todo | |||||
| } | |||||
| /** | |||||
| * Clear memory asset and loader cache. Browser cache and custom cache storage is not cleared with this. | |||||
| */ | |||||
| clearCache(): void { | |||||
| this._cachedAssets = [] | |||||
| this.unregisterAllFiles() | |||||
| this.clearLoaderCache() | |||||
| } | |||||
| unregisterAllFiles(): void { | |||||
| const keys = [...this._fileDatabase.keys()] | |||||
| for (const key of keys) { | |||||
| this.unregisterFile(key) | |||||
| } | |||||
| } | |||||
| clearLoaderCache(): void { | |||||
| for (const lc of this._loaderCache) { | |||||
| lc.loader?.dispose?.() | |||||
| } | |||||
| this._loaderCache = [] | |||||
| } | |||||
| // endregion | |||||
| // region utils | |||||
| resolveURL(url: string): string { | |||||
| return this._loadingManager.resolveURL(url) | |||||
| } | |||||
| protected _urlModifier(url: string) { | |||||
| let normalizedURL = decodeURI(url) | |||||
| const rootUrl = this._rootContext?.rootUrl | |||||
| if (!normalizedURL.includes('://') && rootUrl && !normalizedURL.startsWith(rootUrl)) | |||||
| normalizedURL = rootUrl + normalizedURL | |||||
| normalizedURL = normalizedURL.replace('./', '') // remove ./ | |||||
| normalizedURL = normalizedURL.replace(/^(\/\/)/, '/') // fix for start with // | |||||
| // remove query string | |||||
| normalizedURL = normalizedURL.replace(/\?.*$/, '') | |||||
| const file = this._fileDatabase.get(normalizedURL) | |||||
| if (!file) return url | |||||
| const ext = file.ext | |||||
| if (!ext) { | |||||
| console.error('Unable to determine file extension', file) | |||||
| return url | |||||
| } | |||||
| if (!file.objectUrl) file.objectUrl = URL.createObjectURL(file) + '#' + normalizedURL | |||||
| return file.objectUrl | |||||
| } | |||||
| private _isRootFile(ext?: string, mime?: string) { | |||||
| mime = mime?.toLowerCase() | |||||
| ext = ext?.toLowerCase() | |||||
| return this.importers.find(value => value.root && ( | |||||
| ext && value.ext.includes(ext.toLowerCase()) || | |||||
| mime && value.mime.includes(mime.toLowerCase()) | |||||
| )) != null | |||||
| } | |||||
| // get an importer that can create a loader | |||||
| private _getImporter(name:string, ext?:string, mime?: string, isRoot = false): IImporter | undefined { | |||||
| mime = mime?.toLowerCase() | |||||
| ext = ext?.toLowerCase() | |||||
| return this.importers.find(importer => { | |||||
| if (isRoot && !importer.root) return false | |||||
| if (mime && importer.mime?.find(m => mime === m)) return true | |||||
| if (importer.ext.find(iext => | |||||
| ext && iext === ext | |||||
| || name?.toLowerCase()?.endsWith('.' + iext) | |||||
| || iext?.startsWith('data:') && name?.startsWith(iext))) return true | |||||
| return false | |||||
| }) | |||||
| } | |||||
| // get a loader that can load a file. | |||||
| private _getLoader(name?:string, ext?:string, mime?: string): ILoader | undefined { | |||||
| if (!ext && !mime && name) ext = parseFileExtension(name).toLowerCase() | |||||
| mime = mime?.toLowerCase() | |||||
| ext = ext?.toLowerCase() | |||||
| return (name ? this._loadingManager.getHandler(name) as ILoader : undefined) | |||||
| || this._loaderCache.find((lc)=> ext && lc.ext.includes(ext) || mime && lc.mime.includes(mime))?.loader | |||||
| } | |||||
| private _createLoader(name:string, ext?:string, mime?: string): ILoader | undefined { // todo: remove/destroy loader. | |||||
| const importer = this._getImporter(name, ext, mime) | |||||
| if (!importer) return undefined | |||||
| const loader = importer.ctor(this) | |||||
| if (!loader) return undefined | |||||
| importer.ext.forEach(iext => { | |||||
| const regex = new RegExp(iext.startsWith('data:') ? '^' + iext + '\\/' : '\\.' + iext + '$', 'i') | |||||
| this._loadingManager.addHandler(regex, loader) | |||||
| }) | |||||
| importer.mime?.forEach(imime => { | |||||
| const regex = new RegExp('^data:' + imime + '$', 'i') | |||||
| this._loadingManager.addHandler(regex, loader) | |||||
| }) | |||||
| this._loaderCache.push({loader, ext: importer.ext, mime: importer.mime}) | |||||
| this.dispatchEvent({type: 'loaderCreate', loader}) | |||||
| return loader | |||||
| } | |||||
| // endregion | |||||
| // region Loader Event Dispatchers | |||||
| protected _onLoad() { | |||||
| this.dispatchEvent({type: 'onLoad'}) | |||||
| } | |||||
| protected _onProgress(url: string, loaded: number, total: number) { | |||||
| this.dispatchEvent({type: 'onProgress', url, loaded, total}) | |||||
| } | |||||
| protected _onError(url: string) { | |||||
| this.dispatchEvent({type: 'onError', url}) | |||||
| } | |||||
| protected _onStart(url: string, loaded: number, total: number) { | |||||
| this.dispatchEvent({type: 'onStart', url, loaded, total}) | |||||
| } | |||||
| // endregion | |||||
| // region deprecated | |||||
| /** | |||||
| * @deprecated use {@link processRaw} instead | |||||
| * @param res | |||||
| * @param options | |||||
| */ | |||||
| public async processImported(res: any, options: ProcessRawOptions): Promise<any[]> { | |||||
| console.error('processImported is deprecated. Use processRaw instead.') | |||||
| return await this.processRaw(res, options) | |||||
| } | |||||
| // endregion | |||||
| } |
| import {ImportAssetOptions, ImportResult, ProcessRawOptions, RootSceneImportResult} from './IAssetImporter' | |||||
| import { | |||||
| BaseEvent, | |||||
| Cache as threeCache, | |||||
| Camera, | |||||
| EventDispatcher, | |||||
| LinearFilter, | |||||
| LinearMipmapLinearFilter, | |||||
| LoadingManager, | |||||
| PerspectiveCamera, | |||||
| TextureLoader, | |||||
| } from 'three' | |||||
| import {ISerializedConfig, IViewerPlugin, ThreeViewer} from '../viewer' | |||||
| import {AssetImporter} from './AssetImporter' | |||||
| import {generateUUID, getTextureDataType, overrideThreeCache} from '../three' | |||||
| import {IAsset} from './IAsset' | |||||
| import { | |||||
| AddObjectOptions, | |||||
| ICamera, | |||||
| iCameraCommons, | |||||
| IMaterial, | |||||
| iMaterialCommons, | |||||
| IObject3D, | |||||
| iObjectCommons, | |||||
| ISceneEvent, | |||||
| PerspectiveCamera2, | |||||
| upgradeTexture, | |||||
| } from '../core' | |||||
| import {Importer} from './Importer' | |||||
| import {MaterialManager} from './MaterialManager' | |||||
| import {DRACOLoader2, GLTFLoader2, JSONMaterialLoader, MTLLoader2, OBJLoader2, ZipLoader} from './import' | |||||
| import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js' | |||||
| import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js' | |||||
| import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js' | |||||
| import {Class, ValOrArr} from 'ts-browser-helpers' | |||||
| import {ILoader} from './IImporter' | |||||
| import {AssetExporter} from './AssetExporter' | |||||
| import {IExporter} from './IExporter' | |||||
| import {GLTFExporter2} from './export' | |||||
| export interface AssetManagerOptions{ | |||||
| simpleCache?: boolean // simple memory based cache for downloaded files, default = false | |||||
| storage?: Cache | Storage // cache storage for downloaded files, can use with `caches.open` default = undefined | |||||
| } | |||||
| export type ImportAddOptions = ImportAssetOptions & AddObjectOptions | |||||
| export type AddRawOptions = ProcessRawOptions & AddObjectOptions | |||||
| export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}, 'loadAsset'> { | |||||
| static readonly PluginType = 'AssetManager' | |||||
| readonly viewer: ThreeViewer | |||||
| readonly importer: AssetImporter | |||||
| readonly exporter: AssetExporter | |||||
| readonly materials: MaterialManager | |||||
| // private readonly _linkDropzone: boolean | |||||
| readonly storage?: Cache | Storage | |||||
| constructor(viewer: ThreeViewer, {simpleCache = false, storage}: AssetManagerOptions = {}) { | |||||
| super() | |||||
| this._sceneUpdated = this._sceneUpdated.bind(this) | |||||
| this.addAsset = this.addAsset.bind(this) | |||||
| this.addRaw = this.addRaw.bind(this) | |||||
| this.addImported = this.addImported.bind(this) | |||||
| this.importer = new AssetImporter(!!viewer.getPlugin('debug')) | |||||
| this.exporter = new AssetExporter() | |||||
| this.materials = new MaterialManager() | |||||
| this.viewer = viewer | |||||
| this.viewer.scene.addEventListener('addSceneObject', this._sceneUpdated) | |||||
| this.viewer.scene.addEventListener('materialChanged', this._sceneUpdated) | |||||
| this._initCacheStorage(simpleCache, storage) | |||||
| this.storage = storage | |||||
| this.importer.addEventListener('processRaw', (event)=>{ | |||||
| // console.log('preprocess mat', mat) | |||||
| const mat = event.data as IMaterial | |||||
| if (!mat || !mat.isMaterial || !mat.uuid) return | |||||
| if (this.materials?.findMaterial(mat.uuid)) { | |||||
| console.warn('imported material uuid already exists, creating new uuid') | |||||
| mat.uuid = generateUUID() | |||||
| if (mat.userData.uuid) mat.userData.uuid = mat.uuid | |||||
| } | |||||
| // todo: check for name exists also | |||||
| this.materials.registerMaterial(mat) | |||||
| }) | |||||
| this.importer.addEventListener('processRawStart', (event)=>{ | |||||
| // console.log('preprocess mat', mat) | |||||
| const res = event.data! | |||||
| // if (!res.assetType) { | |||||
| // if (res.isBufferGeometry) { // for eg stl todo | |||||
| // res = new Mesh(res, new MeshStandardMaterial()) | |||||
| // } | |||||
| // if (res.isObject3D) { | |||||
| // } | |||||
| // } | |||||
| if (res.isObject3D) { | |||||
| // todo replace lights | |||||
| // if (res.isLight) { | |||||
| // res = upgradeThreejsLight(res) | |||||
| // } else { | |||||
| // const lights: any[] = [] | |||||
| // res.traverse((rr: any)=>{ | |||||
| // if (rr !== res && rr.isLight) lights.push(rr) | |||||
| // }) | |||||
| // for (const light of lights) { | |||||
| // upgradeThreejsLight(light) | |||||
| // } | |||||
| // res = new Object3DModel(res, options as any) | |||||
| // } | |||||
| const cameras: Camera[] = [] | |||||
| res.traverse((obj: any) => { | |||||
| if (obj.material) { | |||||
| const materials = Array.isArray(obj.material) ? obj.material : [obj.material] | |||||
| const newMaterials = [] | |||||
| for (const material of materials) { | |||||
| const mat = this.materials.convertToIMaterial(material) || material | |||||
| mat.uuid = material.uuid | |||||
| mat.userData.uuid = material.uuid | |||||
| newMaterials.push(mat) | |||||
| } | |||||
| if (Array.isArray(obj.material)) obj.material = newMaterials | |||||
| else obj.material = newMaterials[0] | |||||
| } | |||||
| if (obj.isCamera) cameras.push(obj) | |||||
| }) | |||||
| for (const camera of cameras) { | |||||
| // todo: OrthographicCamera | |||||
| if (!(camera as PerspectiveCamera).isPerspectiveCamera || !camera.parent) { | |||||
| iCameraCommons.upgradeCamera.call(camera) | |||||
| } else { | |||||
| const newCamera: ICamera = (camera as any).iCamera ?? new PerspectiveCamera2('', this.viewer.canvas).copy(camera) | |||||
| if (camera === newCamera) continue | |||||
| ;(newCamera as any).uuid = camera.uuid | |||||
| newCamera.userData.uuid = camera.uuid | |||||
| ;(camera as any).iCamera = newCamera | |||||
| camera.parent.children.splice(camera.parent.children.indexOf(camera), 1, newCamera) | |||||
| } | |||||
| } | |||||
| iObjectCommons.upgradeObject3D.call(res) | |||||
| } else if (res.isMaterial) { | |||||
| iMaterialCommons.upgradeMaterial.call(res) | |||||
| // todo update res by generating new material? | |||||
| } else if (res.isTexture) { | |||||
| upgradeTexture.call(res) | |||||
| if (event?.options?.generateMipmaps !== undefined) | |||||
| res.generateMipmaps = event?.options.generateMipmaps | |||||
| if (!res.generateMipmaps && !res.isRenderTargetTexture) { // todo: do we need to check more? | |||||
| res.minFilter = res.minFilter === LinearMipmapLinearFilter ? LinearFilter : res.minFilter | |||||
| res.magFilter = res.magFilter === LinearMipmapLinearFilter ? LinearFilter : res.magFilter | |||||
| } | |||||
| } | |||||
| // todo other asset/object types? | |||||
| }) | |||||
| this._addImporters() | |||||
| this._addExporters() | |||||
| } | |||||
| async addAsset<T extends ImportResult = ImportResult>(assetOrPath?: string | IAsset | IAsset[], options?: ImportAddOptions): Promise<(T|undefined)[]> { | |||||
| if (!this.importer || !this.viewer) return [] | |||||
| const imported = await this.importer.import<T>(assetOrPath, options) | |||||
| if (!imported) { | |||||
| console.warn('Unable to import', assetOrPath, imported) | |||||
| return [] | |||||
| } | |||||
| return this.loadImported<(T|undefined)[]>(imported, options) | |||||
| } | |||||
| // materials: IMaterial[] = [] | |||||
| // textures: ITexture[] = [] | |||||
| async loadImported<T extends ValOrArr<ImportResult|undefined> = ImportResult>(imported: T, options?: AddObjectOptions): Promise<T | never[]> { | |||||
| const arr: (ImportResult|undefined)[] = Array.isArray(imported) ? imported : [imported] | |||||
| for (const obj of arr) { | |||||
| if (!obj) continue | |||||
| switch (obj.assetType) { | |||||
| case 'material': | |||||
| this.materials.registerMaterial(<IMaterial>obj) | |||||
| break | |||||
| case 'texture': | |||||
| break | |||||
| case 'model': | |||||
| case 'light': | |||||
| case 'camera': | |||||
| await this.viewer.addSceneObject(<IObject3D|RootSceneImportResult>obj, options) // todo update references in scene update event | |||||
| break | |||||
| case 'config': | |||||
| if (options?.importConfig !== false) await this.viewer.importConfig(<ISerializedConfig>obj) | |||||
| break | |||||
| default: | |||||
| // legacy | |||||
| if (obj.type && typeof obj.type === 'string' && (Array.isArray((obj as any).plugins) || | |||||
| (obj as any).type === 'ThreeViewer' || this.viewer.getPlugin((obj as any).type))) { | |||||
| await this.viewer.importConfig(<ISerializedConfig>obj) | |||||
| } | |||||
| break | |||||
| } | |||||
| this.dispatchEvent({type: 'loadAsset', data: obj}) | |||||
| } | |||||
| return imported || [] | |||||
| } | |||||
| /** | |||||
| * same as {@link loadImported} | |||||
| * @param imported | |||||
| * @param options | |||||
| */ | |||||
| async addProcessedAssets<T extends ImportResult|undefined = ImportResult>(imported: (T|undefined)[], options?: AddObjectOptions): Promise<(T | undefined)[]> { | |||||
| return this.loadImported(imported, options) | |||||
| } | |||||
| async addAssetSingle<T extends ImportResult = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> { | |||||
| return !asset ? undefined : (await this.addAsset<T>(asset, options))?.[0] | |||||
| } | |||||
| // processAndAddObjects | |||||
| async addRaw<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: AddRawOptions = {}): Promise<(T|undefined)[]> { | |||||
| const r = await this.importer.processRaw<T>(res, options) | |||||
| return this.loadImported<T[]>(r, options) | |||||
| } | |||||
| async addRawSingle<T extends ImportResult|undefined = ImportResult|undefined>(res: T, options: AddRawOptions = {}): Promise<T|undefined> { | |||||
| return (await this.addRaw<T>(res, options))?.[0] | |||||
| } | |||||
| private _sceneUpdated(event: ISceneEvent) { // todo: check if objects are added some other way. | |||||
| if (event.type === 'addSceneObject') { | |||||
| const target = event.object as ImportResult | |||||
| switch (target.assetType) { | |||||
| case 'material': | |||||
| this.materials.registerMaterial(<IMaterial>target) | |||||
| break | |||||
| case 'texture': | |||||
| break | |||||
| case 'model': | |||||
| case 'light': | |||||
| case 'camera': | |||||
| break | |||||
| default: | |||||
| break | |||||
| } | |||||
| } else if (event.type === 'materialChanged') { | |||||
| const target = event.material as IMaterial | IMaterial[] | undefined | |||||
| const targets = Array.isArray(target) ? target : target ? [target] : [] | |||||
| for (const t of targets) { | |||||
| this.materials.registerMaterial(t) | |||||
| } | |||||
| } else { | |||||
| console.error('Unexpected') | |||||
| } | |||||
| } | |||||
| dispose() { | |||||
| this.importer.dispose() | |||||
| this.materials.dispose() | |||||
| this.viewer.scene.removeEventListener('addSceneObject', this._sceneUpdated) | |||||
| this.viewer.scene.removeEventListener('materialChanged', this._sceneUpdated) | |||||
| this.exporter.dispose() | |||||
| } | |||||
| protected _addImporters() { | |||||
| const viewer = this.viewer | |||||
| if (!viewer) return | |||||
| console.log(['mat', ...this.materials.templates.map(t=>t.typeSlug!).filter(v=>v)]) | |||||
| const importers: Importer[] = [ | |||||
| new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'svg', 'ico', 'data:image'], [ | |||||
| 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon', | |||||
| ], false), // todo: use ImageBitmapLoader if supported (better performance) | |||||
| new Importer<JSONMaterialLoader>(JSONMaterialLoader, | |||||
| ['mat', ...this.materials.templates.map(t=>t.typeSlug!).filter(v=>v)], // todo add others | |||||
| [], false, (loader)=>{ | |||||
| if (loader) loader.viewer = this.viewer | |||||
| return loader | |||||
| }), | |||||
| new Importer(class extends RGBELoader { | |||||
| constructor(manager: LoadingManager) { | |||||
| super(manager) | |||||
| this.setDataType(getTextureDataType(viewer.renderManager.renderer)) | |||||
| } | |||||
| }, ['hdr'], ['image/vnd.radiance'], false), | |||||
| new Importer(class extends EXRLoader { | |||||
| constructor(manager: LoadingManager) { | |||||
| super(manager) | |||||
| this.setDataType(getTextureDataType(viewer.renderManager.renderer)) | |||||
| } | |||||
| }, ['exr'], ['image/x-exr'], false), | |||||
| new Importer(FBXLoader, ['fbx'], ['model/fbx'], true), | |||||
| new Importer(ZipLoader, ['zip'], ['application/zip'], true), | |||||
| new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true), | |||||
| new Importer(MTLLoader2 as any as Class<ILoader>, ['mtl'], ['model/mtl'], false), | |||||
| new Importer<GLTFLoader2>(GLTFLoader2, ['gltf', 'glb', 'data:model/gltf'], ['model/gltf', 'model/gltf+json', 'model/gltf-binary'], true, (l, _, i) => l?.setup(this.viewer, i.extensions)), | |||||
| new Importer(DRACOLoader2, ['drc'], ['model/mesh+draco'], true), | |||||
| ] | |||||
| this.importer.addImporter(...importers) | |||||
| } | |||||
| protected _addExporters() { | |||||
| const exporters: IExporter[] = [ | |||||
| {ext: ['gltf', 'glb'], extensions: [], ctor: (_, exporter)=>{ | |||||
| const ex = new GLTFExporter2() | |||||
| // This should be added at the end. | |||||
| ex.setup(this.viewer, exporter.extensions) | |||||
| return ex | |||||
| }}, | |||||
| ] | |||||
| this.exporter.addExporter(...exporters) | |||||
| } | |||||
| private _initCacheStorage(simpleCache?: boolean, storage?: Cache | Storage) { | |||||
| if (simpleCache || storage) { | |||||
| // three.js built-in simple memory cache. used in FileLoader.js todo: use local storage somehow | |||||
| if (simpleCache) threeCache.enabled = true | |||||
| if (storage && window.Cache && typeof window.Cache === 'function' && storage instanceof window.Cache) { | |||||
| overrideThreeCache(storage) | |||||
| // todo: clear cache | |||||
| } | |||||
| } | |||||
| } | |||||
| // region deprecated | |||||
| /** | |||||
| * @deprecated use addRaw instead | |||||
| * @param res | |||||
| * @param options | |||||
| */ | |||||
| async addImported<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: AddRawOptions = {}): Promise<(T|undefined)[]> { | |||||
| console.error('addImported is deprecated, use addRaw instead') | |||||
| return this.addRaw(res, options) | |||||
| } | |||||
| /** | |||||
| * @deprecated use addAsset instead | |||||
| * @param path | |||||
| * @param options | |||||
| */ | |||||
| public async addFromPath(path: string, options: ImportAddOptions = {}): Promise<any[]> { | |||||
| console.error('addFromPath is deprecated, use addAsset instead') | |||||
| return this.addAsset(path, options) | |||||
| } | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.exportConfig} instead | |||||
| * @param binary - if set to false, encodes all the array buffers to base64 | |||||
| */ | |||||
| exportViewerConfig(binary = true): Record<string, any> { | |||||
| if (!this.viewer) return {} | |||||
| console.error('exportViewerConfig is deprecated, use viewer.toJSON instead') | |||||
| return this.viewer.toJSON(binary, undefined) | |||||
| } | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.exportPluginsConfig} instead | |||||
| * @param filter | |||||
| */ | |||||
| exportPluginPresets(filter?: string[]) { | |||||
| console.error('exportPluginPresets is deprecated, use viewer.exportPluginsConfig instead') | |||||
| return this.viewer?.exportPluginsConfig(filter) | |||||
| } | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.exportPluginConfig} instead | |||||
| * @param plugin | |||||
| */ | |||||
| exportPluginPreset(plugin: IViewerPlugin) { | |||||
| console.error('exportPluginPreset is deprecated, use viewer.exportPluginConfig instead') | |||||
| return this.viewer?.exportPluginConfig(plugin) | |||||
| } | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.importPluginConfig} instead | |||||
| * @param json | |||||
| * @param plugin | |||||
| */ | |||||
| async importPluginPreset(json: any, plugin?: IViewerPlugin) { | |||||
| console.error('importPluginPreset is deprecated, use viewer.importPluginConfig instead') | |||||
| return this.viewer?.importPluginConfig(json, plugin) | |||||
| } | |||||
| // todo continue from here by moving functions to the viewer. | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.importConfig} instead | |||||
| * @param viewerConfig | |||||
| */ | |||||
| async importViewerConfig(viewerConfig: any) { | |||||
| return this.viewer?.importConfig(viewerConfig) | |||||
| } | |||||
| /** | |||||
| * @deprecated use {@link ThreeViewer.fromJSON} instead | |||||
| * @param viewerConfig | |||||
| */ | |||||
| applyViewerConfig(viewerConfig: any, resources?: any) { | |||||
| console.error('applyViewerConfig is deprecated, use viewer.fromJSON instead') | |||||
| return this.viewer?.fromJSON(viewerConfig, resources) | |||||
| } | |||||
| /** | |||||
| * @deprecated moved to {@link ThreeViewer.loadConfigResources} | |||||
| * @param json | |||||
| * @param extraResources - preloaded resources in the format of viewer config resources. | |||||
| */ | |||||
| async importConfigResources(json: any, extraResources?: any) { | |||||
| if (!this.importer) throw 'Importer not initialized yet.' | |||||
| // console.log(json) | |||||
| if (json.__isLoadedResources) return json | |||||
| return this.viewer?.loadConfigResources(json, extraResources) | |||||
| } | |||||
| // endregion | |||||
| } |
| import {ImportResult} from './IAssetImporter' | |||||
| export type IAssetID = string | |||||
| export type IFile = Blob & Partial<File> & { | |||||
| objectUrl?: string, // URL created from URL.createObjectURL, used to revoke the objectUrl with URL.revokeObjectURL | |||||
| ext?: string // extension of the file without the dot | |||||
| mime?: string // mime type of the file | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __loadedAsset?: ImportResult | ImportResult[] // used by asset manager to store the loaded asset | |||||
| } | |||||
| export interface IAsset { | |||||
| id?: IAssetID; | |||||
| path: string; | |||||
| file?: IFile, | |||||
| // variants?: IAssetID[], | |||||
| preImportedRaw?: Promise<ImportResult | ImportResult[] | undefined> // todo type | |||||
| preImported?: ImportResult[] // todo type | |||||
| [id: string]: any | |||||
| } | |||||
| export interface IAssetList { | |||||
| basePath?: string; | |||||
| assets: IAsset[]; | |||||
| } |
| import {BaseEvent, EventDispatcher, LoadingManager, Object3D} from 'three' | |||||
| import {AnyOptions, IDisposable} from 'ts-browser-helpers' | |||||
| import {IAsset, IFile} from './IAsset' | |||||
| import {ILoader} from './IImporter' | |||||
| import {ICamera, IMaterial, IObject3D, ITexture} from '../core' | |||||
| import {ISerializedConfig, ISerializedViewerConfig} from '../viewer' | |||||
| import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||||
| export interface RootSceneImportResult extends Object3D { | |||||
| readonly visible: true | |||||
| importedViewerConfig?: ISerializedViewerConfig | |||||
| userData: { | |||||
| rootSceneModelRoot?: true | |||||
| __importData?: any | |||||
| gltfExtras?: GLTF['userData'] | |||||
| gltfAsset?: GLTF['asset'] | |||||
| [key: string]: any | |||||
| } | |||||
| } | |||||
| export type ImportResultObject = IObject3D | ITexture | ICamera | ISerializedConfig | ISerializedViewerConfig | RootSceneImportResult | IMaterial | |||||
| export interface ImportResultExtras { | |||||
| constructor: any | |||||
| assetImporterProcessed?: boolean | |||||
| isObject3D?: boolean | |||||
| isCamera?: boolean | |||||
| isMaterial?: boolean | |||||
| isTexture?: boolean | |||||
| userData?: IImportResultUserData | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __rootPath?: string | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __rootBlob?: IFile | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __disposed?: boolean | |||||
| [key: string]: any | |||||
| } | |||||
| export type ImportResult = ImportResultObject & ImportResultExtras | |||||
| export interface IImportResultUserData{ | |||||
| rootPath?: string | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __importData?: any // extra arbitrary data saved by the importer that can be used by the plugins (like gltf material variants) | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __needsSourceBuffer?: boolean // This can be set to true in the importer to indicate that the source buffer should be loaded and cached in the userdata during processRaw | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __sourceBuffer?: ArrayBuffer // Cache d source buffer for the asset (only cached when __needsSourceBuffer is set) | |||||
| // eslin t-disable-next-line @typescript-eslint/naming-convention | |||||
| __sourceBlob?: IFile // Cache d source blob for the asset | |||||
| } | |||||
| export type ProcessRawOptions = { | |||||
| processRaw?: boolean, // defau lt = true, toggle to control the processing of the raw objects in the proecssRaw method | |||||
| forceImporterReprocess?: boolean, // defau lt = false. If true, the importer will reprocess the imported objects, even if they are already processed. | |||||
| rootPath?: string, // internal use | |||||
| generateMipmaps?: boolean|undefined, // defau lt = undefined, only used for textures | |||||
| autoImportZipContents?: boolean, // defau lt = true, if true, the importer will automatically import the contents of zip files, if zip importer is registered. | |||||
| // inter nal | |||||
| _testDataTextureComplete?: boolean, // defau lt = false, if set to true, it will test if the data textures are complete. [internal use] | |||||
| /** | |||||
| * @deprecated use processRaw instead | |||||
| */ | |||||
| processImported?: boolean, // same as processRaw | |||||
| } & AnyOptions | |||||
| export interface LoadFileOptions { | |||||
| fileHandler?: any, // custom {@link ILoader} for the file | |||||
| /** | |||||
| * Query string to add to the url. Default = undefined | |||||
| */ | |||||
| queryString?: string, | |||||
| rootPath?: string, // internal use | |||||
| } | |||||
| export type ImportFilesOptions = ProcessRawOptions & LoadFileOptions & {allowedExtensions?: string[]} | |||||
| export type ImportAssetOptions = { | |||||
| /** | |||||
| * Default = false. If true, the asset will be imported again on subsequent calls, even if it is already imported. | |||||
| */ | |||||
| forceImport?: boolean, | |||||
| /** | |||||
| * If true or not specified, and any of the assets is disposed(only root objects are checked, not children), all assets will be imported in this call. If false, old assets will be returned. | |||||
| * Default = true. | |||||
| */ | |||||
| reimportDisposed?: boolean, | |||||
| /** | |||||
| * Path override to use for the asset. This will be used in the importer as override to path inside the asset/cached asset. | |||||
| */ | |||||
| pathOverride?: string, | |||||
| /** | |||||
| * Mime type to use when importing the file, if not specified, it will be determined from the file extension. | |||||
| */ | |||||
| mimeType?: string, | |||||
| /** | |||||
| * Pass a custom file to use for the import. This will be used in the importer, and nothing will be fetched from the path | |||||
| */ | |||||
| importedFile?: IFile, | |||||
| } & ProcessRawOptions & LoadFileOptions & AnyOptions | |||||
| export type IAssetImporterEventTypes = 'onLoad' | 'onProgress' | 'onStop' | 'onError' | 'onStart' | 'loaderCreate' | 'importFile' | 'importFiles' | 'processRaw' | 'processRawStart' | |||||
| export interface IAssetImporter extends EventDispatcher<BaseEvent, IAssetImporterEventTypes>, IDisposable { | |||||
| readonly loadingManager: LoadingManager | |||||
| readonly cachedAssets: IAsset[] | |||||
| /** | |||||
| * Import single or multiple assets(like in case of zip files) from a path(url) or an {@link IAsset}. | |||||
| * @param assetOrPath - The path or asset to import | |||||
| * @param options - Options for the import | |||||
| */ | |||||
| import<T extends ImportResult = ImportResult>(assetOrPath?: IAsset | string, options?: ImportAssetOptions): Promise<(T|undefined)[]>; | |||||
| /** | |||||
| * Import a single asset from a path(url) or an {@link IAsset}. | |||||
| * @param asset | |||||
| * @param options | |||||
| */ | |||||
| importSingle<T extends ImportResult = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined>; | |||||
| /** | |||||
| * Import multiple local files/blobs from a map of files. | |||||
| * @param files | |||||
| * @param options | |||||
| */ | |||||
| importFiles(files: Map<string, IFile>, options?: ImportFilesOptions): Promise<Map<string, any[]> | undefined>; | |||||
| /** | |||||
| * Register a file to a specific path, so this file will be used when importing the path. | |||||
| * @param path | |||||
| * @param file | |||||
| */ | |||||
| registerFile(path: string, file?: IFile): ILoader | undefined; | |||||
| /** | |||||
| * Unregister a file from a specific path. | |||||
| * @param path | |||||
| */ | |||||
| unregisterFile(path: string): void; | |||||
| /** | |||||
| * Process the raw output from the loaders and return the updated/patched-objects. | |||||
| * @param res | |||||
| * @param options | |||||
| */ | |||||
| processRaw(res: any, options: ProcessRawOptions): Promise<any[]> | |||||
| } | |||||
| import {AnyOptions, IEventDispatcher} from 'ts-browser-helpers' | |||||
| import {IObject3D} from '../core' | |||||
| import {GLTFExporter2Options} from './export/GLTFExporter2' | |||||
| export type BlobExt = Blob&{ext:string} | |||||
| export interface IExportParser { | |||||
| // parse(obj: any, options: AnyOptions): any; | |||||
| parseAsync(obj: any, options: AnyOptions): Promise<Blob> | |||||
| } | |||||
| export interface IExporter { | |||||
| extensions?: any[] | |||||
| ext: string[]; | |||||
| ctor: (assetExporter: IAssetExporter, exporter: IExporter)=>IExportParser|undefined; | |||||
| } | |||||
| export type ExportFileOptions = { | |||||
| /** | |||||
| * Extension to export to, default for object/scene = glb, default for viewerConfig = json, default for material = mat, otherwise determined by exporter | |||||
| */ | |||||
| exportExt?: string, | |||||
| /** | |||||
| * Export the viewer config (scene settings). | |||||
| * only works for rootSceneModelRoot. default = true | |||||
| */ | |||||
| viewerConfig?: boolean, | |||||
| } & GLTFExporter2Options & AnyOptions | |||||
| export interface IAssetExporter extends IEventDispatcher<'exportFile' | 'exporterCreate'>{ | |||||
| getExporter(...ext: string[]): IExporter|undefined | |||||
| exportObject(obj?: IObject3D, options?: ExportFileOptions): Promise<BlobExt|undefined> | |||||
| // processors: ObjectProcessorMap<TAssetTypes> | |||||
| } |
| import {Loader} from 'three' | |||||
| import {IAssetImporter} from './IAssetImporter' | |||||
| import {AnyOptions, IDisposable} from 'ts-browser-helpers' | |||||
| export interface ILoader<T = any, T2 = any> extends Loader, Partial<IDisposable> { | |||||
| loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any>; | |||||
| /** | |||||
| * Transform after load, like convert geometry to mesh, etc. for reference see {@link DRACOLoader2} | |||||
| * @param res - result of load | |||||
| * @param options | |||||
| */ | |||||
| transform?(res: T, options: AnyOptions): T2|Promise<T2> | |||||
| } | |||||
| export interface IImporter { | |||||
| ext: string[]; | |||||
| mime: string[]; | |||||
| root: boolean; | |||||
| extensions?: any[]; // extra plugins/extensions for this importer, like for gltf loader. | |||||
| ctor: (assetImporter: IAssetImporter)=>ILoader|undefined; | |||||
| } |
| import {IAssetImporter} from './IAssetImporter' | |||||
| import {IImporter, ILoader} from './IImporter' | |||||
| import {Class} from 'ts-browser-helpers' | |||||
| /** | |||||
| * Importer for loading files through AssetImporter. By default, it's a wrapper for threejs loaders. | |||||
| */ | |||||
| export class Importer<T extends ILoader = ILoader> implements IImporter { | |||||
| cls?: Class<T> | |||||
| onCtor?: (l: T|undefined, ai: IAssetImporter, i: IImporter) => T|undefined | |||||
| ctor(assetImporter: IAssetImporter): ILoader | undefined { // attach all created loaders to this instance and create dispose method to dispose all. | |||||
| const loader = this.cls && new this.cls(assetImporter.loadingManager) | |||||
| return typeof this.onCtor === 'function' ? this.onCtor(loader, assetImporter, this) : loader | |||||
| } | |||||
| /** | |||||
| * Supported ext, must be in lower case. | |||||
| */ | |||||
| ext: string[] // ['json', 'png', 'jpg', 'data:image/png'...] | |||||
| /** | |||||
| * Supported mime types, must be in lower case. | |||||
| */ | |||||
| mime: string[] | |||||
| root: boolean | |||||
| extensions: any[] = [] | |||||
| constructor(cls: Class<T>, ext: string[], mime: string[], root: boolean, onCtor?: (l: T|undefined, ai: IAssetImporter, i: Importer) => T|undefined) { | |||||
| this.cls = cls | |||||
| this.ext = ext.filter(Boolean).map(e => e.toLowerCase()) | |||||
| this.mime = mime.filter(Boolean).map(e => e.toLowerCase()) | |||||
| this.root = root | |||||
| this.onCtor = onCtor | |||||
| } | |||||
| } |
| import {BaseEvent, ColorManagement, EventDispatcher, Material} from 'three' | |||||
| import { | |||||
| IMaterial, | |||||
| iMaterialCommons, | |||||
| IMaterialEvent, | |||||
| IMaterialParameters, | |||||
| IMaterialTemplate, | |||||
| ITexture, | |||||
| PhysicalMaterial, | |||||
| UnlitMaterial, | |||||
| } from '../core' | |||||
| import {downloadFile} from 'ts-browser-helpers' | |||||
| import {MaterialExtension} from '../materials' | |||||
| import {generateUUID} from '../three/utils/misc' | |||||
| export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> { | |||||
| readonly templates: IMaterialTemplate[] = [ | |||||
| PhysicalMaterial.MaterialTemplate, | |||||
| UnlitMaterial.MaterialTemplate, | |||||
| ] | |||||
| private _materials: IMaterial[] = [] | |||||
| constructor() { | |||||
| super() | |||||
| } | |||||
| /** | |||||
| * @param info: uuid or template name or material type | |||||
| */ | |||||
| public findOrCreate(info: string, params?: IMaterialParameters): IMaterial | undefined { | |||||
| let mat = this.findMaterial(info) | |||||
| if (!mat) mat = this.create(info, params) | |||||
| return mat | |||||
| } | |||||
| /** | |||||
| * Create a material from the template name or material type | |||||
| * @param nameOrType | |||||
| * @param register | |||||
| * @param params | |||||
| */ | |||||
| public create<TM extends IMaterial>(nameOrType: string, {register = true, ...params}: IMaterialParameters&{register?: boolean} = {}): TM | undefined { | |||||
| let template: IMaterialTemplate<any> = {materialType: nameOrType, name: nameOrType} | |||||
| while (!template.generator) { // looping so that we can inherit templates, not fully implemented yet | |||||
| const t2 = this.findTemplate(template.materialType) // todo add a baseTemplate property to the template? | |||||
| if (!t2) { | |||||
| console.error('Template has no generator or materialType', template, nameOrType) | |||||
| return undefined | |||||
| } | |||||
| template = {...template, ...t2} | |||||
| } | |||||
| const material = this._create<TM>(template, params) | |||||
| if (material && register !== false) this.registerMaterial(material) | |||||
| return material | |||||
| } | |||||
| // make global function? | |||||
| protected _create<TM extends IMaterial>(template: IMaterialTemplate<TM>, oldMaterial?: IMaterialParameters|Partial<TM>): TM|undefined { | |||||
| if (!template.generator) { | |||||
| console.error('Template has no generator', template) | |||||
| return undefined | |||||
| } | |||||
| const legacyColors = (oldMaterial as any)?.metadata && (oldMaterial as any)?.metadata.version <= 4.5 | |||||
| const lastColorManagementEnabled = ColorManagement.enabled | |||||
| if (legacyColors) ColorManagement.enabled = false | |||||
| const material = template.generator(template.params || {}) | |||||
| if (oldMaterial && material) material.setValues(oldMaterial, true) | |||||
| if (legacyColors) ColorManagement.enabled = lastColorManagementEnabled | |||||
| return material | |||||
| } | |||||
| public findTemplate(nameOrType: string, withGenerator = false): IMaterialTemplate|undefined { | |||||
| if (!nameOrType) return undefined | |||||
| return this.templates.find(v => (v.name === nameOrType || v.materialType === nameOrType) && (!withGenerator || v.generator)) | |||||
| || this.templates.find(v => v.alias?.includes(nameOrType) && (!withGenerator || v.generator)) | |||||
| } | |||||
| protected _getMapsForMaterial(material: IMaterial) { | |||||
| const maps = new Set<ITexture>() | |||||
| // todo use MaterialProperties or similar to find the maps in the material. This is a bit hacky | |||||
| for (const val of Object.values(material)) { | |||||
| if (val && val.isTexture) { | |||||
| maps.add(val) | |||||
| } | |||||
| } | |||||
| for (const val of Object.values(material.userData ?? {})) { | |||||
| if (val && (val as any).isTexture) { | |||||
| maps.add(val as ITexture) | |||||
| } | |||||
| } | |||||
| return maps | |||||
| } | |||||
| protected _disposeMaterial = (e: {target?: IMaterial})=>{ | |||||
| const mat = e.target | |||||
| if (!mat || mat.assetType !== 'material') return | |||||
| mat.setDirty() | |||||
| const maps = this._getMapsForMaterial(mat) | |||||
| maps.forEach(map=>{ | |||||
| const mats = map.userData.__appliedMaterials! | |||||
| mats?.delete(mat) | |||||
| if (!mats || map.userData.disposeOnIdle === false) return | |||||
| if (mats.size === 0) map.dispose() | |||||
| }) | |||||
| this.unregisterMaterial(mat) | |||||
| } | |||||
| private _materialMaps = new Map<string, Set<ITexture>>() | |||||
| protected _materialUpdate = (e: IMaterialEvent<'materialUpdate'>)=>{ | |||||
| const mat = e.material || e.target | |||||
| if (!mat || mat.assetType !== 'material') return | |||||
| this._refreshTextureRefs(mat) | |||||
| } | |||||
| private _refreshTextureRefs(mat: any) { | |||||
| const newMaps = this._getMapsForMaterial(mat) | |||||
| const oldMaps = this._materialMaps.get(mat.uuid) || new Set<ITexture>() | |||||
| newMaps.forEach(map => { | |||||
| if (!oldMaps.has(map)) { | |||||
| if (!map.userData.__appliedMaterials) map.userData.__appliedMaterials = new Set<IMaterial>() | |||||
| map.userData.__appliedMaterials.add(mat) | |||||
| } | |||||
| }) | |||||
| oldMaps.forEach(map => { | |||||
| if (!newMaps.has(map)) { | |||||
| if (!map.userData.__appliedMaterials) return | |||||
| const mats = map.userData.__appliedMaterials | |||||
| mats?.delete(mat) | |||||
| if (!mats || map.userData.disposeOnIdle === false) return | |||||
| if (mats.size === 0) map.dispose() | |||||
| } | |||||
| }) | |||||
| this._materialMaps.set(mat.uuid, newMaps) | |||||
| } | |||||
| public registerMaterial(material: IMaterial): void { | |||||
| if (!material) return | |||||
| if (this._materials.includes(material)) return | |||||
| const mat = this.findMaterial(material.uuid) | |||||
| if (mat) { | |||||
| console.warn('Material UUID already exists', material, mat) | |||||
| return | |||||
| } | |||||
| // console.warn('Registering material', material) | |||||
| material.addEventListener('dispose', this._disposeMaterial) | |||||
| material.addEventListener('materialUpdate', this._materialUpdate) // from set dirty | |||||
| material.registerMaterialExtensions?.(this._materialExtensions) | |||||
| this._materials.push(material) | |||||
| this._refreshTextureRefs(material) | |||||
| } | |||||
| registerMaterials(materials: IMaterial[]): void { | |||||
| materials.forEach(material => this.registerMaterial(material)) | |||||
| } | |||||
| /** | |||||
| * This is done automatically on material dispose. | |||||
| * @param material | |||||
| */ | |||||
| public unregisterMaterial(material: IMaterial): void { | |||||
| this._materials = this._materials.filter(v=>v.uuid !== material.uuid) | |||||
| material.unregisterMaterialExtensions?.(this._materialExtensions) | |||||
| material.removeEventListener('dispose', this._disposeMaterial) | |||||
| material.removeEventListener('materialUpdate', this._materialUpdate) | |||||
| } | |||||
| clearMaterials(): void { | |||||
| [...this._materials].forEach(material => this.unregisterMaterial(material)) | |||||
| } | |||||
| public registerMaterialTemplate(template: IMaterialTemplate): void { | |||||
| if (!template.templateUUID) template.templateUUID = generateUUID() | |||||
| const mat = this.templates.find(v=>v.templateUUID === template.templateUUID) | |||||
| if (mat) { | |||||
| console.error('MaterialTemplate already exists', template, mat) | |||||
| return | |||||
| } | |||||
| this.templates.push(template) | |||||
| } | |||||
| public unregisterMaterialTemplate(template: IMaterialTemplate): void { | |||||
| const i = this.templates.findIndex(v=>v.templateUUID === template.templateUUID) | |||||
| if (i >= 0) this.templates.splice(i, 1) | |||||
| } | |||||
| dispose() { | |||||
| for (const material of this._materials) { | |||||
| material.dispose() | |||||
| } | |||||
| this._materials = [] | |||||
| return | |||||
| } | |||||
| public findMaterial(uuid: string): IMaterial | undefined { | |||||
| return !uuid ? undefined : this._materials.find(v=>v.uuid === uuid) | |||||
| } | |||||
| public findMaterialsByName(name: string): IMaterial[] { | |||||
| return this._materials.filter(v=>v.name === name) | |||||
| } | |||||
| public getMaterialsOfType<TM extends IMaterial = IMaterial>(typeSlug: string | undefined): TM[] { | |||||
| return typeSlug ? this._materials.filter(v=>v.constructor.TypeSlug === typeSlug) as TM[] : [] | |||||
| } | |||||
| public getAllMaterials(): IMaterial[] { | |||||
| return [...this._materials] | |||||
| } | |||||
| // processModel(object: IModel, options: AnyOptions): IModel { | |||||
| // const k = this._processModel(object, options) | |||||
| // safeSetProperty(object, 'modelObject', k) | |||||
| // return object | |||||
| // } | |||||
| // protected abstract _processModel(object: any, options: AnyOptions): any | |||||
| convertToIMaterial(material: Material&{assetType?:'material', iMaterial?: IMaterial}, options: {useSourceMaterial?:boolean, materialTemplate?: string} = {}): IMaterial|undefined { | |||||
| if (!material) return | |||||
| if (material.assetType) return <IMaterial>material | |||||
| if (material.iMaterial?.assetType) return material.iMaterial | |||||
| const uuid = material.userData?.uuid || material.uuid | |||||
| let mat = this.findMaterial(uuid) | |||||
| if (!mat) { | |||||
| const ignoreSource = options.useSourceMaterial === false || !material.isMaterial | |||||
| const template = options.materialTemplate || (!ignoreSource && material.type ? material.type || 'physical' : 'physical') | |||||
| mat = this.create(template, ignoreSource ? undefined : material) | |||||
| } else { | |||||
| console.warn('Material with the same uuid already exists, copying properties') | |||||
| if (material.type !== mat.type) console.error('Material type mismatch, delete previous material first?', material.type, mat.type) | |||||
| mat.setValues(material) | |||||
| } | |||||
| if (mat) { | |||||
| mat.uuid = uuid | |||||
| mat.userData.uuid = uuid | |||||
| material.iMaterial = mat | |||||
| } else { | |||||
| console.warn('Failed to convert material to IMaterial, just upgrading', material, options) | |||||
| mat = iMaterialCommons.upgradeMaterial.call(material) | |||||
| } | |||||
| return mat | |||||
| } | |||||
| // processMaterial(material: IMaterial, options: AnyOptions&{useSourceMaterial?:boolean, materialTemplate?: string, register?: boolean}): IMaterial { | |||||
| // if (!material.materialObject) | |||||
| // material = (this._processMaterial(material, {...options, register: false}))! | |||||
| // if (options.register !== false) this.registerMaterial(material) | |||||
| // | |||||
| // return material | |||||
| // } | |||||
| protected _materialExtensions: MaterialExtension[] = [] | |||||
| registerMaterialExtension(extension: MaterialExtension): void { | |||||
| if (this._materialExtensions.includes(extension)) return | |||||
| this._materialExtensions.push(extension) | |||||
| for (const mat of this._materials) mat.registerMaterialExtensions?.([extension]) | |||||
| } | |||||
| unregisterMaterialExtension(extension: MaterialExtension): void { | |||||
| const i = this._materialExtensions.indexOf(extension) | |||||
| if (i < 0) return | |||||
| this._materialExtensions.splice(i, 1) | |||||
| for (const mat of this._materials) mat.unregisterMaterialExtensions?.([extension]) | |||||
| } | |||||
| clearExtensions() { | |||||
| [...this._materialExtensions].forEach(v=>this.unregisterMaterialExtension(v)) | |||||
| } | |||||
| exportMaterial(material: IMaterial, filename?: string, minify = true, download = false): File { | |||||
| const serialized = material.toJSON() | |||||
| const json = JSON.stringify(serialized, null, minify ? 0 : 2) | |||||
| const name = (filename || material.name || 'physical_material') + '.' + material.constructor.TypeSlug | |||||
| const blob = new File([json], name, {type: 'application/json'}) | |||||
| if (download) downloadFile(blob) | |||||
| return blob | |||||
| } | |||||
| applyMaterial(material: IMaterial, nameOrUuid: string): boolean { | |||||
| const mType = Object.getPrototypeOf(material).constructor.TYPE | |||||
| let currentMats = this.findMaterialsByName(nameOrUuid) | |||||
| if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameOrUuid) as any] | |||||
| let applied = false | |||||
| for (const c of currentMats) { | |||||
| // console.log(c) | |||||
| if (!c) continue | |||||
| if (c.userData.__isVariation) continue | |||||
| const cType = Object.getPrototypeOf(c).constructor.TYPE | |||||
| // console.log(cType, mType) | |||||
| if (cType === mType) { | |||||
| const n = c.name | |||||
| c.setValues(material) | |||||
| c.name = n | |||||
| applied = true | |||||
| } else { | |||||
| // todo | |||||
| // if ((c as any)['__' + mType]) continue | |||||
| const newMat = (c as any)['__' + mType] || this.create(mType) | |||||
| if (!newMat) continue | |||||
| const n = c.name | |||||
| newMat.setValues(material) | |||||
| newMat.name = n | |||||
| const meshes = c.appliedMeshes | |||||
| for (const mesh of [...meshes ?? []]) { | |||||
| if (!mesh) continue | |||||
| mesh.material = newMat | |||||
| applied = true | |||||
| } | |||||
| (c as any)['__' + mType] = newMat | |||||
| } | |||||
| } | |||||
| return applied | |||||
| } | |||||
| } | |||||
| import {GLTFExporter, GLTFExporterOptions, GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js' | |||||
| import {IExportParser} from '../IExporter' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {GLTFWriter2} from './GLTFWriter2' | |||||
| import {Object3D} from 'three' | |||||
| import {ThreeViewer} from '../../viewer' | |||||
| import {GLTFObject3DExtrasExtension} from '../gltf/GLTFObject3DExtrasExtension' | |||||
| import {GLTFLightExtrasExtension} from '../gltf/GLTFLightExtrasExtension' | |||||
| import {GLTFMaterialsBumpMapExtension} from '../gltf/GLTFMaterialsBumpMapExtension' | |||||
| import {GLTFMaterialsDisplacementMapExtension} from '../gltf/GLTFMaterialsDisplacementMapExtension' | |||||
| import {GLTFMaterialsLightMapExtension} from '../gltf/GLTFMaterialsLightMapExtension' | |||||
| import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension' | |||||
| import {GLTFMaterialExtrasExtension} from '../gltf/GLTFMaterialExtrasExtension' | |||||
| import {GLTFViewerConfigExtension} from '../gltf/GLTFViewerConfigExtension' | |||||
| export type GLTFExporter2Options = GLTFExporterOptions & { | |||||
| /** | |||||
| * embed images in glb even when remote url is available, default = false | |||||
| */ | |||||
| embedUrlImages?: boolean, | |||||
| /** | |||||
| * export viewer config (scene settings) | |||||
| */ | |||||
| viewerConfig?: boolean, | |||||
| /** | |||||
| * Extension to export to, default for object/scene = glb | |||||
| */ | |||||
| exportExt?: string, | |||||
| preserveUUIDs?: boolean, | |||||
| externalImagesInExtras?: boolean, // see GLTFDracoExporter and extras extension | |||||
| encodeUint16Rgbe?: boolean // see GLTFViewerExport->processViewer, default = true | |||||
| } | |||||
| export class GLTFExporter2 extends GLTFExporter implements IExportParser { | |||||
| register(callback: (writer: GLTFWriter2)=>GLTFExporterPlugin): this { | |||||
| return super.register(callback as any) | |||||
| } | |||||
| async parseAsync(obj: any, options: AnyOptions): Promise<Blob> { | |||||
| if (!obj) throw new Error('No object to export') | |||||
| const gltf = !obj.__isGLTFOutput && (Array.isArray(obj) || obj.isObject3D) ? await new Promise((resolve, reject) => this.parse(obj, resolve, reject, options)) : obj | |||||
| if (gltf && typeof gltf === 'object' && !gltf.byteLength) { // byteLength is for ArrayBuffer | |||||
| return new Blob([JSON.stringify(gltf, (k, v)=> k.startsWith('__') ? undefined : v, options.jsonSpaces ?? 2)], {type: 'model/gltf+json'}) | |||||
| } else if (gltf) { | |||||
| return new Blob([gltf as ArrayBuffer], {type: 'model/gltf+binary'}) | |||||
| } else { | |||||
| throw new Error('GLTFExporter2.parse() failed') | |||||
| } | |||||
| } | |||||
| parse( | |||||
| input: Object3D | Object3D[], | |||||
| onDone: (gltf: ArrayBuffer | {[key: string]: any}) => void, | |||||
| onError: (error: ErrorEvent) => void, | |||||
| options: GLTFExporter2Options = {}, | |||||
| ): any { | |||||
| const gltfOptions: GLTFWriter2['options'] = { | |||||
| // default options | |||||
| binary: false, | |||||
| trs: false, | |||||
| onlyVisible: true, | |||||
| truncateDrawRange: true, | |||||
| externalImagesInExtras: !options.embedUrlImages && options.externalImagesInExtras || false, // this is handled in gltfMaterialExtrasWriter, also see GLTFDracoExporter | |||||
| maxTextureSize: Infinity, | |||||
| animations: [], | |||||
| includeCustomExtensions: true, | |||||
| exporterOptions: options, | |||||
| } | |||||
| if (options.exportExt === 'glb') { | |||||
| gltfOptions.binary = true | |||||
| } | |||||
| if (options.preserveUUIDs !== false) { // default true | |||||
| (Array.isArray(input) ? input : [input]).forEach((obj: Object3D) => | |||||
| obj.traverse((obj1: Object3D) => { | |||||
| if (obj1.uuid) obj1.userData.gltfUUID = obj1.uuid | |||||
| })) | |||||
| } | |||||
| // animations | |||||
| (Array.isArray(input) ? input : [input]).forEach((obj: Object3D) => | |||||
| obj.traverse((obj1: Object3D) => { | |||||
| if (obj1.animations) { | |||||
| for (const animation of obj1.animations) { | |||||
| if ((animation as any).__gltfExport !== false && !gltfOptions.animations!.includes(animation)) { | |||||
| gltfOptions.animations!.push(...obj1.animations) | |||||
| } | |||||
| } | |||||
| } | |||||
| })) | |||||
| return super.parse(input, (o: any)=> { | |||||
| if (options.preserveUUIDs !== false) { // default true | |||||
| (Array.isArray(input) ? input : [input]).forEach((obj: Object3D) => | |||||
| obj.traverse((obj1: Object3D) => { | |||||
| delete obj1.userData.gltfUUID | |||||
| })) | |||||
| } | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| onDone(Object.assign(o, {__isGLTFOutput: true})) | |||||
| // @ts-expect-error wrong ts | |||||
| }, onError, gltfOptions, new GLTFWriter2()) | |||||
| } | |||||
| static ExportExtensions: ((parser: GLTFWriter2) => GLTFExporterPlugin)[] = [ | |||||
| GLTFMaterialExtrasExtension.Export, | |||||
| GLTFObject3DExtrasExtension.Export, | |||||
| GLTFLightExtrasExtension.Export, | |||||
| GLTFMaterialsBumpMapExtension.Export, | |||||
| GLTFMaterialsDisplacementMapExtension.Export, | |||||
| GLTFMaterialsLightMapExtension.Export, | |||||
| GLTFMaterialsAlphaMapExtension.Export, | |||||
| ] | |||||
| setup(viewer: ThreeViewer, extraExtensions?: ((parser: GLTFWriter2) => GLTFExporterPlugin)[]): this { | |||||
| for (const ext of GLTFExporter2.ExportExtensions) this.register(ext) | |||||
| if (extraExtensions) for (const ext of extraExtensions) this.register(ext) | |||||
| // should be last | |||||
| this.register(this.gltfViewerWriter(viewer)) | |||||
| return this | |||||
| } | |||||
| gltfViewerWriter(viewer: ThreeViewer): (parser: GLTFWriter2) => GLTFExporterPlugin { | |||||
| return (writer: GLTFWriter2) => ({ | |||||
| afterParse: (input: any)=>{ | |||||
| input = Array.isArray(input) ? input[0] : input | |||||
| if (!input?.userData?.rootSceneModelRoot || | |||||
| writer.options?.exporterOptions?.viewerConfig === false || | |||||
| input?.userData?.__exportViewerConfig === false | |||||
| ) return | |||||
| GLTFViewerConfigExtension.ExportViewerConfig(viewer, writer) | |||||
| }, | |||||
| }) | |||||
| } | |||||
| } |
| import {GLTFExporter, GLTFExporterOptions} from 'three/examples/jsm/exporters/GLTFExporter.js' | |||||
| import {BufferGeometry, Material, MeshStandardMaterial, Object3D, PixelFormat, Texture} from 'three' | |||||
| import {blobToDataURL} from 'ts-browser-helpers' | |||||
| import type {GLTFExporter2Options} from './GLTFExporter2' | |||||
| export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter { | |||||
| options: GLTFExporterOptions & { | |||||
| externalImagesInExtras: boolean, | |||||
| exporterOptions: GLTFExporter2Options | |||||
| } | |||||
| serializeUserData(object: Object3D | Material | BufferGeometry, objectDef: any): void { | |||||
| const userData = object.userData | |||||
| const temp: any = {} | |||||
| if (userData.__disposed) { | |||||
| console.error('Serializing a disposed object', object) | |||||
| } | |||||
| Object.entries(userData).forEach(([key, value]: any) => { | |||||
| if (!value || | |||||
| typeof value === 'function' || | |||||
| value.isObject3D || | |||||
| value.isTexture || | |||||
| value.isMaterial || | |||||
| value.assetType != null || | |||||
| key.startsWith('_') || // private data | |||||
| key !== 'uuid' // always save uuid, even if its ignored | |||||
| ) { | |||||
| temp[key] = value | |||||
| delete userData[key] | |||||
| } | |||||
| }) | |||||
| super.serializeUserData(object, objectDef) | |||||
| Object.entries(temp).forEach(([key, value]) => { | |||||
| userData[key] = value | |||||
| delete temp[key] | |||||
| }) | |||||
| } | |||||
| processObjects(objects: Object3D[]) { | |||||
| if (objects.length === 1 && objects[0]?.userData.rootSceneModelRoot) { | |||||
| // objects[0].isScene = true | |||||
| this.processScene(objects[0]) | |||||
| // delete objects[0].isScene | |||||
| } else | |||||
| super.processObjects(objects) | |||||
| } | |||||
| protected _defaultMaterial = new MeshStandardMaterial() | |||||
| /** | |||||
| * Checks for shader material and does the same thing... | |||||
| * @param material | |||||
| */ | |||||
| processMaterial(material: Material): number|null { | |||||
| if (this.cache.materials.has(material)) return this.cache.materials.get(material)! | |||||
| let mat = material as any | |||||
| // set default material when material is null. shader material is processed further below for custom extensions like diamonds. | |||||
| if (!mat || mat.isShaderMaterial) mat = this._defaultMaterial | |||||
| const defIndex = super.processMaterial(mat) | |||||
| if (defIndex === null) { | |||||
| console.error('GLTFWriter2: Unexpected error: Failed to process material', material) | |||||
| return null | |||||
| } | |||||
| // when not a shader material | |||||
| if (!material || mat === material) return defIndex // todo: this line needds to be tested. | |||||
| // when shader material | |||||
| const defaultDef = JSON.stringify(this.json.materials[defIndex]) | |||||
| const materialDef = JSON.parse(defaultDef) // for deep clone | |||||
| // console.log(defIndex, defaultDef, materialDef) | |||||
| this.serializeUserData(material, materialDef) | |||||
| this._invokeAll((ext)=>{ | |||||
| ext.writeMaterial && ext.writeMaterial(material, materialDef) | |||||
| }) | |||||
| // todo: test remove this | |||||
| // if (JSON.stringify(materialDef) === defaultDef) { | |||||
| // return defIndex | |||||
| // } | |||||
| const index = this.json.materials.push(materialDef) - 1 | |||||
| this.cache.materials.set(material, index) | |||||
| return index | |||||
| } | |||||
| /** | |||||
| * Same as processImage but for image blobs | |||||
| * @param blob | |||||
| * @param texture | |||||
| */ | |||||
| processImageBlob(blob: Blob, texture: Texture) { | |||||
| if (!blob) return -1 | |||||
| const cache = this.cache | |||||
| const options = this.options | |||||
| const pending = this.pending | |||||
| const json = this.json | |||||
| const image = texture.image | |||||
| if (!cache.images.has(image)) cache.images.set(image, {}) | |||||
| const cachedImages = cache.images.get(image) | |||||
| const key = blob.type + ':flipY/' + texture.flipY.toString() | |||||
| if (cachedImages[ key ] !== undefined) return cachedImages[ key ] | |||||
| if (!json.images) json.images = [] | |||||
| const imageDef: any = {mimeType: blob.type} | |||||
| if (options.binary === true) { | |||||
| pending.push(new Promise<void>((resolve)=>{ | |||||
| this.processBufferViewImage(blob).then((bufferViewIndex: number)=>{ | |||||
| imageDef.bufferView = bufferViewIndex | |||||
| resolve() | |||||
| }) | |||||
| })) | |||||
| } else { | |||||
| pending.push(blobToDataURL(blob).then((dataURL: string)=>{ | |||||
| imageDef.uri = dataURL | |||||
| })) | |||||
| } | |||||
| const index = json.images.push(imageDef) - 1 | |||||
| cachedImages[ key ] = index | |||||
| return index | |||||
| } | |||||
| processSampler(map: Texture) { | |||||
| const samplerIndex = super.processSampler(map) | |||||
| // todo: uncomment when sampler extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645 | |||||
| // const samplerDef = this.json.samplers[samplerIndex] | |||||
| // if (!samplerDef.extras) samplerDef.extras = {} | |||||
| // samplerDef.extras.uuid = map.uuid | |||||
| return samplerIndex | |||||
| } | |||||
| processTexture(map: Texture) { | |||||
| const cache = this.cache | |||||
| const json = this.json | |||||
| if (cache.textures.has(map)) return cache.textures.get(map)! | |||||
| const srcData = map.source.data | |||||
| const mimeType = map.userData.mimeType | |||||
| if (map.userData.rootPath && | |||||
| !this.options.exporterOptions.embedUrlImages | |||||
| && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | |||||
| ) { | |||||
| map.source.data = null // handled below in GLTFWriter2.processImage | |||||
| delete map.userData.mimeType // for extensions like ktx2 | |||||
| } | |||||
| const processed = super.processTexture(map) | |||||
| const textureDef = json.textures[processed] | |||||
| if (!textureDef) { | |||||
| console.error('No texture def', processed, map) | |||||
| return processed | |||||
| } | |||||
| // if (!textureDef.extras) textureDef.extras = {} | |||||
| const imageDef = json.images ? json.images[textureDef.source] : null | |||||
| if (imageDef) { | |||||
| if (!imageDef.extras) imageDef.extras = {} | |||||
| if (map.source) imageDef.extras.uuid = map.source.uuid | |||||
| imageDef.extras.t_uuid = map.uuid // todo: remove when extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645 | |||||
| } | |||||
| // map uuid saved in processSampler. | |||||
| if (map.userData.rootPath && !this.options.exporterOptions.embedUrlImages | |||||
| && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | |||||
| ) { | |||||
| map.source.data = srcData | |||||
| map.userData.mimeType = mimeType | |||||
| if (!textureDef) { | |||||
| console.error('textureDef is null', processed, map) | |||||
| return processed | |||||
| } | |||||
| if (textureDef.source >= 0) { | |||||
| console.warn('textureDef.source is already set', processed, map) | |||||
| } else { | |||||
| textureDef.source = this.processImageUri(map.image, map.userData.rootPath, map.flipY, mimeType) | |||||
| } | |||||
| } | |||||
| if (textureDef.source < 0) { | |||||
| console.error('textureDef.source cannot be saved', textureDef, map) | |||||
| } | |||||
| return processed | |||||
| } | |||||
| // Add extra check for null images. This is set in processTexture when we have a rootPath | |||||
| processImage(image: any, format: PixelFormat, flipY: boolean, mimeType = 'image/png') { | |||||
| if (!image) return -1 | |||||
| return super.processImage(image, format, flipY, mimeType) | |||||
| } | |||||
| /** | |||||
| * Used in GLTFWriter2.processTexture for rootPath. Note that this does not check for options.exporterOptions.embedUrlImages, it must be done separately. | |||||
| * @param image | |||||
| * @param uri | |||||
| * @param flipY | |||||
| * @param mimeType | |||||
| */ | |||||
| processImageUri(image: any, uri: string, flipY: boolean, mimeType = 'image/png') { | |||||
| const cache = this.cache | |||||
| const json = this.json | |||||
| if (!cache.images.has(image)) cache.images.set(image, {}) | |||||
| const cachedImages = cache.images.get(image) | |||||
| const key = mimeType + ':flipY/' + flipY.toString() | |||||
| if (cachedImages[ key ] !== undefined) return cachedImages[ key ] | |||||
| if (!json.images) json.images = [] | |||||
| const imageDef: any = { | |||||
| mimeType, uri, | |||||
| extras: {flipY}, | |||||
| } | |||||
| const index = json.images.push(imageDef) - 1 | |||||
| cachedImages[ key ] = index | |||||
| return index | |||||
| } | |||||
| } |
| import {IExportParser} from '../IExporter' | |||||
| export class SimpleJSONExporter implements IExportParser { | |||||
| async parseAsync(obj: any, {jsonSpaces = 2}): Promise<Blob> { | |||||
| return new Blob([JSON.stringify(obj, null, jsonSpaces)], {type: 'application/json'}) | |||||
| } | |||||
| } | |||||
| import {IExportParser} from '../IExporter' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| export class SimpleTextExporter implements IExportParser { | |||||
| async parseAsync(obj: any, _: AnyOptions): Promise<Blob> { | |||||
| return new Blob([obj], {type: 'text/plain'}) | |||||
| } | |||||
| } |
| export {GLTFExporter2, type GLTFExporter2Options} from './GLTFExporter2' | |||||
| export {GLTFWriter2} from './GLTFWriter2' | |||||
| export {SimpleJSONExporter} from './SimpleJSONExporter' | |||||
| export {SimpleTextExporter} from './SimpleTextExporter' |
| import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import {ObjectLoader} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| export class GLTFLightExtrasExtension { | |||||
| static readonly WebGiLightExtrasExtension = 'WEBGI_light_extras' | |||||
| /** | |||||
| * Also {@link Export} | |||||
| * @param _ | |||||
| */ | |||||
| static Import = (_: GLTFParser): GLTFLoaderPlugin =>({ | |||||
| name: '__' + this.WebGiLightExtrasExtension, // __ is prefix so that the extension is added to userdata, and we can process later in afterRoot | |||||
| afterRoot: async(result: GLTF) => { | |||||
| const scenes = result.scenes || (result.scene ? [result.scene] : []) | |||||
| scenes.forEach(s=>{ | |||||
| s.traverse((o: any)=>{ | |||||
| if (!o.isLight) return | |||||
| const ext = o.userData?.gltfExtensions?.[this.WebGiLightExtrasExtension] | |||||
| if (!ext) { | |||||
| return | |||||
| } | |||||
| // castShadow is in GLTFObject3DExtrasExtension | |||||
| if (!o.shadow && ext.shadow) { | |||||
| console.error('Light has no shadow, cannot import', o, ext) | |||||
| } | |||||
| // keep updated with ObjectLoader.js | |||||
| if (ext.shadow && o.shadow) { | |||||
| if (ext.shadow.bias !== undefined) o.shadow.bias = ext.shadow.bias | |||||
| if (ext.shadow.normalBias !== undefined) o.shadow.normalBias = ext.shadow.normalBias | |||||
| if (ext.shadow.radius !== undefined) o.shadow.radius = ext.shadow.radius | |||||
| if (ext.shadow.mapSize !== undefined) o.shadow.mapSize.fromArray(ext.shadow.mapSize) | |||||
| if (ext.shadow.camera !== undefined) { | |||||
| o.shadow.camera = new ObjectLoader().parseObject(ext.shadow.camera, {}, {}, {}, {}) | |||||
| } | |||||
| } | |||||
| delete o.userData.gltfExtensions[this.WebGiLightExtrasExtension] | |||||
| }) | |||||
| }) | |||||
| }, | |||||
| }) | |||||
| /** | |||||
| * Also {@link Import} | |||||
| */ | |||||
| static Export = (w: GLTFWriter): GLTFExporterPlugin=> ({ | |||||
| writeNode: (object: any, nodeDef: any)=>{ | |||||
| if (!object?.isLight) return | |||||
| if (!nodeDef.extensions) nodeDef.extensions = {} | |||||
| const dat: any = {} | |||||
| if (object.shadow) { // castShadow is in GLTFObject3DExtrasExtension | |||||
| dat.shadow = object.shadow.toJSON() | |||||
| } | |||||
| if (Object.keys(dat).length > 0) { | |||||
| nodeDef.extensions[this.WebGiLightExtrasExtension] = dat | |||||
| w.extensionsUsed[this.WebGiLightExtrasExtension] = true | |||||
| } | |||||
| }, | |||||
| }) | |||||
| } |
| import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import {ThreeSerialization} from '../../utils/serialization' | |||||
| import {DoubleSide, Material} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| export class GLTFMaterialExtrasExtension { | |||||
| static readonly WebGiMaterialExtrasExtension = 'WEBGI_material_extras' | |||||
| /** | |||||
| * for physical material | |||||
| * Also {@link Export} | |||||
| * @param loadConfigResources | |||||
| */ | |||||
| static Import = (loadConfigResources: (res: any)=>any)=> (_: GLTFParser): GLTFLoaderPlugin=>({ | |||||
| name: '__' + GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension, // __ is prefix so that the extension is added to userdata, and we can process later in afterRoot | |||||
| afterRoot: async(result: GLTF) => { | |||||
| const scenes = result.scenes || (result.scene ? [result.scene] : []) | |||||
| for (const s of scenes) { | |||||
| const resExt = s.userData?.gltfExtensions?.[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] // Note: see exporter for details of material extra resources in scene. | |||||
| const resources = resExt?.resources ? await loadConfigResources(resExt.resources) : {} | |||||
| s.traverse((obj: any)=>{ | |||||
| const o = obj?.material | |||||
| if (!o?.isMaterial) return | |||||
| const ext = o.userData?.gltfExtensions?.[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] | |||||
| if (!ext) return | |||||
| // extras from MaterialLoader.js | |||||
| if (ext.emissiveIntensity !== undefined) o.emissiveIntensity = ext.emissiveIntensity // kept for old versions, this is not saved in extras because of KHR_materials_emissive_strength | |||||
| // bumpMap, displacementMap, lightMap, alphaMap moved to separate extensions | |||||
| // if (material.shininess !== undefined) dat.shininess = material.shininess | |||||
| if (ext.fog !== undefined) o.fog = ext.fog | |||||
| if (ext.flatShading !== undefined) o.flatShading = ext.flatShading | |||||
| if (ext.blending !== undefined) o.blending = ext.blending | |||||
| // if (ext.combine !== undefined) o.combine = ext.combine | |||||
| if (ext.side !== undefined) o.side = ext.side | |||||
| if (ext.shadowSide !== undefined) o.shadowSide = ext.shadowSide | |||||
| if (ext.depthFunc !== undefined) o.depthFunc = ext.depthFunc | |||||
| if (ext.depthTest !== undefined) o.depthTest = ext.depthTest | |||||
| if (ext.depthWrite !== undefined) o.depthWrite = ext.depthWrite | |||||
| if (ext.colorWrite !== undefined) o.colorWrite = ext.colorWrite | |||||
| if (ext.vertexColors !== undefined) o.vertexColors = ext.vertexColors // this is override, it is also set in GLTFLoader if geometry has vertex colors, todo: check how to do this in a better way | |||||
| if (ext.alphaTest !== undefined) o.alphaTest = ext.alphaTest | |||||
| // if (ext.transparent !== undefined) o.transparent = ext.transparent // this is set by GLTFLoader based on alpha mode | |||||
| // if (ext.envMapIntensity !== undefined) o.envMapIntensity = ext.envMapIntensity // this is set in global by rootscene | |||||
| // if (ext.stencilWrite !== undefined) o.stencilWrite = ext.stencilWrite | |||||
| // if (ext.stencilWriteMask !== undefined) o.stencilWriteMask = ext.stencilWriteMask | |||||
| // if (ext.stencilFunc !== undefined) o.stencilFunc = ext.stencilFunc | |||||
| // if (ext.stencilRef !== undefined) o.stencilRef = ext.stencilRef | |||||
| // if (ext.stencilFuncMask !== undefined) o.stencilFuncMask = ext.stencilFuncMask | |||||
| // if (ext.stencilFail !== undefined) o.stencilFail = ext.stencilFail | |||||
| // if (ext.stencilZFail !== undefined) o.stencilZFail = ext.stencilZFail | |||||
| // if (ext.stencilZPass !== undefined) o.stencilZPass = ext.stencilZPass | |||||
| if (ext.wireframe !== undefined) o.wireframe = ext.wireframe | |||||
| if (ext.wireframeLinewidth !== undefined) o.wireframeLinewidth = ext.wireframeLinewidth | |||||
| if (ext.wireframeLinecap !== undefined) o.wireframeLinecap = ext.wireframeLinecap | |||||
| if (ext.wireframeLinejoin !== undefined) o.wireframeLinejoin = ext.wireframeLinejoin | |||||
| if (ext.rotation !== undefined) o.rotation = ext.rotation | |||||
| // if (ext.linewidth !== 1) o.linewidth = ext.linewidth | |||||
| // if (ext.dashSize !== undefined) o.dashSize = ext.dashSize | |||||
| // if (ext.gapSize !== undefined) o.gapSize = ext.gapSize | |||||
| // if (ext.scale !== undefined) o.scale = ext.scale | |||||
| if (ext.polygonOffset !== undefined) o.polygonOffset = ext.polygonOffset | |||||
| if (ext.polygonOffsetFactor !== undefined) o.polygonOffsetFactor = ext.polygonOffsetFactor | |||||
| if (ext.polygonOffsetUnits !== undefined) o.polygonOffsetUnits = ext.polygonOffsetUnits | |||||
| if (ext.dithering !== undefined) o.dithering = ext.dithering | |||||
| if (ext.alphaToCoverage !== undefined) o.alphaToCoverage = ext.alphaToCoverage | |||||
| if (ext.premultipliedAlpha !== undefined) o.premultipliedAlpha = ext.premultipliedAlpha | |||||
| // if (ext.visible !== undefined) o.visible = ext.visible | |||||
| if (ext.toneMapped !== undefined) o.toneMapped = ext.toneMapped | |||||
| // we are ignoring the normalScale set from the GLTFLoader, because it is not correct in some cases. | |||||
| // todo: check if this is still the case | |||||
| if (ext.normalScale !== undefined && o.normalScale !== undefined) { | |||||
| if (Array.isArray(ext.normalScale)) o.normalScale.fromArray(ext.normalScale) | |||||
| else if (typeof ext.normalScale === 'number') o.normalScale.set(ext.normalScale, ext.normalScale) | |||||
| else console.warn('normalScale is not an array or number', ext.normalScale) | |||||
| } | |||||
| if (ext.reflectivity !== undefined) o.reflectivity = ext.reflectivity // this is not present in the extras exporter because KHR_materials_ior, todo: kept for backward compatibility, remove ? | |||||
| // if (ext.refractionRatio !== undefined) o.refractionRatio = ext.refractionRatio | |||||
| // todo forceSinglePass | |||||
| // todo: make extension for these like GLTFMaterialsBumpMapExtension | |||||
| // if ( ext.gradientMap !== undefined ) o.gradientMap = getTexture( json.gradientMap ); | |||||
| Object.entries(ext).forEach(([key, value]: [string, any])=>{ | |||||
| if (key.startsWith('_')) return | |||||
| if (value && value.resource && typeof value.resource === 'string') { | |||||
| o[key] = ThreeSerialization.Deserialize(value, o[key], resources) | |||||
| } | |||||
| }) | |||||
| delete o.userData.gltfExtensions[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] | |||||
| }) | |||||
| // todo: check for resources that are not used and dispose them? see todo in ThreeViewer.fromJSON | |||||
| if (resExt) delete s.userData.gltfExtensions[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] | |||||
| } | |||||
| }, | |||||
| }) | |||||
| /** | |||||
| * Also see {@link Import} | |||||
| * @param w | |||||
| * @constructor | |||||
| */ | |||||
| static Export = (w: GLTFWriter): GLTFExporterPlugin&{materialExternalResources:any, serializedMeta: any}=> ({ | |||||
| writeMaterial(material: Material & any, matDef: any) { | |||||
| if (!material?.isMaterial) return | |||||
| if (!matDef.extensions) matDef.extensions = {} | |||||
| const dat: any = {} | |||||
| // non-default stuff from MaterialLoader.js | |||||
| // hardcode fix for: emissive components are limited to stay within the 0 - 1 range to accommodate glTF spec. see threejs: #21849 and #22000. | |||||
| // not needed anymore. | |||||
| // if (material.emissiveIntensity !== undefined && material.emissive?.isColor) { | |||||
| // const emissive = material.emissive.clone().multiplyScalar(material.emissiveIntensity) | |||||
| // const maxEmissiveComponent = Math.max(emissive.r, emissive.g, emissive.b) | |||||
| // if (maxEmissiveComponent > 1) { | |||||
| // dat.emissiveIntensity = maxEmissiveComponent | |||||
| // } | |||||
| // } | |||||
| // bumpMap, lightMap, alphaMap moved to separate extensions | |||||
| // if (material.shininess !== undefined) dat.shininess = material.shininess | |||||
| if (material.fog !== undefined) dat.fog = material.fog | |||||
| if (material.flatShading !== undefined) dat.flatShading = material.flatShading | |||||
| if (material.blending !== undefined) dat.blending = material.blending | |||||
| // if (material.combine !== undefined) dat.combine = material.combine | |||||
| if (material.side !== undefined && material.side !== DoubleSide) dat.side = material.side // DoubleSide handled in GLTF | |||||
| if (material.shadowSide !== undefined) dat.shadowSide = material.shadowSide | |||||
| if (material.depthFunc !== undefined) dat.depthFunc = material.depthFunc | |||||
| if (material.depthTest !== undefined) dat.depthTest = material.depthTest | |||||
| if (material.depthWrite !== undefined) dat.depthWrite = material.depthWrite | |||||
| if (material.colorWrite !== undefined) dat.colorWrite = material.colorWrite | |||||
| if (material.vertexColors !== undefined) dat.vertexColors = material.vertexColors // this is override, it is also set in GLTFLoader if geometry has vertex colors, todo: check how to do this in a better way | |||||
| if (material.alphaTest !== undefined) dat.alphaTest = material.alphaTest | |||||
| // if (material.envMapIntensity !== undefined) dat.envMapIntensity = material.envMapIntensity | |||||
| // if (material.stencilWrite !== undefined) dat.stencilWrite = material.stencilWrite | |||||
| // if (material.stencilWriteMask !== undefined) dat.stencilWriteMask = material.stencilWriteMask | |||||
| // if (material.stencilFunc !== undefined) dat.stencilFunc = material.stencilFunc | |||||
| // if (material.stencilRef !== undefined) dat.stencilRef = material.stencilRef | |||||
| // if (material.stencilFuncMask !== undefined) dat.stencilFuncMask = material.stencilFuncMask | |||||
| // if (material.stencilFail !== undefined) dat.stencilFail = material.stencilFail | |||||
| // if (material.stencilZFail !== undefined) dat.stencilZFail = material.stencilZFail | |||||
| // if (material.stencilZPass !== undefined) dat.stencilZPass = material.stencilZPass | |||||
| if (material.wireframe !== undefined) dat.wireframe = material.wireframe | |||||
| if (material.wireframeLinewidth !== undefined) dat.wireframeLinewidth = material.wireframeLinewidth | |||||
| if (material.wireframeLinecap !== undefined) dat.wireframeLinecap = material.wireframeLinecap | |||||
| if (material.wireframeLinejoin !== undefined) dat.wireframeLinejoin = material.wireframeLinejoin | |||||
| if (material.rotation !== undefined) dat.rotation = material.rotation | |||||
| // if (material.linewidth !== 1) dat.linewidth = material.linewidth | |||||
| // if (material.dashSize !== undefined) dat.dashSize = material.dashSize | |||||
| // if (material.gapSize !== undefined) dat.gapSize = material.gapSize | |||||
| // if (material.scale !== undefined) dat.scale = material.scale | |||||
| if (material.polygonOffset !== undefined) dat.polygonOffset = material.polygonOffset | |||||
| if (material.polygonOffsetFactor !== undefined) dat.polygonOffsetFactor = material.polygonOffsetFactor | |||||
| if (material.polygonOffsetUnits !== undefined) dat.polygonOffsetUnits = material.polygonOffsetUnits | |||||
| if (material.dithering !== undefined) dat.dithering = material.dithering | |||||
| if (material.alphaToCoverage !== undefined) dat.alphaToCoverage = material.alphaToCoverage | |||||
| if (material.premultipliedAlpha !== undefined) dat.premultipliedAlpha = material.premultipliedAlpha | |||||
| // if (material.visible !== undefined) dat.visible = material.visible | |||||
| if (material.toneMapped !== undefined) dat.toneMapped = material.toneMapped | |||||
| // ignoring data from the GLTFExporter. | |||||
| if (material.normalScale !== undefined) dat.normalScale = [material.normalScale.x, material.normalScale.y] | |||||
| // if (material.reflectivity !== undefined) dat.reflectivity = material.reflectivity // see KHR_materials_ior, and comments in parser. | |||||
| // if (material.refractionRatio !== undefined) dat.refractionRatio = material.refractionRatio | |||||
| // todo: make extension for this like GLTFMaterialsBumpMapExtension | |||||
| // if ( material.gradientMap !== undefined ) dat.gradientMap = getTexture( json.gradientMap ); | |||||
| const resources = this.materialExternalResources[material.uuid] | |||||
| if (resources) { | |||||
| Object.entries(resources).forEach(([k, v]) => { | |||||
| if (k.startsWith('_')) return | |||||
| dat[k] = ThreeSerialization.Serialize(v, this.serializedMeta) | |||||
| }) | |||||
| } | |||||
| if (Object.keys(dat).length > 0) { | |||||
| matDef.extensions[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] = dat | |||||
| w.extensionsUsed[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] = true | |||||
| } | |||||
| }, | |||||
| materialExternalResources: {}, | |||||
| serializedMeta: { | |||||
| images: {}, | |||||
| textures: {}, | |||||
| }, | |||||
| beforeParse(input) { | |||||
| this.materialExternalResources = {} | |||||
| // externalImagesInExtras: this is required because gltf-transform doesnt support external images in glb | |||||
| // see https://github.com/donmccurdy/glTF-Transform/discussions/644 | |||||
| if (!w.options.externalImagesInExtras) return | |||||
| const materials: (Material&any)[] = []; | |||||
| (Array.isArray(input) ? input : [input]).forEach(obj=>{ | |||||
| obj?.traverse((o: any)=>{ | |||||
| if (o && o.material?.isMaterial) materials.push(o.material) | |||||
| }) | |||||
| }) | |||||
| materials.forEach(material=>{ | |||||
| if (material) { | |||||
| if (!this.materialExternalResources[material.uuid]) | |||||
| this.materialExternalResources[material.uuid] = {} | |||||
| this.materialExternalResources[material.uuid].__materialRef = material | |||||
| Object.entries(material).forEach(([k, v]: [string, any])=>{ | |||||
| if (k.startsWith('_')) return | |||||
| if (!v) return | |||||
| if (!v.isTexture) return | |||||
| if ( | |||||
| v.userData.rootPath | |||||
| && (v.userData.rootPath.startsWith('http') || v.userData.rootPath.startsWith('data:')) | |||||
| ) { | |||||
| material[k] = null | |||||
| this.materialExternalResources[material.uuid][k] = v | |||||
| } | |||||
| }) | |||||
| } | |||||
| }) | |||||
| }, | |||||
| afterParse(_) { | |||||
| const vals = Object.values(this.materialExternalResources) | |||||
| if (vals.length < 1) return | |||||
| vals.forEach((resources: any)=>{ | |||||
| const mat = resources.__materialRef | |||||
| if (!mat) return | |||||
| Object.entries(resources).forEach(([k, v]: [string, any])=>{ | |||||
| if (k.startsWith('_')) return | |||||
| if (!v) return | |||||
| mat[k] = v | |||||
| }) | |||||
| delete this.materialExternalResources[mat.uuid] | |||||
| }) | |||||
| const scene = w.json.scenes[w.json.scene || 0] | |||||
| if (!scene.extensions) scene.extensions = {} | |||||
| scene.extensions[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] = { | |||||
| resources: this.serializedMeta, | |||||
| } | |||||
| w.extensionsUsed[GLTFMaterialExtrasExtension.WebGiMaterialExtrasExtension] = true | |||||
| // console.log(w) | |||||
| }, | |||||
| }) | |||||
| } |
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {MeshStandardMaterial} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| /** | |||||
| * Alpha Map Extension | |||||
| * | |||||
| * alphaTexture is added to the material | |||||
| * This is separate from the alpha in base color texture. This is used when that is not supported in the viewer | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_alphamap.html | |||||
| */ | |||||
| export class GLTFMaterialsAlphaMapExtension { | |||||
| static readonly WebGiMaterialsAlphaMapExtension = 'WEBGI_materials_alphamap' | |||||
| static Import = (parser: GLTFParser): GLTFLoaderPlugin=> new GLTFMaterialsAlphaMapExtensionImport(parser) | |||||
| static Export = (writer: GLTFWriter): GLTFExporterPlugin => new GLTFMaterialsAlphaMapExtensionExport(writer) | |||||
| } | |||||
| class GLTFMaterialsAlphaMapExtensionImport { | |||||
| public name: string | |||||
| constructor(public parser: GLTFParser) { | |||||
| this.name = GLTFMaterialsAlphaMapExtension.WebGiMaterialsAlphaMapExtension | |||||
| } | |||||
| // getMaterialType(materialIndex: number) { // todo: required? | |||||
| // | |||||
| // const parser = this.parser | |||||
| // const materialDef = parser.json.materials[ materialIndex ] | |||||
| // | |||||
| // if (!materialDef.extensions || !materialDef.extensions[ this.name ]) return null | |||||
| // | |||||
| // return MeshPhysicalMaterial | |||||
| // | |||||
| // } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[ materialIndex ] | |||||
| if (!materialDef.extensions || !materialDef.extensions[ this.name ]) { | |||||
| return Promise.resolve() | |||||
| } | |||||
| const pending = [] | |||||
| const extension = materialDef.extensions[ this.name ] | |||||
| if (extension.alphaTexture !== undefined) { | |||||
| pending.push(parser.assignTexture(materialParams, 'alphaMap', extension.alphaTexture)) | |||||
| } | |||||
| return Promise.all(pending) | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsAlphaMapExtensionImport} | |||||
| class GLTFMaterialsAlphaMapExtensionExport { | |||||
| public name: string | |||||
| constructor(public writer: GLTFWriter) { | |||||
| this.name = GLTFMaterialsAlphaMapExtension.WebGiMaterialsAlphaMapExtension | |||||
| } | |||||
| writeMaterial(material: MeshStandardMaterial, materialDef: any) { | |||||
| if (!material.isMeshStandardMaterial || !material.alphaMap) return | |||||
| const writer = this.writer | |||||
| const extensionsUsed = writer.extensionsUsed | |||||
| const extensionDef: any = {} | |||||
| if (material.alphaMap) { | |||||
| const alphaMapDef = {index: writer.processTexture(material.alphaMap)} | |||||
| writer.applyTextureTransform(alphaMapDef, material.alphaMap) | |||||
| extensionDef.alphaTexture = alphaMapDef | |||||
| } | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| materialDef.extensions[ this.name ] = extensionDef | |||||
| extensionsUsed[ this.name ] = true | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsAlphaMapExtensionExport} |
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {MeshStandardMaterial} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| /** | |||||
| * Bump Map Extension | |||||
| * | |||||
| * bumpTexture and bumpScale are added to the material | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_bumpmap.html | |||||
| */ | |||||
| export class GLTFMaterialsBumpMapExtension { | |||||
| static readonly WebGiMaterialsBumpMapExtension = 'WEBGI_materials_bumpmap' | |||||
| static Import = (parser: GLTFParser): GLTFLoaderPlugin=> new GLTFMaterialsBumpMapExtensionImport(parser) | |||||
| static Export = (writer: GLTFWriter): GLTFExporterPlugin => new GLTFMaterialsBumpMapExtensionExport(writer) | |||||
| } | |||||
| class GLTFMaterialsBumpMapExtensionImport { | |||||
| public name: string | |||||
| constructor(public parser: GLTFParser) { | |||||
| this.name = GLTFMaterialsBumpMapExtension.WebGiMaterialsBumpMapExtension | |||||
| } | |||||
| // getMaterialType(materialIndex: number) { // todo: required? | |||||
| // | |||||
| // const parser = this.parser | |||||
| // const materialDef = parser.json.materials[ materialIndex ] | |||||
| // | |||||
| // if (!materialDef.extensions || !materialDef.extensions[ this.name ]) return null | |||||
| // | |||||
| // return MeshPhysicalMaterial | |||||
| // | |||||
| // } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[ materialIndex ] | |||||
| if (!materialDef.extensions || !materialDef.extensions[ this.name ]) { | |||||
| return Promise.resolve() | |||||
| } | |||||
| const pending = [] | |||||
| const extension = materialDef.extensions[ this.name ] | |||||
| if (extension.bumpScale !== undefined) { | |||||
| materialParams.bumpScale = extension.bumpScale | |||||
| } | |||||
| if (extension.bumpTexture !== undefined) { | |||||
| pending.push(parser.assignTexture(materialParams, 'bumpMap', extension.bumpTexture)) | |||||
| } | |||||
| return Promise.all(pending) | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsBumpMapExtensionImport} | |||||
| class GLTFMaterialsBumpMapExtensionExport { | |||||
| public name: string | |||||
| constructor(public writer: GLTFWriter) { | |||||
| this.name = GLTFMaterialsBumpMapExtension.WebGiMaterialsBumpMapExtension | |||||
| } | |||||
| writeMaterial(material: MeshStandardMaterial, materialDef: any) { | |||||
| if (!material.isMeshStandardMaterial || material.bumpScale === 0) return | |||||
| const writer = this.writer | |||||
| const extensionsUsed = writer.extensionsUsed | |||||
| const extensionDef: any = {} | |||||
| extensionDef.bumpScale = material.bumpScale | |||||
| if (material.bumpMap) { | |||||
| const bumpMapDef = {index: writer.processTexture(material.bumpMap)} | |||||
| writer.applyTextureTransform(bumpMapDef, material.bumpMap) | |||||
| extensionDef.bumpTexture = bumpMapDef | |||||
| } | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| materialDef.extensions[ this.name ] = extensionDef | |||||
| extensionsUsed[ this.name ] = true | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsBumpMapExtensionExport} |
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {MeshStandardMaterial} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| /** | |||||
| * Displacement Map Extension | |||||
| * | |||||
| * displacementTexture and displacementScale are added to the material | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_displacementmap.html | |||||
| */ | |||||
| export class GLTFMaterialsDisplacementMapExtension { | |||||
| static readonly WebGiMaterialsDisplacementMapExtension = 'WEBGI_materials_displacementmap' | |||||
| static Import = (parser: GLTFParser): GLTFLoaderPlugin=> new GLTFMaterialsDisplacementMapExtensionImport(parser) | |||||
| static Export = (writer: GLTFWriter): GLTFExporterPlugin => new GLTFMaterialsDisplacementMapExtensionExport(writer) | |||||
| } | |||||
| class GLTFMaterialsDisplacementMapExtensionImport { | |||||
| public name: string | |||||
| constructor(public parser: GLTFParser) { | |||||
| this.name = GLTFMaterialsDisplacementMapExtension.WebGiMaterialsDisplacementMapExtension | |||||
| } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[ materialIndex ] | |||||
| if (!materialDef.extensions || !materialDef.extensions[ this.name ]) { | |||||
| return Promise.resolve() | |||||
| } | |||||
| const pending = [] | |||||
| const extension = materialDef.extensions[ this.name ] | |||||
| if (extension.displacementScale !== undefined) { | |||||
| materialParams.displacementScale = extension.displacementScale | |||||
| } | |||||
| if (extension.displacementBias !== undefined) { | |||||
| materialParams.displacementBias = extension.displacementBias | |||||
| } | |||||
| if (extension.displacementTexture !== undefined) { | |||||
| pending.push(parser.assignTexture(materialParams, 'displacementMap', extension.displacementTexture)) | |||||
| } | |||||
| return Promise.all(pending) | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsDisplacementMapExtensionImport} | |||||
| class GLTFMaterialsDisplacementMapExtensionExport { | |||||
| public name: string | |||||
| constructor(public writer: GLTFWriter) { | |||||
| this.name = GLTFMaterialsDisplacementMapExtension.WebGiMaterialsDisplacementMapExtension | |||||
| } | |||||
| writeMaterial(material: MeshStandardMaterial, materialDef: any) { | |||||
| if (!material.isMeshStandardMaterial || material.displacementScale === 0) return | |||||
| const writer = this.writer | |||||
| const extensionsUsed = writer.extensionsUsed | |||||
| const extensionDef: any = {} | |||||
| extensionDef.displacementScale = material.displacementScale | |||||
| extensionDef.displacementBias = material.displacementBias | |||||
| if (material.displacementMap) { | |||||
| const displacementMapDef = {index: writer.processTexture(material.displacementMap)} | |||||
| writer.applyTextureTransform(displacementMapDef, material.displacementMap) | |||||
| extensionDef.displacementTexture = displacementMapDef | |||||
| } | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| materialDef.extensions[ this.name ] = extensionDef | |||||
| extensionsUsed[ this.name ] = true | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsDisplacementMapExtensionExport} |
| import type {GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {MeshStandardMaterial} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| /** | |||||
| * Light Map Extension | |||||
| * | |||||
| * lightMapTexture and lightMapIntensity are added to the material | |||||
| * | |||||
| * Specification: https://webgi.xyz/docs/gltf-extensions/WEBGI_materials_lightmap.html | |||||
| */ | |||||
| export class GLTFMaterialsLightMapExtension { | |||||
| static readonly WebGiMaterialsLightMapExtension = 'WEBGI_materials_lightmap' | |||||
| static Import = (parser: GLTFParser): GLTFLoaderPlugin=> new GLTFMaterialsLightMapExtensionImport(parser) | |||||
| static Export = (writer: GLTFWriter): GLTFExporterPlugin => new GLTFMaterialsLightMapExtensionExport(writer) | |||||
| } | |||||
| class GLTFMaterialsLightMapExtensionImport { | |||||
| public name: string | |||||
| constructor(public parser: GLTFParser) { | |||||
| this.name = GLTFMaterialsLightMapExtension.WebGiMaterialsLightMapExtension | |||||
| } | |||||
| // getMaterialType(materialIndex: number) { // todo: required? | |||||
| // | |||||
| // const parser = this.parser | |||||
| // const materialDef = parser.json.materials[ materialIndex ] | |||||
| // | |||||
| // if (!materialDef.extensions || !materialDef.extensions[ this.name ]) return null | |||||
| // | |||||
| // return MeshPhysicalMaterial | |||||
| // | |||||
| // } | |||||
| async extendMaterialParams(materialIndex: number, materialParams: any) { | |||||
| const parser = this.parser | |||||
| const materialDef = parser.json.materials[ materialIndex ] | |||||
| if (!materialDef.extensions || !materialDef.extensions[ this.name ]) { | |||||
| return Promise.resolve() | |||||
| } | |||||
| const pending = [] | |||||
| const extension = materialDef.extensions[ this.name ] | |||||
| if (extension.lightMapIntensity !== undefined) { | |||||
| materialParams.lightMapIntensity = extension.lightMapIntensity | |||||
| } | |||||
| if (extension.lightMapTexture !== undefined) { | |||||
| pending.push(parser.assignTexture(materialParams, 'lightMap', extension.lightMapTexture)) | |||||
| } | |||||
| return Promise.all(pending) | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsLightMapExtensionImport} | |||||
| class GLTFMaterialsLightMapExtensionExport { | |||||
| public name: string | |||||
| constructor(public writer: GLTFWriter) { | |||||
| this.name = GLTFMaterialsLightMapExtension.WebGiMaterialsLightMapExtension | |||||
| } | |||||
| writeMaterial(material: MeshStandardMaterial, materialDef: any) { | |||||
| if (!material.isMeshStandardMaterial || material.lightMapIntensity === 0) return | |||||
| const writer = this.writer | |||||
| const extensionsUsed = writer.extensionsUsed | |||||
| const extensionDef: any = {} | |||||
| extensionDef.lightMapIntensity = material.lightMapIntensity | |||||
| if (material.lightMap) { | |||||
| const lightMapDef = {index: writer.processTexture(material.lightMap)} | |||||
| writer.applyTextureTransform(lightMapDef, material.lightMap) | |||||
| extensionDef.lightMapTexture = lightMapDef | |||||
| } | |||||
| materialDef.extensions = materialDef.extensions || {} | |||||
| materialDef.extensions[ this.name ] = extensionDef | |||||
| extensionsUsed[ this.name ] = true | |||||
| } | |||||
| } | |||||
| export type {GLTFMaterialsLightMapExtensionExport} |
| import type {GLTF, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {Object3D} from 'three' | |||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| export class GLTFObject3DExtrasExtension { | |||||
| static readonly WebGiObject3DExtrasExtension = 'WEBGI_object3d_extras' | |||||
| /** | |||||
| * Also {@link Export} | |||||
| * @param _ | |||||
| */ | |||||
| static Import = (_: GLTFParser): GLTFLoaderPlugin =>({ | |||||
| name: '__' + this.WebGiObject3DExtrasExtension, // __ is prefix so that the extension is added to userdata, and we can process later in afterRoot | |||||
| afterRoot: async(result: GLTF) => { | |||||
| const scenes = result.scenes || (result.scene ? [result.scene] : []) | |||||
| scenes.forEach(s=>{ | |||||
| s.traverse((o: any)=>{ | |||||
| if (!o.isObject3D) return | |||||
| const ext = o.userData?.gltfExtensions?.[this.WebGiObject3DExtrasExtension] | |||||
| if (!ext) { | |||||
| if (o.isLight && !o.isAmbientLight) o.castShadow = true | |||||
| return | |||||
| } | |||||
| const hasShadowDef = ext.castShadow !== undefined || ext.receiveShadow !== undefined | |||||
| if (ext.castShadow !== undefined) o.castShadow = ext.castShadow | |||||
| if (ext.receiveShadow !== undefined) o.receiveShadow = ext.receiveShadow | |||||
| if (ext.visible !== undefined) o.visible = ext.visible | |||||
| if (ext.frustumCulled !== undefined) o.frustumCulled = ext.frustumCulled | |||||
| if (ext.renderOrder !== undefined) o.renderOrder = ext.renderOrder | |||||
| // if (ext.userData !== undefined) o.userData = ext.userData | |||||
| if (ext.layers !== undefined) o.layers.mask = ext.layers | |||||
| if (hasShadowDef) { | |||||
| o.userData.__keepShadowDef = true | |||||
| } | |||||
| delete o.userData.gltfExtensions[this.WebGiObject3DExtrasExtension] | |||||
| }) | |||||
| }) | |||||
| }, | |||||
| }) | |||||
| /** | |||||
| * Also {@link Import} | |||||
| * @param w | |||||
| * @constructor | |||||
| */ | |||||
| static Export = (w: GLTFWriter): GLTFExporterPlugin => ({ | |||||
| writeNode: (object: Object3D, nodeDef: any)=>{ | |||||
| if (!object?.isObject3D) return | |||||
| if (!nodeDef.extensions) nodeDef.extensions = {} | |||||
| const dat: any = {} | |||||
| // non-default stuff from ObjectLoader.js | |||||
| if (object.castShadow !== undefined) dat.castShadow = object.castShadow | |||||
| if (object.receiveShadow !== undefined) dat.receiveShadow = object.receiveShadow | |||||
| if (object.visible === false) dat.visible = false | |||||
| if (object.frustumCulled === false) dat.frustumCulled = false | |||||
| if (object.renderOrder !== 0) dat.renderOrder = object.renderOrder | |||||
| if (object.layers.mask !== 1) dat.layers = object.layers.mask | |||||
| if (object.matrixAutoUpdate === false) dat.matrixAutoUpdate = false | |||||
| if (Object.keys(dat).length > 0) { | |||||
| nodeDef.extensions[this.WebGiObject3DExtrasExtension] = dat | |||||
| w.extensionsUsed[this.WebGiObject3DExtrasExtension] = true | |||||
| } | |||||
| }, | |||||
| }) | |||||
| } |
| import type {GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader' | |||||
| import type {GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | |||||
| import {ISerializedViewerConfig, ThreeViewer} from '../../viewer' | |||||
| import {Group, ImageUtils} from 'three' | |||||
| import {RGBEPNGLoader} from '../import/RGBEPNGLoader' | |||||
| import {SerializationResourcesType} from '../../utils/serialization' | |||||
| export class GLTFViewerConfigExtension { | |||||
| static readonly ViewerConfigGLTFExtension = 'WEBGI_viewer' | |||||
| // region Import | |||||
| /** | |||||
| * Import viewer config from glTF(exported from {@link GLTFViewerConfigExtension.ExportViewerConfig}) and sets in scene.importedViewerConfig | |||||
| * Must be called from afterRoot in gltf loader. Used in {@link GLTFLoader2.setup} | |||||
| * Only imports, does not apply. | |||||
| * @param parser | |||||
| * @param viewer | |||||
| * @param resultScenes | |||||
| * @param scene | |||||
| */ | |||||
| static async ImportViewerConfig(parser: GLTFParser, viewer: ThreeViewer, resultScenes: Group[], scene?: any): Promise<Partial<ISerializedViewerConfig>> { | |||||
| if (!scene) { | |||||
| const scenes = parser.json.scenes || [] | |||||
| if (scenes.length !== 1) { | |||||
| for (const scene1 of scenes) { | |||||
| await this.ImportViewerConfig(parser, viewer, [resultScenes[scenes.indexOf(scene1)]] || resultScenes, scene1) | |||||
| } | |||||
| return {} | |||||
| } | |||||
| scene = scenes[0] | |||||
| } | |||||
| const resultScene = resultScenes[0] | |||||
| const viewerConfig: Partial<ISerializedViewerConfig> = scene.extensions?.[this.ViewerConfigGLTFExtension] | |||||
| // console.log({...viewerConfig?.resources}) | |||||
| if (!viewerConfig) return {} | |||||
| if (viewerConfig.resources) { | |||||
| await this._parseArrayBuffers(viewerConfig.resources, parser) | |||||
| // Find empty resources and try to find them in the glTF as a dependency by saved UUID. | |||||
| const extraResources = await this._parseExtraResources(viewerConfig.resources, parser, viewer) | |||||
| viewerConfig.resources = await viewer.loadConfigResources(viewerConfig.resources || {}, extraResources) | |||||
| ;(resultScene as any).importedViewerConfig = viewerConfig // todo | |||||
| } | |||||
| return viewerConfig | |||||
| } | |||||
| /** | |||||
| * Find resources in parser from uuid | |||||
| * @param currentResources | |||||
| * @param parser | |||||
| * @param viewer | |||||
| * @private | |||||
| */ | |||||
| private static async _parseExtraResources(currentResources: {textures?: Record<string, any>, materials?: Record<string, any>}, parser: GLTFParser, viewer: ThreeViewer) { | |||||
| const extraResources: any = { | |||||
| textures: {}, | |||||
| materials: {}, | |||||
| } | |||||
| if (currentResources.textures && parser.json.textures) | |||||
| for (const [uuid, texture] of [...Object.entries(currentResources.textures)]) { | |||||
| // console.log(texture) // todo: texture should be {} but its {userData:undefined}, why? | |||||
| if ((texture as any).uuid || !uuid) continue | |||||
| delete currentResources.textures[uuid] | |||||
| const texIndex = parser.json.textures.findIndex((t: any) => | |||||
| t.extras?.uuid === uuid || | |||||
| parser.json.samplers?.[t.sampler]?.extras?.uuid === uuid || | |||||
| parser.json.images?.[t.source]?.extras?.t_uuid === uuid | |||||
| ) | |||||
| // This HAS To be called from afterRoot in gltf loader. | |||||
| // And make sure that texture is not cloned in any gltf extension like khr_texture_transform, which happens in three.js by default and it's commented in custom fork. | |||||
| if (texIndex >= 0) | |||||
| extraResources.textures[uuid] = await parser.getDependency('texture', texIndex) | |||||
| } | |||||
| // todo: need to test, because materials are also cloned in GLTFLoader.js | |||||
| if (currentResources.materials && parser.json.materials) | |||||
| for (const [uuid, material] of [...Object.entries(currentResources.materials)]) { | |||||
| // console.log(material) | |||||
| if ((material as any).uuid || !uuid) continue | |||||
| delete currentResources.materials[uuid] | |||||
| const matIndex = parser.json.materials.findIndex((m: any) => m.extras?.uuid === uuid) | |||||
| if (matIndex >= 0) { | |||||
| const mat = await parser.getDependency('material', matIndex) | |||||
| extraResources.materials[uuid] = viewer.assetManager.materials.convertToIMaterial(mat) | |||||
| } | |||||
| } | |||||
| // todo: do same for other dependencies? | |||||
| return extraResources | |||||
| } | |||||
| private static async _parseArrayBuffers(resources: Partial<SerializationResourcesType>, parser: GLTFParser) { | |||||
| const buffers: any = [] | |||||
| Object.values(resources).forEach((res: any) => { | |||||
| Object.values(res).forEach((item: any) => { | |||||
| if (!item.url) return | |||||
| if (item.url.type === 'Uint16Array' && item.url.data) { | |||||
| // item.url.data = new Uint16Array(item.url.data) | |||||
| buffers.push(item.url) | |||||
| } | |||||
| if (item.url.type === 'Uint8Array' && item.url.data) { | |||||
| // item.url.data = new Uint8Array(item.url.data) | |||||
| buffers.push(item.url) | |||||
| } | |||||
| }) | |||||
| }) | |||||
| for (const buff of buffers) { | |||||
| const imgIndex = buff.data.image | |||||
| const img = parser.json.images[imgIndex] | |||||
| const bufferView = await parser.getDependency('bufferView', img.bufferView) | |||||
| // todo: add more checks | |||||
| if (img.mimeType.startsWith('image/') && buff.type === 'Uint16Array' && buff.encoding === 'rgbe') { | |||||
| // todo: find a optimal way, this has too many cross conversions | |||||
| // const view2 = (bufferView as ArrayBuffer).slice(0, bufferView.byteLength - 4) | |||||
| const blob = new Blob([bufferView]) | |||||
| // const blob2 = new Blob([await blob.text()], {type: img.mimeType}) | |||||
| let url = URL.createObjectURL(blob) | |||||
| if ((buff.encodingVersion || 1) < 2) { | |||||
| url = 'data:image/png;base64,' + btoa(await blob.text()) | |||||
| } | |||||
| // fetch(url).then(async r=>r.blob()).then(b=>console.log(b)) | |||||
| // console.log(view2) | |||||
| buff.data = (await new RGBEPNGLoader().parseAsync(url, undefined, true)).data | |||||
| URL.revokeObjectURL(url) | |||||
| delete buff.encoding | |||||
| delete buff.encodingVersion | |||||
| } else { | |||||
| buff.data = bufferView | |||||
| } | |||||
| } | |||||
| } | |||||
| // endregion | |||||
| // region Export | |||||
| /** | |||||
| * Export viewer config to glTF(can be imported by {@link GLTFViewerConfigExtension.ImportViewerConfig}). | |||||
| * Used in {@link GLTFExporter2} | |||||
| * @param viewer | |||||
| * @param writer | |||||
| * @constructor | |||||
| */ | |||||
| static ExportViewerConfig(viewer: ThreeViewer, writer: GLTFWriter): void { | |||||
| const viewerData = viewer.toJSON(true, undefined) | |||||
| const json = writer.json | |||||
| this._bundleExtraResources(json, viewerData) | |||||
| this._bundleArrayBuffers(viewerData, writer) | |||||
| const scene = writer.json.scenes[writer.json.scene || 0] | |||||
| if (!scene.extensions) scene.extensions = {} | |||||
| writer.extensionsUsed[this.ViewerConfigGLTFExtension] = true | |||||
| scene.extensions[this.ViewerConfigGLTFExtension] = viewerData | |||||
| } | |||||
| private static _bundleArrayBuffers(viewerData: any, writer: GLTFWriter) { | |||||
| // For DataTextures like env map with custom rgbe encoding | |||||
| // Create objects of TypedArray | |||||
| const buffers: any = [] | |||||
| Object.values(viewerData.resources).forEach((res: any) => { | |||||
| if (res) Object.values(res).forEach((item: any) => { | |||||
| if (!item.url) return | |||||
| if (item.url.type === 'Uint16Array' && item.url.data) { | |||||
| if (!(item.url.data instanceof Uint16Array)) item.url.data = new Uint16Array(item.url.data) | |||||
| buffers.push(item.url) | |||||
| } | |||||
| if (item.url.type === 'Uint8Array' && item.url.data) { | |||||
| if (!(item.url.data instanceof Uint8Array)) item.url.data = new Uint8Array(item.url.data) | |||||
| buffers.push(item.url) // todo: just use jpeg or PNG for this | |||||
| } | |||||
| }) | |||||
| }) | |||||
| // console.log(writer) | |||||
| for (const buffer of buffers) { | |||||
| // todo:[update: done one case below] check if buffer is of image, if yes convert to rgbe with png compression blob. [or this can be done while serializing the DataTexture] | |||||
| let mime = 'application/octet-stream' | |||||
| if (buffer.mimeType) mime = buffer.mimeType | |||||
| // console.log(buffer, buffer.data) | |||||
| const encodeUint16Rgbe = writer.options.exporterOptions.encodeUint16Rgbe // disabled for now, todo: add a UI option to enable this | |||||
| if (encodeUint16Rgbe && buffer.type === 'Uint16Array' && buffer.width > 0 && buffer.height > 0) { // import for this is handled in gltf.ts:importViewer. | |||||
| // todo: also check if this is indeed an hdr image or something else like LUT or other kind of embedded file. | |||||
| // todo: can we optimize this? this is too many steps | |||||
| const d = halfFloatToRgbe(buffer.data, 4) | |||||
| const id = new ImageData(d, buffer.width, buffer.height) | |||||
| // @ts-expect-error patched three | |||||
| const b64 = ImageUtils.getDataURL(id, true).split(',')[1] | |||||
| // console.log(b64) | |||||
| const encodingVersion: any = 2 | |||||
| mime = 'image/png' | |||||
| if (encodingVersion === 1) { | |||||
| buffer.data = atob(b64) | |||||
| } else if (encodingVersion === 2) { | |||||
| buffer.data = Uint8Array.from(atob(b64), c => c.charCodeAt(0)) | |||||
| } else { | |||||
| throw new Error('Invalid encoding version') | |||||
| } | |||||
| buffer.encoding = 'rgbe' | |||||
| buffer.encodingVersion = encodingVersion | |||||
| } | |||||
| // console.log(mime, buffer) | |||||
| // const blob = new Blob([buffer.data], {type: mime}) | |||||
| if (!writer.json.images) writer.json.images = [] | |||||
| const img: any = { | |||||
| mimeType: mime, | |||||
| } | |||||
| // console.log(buffer, img) | |||||
| const imgIndex = writer.json.images.push(img) - 1 | |||||
| const data = buffer.data | |||||
| img.bufferView = writer.processBufferViewImageBuffer(data) | |||||
| // console.log(buffer) | |||||
| buffer.data = {image: imgIndex} | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Find the resources that are in the viewer config AND in writer.json and use the ones in writer and remove from viewer Config. | |||||
| * For now (for the lack of a better way) we can let the resources be exported twice and removed from resources. Overhead will be just for some images. | |||||
| * @param json | |||||
| * @param viewerData | |||||
| * @private | |||||
| */ | |||||
| private static _bundleExtraResources(json: GLTFWriter['json'], viewerData: any) { | |||||
| if (json.textures && json.samplers && json.images && viewerData.resources.textures) | |||||
| [...Object.entries(viewerData.resources.textures)].forEach(([uuid, texture]: [string, any]) => { | |||||
| const tex = json.textures.find((t: any) => // find same texture in gltf writer | |||||
| t.extras?.uuid === uuid || | |||||
| json.samplers[t.sampler]?.extras?.uuid === uuid || | |||||
| json.images[t.source]?.extras?.t_uuid === uuid // todo: remove t_uuid when sampler extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645 | |||||
| ) | |||||
| if (!tex) return | |||||
| // console.log('Removing texture', uuid, tex, texture) | |||||
| if (texture.image && viewerData.resources.images && viewerData.resources.images[texture.image]) { | |||||
| delete viewerData.resources.images[texture.image] // assuming images are only referenced once. | |||||
| } | |||||
| viewerData.resources.textures[uuid] = {} // set to empty, can be read from the gltf data after loading gltf | |||||
| }) | |||||
| // todo: test | |||||
| if (json.materials && viewerData.resources.materials) | |||||
| [...Object.entries(viewerData.resources.materials)].forEach(([uuid, _]: [string, any]) => { | |||||
| const mat = json.materials.find((m: any) => m.extras?.uuid === uuid) // same material in gltf writer | |||||
| if (!mat) return | |||||
| viewerData.resources.materials[uuid] = {} // set to empty, can be read from the gltf data after loading gltf | |||||
| }) | |||||
| // todo: do same for object references? | |||||
| } | |||||
| // endregion | |||||
| } | |||||
| // adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L235 | |||||
| // channels = 4 for RGBA data or 3 for RGB data. buffer from THREE.DataTexture | |||||
| function halfFloatToRgbe(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray { | |||||
| let r, g, b, v, s | |||||
| const l = buffer.byteLength / (channels * 2) | 0 | |||||
| res = res || new Uint8ClampedArray(l * 4) | |||||
| for (let i = 0;i < l;i++) { | |||||
| r = buffer[i * channels]; g = buffer[i * channels + 1]; b = buffer[i * channels + 2] | |||||
| v = Math.max(Math.max(r, g), b) | |||||
| const e = Math.ceil(Math.log2(v)); s = Math.pow(2, e - 8) | |||||
| res[i * 4] = r / s | 0 | |||||
| res[i * 4 + 1] = g / s | 0 | |||||
| res[i * 4 + 2] = b / s | 0 | |||||
| res[i * 4 + 3] = e + 128 | |||||
| } | |||||
| return res | |||||
| } |
| export {GLTFLightExtrasExtension} from './GLTFLightExtrasExtension' | |||||
| export {GLTFMaterialExtrasExtension} from './GLTFMaterialExtrasExtension' | |||||
| export {GLTFMaterialsAlphaMapExtension} from './GLTFMaterialsAlphaMapExtension' | |||||
| export {GLTFMaterialsBumpMapExtension} from './GLTFMaterialsBumpMapExtension' | |||||
| export {GLTFMaterialsDisplacementMapExtension} from './GLTFMaterialsDisplacementMapExtension' | |||||
| export {GLTFMaterialsLightMapExtension} from './GLTFMaterialsLightMapExtension' | |||||
| export {GLTFObject3DExtrasExtension} from './GLTFObject3DExtrasExtension' | |||||
| export {GLTFViewerConfigExtension} from './GLTFViewerConfigExtension' |
| import {DRACOLoader} from 'three/examples/jsm/loaders/DRACOLoader.js' | |||||
| import {BufferGeometry, Color, LoadingManager, Mesh, MeshStandardMaterial} from 'three' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {ILoader} from '../IImporter' | |||||
| export class DRACOLoader2 extends DRACOLoader implements ILoader<BufferGeometry, Mesh|undefined> { | |||||
| public encoderPending: Promise<any>|null = null | |||||
| public encoderConfig: any = {type: 'js'} | |||||
| public static DRACO_LIBRARY_PATH = 'https://cdn.jsdelivr.net/gh/google/draco@1.4.1/javascript/' // https://github.com/google/draco | |||||
| // public static DRACO_LIBRARY_PATH = 'https://www.gstatic.com/draco/versioned/decoders/1.4.1/' | |||||
| // public static DRACO_LIBRARY_PATH = 'https://threejs.org/examples/jsm/libs/draco/' | |||||
| constructor(manager?: LoadingManager) { | |||||
| super(manager) | |||||
| this.setDecoderPath(DRACOLoader2.DRACO_LIBRARY_PATH) | |||||
| this.setDecoderConfig({type: 'js'}) // todo: hack for now, encoder works with wasm, maybe not decoder. | |||||
| } | |||||
| transform(res: BufferGeometry, _: AnyOptions): Mesh|undefined { | |||||
| if (!res.attributes?.normal) res.computeVertexNormals() | |||||
| // todo set mesh name from options/path | |||||
| return res ? new Mesh(res, new MeshStandardMaterial({color: new Color(1, 1, 1)})) : undefined | |||||
| } | |||||
| preload(decoder = true, encoder = false): DRACOLoader { | |||||
| if (decoder) super.preload() | |||||
| if (encoder) this.initEncoder() | |||||
| return this | |||||
| } | |||||
| public async initEncoder() { | |||||
| if (this.encoderPending) return this.encoderPending | |||||
| // this.setDecoderConfig({type: 'js'}) // todo: hack for now. | |||||
| const useJS = typeof WebAssembly !== 'object' || this.encoderConfig.type === 'js' | |||||
| const librariesPending = [] | |||||
| if (useJS) { | |||||
| librariesPending.push(this._loadLibrary('draco_encoder.js', 'text')) | |||||
| } else { | |||||
| // todo: not tested | |||||
| librariesPending.push(this._loadLibrary('draco_wasm_wrapper.js', 'text')) | |||||
| librariesPending.push(this._loadLibrary('draco_encoder.wasm', 'arraybuffer')) | |||||
| } | |||||
| this.encoderPending = Promise.all(librariesPending) | |||||
| .then((libraries) => { | |||||
| const jsContent = libraries[ 0 ] | |||||
| if (!useJS) { | |||||
| this.encoderConfig.wasmBinary = libraries[ 1 ] | |||||
| } | |||||
| const eval2 = eval | |||||
| return eval2(jsContent + '\nDracoEncoderModule;')?.() | |||||
| }) | |||||
| return this.encoderPending | |||||
| } | |||||
| public async initDecoder() { | |||||
| await (this as any)._initDecoder() | |||||
| const jsContent = await fetch((this as any).workerSourceURL).then(async response => response.text()).then(text => { | |||||
| const i = text.indexOf('/* worker */') | |||||
| if (i < 1) throw new Error('unable to load decoder module') | |||||
| return text.substring(0, i - 1) | |||||
| }) | |||||
| const eval2 = eval | |||||
| return eval2(jsContent + '\nDracoDecoderModule;')?.() | |||||
| } | |||||
| } |
| import {GLTF, GLTFLoader, GLTFLoaderPlugin, GLTFParser} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||||
| import {LoadingManager, Object3D} from 'three' | |||||
| import {AnyOptions, safeSetProperty} from 'ts-browser-helpers' | |||||
| import {ThreeViewer} from '../../viewer/ThreeViewer' | |||||
| import {generateUUID} from '../../three/utils/misc' | |||||
| import {GLTFViewerConfigExtension} from '../gltf/GLTFViewerConfigExtension' | |||||
| import {GLTFMaterialExtrasExtension} from '../gltf/GLTFMaterialExtrasExtension' | |||||
| import {GLTFObject3DExtrasExtension} from '../gltf/GLTFObject3DExtrasExtension' | |||||
| import {GLTFLightExtrasExtension} from '../gltf/GLTFLightExtrasExtension' | |||||
| import {GLTFMaterialsBumpMapExtension} from '../gltf/GLTFMaterialsBumpMapExtension' | |||||
| import {GLTFMaterialsLightMapExtension} from '../gltf/GLTFMaterialsLightMapExtension' | |||||
| import {GLTFMaterialsDisplacementMapExtension} from '../gltf/GLTFMaterialsDisplacementMapExtension' | |||||
| import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension' | |||||
| import {RootSceneImportResult} from '../IAssetImporter' | |||||
| import {ILoader} from '../IImporter' | |||||
| export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | |||||
| isGLTFLoader2 = true | |||||
| constructor(manager: LoadingManager) { | |||||
| super(manager) | |||||
| } | |||||
| static ImportExtensions: ((parser: GLTFParser) => GLTFLoaderPlugin)[] = [ | |||||
| GLTFObject3DExtrasExtension.Import, | |||||
| GLTFLightExtrasExtension.Import, | |||||
| GLTFMaterialsBumpMapExtension.Import, | |||||
| GLTFMaterialsDisplacementMapExtension.Import, | |||||
| GLTFMaterialsLightMapExtension.Import, | |||||
| GLTFMaterialsAlphaMapExtension.Import, | |||||
| ] | |||||
| transform(res: GLTF, _: AnyOptions): Object3D|undefined { | |||||
| // todo: support loading of multiple scenes? | |||||
| const scene: RootSceneImportResult|undefined = res ? res.scene || !!res.scenes && res.scenes.length > 0 && res.scenes[0] : undefined as any | |||||
| if (!scene) return undefined | |||||
| if (res.animations.length > 0) scene.animations = res.animations | |||||
| scene.traverse((node: Object3D) => { | |||||
| if (node.userData.gltfUUID) { // saved in GLTFExporter2 | |||||
| safeSetProperty(node, 'uuid', node.userData.gltfUUID, true, true) | |||||
| delete node.userData.gltfUUID // have issue with cloning if we don't dispose. | |||||
| } | |||||
| }) | |||||
| if (!scene.userData) scene.userData = {} | |||||
| if (res.userData) scene.userData.gltfExtras = res.userData // todo: put back in gltf in GLTFExporter2 | |||||
| if (res.cameras) res.cameras.forEach(c => !c.parent && scene.add(c)) | |||||
| if (res.asset) scene.userData.gltfAsset = res.asset // todo: put back in gltf in GLTFExporter2 | |||||
| return scene | |||||
| } | |||||
| register(callback: (parser: GLTFParser) => GLTFLoaderPlugin): this { | |||||
| return super.register(callback) as this | |||||
| } | |||||
| setup(viewer: ThreeViewer, extraExtensions: ((parser: GLTFParser) => GLTFLoaderPlugin)[]): this { | |||||
| this.register(GLTFMaterialExtrasExtension.Import(viewer.loadConfigResources)) | |||||
| for (const ext of extraExtensions) this.register(ext) | |||||
| for (const ext of GLTFLoader2.ImportExtensions) this.register(ext) | |||||
| // Note: this should be last | |||||
| this.register(this.gltfViewerParser(viewer)) | |||||
| return this | |||||
| } | |||||
| // loads the viewer config and handles loading the draco loader for extension | |||||
| gltfViewerParser = (viewer: ThreeViewer): (p: GLTFParser)=>GLTFLoaderPlugin => { | |||||
| return (parser: GLTFParser) => { | |||||
| const tempPathDrc = generateUUID() + '.drc' | |||||
| const tempPathKtx2 = generateUUID() + '.ktx2' | |||||
| const needsDrc = parser.json?.extensionsRequired?.includes?.('KHR_draco_mesh_compression') | |||||
| if (needsDrc) { | |||||
| const drc = viewer.assetManager.importer.registerFile(tempPathDrc) | |||||
| drc && this.setDRACOLoader(drc as any) // todo: check class? | |||||
| } | |||||
| const needsMeshOpt = parser.json?.extensionsUsed?.includes?.('EXT_meshopt_compression') | |||||
| if (needsMeshOpt) { | |||||
| if ((window as any).MeshoptDecoder) { // added by the plugin or by the user | |||||
| this.setMeshoptDecoder((window as any).MeshoptDecoder) | |||||
| parser.options.meshoptDecoder = (window as any).MeshoptDecoder as any | |||||
| } else { | |||||
| console.error('Add GLTFMeshOptPlugin to viewer to enable EXT_meshopt_compression decode') | |||||
| } | |||||
| } | |||||
| const needsBasisU = parser.json?.extensionsUsed?.includes?.('KHR_texture_basisu') | |||||
| if (needsBasisU) { | |||||
| const ktx2 = viewer.assetManager.importer.registerFile(tempPathKtx2) | |||||
| if (ktx2) { | |||||
| this.setKTX2Loader(ktx2 as any) // todo: check class? | |||||
| parser.options.ktx2Loader = ktx2 as any | |||||
| } | |||||
| } | |||||
| return {name: 'GLTF2_HELPER_PLUGIN', afterRoot: async(result: GLTF) => { | |||||
| if (needsDrc) viewer.assetManager.importer.unregisterFile(tempPathDrc) | |||||
| if (needsBasisU) viewer.assetManager.importer.unregisterFile(tempPathKtx2) | |||||
| await GLTFViewerConfigExtension.ImportViewerConfig(parser, viewer, result.scenes || [result.scene]) | |||||
| }} | |||||
| } | |||||
| } | |||||
| } | |||||
| import {SimpleJSONLoader} from './SimpleJSONLoader' | |||||
| import {ThreeViewer} from '../../viewer' | |||||
| import {getEmptyMeta, SerializationMetaType, ThreeSerialization} from '../../utils/serialization' | |||||
| export class JSONMaterialLoader extends SimpleJSONLoader { | |||||
| viewer?: ThreeViewer | |||||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any> { | |||||
| if (!this.viewer) throw 'Viewer not set in JSONMaterialLoader.' | |||||
| const json = await super.loadAsync(url, onProgress) as any | |||||
| const meta: SerializationMetaType = getEmptyMeta() | |||||
| const json2 = {...json} | |||||
| if (json.images) { | |||||
| if (Array.isArray(json.images)) meta.images = Object.fromEntries(json.images.map((i: any) => [i.uuid, i])) | |||||
| else meta.images = json.images | |||||
| delete json2.images | |||||
| } | |||||
| if (json.textures) { | |||||
| if (Array.isArray(json.textures)) meta.textures = Object.fromEntries(json.textures.map((t: any) => [t.uuid, t])) | |||||
| else meta.textures = json.textures | |||||
| delete json2.textures | |||||
| } | |||||
| if (json.materials) { | |||||
| if (Array.isArray(json.materials)) meta.materials = Object.fromEntries(json.materials.map((m: any) => [m.uuid, m])) | |||||
| else meta.materials = json.materials | |||||
| delete json2.materials | |||||
| } | |||||
| const resources = await this.viewer.loadConfigResources(meta) | |||||
| return ThreeSerialization.Deserialize(json2, undefined, resources) | |||||
| } | |||||
| } |
| /* eslint-disable */ | |||||
| // @ts-nocheck | |||||
| // r152 jsm/MTLLoader.js | |||||
| import { | |||||
| Color, | |||||
| DefaultLoadingManager, | |||||
| FileLoader, | |||||
| FrontSide, | |||||
| Loader, | |||||
| LoaderUtils, | |||||
| MeshPhongMaterial, | |||||
| RepeatWrapping, | |||||
| SRGBColorSpace, | |||||
| TextureLoader, | |||||
| Vector2, | |||||
| } from 'three'; | |||||
| /** | |||||
| * Loads a Wavefront .mtl file specifying materials | |||||
| */ | |||||
| class MTLLoader2 extends Loader { | |||||
| constructor(manager) { | |||||
| super(manager); | |||||
| } | |||||
| /** | |||||
| * Loads and parses a MTL asset from a URL. | |||||
| * | |||||
| * @param {String} url - URL to the MTL file. | |||||
| * @param {Function} [onLoad] - Callback invoked with the loaded object. | |||||
| * @param {Function} [onProgress] - Callback for download progress. | |||||
| * @param {Function} [onError] - Callback for download errors. | |||||
| * | |||||
| * @link setPath setResourcePath | |||||
| * | |||||
| * @note In order for relative texture references to resolve correctly | |||||
| * you must call setResourcePath() explicitly prior to load. | |||||
| */ | |||||
| load(url, onLoad, onProgress, onError) { | |||||
| const scope = this; | |||||
| const path = (this.path === '') ? LoaderUtils.extractUrlBase(url) : this.path; | |||||
| const loader = new FileLoader(this.manager); | |||||
| loader.setPath(this.path); | |||||
| loader.setRequestHeader(this.requestHeader); | |||||
| loader.setWithCredentials(this.withCredentials); | |||||
| loader.load(url, function (text) { | |||||
| try { | |||||
| onLoad(scope.parse(text, path)); | |||||
| } catch (e) { | |||||
| if (onError) { | |||||
| onError(e); | |||||
| } else { | |||||
| console.error(e); | |||||
| } | |||||
| scope.manager.itemError(url); | |||||
| } | |||||
| }, onProgress, onError); | |||||
| } | |||||
| setMaterialOptions(value) { | |||||
| this.materialOptions = value; | |||||
| return this; | |||||
| } | |||||
| /** | |||||
| * Parses a MTL file. | |||||
| * | |||||
| * @param {String} text - Content of MTL file | |||||
| * @return {MaterialCreator} | |||||
| * | |||||
| * @link setPath setResourcePath | |||||
| * | |||||
| * @note In order for relative texture references to resolve correctly | |||||
| * you must call setResourcePath() explicitly prior to parse. | |||||
| */ | |||||
| parse(text, path) { | |||||
| const lines = text.split('\n'); | |||||
| let info = {}; | |||||
| const delimiter_pattern = /\s+/; | |||||
| const materialsInfo = {}; | |||||
| for (let i = 0; i < lines.length; i++) { | |||||
| let line = lines[i]; | |||||
| line = line.trim(); | |||||
| if (line.length === 0 || line.charAt(0) === '#') { | |||||
| // Blank line or comment ignore | |||||
| continue; | |||||
| } | |||||
| const pos = line.indexOf(' '); | |||||
| let key = (pos >= 0) ? line.substring(0, pos) : line; | |||||
| key = key.toLowerCase(); | |||||
| let value = (pos >= 0) ? line.substring(pos + 1) : ''; | |||||
| value = value.trim(); | |||||
| if (key === 'newmtl') { | |||||
| // New material | |||||
| info = {name: value}; | |||||
| materialsInfo[value] = info; | |||||
| } else { | |||||
| if (key === 'ka' || key === 'kd' || key === 'ks' || key === 'ke') { | |||||
| const ss = value.split(delimiter_pattern, 3); | |||||
| info[key] = [parseFloat(ss[0]), parseFloat(ss[1]), parseFloat(ss[2])]; | |||||
| } else { | |||||
| info[key] = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| const materialCreator = new MaterialCreator(this.resourcePath || path, this.materialOptions); | |||||
| materialCreator.setCrossOrigin(this.crossOrigin); | |||||
| materialCreator.setManager(this.manager); | |||||
| materialCreator.setMaterials(materialsInfo); | |||||
| return materialCreator; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Create a new MTLLoader2.MaterialCreator | |||||
| * @param baseUrl - Url relative to which textures are loaded | |||||
| * @param options - Set of options on how to construct the materials | |||||
| * side: Which side to apply the material | |||||
| * FrontSide (default), THREE.BackSide, THREE.DoubleSide | |||||
| * wrap: What type of wrapping to apply for textures | |||||
| * RepeatWrapping (default), THREE.ClampToEdgeWrapping, THREE.MirroredRepeatWrapping | |||||
| * normalizeRGB: RGBs need to be normalized to 0-1 from 0-255 | |||||
| * Default: false, assumed to be already normalized | |||||
| * ignoreZeroRGBs: Ignore values of RGBs (Ka,Kd,Ks) that are all 0's | |||||
| * Default: false | |||||
| * @constructor | |||||
| */ | |||||
| class MaterialCreator { | |||||
| constructor(baseUrl = '', options = {}) { | |||||
| this.baseUrl = baseUrl; | |||||
| this.options = options; | |||||
| this.materialsInfo = {}; | |||||
| this.materials = {}; | |||||
| this.materialsArray = []; | |||||
| this.nameLookup = {}; | |||||
| this.crossOrigin = 'anonymous'; | |||||
| this.side = (this.options.side !== undefined) ? this.options.side : FrontSide; | |||||
| this.wrap = (this.options.wrap !== undefined) ? this.options.wrap : RepeatWrapping; | |||||
| } | |||||
| setCrossOrigin(value) { | |||||
| this.crossOrigin = value; | |||||
| return this; | |||||
| } | |||||
| setManager(value) { | |||||
| this.manager = value; | |||||
| } | |||||
| setMaterials(materialsInfo) { | |||||
| this.materialsInfo = this.convert(materialsInfo); | |||||
| this.materials = {}; | |||||
| this.materialsArray = []; | |||||
| this.nameLookup = {}; | |||||
| } | |||||
| convert(materialsInfo) { | |||||
| if (!this.options) return materialsInfo; | |||||
| const converted = {}; | |||||
| for (const mn in materialsInfo) { | |||||
| // Convert materials info into normalized form based on options | |||||
| const mat = materialsInfo[mn]; | |||||
| const covmat = {}; | |||||
| converted[mn] = covmat; | |||||
| for (const prop in mat) { | |||||
| let save = true; | |||||
| let value = mat[prop]; | |||||
| const lprop = prop.toLowerCase(); | |||||
| switch (lprop) { | |||||
| case 'kd': | |||||
| case 'ka': | |||||
| case 'ks': | |||||
| // Diffuse color (color under white light) using RGB values | |||||
| if (this.options && this.options.normalizeRGB) { | |||||
| value = [value[0] / 255, value[1] / 255, value[2] / 255]; | |||||
| } | |||||
| if (this.options && this.options.ignoreZeroRGBs) { | |||||
| if (value[0] === 0 && value[1] === 0 && value[2] === 0) { | |||||
| // ignore | |||||
| save = false; | |||||
| } | |||||
| } | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| if (save) { | |||||
| covmat[lprop] = value; | |||||
| } | |||||
| } | |||||
| } | |||||
| return converted; | |||||
| } | |||||
| async preload() { | |||||
| for (const mn in this.materialsInfo) { | |||||
| await this.create(mn); | |||||
| } | |||||
| } | |||||
| getIndex(materialName) { | |||||
| return this.nameLookup[materialName]; | |||||
| } | |||||
| async getAsArray() { | |||||
| let index = 0; | |||||
| for (const mn in this.materialsInfo) { | |||||
| this.materialsArray[index] = await this.create(mn); | |||||
| this.nameLookup[mn] = index; | |||||
| index++; | |||||
| } | |||||
| return this.materialsArray; | |||||
| } | |||||
| async create(materialName) { | |||||
| if (this.materials[materialName] === undefined) { | |||||
| await this.createMaterial_(materialName); | |||||
| } | |||||
| return this.materials[materialName]; | |||||
| } | |||||
| async createMaterial_(materialName) { | |||||
| // Create material | |||||
| const scope = this; | |||||
| const mat = this.materialsInfo[materialName]; | |||||
| const params = { | |||||
| name: materialName, | |||||
| side: this.side | |||||
| }; | |||||
| function resolveURL(baseUrl, url) { | |||||
| if (typeof url !== 'string' || url === '') | |||||
| return ''; | |||||
| // Absolute URL | |||||
| if (/^https?:\/\//i.test(url)) return url; | |||||
| return baseUrl + url; | |||||
| } | |||||
| async function setMapForType(mapType, value) { | |||||
| if (params[mapType]) return; // Keep the first encountered texture | |||||
| const texParams = scope.getTextureParams(value, params); | |||||
| return new Promise((resolve, reject) => { | |||||
| let resolved = false; | |||||
| let res = ()=> (!resolved && (resolved = true) && resolve()) | |||||
| const map = scope.loadTexture(resolveURL(scope.baseUrl, texParams.url), undefined, (map)=>{ | |||||
| params[mapType] = map; | |||||
| res() | |||||
| }, undefined, res); | |||||
| setTimeout(res, 50); // timeout. | |||||
| map.repeat.copy(texParams.scale); | |||||
| map.offset.copy(texParams.offset); | |||||
| map.wrapS = scope.wrap; | |||||
| map.wrapT = scope.wrap; | |||||
| if ( mapType === 'map' || mapType === 'emissiveMap' ) { | |||||
| map.colorSpace = SRGBColorSpace; | |||||
| } | |||||
| }) | |||||
| } | |||||
| /** | |||||
| * | |||||
| * @type {string[]} | |||||
| */ | |||||
| const propList = Array.from(Object.keys(mat?mat:{})); | |||||
| let hasOpacity = propList.includes('d') || propList.includes('D'); | |||||
| for (const prop of propList) { | |||||
| const value = mat[prop]; | |||||
| let n; | |||||
| if (value === '') continue; | |||||
| switch (prop.toLowerCase()) { | |||||
| // Ns is material specular exponent | |||||
| case 'kd': | |||||
| // Diffuse color (color under white light) using RGB values | |||||
| params.color = new Color().fromArray( value ).convertSRGBToLinear(); | |||||
| break; | |||||
| case 'ks': | |||||
| // Specular color (color when light is reflected from shiny surface) using RGB values | |||||
| params.specular = new Color().fromArray( value ).convertSRGBToLinear(); | |||||
| break; | |||||
| case 'ke': | |||||
| // Emissive using RGB values | |||||
| params.emissive = new Color().fromArray( value ).convertSRGBToLinear(); | |||||
| break; | |||||
| case 'map_kd': | |||||
| // Diffuse texture map | |||||
| await setMapForType('map', value); | |||||
| break; | |||||
| case 'map_ks': | |||||
| // Specular map | |||||
| await setMapForType('specularMap', value); | |||||
| break; | |||||
| case 'map_ke': | |||||
| // Emissive map | |||||
| await setMapForType('emissiveMap', value); | |||||
| break; | |||||
| case 'norm': | |||||
| await setMapForType('normalMap', value); | |||||
| break; | |||||
| case 'map_bump': | |||||
| case 'bump': | |||||
| // Bump texture map | |||||
| await setMapForType('bumpMap', value); | |||||
| break; | |||||
| case 'map_d': | |||||
| // Alpha map | |||||
| await setMapForType('alphaMap', value); | |||||
| params.transparent = true; | |||||
| break; | |||||
| case 'ns': | |||||
| // The specular exponent (defines the focus of the specular highlight) | |||||
| // A high exponent results in a tight, concentrated highlight. Ns values normally range from 0 to 1000. | |||||
| params.shininess = parseFloat(value); | |||||
| break; | |||||
| case 'd': | |||||
| n = parseFloat(value); | |||||
| if (n < 1) { | |||||
| params.opacity = n; | |||||
| params.transparent = true; | |||||
| } | |||||
| break; | |||||
| case 'tr': // is this translucency? | |||||
| if (hasOpacity) break; // ignore transparency if opacity is present | |||||
| n = parseFloat(value); | |||||
| if (this.options && this.options.invertTrProperty) n = 1 - n; | |||||
| if (n > 0) { | |||||
| params.opacity = 1 - n; | |||||
| params.transparent = true; | |||||
| } | |||||
| break; | |||||
| default: | |||||
| break; | |||||
| } | |||||
| } | |||||
| this.materials[materialName] = new MeshPhongMaterial(params); | |||||
| return this.materials[materialName]; | |||||
| } | |||||
| getTextureParams(value, matParams) { | |||||
| const texParams = { | |||||
| scale: new Vector2(1, 1), | |||||
| offset: new Vector2(0, 0) | |||||
| }; | |||||
| const items = value.split(/\s+/); | |||||
| let pos; | |||||
| pos = items.indexOf('-bm'); | |||||
| if (pos >= 0) { | |||||
| matParams.bumpScale = parseFloat(items[pos + 1]); | |||||
| items.splice(pos, 2); | |||||
| } | |||||
| pos = items.indexOf('-s'); | |||||
| if (pos >= 0) { | |||||
| texParams.scale.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); | |||||
| items.splice(pos, 4); // we expect 3 parameters here! | |||||
| } | |||||
| pos = items.indexOf('-o'); | |||||
| if (pos >= 0) { | |||||
| texParams.offset.set(parseFloat(items[pos + 1]), parseFloat(items[pos + 2])); | |||||
| items.splice(pos, 4); // we expect 3 parameters here! | |||||
| } | |||||
| texParams.url = items.join(' ').trim(); | |||||
| return texParams; | |||||
| } | |||||
| loadTexture(url, mapping, onLoad, onProgress, onError) { | |||||
| const manager = (this.manager !== undefined) ? this.manager : DefaultLoadingManager; | |||||
| let loader = manager.getHandler(url); | |||||
| if (loader === null) { | |||||
| loader = new TextureLoader(manager); | |||||
| } | |||||
| if (loader.setCrossOrigin) loader.setCrossOrigin(this.crossOrigin); | |||||
| const texture = loader.load(url, onLoad, onProgress, onError); | |||||
| if (mapping !== undefined) texture.mapping = mapping; | |||||
| return texture; | |||||
| } | |||||
| } | |||||
| export {MTLLoader2}; |
| /* eslint-disable */ | |||||
| // @ts-nocheck | |||||
| // threejs r152 OBJLoader. Added auto material loading. | |||||
| import { | |||||
| BufferGeometry, | |||||
| Color, | |||||
| FileLoader, | |||||
| Float32BufferAttribute, | |||||
| Group, | |||||
| LineBasicMaterial, | |||||
| LineSegments, | |||||
| Loader, | |||||
| Material, | |||||
| Mesh, | |||||
| MeshStandardMaterial, | |||||
| Points, | |||||
| PointsMaterial, | |||||
| Vector3 | |||||
| } from 'three'; | |||||
| import {MTLLoader2} from './MTLLoader2' | |||||
| import {ILoader} from '../IImporter' | |||||
| // o object_name | g group_name | |||||
| const _object_pattern = /^[og]\s*(.+)?/; | |||||
| // mtllib file_reference | |||||
| const _material_library_pattern = /^mtllib /; | |||||
| // usemtl material_name | |||||
| const _material_use_pattern = /^usemtl /; | |||||
| // usemap map_name | |||||
| const _map_use_pattern = /^usemap /; | |||||
| const _face_vertex_data_separator_pattern = /\s+/; | |||||
| const _vA = new Vector3(); | |||||
| const _vB = new Vector3(); | |||||
| const _vC = new Vector3(); | |||||
| const _ab = new Vector3(); | |||||
| const _cb = new Vector3(); | |||||
| const _color = new Color(); | |||||
| function ParserState() { | |||||
| const state = { | |||||
| objects: [], | |||||
| object: {}, | |||||
| vertices: [], | |||||
| normals: [], | |||||
| colors: [], | |||||
| uvs: [], | |||||
| materials: {}, | |||||
| materialLibraries: [], | |||||
| startObject: function ( name, fromDeclaration ) { | |||||
| // If the current object (initial from reset) is not from a g/o declaration in the parsed | |||||
| // file. We need to use it for the first parsed g/o to keep things in sync. | |||||
| if ( this.object && this.object.fromDeclaration === false ) { | |||||
| this.object.name = name; | |||||
| this.object.fromDeclaration = ( fromDeclaration !== false ); | |||||
| return; | |||||
| } | |||||
| const previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined ); | |||||
| if ( this.object && typeof this.object._finalize === 'function' ) { | |||||
| this.object._finalize( true ); | |||||
| } | |||||
| this.object = { | |||||
| name: name || '', | |||||
| fromDeclaration: ( fromDeclaration !== false ), | |||||
| geometry: { | |||||
| vertices: [], | |||||
| normals: [], | |||||
| colors: [], | |||||
| uvs: [], | |||||
| hasUVIndices: false | |||||
| }, | |||||
| materials: [], | |||||
| smooth: true, | |||||
| startMaterial: function ( name, libraries ) { | |||||
| const previous = this._finalize( false ); | |||||
| // New usemtl declaration overwrites an inherited material, except if faces were declared | |||||
| // after the material, then it must be preserved for proper MultiMaterial continuation. | |||||
| if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) { | |||||
| this.materials.splice( previous.index, 1 ); | |||||
| } | |||||
| const material = { | |||||
| index: this.materials.length, | |||||
| name: name || '', | |||||
| mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ), | |||||
| smooth: ( previous !== undefined ? previous.smooth : this.smooth ), | |||||
| groupStart: ( previous !== undefined ? previous.groupEnd : 0 ), | |||||
| groupEnd: - 1, | |||||
| groupCount: - 1, | |||||
| inherited: false, | |||||
| clone: function ( index ) { | |||||
| const cloned = { | |||||
| index: ( typeof index === 'number' ? index : this.index ), | |||||
| name: this.name, | |||||
| mtllib: this.mtllib, | |||||
| smooth: this.smooth, | |||||
| groupStart: 0, | |||||
| groupEnd: - 1, | |||||
| groupCount: - 1, | |||||
| inherited: false | |||||
| }; | |||||
| cloned.clone = this.clone.bind( cloned ); | |||||
| return cloned; | |||||
| } | |||||
| }; | |||||
| this.materials.push( material ); | |||||
| return material; | |||||
| }, | |||||
| currentMaterial: function () { | |||||
| if ( this.materials.length > 0 ) { | |||||
| return this.materials[ this.materials.length - 1 ]; | |||||
| } | |||||
| return undefined; | |||||
| }, | |||||
| _finalize: function ( end ) { | |||||
| const lastMultiMaterial = this.currentMaterial(); | |||||
| if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) { | |||||
| lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3; | |||||
| lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart; | |||||
| lastMultiMaterial.inherited = false; | |||||
| } | |||||
| // Ignore objects tail materials if no face declarations followed them before a new o/g started. | |||||
| if ( end && this.materials.length > 1 ) { | |||||
| for ( let mi = this.materials.length - 1; mi >= 0; mi -- ) { | |||||
| if ( this.materials[ mi ].groupCount <= 0 ) { | |||||
| this.materials.splice( mi, 1 ); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Guarantee at least one empty material, this makes the creation later more straight forward. | |||||
| if ( end && this.materials.length === 0 ) { | |||||
| this.materials.push( { | |||||
| name: '', | |||||
| smooth: this.smooth | |||||
| } ); | |||||
| } | |||||
| return lastMultiMaterial; | |||||
| } | |||||
| }; | |||||
| // Inherit previous objects material. | |||||
| // Spec tells us that a declared material must be set to all objects until a new material is declared. | |||||
| // If a usemtl declaration is encountered while this new object is being parsed, it will | |||||
| // overwrite the inherited material. Exception being that there was already face declarations | |||||
| // to the inherited material, then it will be preserved for proper MultiMaterial continuation. | |||||
| if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) { | |||||
| const declared = previousMaterial.clone( 0 ); | |||||
| declared.inherited = true; | |||||
| this.object.materials.push( declared ); | |||||
| } | |||||
| this.objects.push( this.object ); | |||||
| }, | |||||
| finalize: function () { | |||||
| if ( this.object && typeof this.object._finalize === 'function' ) { | |||||
| this.object._finalize( true ); | |||||
| } | |||||
| }, | |||||
| parseVertexIndex: function ( value, len ) { | |||||
| const index = parseInt( value, 10 ); | |||||
| return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; | |||||
| }, | |||||
| parseNormalIndex: function ( value, len ) { | |||||
| const index = parseInt( value, 10 ); | |||||
| return ( index >= 0 ? index - 1 : index + len / 3 ) * 3; | |||||
| }, | |||||
| parseUVIndex: function ( value, len ) { | |||||
| const index = parseInt( value, 10 ); | |||||
| return ( index >= 0 ? index - 1 : index + len / 2 ) * 2; | |||||
| }, | |||||
| addVertex: function ( a, b, c ) { | |||||
| const src = this.vertices; | |||||
| const dst = this.object.geometry.vertices; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); | |||||
| dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); | |||||
| dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); | |||||
| }, | |||||
| addVertexPoint: function ( a ) { | |||||
| const src = this.vertices; | |||||
| const dst = this.object.geometry.vertices; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); | |||||
| }, | |||||
| addVertexLine: function ( a ) { | |||||
| const src = this.vertices; | |||||
| const dst = this.object.geometry.vertices; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); | |||||
| }, | |||||
| addNormal: function ( a, b, c ) { | |||||
| const src = this.normals; | |||||
| const dst = this.object.geometry.normals; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); | |||||
| dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); | |||||
| dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); | |||||
| }, | |||||
| addFaceNormal: function ( a, b, c ) { | |||||
| const src = this.vertices; | |||||
| const dst = this.object.geometry.normals; | |||||
| _vA.fromArray( src, a ); | |||||
| _vB.fromArray( src, b ); | |||||
| _vC.fromArray( src, c ); | |||||
| _cb.subVectors( _vC, _vB ); | |||||
| _ab.subVectors( _vA, _vB ); | |||||
| _cb.cross( _ab ); | |||||
| _cb.normalize(); | |||||
| dst.push( _cb.x, _cb.y, _cb.z ); | |||||
| dst.push( _cb.x, _cb.y, _cb.z ); | |||||
| dst.push( _cb.x, _cb.y, _cb.z ); | |||||
| }, | |||||
| addColor: function ( a, b, c ) { | |||||
| const src = this.colors; | |||||
| const dst = this.object.geometry.colors; | |||||
| if ( src[ a ] !== undefined ) dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] ); | |||||
| if ( src[ b ] !== undefined ) dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] ); | |||||
| if ( src[ c ] !== undefined ) dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] ); | |||||
| }, | |||||
| addUV: function ( a, b, c ) { | |||||
| const src = this.uvs; | |||||
| const dst = this.object.geometry.uvs; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ] ); | |||||
| dst.push( src[ b + 0 ], src[ b + 1 ] ); | |||||
| dst.push( src[ c + 0 ], src[ c + 1 ] ); | |||||
| }, | |||||
| addDefaultUV: function () { | |||||
| const dst = this.object.geometry.uvs; | |||||
| dst.push( 0, 0 ); | |||||
| dst.push( 0, 0 ); | |||||
| dst.push( 0, 0 ); | |||||
| }, | |||||
| addUVLine: function ( a ) { | |||||
| const src = this.uvs; | |||||
| const dst = this.object.geometry.uvs; | |||||
| dst.push( src[ a + 0 ], src[ a + 1 ] ); | |||||
| }, | |||||
| addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) { | |||||
| const vLen = this.vertices.length; | |||||
| let ia = this.parseVertexIndex( a, vLen ); | |||||
| let ib = this.parseVertexIndex( b, vLen ); | |||||
| let ic = this.parseVertexIndex( c, vLen ); | |||||
| this.addVertex( ia, ib, ic ); | |||||
| this.addColor( ia, ib, ic ); | |||||
| // normals | |||||
| if ( na !== undefined && na !== '' ) { | |||||
| const nLen = this.normals.length; | |||||
| ia = this.parseNormalIndex( na, nLen ); | |||||
| ib = this.parseNormalIndex( nb, nLen ); | |||||
| ic = this.parseNormalIndex( nc, nLen ); | |||||
| this.addNormal( ia, ib, ic ); | |||||
| } else { | |||||
| this.addFaceNormal( ia, ib, ic ); | |||||
| } | |||||
| // uvs | |||||
| if ( ua !== undefined && ua !== '' ) { | |||||
| const uvLen = this.uvs.length; | |||||
| ia = this.parseUVIndex( ua, uvLen ); | |||||
| ib = this.parseUVIndex( ub, uvLen ); | |||||
| ic = this.parseUVIndex( uc, uvLen ); | |||||
| this.addUV( ia, ib, ic ); | |||||
| this.object.geometry.hasUVIndices = true; | |||||
| } else { | |||||
| // add placeholder values (for inconsistent face definitions) | |||||
| this.addDefaultUV(); | |||||
| } | |||||
| }, | |||||
| addPointGeometry: function ( vertices ) { | |||||
| this.object.geometry.type = 'Points'; | |||||
| const vLen = this.vertices.length; | |||||
| for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) { | |||||
| const index = this.parseVertexIndex( vertices[ vi ], vLen ); | |||||
| this.addVertexPoint( index ); | |||||
| this.addColor( index ); | |||||
| } | |||||
| }, | |||||
| addLineGeometry: function ( vertices, uvs ) { | |||||
| this.object.geometry.type = 'Line'; | |||||
| const vLen = this.vertices.length; | |||||
| const uvLen = this.uvs.length; | |||||
| for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) { | |||||
| this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) ); | |||||
| } | |||||
| for ( let uvi = 0, l = uvs.length; uvi < l; uvi ++ ) { | |||||
| this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) ); | |||||
| } | |||||
| } | |||||
| }; | |||||
| state.startObject( '', false ); | |||||
| return state; | |||||
| } | |||||
| // | |||||
| class OBJLoader2 extends Loader implements ILoader{ | |||||
| constructor( manager ) { | |||||
| super( manager ); | |||||
| this.materials = null; | |||||
| } | |||||
| load( url, onLoad, onProgress, onError ) { | |||||
| const scope = this; | |||||
| const loader = new FileLoader( this.manager ); | |||||
| loader.setPath( this.path ); | |||||
| loader.setRequestHeader( this.requestHeader ); | |||||
| loader.setWithCredentials( this.withCredentials ); | |||||
| this.currentUrl = url; | |||||
| loader.load( url, async function ( text ) { | |||||
| try { | |||||
| onLoad( await scope.parse( text ) ); | |||||
| } catch ( e ) { | |||||
| if ( onError ) { | |||||
| onError( e ); | |||||
| } else { | |||||
| console.error( e ); | |||||
| } | |||||
| scope.manager.itemError( url ); | |||||
| } | |||||
| this.currentUrl = undefined; | |||||
| }, onProgress, onError ); | |||||
| } | |||||
| declare loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any> | |||||
| setMaterials( materials ) { | |||||
| this.materials = materials; | |||||
| return this; | |||||
| } | |||||
| async parse( text ) { | |||||
| const state = new ParserState(); | |||||
| if ( text.indexOf( '\r\n' ) !== - 1 ) { | |||||
| // This is faster than String.split with regex that splits on both | |||||
| text = text.replace( /\r\n/g, '\n' ); | |||||
| } | |||||
| if ( text.indexOf( '\\\n' ) !== - 1 ) { | |||||
| // join lines separated by a line continuation character (\) | |||||
| text = text.replace( /\\\n/g, '' ); | |||||
| } | |||||
| const lines = text.split( '\n' ); | |||||
| let result = []; | |||||
| for ( let i = 0, l = lines.length; i < l; i ++ ) { | |||||
| const line = lines[ i ].trimStart(); | |||||
| if ( line.length === 0 ) continue; | |||||
| const lineFirstChar = line.charAt( 0 ); | |||||
| // @todo invoke passed in handler if any | |||||
| if ( lineFirstChar === '#' ) continue; // skip comments | |||||
| if ( lineFirstChar === 'v' ) { | |||||
| const data = line.split( _face_vertex_data_separator_pattern ); | |||||
| switch ( data[ 0 ] ) { | |||||
| case 'v': | |||||
| state.vertices.push( | |||||
| parseFloat( data[ 1 ] ), | |||||
| parseFloat( data[ 2 ] ), | |||||
| parseFloat( data[ 3 ] ) | |||||
| ); | |||||
| if ( data.length >= 7 ) { | |||||
| _color.setRGB( | |||||
| parseFloat( data[ 4 ] ), | |||||
| parseFloat( data[ 5 ] ), | |||||
| parseFloat( data[ 6 ] ) | |||||
| ).convertSRGBToLinear(); | |||||
| state.colors.push( _color.r, _color.g, _color.b ); | |||||
| } else { | |||||
| // if no colors are defined, add placeholders so color and vertex indices match | |||||
| state.colors.push( undefined, undefined, undefined ); | |||||
| } | |||||
| break; | |||||
| case 'vn': | |||||
| state.normals.push( | |||||
| parseFloat( data[ 1 ] ), | |||||
| parseFloat( data[ 2 ] ), | |||||
| parseFloat( data[ 3 ] ) | |||||
| ); | |||||
| break; | |||||
| case 'vt': | |||||
| state.uvs.push( | |||||
| parseFloat( data[ 1 ] ), | |||||
| parseFloat( data[ 2 ] ) | |||||
| ); | |||||
| break; | |||||
| } | |||||
| } else if ( lineFirstChar === 'f' ) { | |||||
| const lineData = line.slice( 1 ).trim(); | |||||
| const vertexData = lineData.split( _face_vertex_data_separator_pattern ); | |||||
| const faceVertices = []; | |||||
| // Parse the face vertex data into an easy to work with format | |||||
| for ( let j = 0, jl = vertexData.length; j < jl; j ++ ) { | |||||
| const vertex = vertexData[ j ]; | |||||
| if ( vertex.length > 0 ) { | |||||
| const vertexParts = vertex.split( '/' ); | |||||
| faceVertices.push( vertexParts ); | |||||
| } | |||||
| } | |||||
| // Draw an edge between the first vertex and all subsequent vertices to form an n-gon | |||||
| const v1 = faceVertices[ 0 ]; | |||||
| for ( let j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) { | |||||
| const v2 = faceVertices[ j ]; | |||||
| const v3 = faceVertices[ j + 1 ]; | |||||
| state.addFace( | |||||
| v1[ 0 ], v2[ 0 ], v3[ 0 ], | |||||
| v1[ 1 ], v2[ 1 ], v3[ 1 ], | |||||
| v1[ 2 ], v2[ 2 ], v3[ 2 ] | |||||
| ); | |||||
| } | |||||
| } else if ( lineFirstChar === 'l' ) { | |||||
| const lineParts = line.substring( 1 ).trim().split( ' ' ); | |||||
| let lineVertices = []; | |||||
| const lineUVs = []; | |||||
| if ( line.indexOf( '/' ) === - 1 ) { | |||||
| lineVertices = lineParts; | |||||
| } else { | |||||
| for ( let li = 0, llen = lineParts.length; li < llen; li ++ ) { | |||||
| const parts = lineParts[ li ].split( '/' ); | |||||
| if ( parts[ 0 ] !== '' ) lineVertices.push( parts[ 0 ] ); | |||||
| if ( parts[ 1 ] !== '' ) lineUVs.push( parts[ 1 ] ); | |||||
| } | |||||
| } | |||||
| state.addLineGeometry( lineVertices, lineUVs ); | |||||
| } else if ( lineFirstChar === 'p' ) { | |||||
| const lineData = line.slice( 1 ).trim(); | |||||
| const pointData = lineData.split( ' ' ); | |||||
| state.addPointGeometry( pointData ); | |||||
| } else if ( ( result = _object_pattern.exec( line ) ) !== null ) { | |||||
| // o object_name | |||||
| // or | |||||
| // g group_name | |||||
| // WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869 | |||||
| // let name = result[ 0 ].slice( 1 ).trim(); | |||||
| const name = ( ' ' + result[ 0 ].slice( 1 ).trim() ).slice( 1 ); | |||||
| state.startObject( name ); | |||||
| } else if ( _material_use_pattern.test( line ) ) { | |||||
| // material | |||||
| state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries ); | |||||
| } else if ( _material_library_pattern.test( line ) ) { | |||||
| // mtl file | |||||
| state.materialLibraries.push( line.substring( 7 ).trim() ); | |||||
| let mtl_file = line.substring(7).trim() | |||||
| let handler = this.manager.getHandler(mtl_file) | |||||
| if(!handler && mtl_file.trim().split('?')[0].endsWith('.mtl')) { | |||||
| handler = new MTLLoader2(this.manager) | |||||
| handler.setPath(this.path) | |||||
| handler.setWithCredentials(this.withCredentials) | |||||
| handler.setRequestHeader(this.requestHeader) | |||||
| handler.setResourcePath(this.resourcePath) | |||||
| handler.setCrossOrigin(this.crossOrigin) | |||||
| } | |||||
| if(this.currentUrl.startsWith('http') && !mtl_file.startsWith('http')) { | |||||
| mtl_file = this.currentUrl.substring(0, this.currentUrl.lastIndexOf('/') + 1) + mtl_file | |||||
| } | |||||
| if (!handler) { | |||||
| console.warn("OBJLoader2: Regiser MTLLoader2 or any other material loader to loading manager to load the material file:", mtl_file) | |||||
| } else { | |||||
| const materials = await handler.loadAsync(mtl_file).catch(reason => { | |||||
| console.warn(reason) | |||||
| }) | |||||
| if (materials) this.setMaterials(materials); | |||||
| } | |||||
| } else if ( _map_use_pattern.test( line ) ) { | |||||
| // the line is parsed but ignored since the loader assumes textures are defined MTL files | |||||
| // (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method) | |||||
| console.warn('OBJLoader2: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.'); | |||||
| } else if ( lineFirstChar === 's' ) { | |||||
| result = line.split( ' ' ); | |||||
| // smooth shading | |||||
| // @todo Handle files that have varying smooth values for a set of faces inside one geometry, | |||||
| // but does not define a usemtl for each face set. | |||||
| // This should be detected and a dummy material created (later MultiMaterial and geometry groups). | |||||
| // This requires some care to not create extra material on each smooth value for "normal" obj files. | |||||
| // where explicit usemtl defines geometry groups. | |||||
| // Example asset: examples/models/obj/cerberus/Cerberus.obj | |||||
| /* | |||||
| * http://paulbourke.net/dataformats/obj/ | |||||
| * or | |||||
| * http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf | |||||
| * | |||||
| * From chapter "Grouping" Syntax explanation "s group_number": | |||||
| * "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off. | |||||
| * Polygonal elements use group numbers to put elements in different smoothing groups. For free-form | |||||
| * surfaces, smoothing groups are either turned on or off; there is no difference between values greater | |||||
| * than 0." | |||||
| */ | |||||
| if ( result.length > 1 ) { | |||||
| const value = result[ 1 ].trim().toLowerCase(); | |||||
| state.object.smooth = ( value !== '0' && value !== 'off' ); | |||||
| } else { | |||||
| // ZBrush can produce "s" lines #11707 | |||||
| state.object.smooth = true; | |||||
| } | |||||
| const material = state.object.currentMaterial(); | |||||
| if ( material ) material.smooth = state.object.smooth; | |||||
| } else { | |||||
| // Handle null terminated files without exception | |||||
| if ( line === '\0' ) continue; | |||||
| console.warn( 'THREE.OBJLoader: Unexpected line: "' + line + '"' ); | |||||
| } | |||||
| } | |||||
| state.finalize(); | |||||
| const container = new Group(); | |||||
| container.materialLibraries = [].concat( state.materialLibraries ); | |||||
| const hasPrimitives = ! ( state.objects.length === 1 && state.objects[ 0 ].geometry.vertices.length === 0 ); | |||||
| if ( hasPrimitives === true ) { | |||||
| for ( let i = 0, l = state.objects.length; i < l; i ++ ) { | |||||
| const object = state.objects[ i ]; | |||||
| const geometry = object.geometry; | |||||
| const materials = object.materials; | |||||
| const isLine = ( geometry.type === 'Line' ); | |||||
| const isPoints = ( geometry.type === 'Points' ); | |||||
| let hasVertexColors = false; | |||||
| // Skip o/g line declarations that did not follow with any faces | |||||
| if ( geometry.vertices.length === 0 ) continue; | |||||
| const buffergeometry = new BufferGeometry(); | |||||
| buffergeometry.setAttribute( 'position', new Float32BufferAttribute( geometry.vertices, 3 ) ); | |||||
| if ( geometry.normals.length > 0 ) { | |||||
| buffergeometry.setAttribute( 'normal', new Float32BufferAttribute( geometry.normals, 3 ) ); | |||||
| } | |||||
| if ( geometry.colors.length > 0 ) { | |||||
| hasVertexColors = true; | |||||
| buffergeometry.setAttribute( 'color', new Float32BufferAttribute( geometry.colors, 3 ) ); | |||||
| } | |||||
| if ( geometry.hasUVIndices === true ) { | |||||
| buffergeometry.setAttribute( 'uv', new Float32BufferAttribute( geometry.uvs, 2 ) ); | |||||
| } | |||||
| // Create materials | |||||
| const createdMaterials = []; | |||||
| for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { | |||||
| const sourceMaterial = materials[ mi ]; | |||||
| const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors; | |||||
| let material = state.materials[ materialHash ]; | |||||
| if ( this.materials !== null ) { | |||||
| material = await this.materials.create( sourceMaterial.name ); | |||||
| // mtl etc. loaders probably can't create line materials correctly, copy properties to a line material. | |||||
| if ( isLine && material && ! ( material instanceof LineBasicMaterial ) ) { | |||||
| const materialLine = new LineBasicMaterial(); | |||||
| Material.prototype.copy.call( materialLine, material ); | |||||
| materialLine.color.copy( material.color ); | |||||
| material = materialLine; | |||||
| } else if ( isPoints && material && ! ( material instanceof PointsMaterial ) ) { | |||||
| const materialPoints = new PointsMaterial( { size: 10, sizeAttenuation: false } ); | |||||
| Material.prototype.copy.call( materialPoints, material ); | |||||
| materialPoints.color.copy( material.color ); | |||||
| materialPoints.map = material.map; | |||||
| material = materialPoints; | |||||
| } | |||||
| } | |||||
| if ( material === undefined ) { | |||||
| if ( isLine ) { | |||||
| material = new LineBasicMaterial(); | |||||
| } else if ( isPoints ) { | |||||
| material = new PointsMaterial( { size: 1, sizeAttenuation: false } ); | |||||
| } else { | |||||
| material = new MeshStandardMaterial(); | |||||
| } | |||||
| material.name = sourceMaterial.name; | |||||
| material.flatShading = sourceMaterial.smooth ? false : true; | |||||
| material.vertexColors = hasVertexColors; | |||||
| state.materials[ materialHash ] = material; | |||||
| } | |||||
| createdMaterials.push( material ); | |||||
| } | |||||
| // Create mesh | |||||
| let mesh; | |||||
| if ( createdMaterials.length > 1 ) { | |||||
| for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) { | |||||
| const sourceMaterial = materials[ mi ]; | |||||
| buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi ); | |||||
| } | |||||
| if ( isLine ) { | |||||
| mesh = new LineSegments( buffergeometry, createdMaterials ); | |||||
| } else if ( isPoints ) { | |||||
| mesh = new Points( buffergeometry, createdMaterials ); | |||||
| } else { | |||||
| mesh = new Mesh( buffergeometry, createdMaterials ); | |||||
| } | |||||
| } else { | |||||
| if ( isLine ) { | |||||
| mesh = new LineSegments( buffergeometry, createdMaterials[ 0 ] ); | |||||
| } else if ( isPoints ) { | |||||
| mesh = new Points( buffergeometry, createdMaterials[ 0 ] ); | |||||
| } else { | |||||
| mesh = new Mesh( buffergeometry, createdMaterials[ 0 ] ); | |||||
| } | |||||
| } | |||||
| mesh.name = object.name; | |||||
| container.add( mesh ); | |||||
| } | |||||
| } else { | |||||
| // if there is only the default parser state object with no geometry data, interpret data as point cloud | |||||
| if ( state.vertices.length > 0 ) { | |||||
| const material = new PointsMaterial( { size: 1, sizeAttenuation: false } ); | |||||
| const buffergeometry = new BufferGeometry(); | |||||
| buffergeometry.setAttribute( 'position', new Float32BufferAttribute( state.vertices, 3 ) ); | |||||
| if ( state.colors.length > 0 && state.colors[ 0 ] !== undefined ) { | |||||
| buffergeometry.setAttribute( 'color', new Float32BufferAttribute( state.colors, 3 ) ); | |||||
| material.vertexColors = true; | |||||
| } | |||||
| const points = new Points( buffergeometry, material ); | |||||
| container.add( points ); | |||||
| } | |||||
| } | |||||
| return container; | |||||
| } | |||||
| } | |||||
| export { OBJLoader2 }; |
| import { | |||||
| DataTexture, | |||||
| DataUtils, | |||||
| FileLoader, | |||||
| FloatType, | |||||
| HalfFloatType, | |||||
| LinearFilter, | |||||
| LoadingManager, | |||||
| RGBAFormat, | |||||
| SRGBColorSpace, | |||||
| TextureDataType, | |||||
| } from 'three' | |||||
| import {imageUrlToImageData} from 'ts-browser-helpers' | |||||
| /** | |||||
| * 8bit HDR image in png format | |||||
| * not properly working with files from hdrpng.js but used in {@link GLTFViewerConfigExtension}, so a slightly modified version is used here | |||||
| */ | |||||
| export class RGBEPNGLoader extends FileLoader { | |||||
| type: TextureDataType = HalfFloatType | |||||
| constructor(manager?: LoadingManager) { | |||||
| super(manager) | |||||
| } | |||||
| async loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<any> { | |||||
| const image = await this.parseAsync(url, onProgress, false) | |||||
| const texture = new DataTexture(image.data, image.width, image.height, RGBAFormat, this.type) | |||||
| texture.needsUpdate = true | |||||
| texture.flipY = true | |||||
| texture.colorSpace = SRGBColorSpace | |||||
| texture.minFilter = LinearFilter | |||||
| texture.magFilter = LinearFilter | |||||
| texture.source.data.complete = true | |||||
| return texture | |||||
| } | |||||
| async parseAsync(url: string, onProgress?: (event: ProgressEvent) => void, isFloat16Data = false): Promise<any> { | |||||
| let created = false | |||||
| if (!url.startsWith('data:') && !url.startsWith('blob:')) { | |||||
| this.responseType = 'blob' | |||||
| const blob = await super.loadAsync(url, onProgress) as any as Blob | |||||
| // url = await blobToDataURL(blob) | |||||
| // console.log(url) | |||||
| // url = url.replace('application/octet-stream', 'image/png') | |||||
| url = URL.createObjectURL(blob) | |||||
| created = true | |||||
| } | |||||
| const imageData = await imageUrlToImageData(url) | |||||
| if (created) URL.revokeObjectURL(url) | |||||
| let aType: any = Uint8Array | |||||
| if (this.type === HalfFloatType) aType = Uint16Array | |||||
| else if (this.type === FloatType) aType = Uint32Array | |||||
| const buffer = rgbeToHalfFloat(imageData.data, 4, aType, isFloat16Data) | |||||
| return {data: buffer, width: imageData.width, height: imageData.height} | |||||
| } | |||||
| setDataType(value: TextureDataType) { | |||||
| this.type = value | |||||
| return this | |||||
| } | |||||
| } | |||||
| // adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L253 | |||||
| // channels = 4 for RGBA data or 3 for RGB data. res to use with THREE.DataTexture | |||||
| function rgbeToHalfFloat(buffer: Uint8ClampedArray, channels = 3, type = Uint16Array, float16Data = false): Uint16Array { | |||||
| let s | |||||
| const l = buffer.byteLength >> 2 | |||||
| const res = new type(l * channels) | |||||
| for (let i = 0;i < l;i++) { | |||||
| s = Math.pow(2, buffer[i * 4 + 3] - (128 + 8)) | |||||
| if (float16Data) { | |||||
| res[ i * channels ] = Math.min(buffer[i * 4] * s, 65504) | |||||
| res[ i * channels + 1] = Math.min(buffer[i * 4 + 1] * s, 65504) | |||||
| res[ i * channels + 2] = Math.min(buffer[i * 4 + 2] * s, 65504) | |||||
| } else { | |||||
| res[i * channels] = DataUtils.toHalfFloat(Math.min(buffer[i * 4] * s, 65504)) | |||||
| res[i * channels + 1] = DataUtils.toHalfFloat(Math.min(buffer[i * 4 + 1] * s, 65504)) | |||||
| res[i * channels + 2] = DataUtils.toHalfFloat(Math.min(buffer[i * 4 + 2] * s, 65504)) | |||||
| } | |||||
| // res[i * channels] = Math.min(15360, buffer[i * 4] * s) | |||||
| // res[i * channels + 1] = Math.min(15360, buffer[i * 4 + 1] * s) | |||||
| // res[i * channels + 2] = Math.min(15360, buffer[i * 4 + 2] * s) | |||||
| if (channels === 4) res[i * channels + 3] = DataUtils.toHalfFloat(1) // alpha is always 1 // todo: handle for uint8 and float32 | |||||
| } | |||||
| return res | |||||
| } | |||||
| import {FileLoader} from 'three' | |||||
| export class SimpleJSONLoader extends FileLoader { | |||||
| load(url: string, onLoad?: (response: (any)) => void, onProgress?: (request: ProgressEvent) => void, onError?: (event: ErrorEvent) => void): any { | |||||
| return super.load(url, (res)=>{ | |||||
| try { | |||||
| if (typeof res === 'string') { | |||||
| onLoad?.(JSON.parse(res)) | |||||
| } else { | |||||
| throw new Error('Invalid JSON') | |||||
| } | |||||
| } catch (e: any) { | |||||
| onError?.(e) | |||||
| } | |||||
| }, onProgress, onError) | |||||
| } | |||||
| } | |||||
| import {FileLoader} from 'three' | |||||
| import {unzipSync} from 'three/examples/jsm/libs/fflate.module.js' | |||||
| export class ZipLoader extends FileLoader { | |||||
| load(url: string, onLoad?: (t: any) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: ErrorEvent) => void): void { | |||||
| this.setResponseType('arraybuffer') | |||||
| return super.load(url, (buffer: any)=>{ | |||||
| // const files = blob.arrayBuffer().then(buff => ) | |||||
| const files = unzipSync(new Uint8Array(buffer)) | |||||
| const map = new Map<string, File>(Object.entries(files).map(([path, fileBuffer]) => { | |||||
| return [path, new File([fileBuffer as any], path)] | |||||
| })) | |||||
| onLoad?.(map) | |||||
| }, onProgress, onError) | |||||
| } | |||||
| } |
| export {JSONMaterialLoader} from './JSONMaterialLoader' | |||||
| export {SimpleJSONLoader} from './SimpleJSONLoader' | |||||
| export {MTLLoader2} from './MTLLoader2' | |||||
| export {OBJLoader2} from './OBJLoader2' | |||||
| export {ZipLoader} from './ZipLoader' | |||||
| export {GLTFLoader2} from './GLTFLoader2' | |||||
| export {DRACOLoader2} from './DRACOLoader2' | |||||
| export {RGBEPNGLoader} from './RGBEPNGLoader' |
| export {AssetImporter} from './AssetImporter' | |||||
| export {AssetExporter} from './AssetExporter' | |||||
| export {AssetManager} from './AssetManager' | |||||
| export {Importer} from './Importer' | |||||
| export {MaterialManager} from './MaterialManager' | |||||
| export type {AssetManagerOptions, AddRawOptions, ImportAddOptions} from './AssetManager' | |||||
| export type {IAsset, IFile, IAssetID, IAssetList} from './IAsset' | |||||
| export type {ImportResult, IImportResultUserData, ImportResultObject, IAssetImporter, IAssetImporterEventTypes, ImportAssetOptions, ImportFilesOptions, LoadFileOptions, ProcessRawOptions, RootSceneImportResult, ImportResultExtras} from './IAssetImporter' | |||||
| export type {IAssetExporter, IExporter, IExportParser, ExportFileOptions, BlobExt} from './IExporter' | |||||
| export type {IImporter, ILoader} from './IImporter' | |||||
| export * from './import/index' | |||||
| export * from './export/index' | |||||
| export * from './gltf/index' |
| import {Camera, Vector3} from 'three' | |||||
| import {IObject3D, IObject3DEvent, IObject3DEventTypes, IObject3DUserData, IObjectSetDirtyOptions} from './IObject' | |||||
| import {IShaderPropertiesUpdater} from '../materials' | |||||
| import {ICameraControls, TControlsCtor} from './camera/ICameraControls' | |||||
| /** | |||||
| * Available modes for {@link ICamera.controlsMode} property. | |||||
| * This is defined just for autocomplete, these and any other control type can be added by plugins | |||||
| */ | |||||
| export type TCameraControlsMode = '' | 'orbit' | 'deviceOrientation' | 'firstPerson' | 'pointerLock' | string | |||||
| export interface ICameraUserData extends IObject3DUserData { | |||||
| autoNearFar?: boolean // default = true | |||||
| minNearPlane?: number // default = 0.2 | |||||
| maxFarPlane?: number // default = 1000 | |||||
| autoLookAtTarget?: boolean // default = false, only for when controls and interactions are disabled | |||||
| __lastScale?: Vector3, | |||||
| __isMainCamera?: boolean, | |||||
| // [key: string]: any // commented for noe | |||||
| } | |||||
| export interface ICamera<E extends ICameraEvent = ICameraEvent, ET extends ICameraEventTypes = ICameraEventTypes> extends Camera<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | |||||
| assetType: 'camera' | |||||
| readonly isCamera: true | |||||
| setDirty(options?: ICameraSetDirtyOptions): void; | |||||
| near: number; | |||||
| far: number; | |||||
| readonly isMainCamera: boolean; | |||||
| activateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | |||||
| deactivateMain(options?: Partial<ICameraEvent>, _internal?: boolean, _refresh?: boolean): void; | |||||
| /** | |||||
| * @deprecated use `this` instead | |||||
| */ | |||||
| cameraObject: this | |||||
| readonly controls: ICameraControls|undefined; | |||||
| // getControls<T extends TControls>(): T|undefined; | |||||
| refreshTarget(): void; | |||||
| refreshAspect(setDirty?: boolean): void; | |||||
| /** | |||||
| * Target of camera, in world(global) coordinates. | |||||
| */ | |||||
| target: Vector3, | |||||
| /** | |||||
| * Local position of camera. | |||||
| */ | |||||
| position: Vector3, | |||||
| interactionsEnabled: boolean; | |||||
| readonly canUserInteract: boolean; | |||||
| zoom: number; | |||||
| /** | |||||
| * Camera frustum aspect ratio, window width divided by window height. | |||||
| * It can be managed internally if {@link autoAspect} is true. | |||||
| * @default 1 | |||||
| */ | |||||
| aspect: number; | |||||
| /** | |||||
| * Automatically manage aspect ratio based on window/canvas size. | |||||
| */ | |||||
| autoAspect: boolean; | |||||
| controlsMode?: TCameraControlsMode; // todo add more. | |||||
| // controlsEnabled: boolean; // use controlsMode = '' instead | |||||
| // todo | |||||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||||
| userData: ICameraUserData | |||||
| /** | |||||
| * @deprecated use {@link isMainCamera} instead | |||||
| */ | |||||
| isActiveCamera: boolean; | |||||
| setControlsCtor(key: string, ctor: TControlsCtor, replace?: boolean): void; | |||||
| removeControlsCtor(key: string): void; | |||||
| refreshCameraControls(setDirty?: boolean): void | |||||
| // region inherited type fixes | |||||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | |||||
| traverse(callback: (object: IObject3D) => void): void | |||||
| traverseVisible(callback: (object: IObject3D) => void): void | |||||
| traverseAncestors(callback: (object: IObject3D) => void): void | |||||
| getObjectById(id: number): IObject3D | undefined | |||||
| getObjectByName(name: string): IObject3D | undefined | |||||
| getObjectByProperty(name: string, value: string): IObject3D | undefined | |||||
| copy(source: this, recursive?: boolean, distanceFromTarget?: number, ...args: any[]): this | |||||
| clone(recursive?: boolean): this | |||||
| add(...object: IObject3D[]): this | |||||
| remove(...object: IObject3D[]): this | |||||
| parent: IObject3D | null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| } | |||||
| export type ICameraEventTypes = IObject3DEventTypes | 'update'// | string | |||||
| export type ICameraEvent = Omit<IObject3DEvent, 'type'> & { | |||||
| type: ICameraEventTypes | |||||
| camera?: ICamera | null | |||||
| // change?: string | |||||
| } | |||||
| export type ICameraSetDirtyOptions = IObjectSetDirtyOptions | |||||
| import {BufferGeometry, Event, NormalBufferAttributes, NormalOrGLBufferAttributes} from 'three' | |||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {IObject3D} from './IObject' | |||||
| import {IImportResultUserData} from '../assetmanager' | |||||
| export interface IGeometryUserData extends IImportResultUserData{ | |||||
| disposeOnIdle?: boolean // default: true | |||||
| // [key: string]: any // commented for noe | |||||
| } | |||||
| export interface IGeometry<Attributes extends NormalOrGLBufferAttributes = NormalBufferAttributes> extends BufferGeometry<Attributes, IGeometryEvent, IGeometryEventTypes>, IUiConfigContainer { | |||||
| assetType: 'geometry' | |||||
| setDirty(options?: IGeometrySetDirtyOptions): void; | |||||
| refreshUi(): void; | |||||
| uiConfig?: UiObjectConfig | |||||
| appliedMeshes: Set<IObject3D> | |||||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||||
| userData: IGeometryUserData | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _uiConfig?: UiObjectConfig | |||||
| } | |||||
| export type IGeometryEventTypes = 'dispose' | 'geometryUpdate' // | string | |||||
| export type IGeometryEvent<T extends string = IGeometryEventTypes> = Event & { | |||||
| type: T | |||||
| bubbleToObject?: boolean // bubble event to parent root | |||||
| geometry: IGeometry | |||||
| // change?: string | |||||
| } | |||||
| export type IGeometrySetDirtyOptions = AnyOptions & {bubbleToObject?: boolean} | |||||
| import type {Event, IUniform, Material, MaterialParameters, Shader} from 'three' | |||||
| import type {AnyOptions, IDisposable, IJSONSerializable} from 'ts-browser-helpers' | |||||
| import type {MaterialExtension} from '../materials' | |||||
| import type {IUiConfigContainer} from 'uiconfig.js' | |||||
| import type {SerializationMetaType} from '../utils/serialization' | |||||
| import type {IObject3D} from './IObject' | |||||
| import type {ITexture} from './ITexture' | |||||
| import type {IImportResultUserData} from '../assetmanager' | |||||
| export type IMaterialParameters = MaterialParameters & {customMaterialExtensions?: MaterialExtension[]} | |||||
| export type IMaterialEventTypes = 'dispose' | 'materialUpdate' | 'beforeRender' | 'beforeCompile' | 'afterRender' | 'textureChanged' | |||||
| export type IMaterialEvent<T extends string = IMaterialEventTypes> = Event & { | |||||
| type: T | |||||
| bubbleToObject?: boolean | |||||
| material?: IMaterial | |||||
| texture?: ITexture | |||||
| oldTexture?: ITexture | |||||
| } | |||||
| export type IMaterialSetDirtyOptions = AnyOptions & {bubbleToObject?: boolean} | |||||
| export interface IMaterialUserData extends IImportResultUserData{ | |||||
| uuid?: string // adding to userdata also, so that its saved in gltf | |||||
| disposeOnIdle?: boolean // default: true | |||||
| renderToGBuffer?: boolean | |||||
| /** | |||||
| * Same as {@link renderToGBuffer} for now | |||||
| */ | |||||
| renderToDepth?: boolean | |||||
| // only for materials that have envMapIntensity | |||||
| separateEnvMapIntensity?: boolean // default: false | |||||
| cloneId?: string | |||||
| cloneCount?: number | |||||
| __envIntensity?: number // temp storage for envMapIntensity while rendering | |||||
| __isVariation?: boolean | |||||
| inverseAlphaMap?: boolean // only for physical material right now | |||||
| // [key: string]: any // commented for noe | |||||
| // legacy, to be removed | |||||
| setDirty?: (options?: IMaterialSetDirtyOptions) => void | |||||
| } | |||||
| export interface IMaterial<E extends IMaterialEvent = IMaterialEvent, ET = IMaterialEventTypes> extends Material<E, ET>, IJSONSerializable, IDisposable, IUiConfigContainer { | |||||
| constructor: { | |||||
| TYPE: string | |||||
| TypeSlug: string | |||||
| MaterialProperties?: Record<string, any> | |||||
| MaterialTemplate?: IMaterialTemplate | |||||
| } | |||||
| assetType: 'material' | |||||
| setDirty(options?: IMaterialSetDirtyOptions): void; | |||||
| // clone?: ()=> any; | |||||
| needsUpdate: boolean; | |||||
| // toJSON same as three.js Material.toJSON | |||||
| // toJSON(meta?: any): any; | |||||
| // copyProps should be just setValues | |||||
| setValues(parameters: Material|(MaterialParameters&{type?:string}), allowInvalidType?: boolean, clearCurrentUserData?: boolean): this; | |||||
| toJSON(meta?: SerializationMetaType, _internal?: boolean): any; | |||||
| fromJSON(json: any, meta?: SerializationMetaType, _internal?: boolean): this | null; | |||||
| extraUniformsToUpload?: Record<string, IUniform> | |||||
| materialExtensions?: MaterialExtension[] | |||||
| registerMaterialExtensions?: (customMaterialExtensions: MaterialExtension[]) => void; | |||||
| unregisterMaterialExtensions?: (customMaterialExtensions: MaterialExtension[]) => void; | |||||
| /** | |||||
| * Managed internally, do not change manually | |||||
| */ | |||||
| generator?: IMaterialGenerator | |||||
| /** | |||||
| * Managed internally, do not change manually | |||||
| */ | |||||
| appliedMeshes: Set<IObject3D> | |||||
| lastShader?: Shader | |||||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||||
| userData: IMaterialUserData | |||||
| // optional from subclasses, added here for autocomplete | |||||
| flatShading?: boolean | |||||
| map?: ITexture | null | |||||
| alphaMap?: ITexture | null | |||||
| envMap?: ITexture | null | |||||
| envMapIntensity?: number | |||||
| aoMap?: ITexture | null | |||||
| lightMap?: ITexture | null | |||||
| normalMap?: ITexture | null | |||||
| bumpMap?: ITexture | null | |||||
| aoMapIntensity?: number | |||||
| lightMapIntensity?: number | |||||
| roughnessMap?: ITexture | null | |||||
| metalnessMap?: ITexture | null | |||||
| roughness?: number | |||||
| metalness?: number | |||||
| transmissionMap?: ITexture | null | |||||
| transmission?: number | |||||
| isRawShaderMaterial?: boolean | |||||
| isPhysicalMaterial?: boolean | |||||
| isUnlitMaterial?: boolean | |||||
| // [key: string]: any | |||||
| } | |||||
| export type IMaterialGenerator<T extends IMaterial = IMaterial> = (params: any)=>T | |||||
| export interface IMaterialTemplate<T extends IMaterial = IMaterial, TP = any>{ | |||||
| templateUUID?: string, | |||||
| name: string, | |||||
| typeSlug?: string, | |||||
| alias?: string[], // alternate names | |||||
| materialType: string, | |||||
| generator?: IMaterialGenerator<T>, | |||||
| params?: TP | |||||
| } |
| import {IDisposable} from 'ts-browser-helpers' | |||||
| import {IMaterial, IMaterialEvent} from './IMaterial' | |||||
| import {Event, Object3D} from 'three' | |||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {IGeometry, IGeometryEvent} from './IGeometry' | |||||
| import {IImportResultUserData} from '../assetmanager' | |||||
| import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||||
| export type IObject3DEventTypes = 'dispose' | 'materialUpdate' | 'objectUpdate' | 'geometryChanged' | | |||||
| 'materialChanged' | 'geometryUpdate' | 'added' | 'removed' | 'select' | | |||||
| 'setView' | 'activateMain' | 'cameraUpdate' // from camera | |||||
| // | string | |||||
| export interface IObject3DEvent<T extends string = IObject3DEventTypes> extends Event { | |||||
| type: T | |||||
| object?: IObject3D // object that triggered the event, target might be parent in case of bubbleToParent | |||||
| bubbleToParent?: boolean // bubble event to parent root | |||||
| change?: string | |||||
| material?: IMaterial|undefined|IMaterial[] // from materialUpdate and materialChanged | |||||
| oldMaterial?: IMaterial|undefined|IMaterial[] // from materialChanged | |||||
| geometry?: IGeometry|undefined // from geometryUpdate, geometryChanged | |||||
| oldGeometry?: IGeometry|undefined // from geometryChanged | |||||
| } | |||||
| export interface IObjectSetDirtyOptions { | |||||
| bubbleToParent?: boolean // bubble event to parent root | |||||
| change?: string | |||||
| refreshScene?: boolean // update scene after setting dirty | |||||
| geometryChanged?: boolean // whether to refresh stuff like ground. | |||||
| frameFade?: boolean // for plugins | |||||
| refreshUi?: boolean // for plugins | |||||
| /** | |||||
| * @deprecated use {@link refreshScene} instead | |||||
| */ | |||||
| sceneUpdate?: boolean // update scene after setting dirty | |||||
| [key: string]: any | |||||
| } | |||||
| export interface IObjectProcessor { // todo, should be viewer | |||||
| processObject: (object: IObject3D) => void | |||||
| } | |||||
| export interface IObject3DUserData extends IImportResultUserData { | |||||
| uuid?: string | |||||
| /** | |||||
| * When true, this object will not be exported when exporting the scene with {@link AssetExporter.exportObject} | |||||
| */ | |||||
| excludeFromExport?: boolean | |||||
| autoCentered?: boolean | |||||
| isCentered?: boolean | |||||
| autoScaleRadius?: number | |||||
| autoScaled?: boolean | |||||
| /** | |||||
| * should this object be taken into account when calculating bounding box, default true | |||||
| */ | |||||
| bboxVisible?: boolean | |||||
| /** | |||||
| * Is centered in a parent object. | |||||
| */ | |||||
| pseudoCentered?: boolean | |||||
| license?: string | |||||
| // region root scene model root | |||||
| /** | |||||
| * is it modelRoot in RootScene, used during serialization nad traversing ancestors | |||||
| */ | |||||
| rootSceneModelRoot?: boolean | |||||
| __gltfAsset?: GLTF['asset'] | |||||
| __gltfExtras?: GLTF['userData'] | |||||
| // endregion | |||||
| __objectSetup?: boolean | |||||
| __meshSetup?: boolean | |||||
| // [key: string]: any // commented for noe | |||||
| // legacy | |||||
| /** | |||||
| * @deprecated | |||||
| */ | |||||
| dispose?: any | |||||
| setMaterial?: any | |||||
| setGeometry?: any | |||||
| setDirty?: any | |||||
| /** | |||||
| * Used in {@link GLTFObject3DExtrasExtension} and {@link iObjectCommons.upgradeObject3D} | |||||
| */ | |||||
| __keepShadowDef?: boolean | |||||
| /** | |||||
| * Events that should be bubbled to parent root without the need to set bubbleToParent in the event. | |||||
| * todo: remove support for this | |||||
| */ | |||||
| __autoBubbleToParentEvents?: string[] | |||||
| } | |||||
| export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEventTypes> extends Object3D<E, ET>, IUiConfigContainer, IDisposable { | |||||
| assetType: 'model' | 'light' | 'camera' | |||||
| isLight?: boolean | |||||
| isCamera?: boolean | |||||
| isMesh?: boolean | |||||
| isLine?: boolean | |||||
| // isGroup?: boolean | |||||
| isScene?: boolean | |||||
| // isHelper?: boolean | |||||
| readonly isObject3D: true | |||||
| material?: IMaterial | IMaterial[] | |||||
| /** | |||||
| * Same as material but always returns an array. | |||||
| */ | |||||
| readonly materials?: IMaterial[] | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _currentMaterial?: IMaterial | IMaterial[] | null | |||||
| geometry?: IGeometry | |||||
| morphTargetDictionary?: Record<string, number> | |||||
| morphTargetInfluences?: number[] | |||||
| updateMorphTargets?(): void | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _currentGeometry?: IGeometry | null | |||||
| /** | |||||
| * Dispatches 'objectUpdate' event on object. | |||||
| * @param e | |||||
| */ | |||||
| setDirty(e?: IObjectSetDirtyOptions): void | |||||
| /** | |||||
| * Parent/Ancestor of this object to bubble events to. This is set internally by setupObject3D. | |||||
| */ | |||||
| parentRoot?: IObject3D | null | |||||
| uiConfig?: UiObjectConfig | |||||
| refreshUi(): void | |||||
| // Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation | |||||
| userData: IObject3DUserData | |||||
| /** | |||||
| * | |||||
| * @param autoScaleRadius - optional (taken from userData.autoScaleRadius by default) | |||||
| * @param isCentered - optional (taken from userData.isCentered by default) | |||||
| * @param setDirty - true by default | |||||
| */ | |||||
| autoScale?<T extends IObject3D>(autoScaleRadius?: number, isCentered?: boolean, setDirty?: boolean): T | |||||
| /** | |||||
| * | |||||
| * @param setDirty - true by default | |||||
| */ | |||||
| autoCenter?<T extends IObject3D>(setDirty?: boolean): T | |||||
| /** | |||||
| * @deprecated use object directly | |||||
| */ | |||||
| modelObject: this | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _onMaterialUpdate?: (e: IMaterialEvent<'materialUpdate'>) => void | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| _onGeometryUpdate?: (e: IGeometryEvent<'geometryUpdate'>) => void | |||||
| objectProcessor?: IObjectProcessor | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __disposed?: boolean | |||||
| // region inherited type fixes | |||||
| traverse(callback: (object: IObject3D) => void): void | |||||
| traverseVisible(callback: (object: IObject3D) => void): void | |||||
| traverseAncestors(callback: (object: IObject3D) => void): void | |||||
| getObjectById(id: number): IObject3D | undefined | |||||
| getObjectByName(name: string): IObject3D | undefined | |||||
| getObjectByProperty(name: string, value: string): IObject3D | undefined | |||||
| copy(source: this, recursive?: boolean, ...args: any[]): this | |||||
| clone(recursive?: boolean): this | |||||
| add(...object: IObject3D[]): this | |||||
| remove(...object: IObject3D[]): this | |||||
| parent: IObject3D | null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| } |
| import {IDisposable, PartialRecord} from 'ts-browser-helpers' | |||||
| import {Clock, Event, ShaderMaterial, Texture, Vector2, Vector4, WebGLRenderer} from 'three' | |||||
| import {CreateRenderTargetOptions, IRenderTarget} from '../rendering/RenderTarget' | |||||
| import {IShaderPropertiesUpdater} from '../materials/MaterialExtension' | |||||
| import {IPassID, IPipelinePass} from '../postprocessing/Pass' | |||||
| import {EffectComposer2} from '../postprocessing/EffectComposer2' | |||||
| import {RenderTargetManager} from '../rendering/RenderTargetManager' | |||||
| import {IScene} from './IScene' | |||||
| export type TThreeRendererMode = 'shadowMapRender' | 'backgroundRender' | 'sceneRender' | 'opaqueRender' | 'transparentRender' | 'transmissionRender' | 'mainRenderPass' | 'screenSpaceRendering' | |||||
| export type TThreeRendererModeUserData = PartialRecord<TThreeRendererMode, boolean> | |||||
| export interface IAnimationLoopEvent extends Event{ | |||||
| renderer: IWebGLRenderer | |||||
| deltaTime: number | |||||
| time: number | |||||
| xrFrame?: XRFrame | |||||
| } | |||||
| export interface IRenderManagerUpdateEvent extends Event{ | |||||
| change: 'registerPass' | 'unregisterPass' | 'useLegacyLights' | 'passRefresh' | 'size' | 'rebuild' | string | |||||
| data: any | |||||
| pass: IPipelinePass | |||||
| } | |||||
| export type IRenderManagerEvent = Partial<IAnimationLoopEvent>&Partial<IRenderManagerUpdateEvent>&Event & { | |||||
| [key: string]: any | |||||
| } | |||||
| export type IRenderManagerEventTypes = 'animationLoop'|'update'|'resize'|'contextLost'|'contextRestored' | |||||
| export interface IRenderManager<E extends IRenderManagerEvent = IRenderManagerEvent, ET extends string = IRenderManagerEventTypes> extends RenderTargetManager<E, ET>, IDisposable, IShaderPropertiesUpdater{ | |||||
| readonly renderer: IWebGLRenderer | |||||
| readonly needsRender: boolean | |||||
| rebuildPipeline(setDirty?: boolean): void | |||||
| setSize(width: number, height: number): void | |||||
| render(scene: IScene): void | |||||
| reset(): void | |||||
| resetShadows(): void | |||||
| refreshPasses(): void | |||||
| registerPass(pass: IPipelinePass, replaceId?: boolean): void | |||||
| unregisterPass(pass: IPipelinePass): void | |||||
| readonly frameCount: number | |||||
| readonly totalFrameCount: number | |||||
| pipeline: IPassID[] | |||||
| composer: EffectComposer2 | |||||
| readonly passes: IPipelinePass[] | |||||
| readonly isWebGL2: boolean | |||||
| readonly composerTarget: IRenderTarget | |||||
| readonly renderSize: Vector2 | |||||
| renderScale: number | |||||
| readonly context: WebGLRenderingContext | |||||
| useLegacyLights: boolean | |||||
| webglRenderer: WebGLRenderer | |||||
| clock: Clock | |||||
| blit(destination: IRenderTarget|undefined|null, options?: {source?: Texture, viewport?: Vector4, material?: ShaderMaterial, clear?: boolean}): void | |||||
| } | |||||
| export interface IRenderManagerOptions { | |||||
| canvas: HTMLCanvasElement, | |||||
| alpha?: boolean, // default = true | |||||
| targetOptions?: CreateRenderTargetOptions | |||||
| rgbm?: boolean, | |||||
| msaa?: boolean, | |||||
| depthBuffer?: boolean, | |||||
| } | |||||
| export interface IWebGLRenderer<TManager extends IRenderManager=IRenderManager> extends WebGLRenderer { | |||||
| renderManager: TManager | |||||
| userData: TThreeRendererModeUserData & { | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __isIWebGLRenderer: true | |||||
| [key: string]: any | |||||
| } | |||||
| renderWithModes(ud: TThreeRendererModeUserData, render: ()=>void): void | |||||
| // legacy | |||||
| /** | |||||
| * @deprecated use {@link renderManager} instead | |||||
| */ | |||||
| baseRenderer?: IRenderManager | |||||
| } | |||||
| export function upgradeWebGLRenderer<TManager extends IRenderManager=IRenderManager>(this: IWebGLRenderer<TManager>, manager: TManager): IWebGLRenderer<TManager> { | |||||
| if (this.userData?.__isIWebGLRenderer) return this | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| if (!this.userData) this.userData = {__isIWebGLRenderer: true} | |||||
| this.userData.__isIWebGLRenderer = true | |||||
| if (!this.renderWithModes) this.renderWithModes = renderWithModes | |||||
| this.renderManager = manager | |||||
| // legacy | |||||
| if (!this.baseRenderer) { | |||||
| Object.defineProperty(this, 'baseRenderer', { | |||||
| get: ()=>{ | |||||
| console.warn('IWebGLRenderer.baseRenderer is deprecated, use IWebGLRenderer.renderManager instead') | |||||
| return this.renderManager | |||||
| }, | |||||
| }) | |||||
| } | |||||
| return this | |||||
| } | |||||
| function renderWithModes(this: IWebGLRenderer, ud: TThreeRendererModeUserData, render: ()=>void) { | |||||
| const rud = this.userData | |||||
| const {backgroundRender, transparentRender, shadowMapRender, mainRenderPass, opaqueRender, transmissionRender, sceneRender, screenSpaceRendering} = rud | |||||
| if (ud.backgroundRender !== undefined) rud.backgroundRender = ud.backgroundRender | |||||
| if (ud.transparentRender !== undefined) rud.transparentRender = ud.transparentRender | |||||
| if (ud.shadowMapRender !== undefined) rud.shadowMapRender = ud.shadowMapRender | |||||
| if (ud.mainRenderPass !== undefined) rud.mainRenderPass = ud.mainRenderPass | |||||
| if (ud.opaqueRender !== undefined) rud.opaqueRender = ud.opaqueRender | |||||
| if (ud.sceneRender !== undefined) rud.sceneRender = ud.sceneRender | |||||
| if (ud.transmissionRender !== undefined) rud.transmissionRender = ud.transmissionRender | |||||
| if (ud.screenSpaceRendering !== undefined) rud.screenSpaceRendering = ud.screenSpaceRendering | |||||
| render() | |||||
| rud.backgroundRender = backgroundRender | |||||
| rud.transparentRender = transparentRender | |||||
| rud.shadowMapRender = shadowMapRender | |||||
| rud.mainRenderPass = mainRenderPass | |||||
| rud.opaqueRender = opaqueRender | |||||
| rud.sceneRender = sceneRender | |||||
| rud.transmissionRender = transmissionRender | |||||
| rud.screenSpaceRendering = screenSpaceRendering | |||||
| } | |||||
| /** | |||||
| * @deprecated renamed to {@link renderWithModes}, use {@link IWebGLRenderer.renderWithModes} | |||||
| */ | |||||
| export const setThreeRendererMode = renderWithModes |
| import {IObject3D, IObject3DEvent, IObject3DEventTypes, IObject3DUserData, IObjectSetDirtyOptions} from './IObject' | |||||
| import {Color, Scene} from 'three' | |||||
| import {IShaderPropertiesUpdater} from '../materials/MaterialExtension' | |||||
| import {ICamera} from './ICamera' | |||||
| import {Box3B} from '../three/math/Box3B' | |||||
| import {ITexture} from './ITexture' | |||||
| export interface AddObjectOptions { | |||||
| addToRoot?: boolean // default = false | |||||
| // TODO; add more options | |||||
| autoCenter?: boolean, | |||||
| license?: string, | |||||
| autoScale?: boolean | |||||
| autoScaleRadius?: number // default = 2 | |||||
| importConfig?: boolean // any attached viewer config will be ignored if this is set to true | |||||
| } | |||||
| // | string | |||||
| export type ISceneEventTypes = IObject3DEventTypes | 'sceneUpdate' | 'addSceneObject' | | |||||
| 'mainCameraChange' | 'mainCameraUpdate' | 'environmentChanged' | 'backgroundChanged' | | |||||
| 'update' | // todo: deprecate, use 'sceneUpdate' instead | |||||
| 'textureAdded' | // todo remove | |||||
| 'activeCameraChange' | 'activeCameraUpdate' | // todo: deprecate | |||||
| 'sceneMaterialUpdate' // todo deprecate: use 'materialUpdate' instead | |||||
| // | string | |||||
| export interface ISceneEvent<T extends string = ISceneEventTypes> extends IObject3DEvent<T> { | |||||
| scene?: IScene | null | |||||
| // change?: string | |||||
| } | |||||
| export type ISceneSetDirtyOptions = IObjectSetDirtyOptions & { | |||||
| } | |||||
| export type ISceneUserData = IObject3DUserData | |||||
| export type IWidget = IObject3D // todo | |||||
| export interface IScene<E extends ISceneEvent = ISceneEvent, ET extends ISceneEventTypes = ISceneEventTypes> | |||||
| extends Scene<E, ET>, IObject3D<E, ET>, IShaderPropertiesUpdater { | |||||
| readonly visible: boolean; | |||||
| readonly isScene: true; | |||||
| mainCamera: ICamera; | |||||
| type: 'Scene'; | |||||
| toJSON(): any; // todo | |||||
| modelRoot: IObject3D; | |||||
| // sceneObjects: ISceneObject[]; | |||||
| // environmentLight?: IEnvironmentLight; | |||||
| // processors: ObjectProcessorMap<'environment' | 'background'> | |||||
| addObject<T extends IObject3D>(imported: T, options?: AddObjectOptions): T; | |||||
| setDirty(e?: ISceneSetDirtyOptions): void | |||||
| // sceneBounds: Box3B; // last computed scene bounds | |||||
| getBounds(precise?: boolean, ignoreInvisible?: boolean): Box3B; | |||||
| backgroundIntensity: number; | |||||
| envMapIntensity: number; | |||||
| fixedEnvMapDirection: boolean; | |||||
| environment: ITexture | null; | |||||
| background: ITexture | Color | null | 'environment'; | |||||
| backgroundColor: Color | null; | |||||
| // addWidget(widget: IWidget, options?: AnyOptions): void; // todo | |||||
| defaultCamera: ICamera | |||||
| userData: ISceneUserData | |||||
| // region deprecated | |||||
| /** | |||||
| * @deprecated use {@link IObject3D.getObjectByName} instead | |||||
| * @param name | |||||
| * @param parent | |||||
| */ | |||||
| findObjectsByName(name: string, parent?: any): any[]; | |||||
| /** | |||||
| * @deprecated renamed to {@link mainCamera} | |||||
| */ | |||||
| activeCamera: ICamera; | |||||
| // endregion | |||||
| // region inherited type fixes | |||||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | |||||
| traverse(callback: (object: IObject3D) => void): void | |||||
| traverseVisible(callback: (object: IObject3D) => void): void | |||||
| traverseAncestors(callback: (object: IObject3D) => void): void | |||||
| getObjectById(id: number): IObject3D | undefined | |||||
| getObjectByName(name: string): IObject3D | undefined | |||||
| getObjectByProperty(name: string, value: string): IObject3D | undefined | |||||
| copy(source: this, recursive?: boolean): this | |||||
| clone(recursive?: boolean): this | |||||
| add(...object: IObject3D[]): this | |||||
| remove(...object: IObject3D[]): this | |||||
| parent: IObject3D | null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| } |
| import {IMaterial} from './IMaterial' | |||||
| import {Texture} from 'three' | |||||
| export interface ITextureUserData{ | |||||
| disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material | |||||
| __appliedMaterials?: Set<IMaterial> | |||||
| } | |||||
| export interface ITexture extends Texture { | |||||
| assetType?: 'texture' | |||||
| userData: ITextureUserData | |||||
| readonly isTexture: true | |||||
| isDataTexture?: boolean | |||||
| isCubeTexture?: boolean | |||||
| isVideoTexture?: boolean | |||||
| isCanvasTexture?: boolean | |||||
| isCompressedTexture?: boolean | |||||
| is3DDataTexture?: boolean | |||||
| } | |||||
| export function upgradeTexture(this: ITexture) { | |||||
| this.assetType = 'texture' | |||||
| if (!this.userData) this.userData = {} | |||||
| if (!this.userData.__appliedMaterials) this.userData.__appliedMaterials = new Set() | |||||
| // todo: uiconfig, dispose, etc | |||||
| } |
| import {IUiConfigContainer} from 'uiconfig.js' | |||||
| import {Camera, Event, EventDispatcher, Object3D, Vector3} from 'three' | |||||
| export interface ICameraControls<TEvents = 'change'|string> extends IUiConfigContainer<void, 'panel'>, EventDispatcher<Event, TEvents> { | |||||
| object: Object3D | |||||
| enabled: boolean | |||||
| dispose(): void | |||||
| update(): void | |||||
| // optional items | |||||
| target?: Vector3 | |||||
| autoRotate?: boolean | |||||
| minDistance?: number | |||||
| maxDistance?: number | |||||
| minZoom?: number | |||||
| maxZoom?: number | |||||
| enableDamping?: boolean | |||||
| enableZoom?: boolean | |||||
| enableRotate?: boolean | |||||
| } | |||||
| export type TControlsCtor = (camera: Camera, domElement?: HTMLCanvasElement|Document)=>ICameraControls |
| import {Camera, Event, IUniform, Object3D, PerspectiveCamera, Vector3} from 'three' | |||||
| import {generateUiConfig, UiObjectConfig, uiSlider, uiVector} from 'uiconfig.js' | |||||
| import {onChange, serialize} from 'ts-browser-helpers' | |||||
| import type {ICamera, ICameraEvent, ICameraUserData, TCameraControlsMode} from '../ICamera' | |||||
| import {ICameraSetDirtyOptions} from '../ICamera' | |||||
| import type {ICameraControls, TControlsCtor} from './ICameraControls' | |||||
| import {OrbitControls3} from '../../three/controls/OrbitControls3' | |||||
| import {IObject3D} from '../IObject' | |||||
| import {ThreeSerialization} from '../../utils/serialization' | |||||
| import {iCameraCommons} from '../object/iCameraCommons' | |||||
| // todo: maybe change domElement to some wrapper/base class of viewer | |||||
| export class PerspectiveCamera2 extends PerspectiveCamera implements ICamera { | |||||
| assetType = 'camera' as const | |||||
| get controls(): ICameraControls | undefined { | |||||
| return this._controls | |||||
| } | |||||
| @serialize('camControls') | |||||
| private _controls?: ICameraControls | |||||
| private _currentControlsMode: TCameraControlsMode = '' | |||||
| @onChange(PerspectiveCamera2.prototype.refreshCameraControls) | |||||
| controlsMode: TCameraControlsMode | |||||
| /** | |||||
| * It should be the canvas actually | |||||
| * @private | |||||
| */ | |||||
| private _canvas?: HTMLCanvasElement | |||||
| get isMainCamera(): boolean { | |||||
| return this.userData.__isMainCamera || false | |||||
| } | |||||
| @serialize() | |||||
| userData: ICameraUserData = {} | |||||
| @onChange(PerspectiveCamera2.prototype.setDirty) | |||||
| @uiSlider('Field Of View', [1, 180], 0.001) | |||||
| @serialize() fov: number | |||||
| @onChange(PerspectiveCamera2.prototype.setDirty) | |||||
| @serialize() focus: number | |||||
| @onChange(PerspectiveCamera2.prototype.setDirty) | |||||
| // @uiSlider('Zoom', [0.001, 20], 0.001) | |||||
| @serialize() zoom: number | |||||
| @uiVector('Position') | |||||
| @serialize() readonly position: Vector3 | |||||
| @onChange(PerspectiveCamera2.prototype.setDirty) | |||||
| @uiVector('Target') | |||||
| @serialize() readonly target: Vector3 = new Vector3(0, 0, 0) | |||||
| @serialize() | |||||
| @onChange(PerspectiveCamera2.prototype.refreshAspect) | |||||
| autoAspect: boolean | |||||
| /** | |||||
| * Near clipping plane. This is managed by RootScene for active cameras | |||||
| */ | |||||
| @onChange(PerspectiveCamera2.prototype._nearFarChanged) | |||||
| near = 0.01 | |||||
| /** | |||||
| * Far clipping plane. This is managed by RootScene for active cameras | |||||
| */ | |||||
| @onChange(PerspectiveCamera2.prototype._nearFarChanged) | |||||
| far = 50 | |||||
| constructor(controlsMode?: TCameraControlsMode, domElement?: HTMLCanvasElement, autoAspect?: boolean, fov?: number, aspect?: number) { | |||||
| super(fov, aspect) | |||||
| this._canvas = domElement | |||||
| this.autoAspect = autoAspect || !!domElement | |||||
| iCameraCommons.upgradeCamera.call(this) // todo: test if autoUpgrade = false works as expected if we call upgradeObject3D externally after constructor, because we have setDirty, refreshTarget below. | |||||
| this.controlsMode = controlsMode || '' | |||||
| this.refreshTarget(undefined, false) | |||||
| // if (!camera) | |||||
| // this.targetUpdated(false) | |||||
| this.setDirty() | |||||
| // if (domElement) | |||||
| // domElement.style.touchAction = 'none' // this is done in orbit controls anyway | |||||
| // const ae = this._canvas.addEventListener | |||||
| // todo: this breaks UI. | |||||
| // this._canvas.addEventListener = (type: string, listener: any, options1: any) => { // see https://github.com/mrdoob/three.js/pull/19782 | |||||
| // ae(type, listener, type === 'wheel' && typeof options1 !== 'boolean' ? { | |||||
| // ...typeof options1 === 'object' ? options1 : {}, | |||||
| // capture: false, | |||||
| // passive: false, | |||||
| // } : options1) | |||||
| // } | |||||
| // this.refreshCameraControls() // this is done on set controlsMode | |||||
| // const target = this.target | |||||
| } | |||||
| // @serialize('camOptions') //todo handle deserialization of this | |||||
| // region interactionsEnabled | |||||
| private _interactionsEnabled = true | |||||
| get canUserInteract() { | |||||
| return this._interactionsEnabled && this.isMainCamera && this.controlsMode !== '' | |||||
| } | |||||
| get interactionsEnabled(): boolean { | |||||
| return this._interactionsEnabled | |||||
| } | |||||
| set interactionsEnabled(value: boolean) { | |||||
| if (this._interactionsEnabled !== value) { | |||||
| this._interactionsEnabled = value | |||||
| this.refreshCameraControls(true) | |||||
| } | |||||
| } | |||||
| // endregion | |||||
| // region refreshing | |||||
| setDirty(options?: ICameraSetDirtyOptions|Event): void { | |||||
| if (!this._positionWorld) return // class not initialized | |||||
| this.getWorldPosition(this._positionWorld) | |||||
| iCameraCommons.setDirty.call(this, options) | |||||
| this._camUi.forEach(u=>u?.uiRefresh?.(false, 'postFrame', 1)) // because camera changes a lot. so we dont want to deep refresh ui on every change | |||||
| } | |||||
| /** | |||||
| * when aspect ratio is set to auto it must be refreshed on resize, this is done by the viewer for the main camera. | |||||
| * @param setDirty | |||||
| */ | |||||
| refreshAspect(setDirty = true): void { | |||||
| if (this.autoAspect) { | |||||
| if (!this._canvas) console.error('cannot calculate aspect ratio without canvas/container') | |||||
| else { | |||||
| this.aspect = this._canvas.clientWidth / this._canvas.clientHeight | |||||
| this.updateProjectionMatrix?.() | |||||
| } | |||||
| } | |||||
| if (setDirty) this.setDirty() | |||||
| // console.log('refreshAspect', this._options.aspect) | |||||
| } | |||||
| protected _nearFarChanged() { | |||||
| if (this.view === undefined) return // not initialized yet | |||||
| this.updateProjectionMatrix?.() | |||||
| } | |||||
| refreshUi = iCameraCommons.refreshUi | |||||
| refreshTarget = iCameraCommons.refreshTarget | |||||
| activateMain = iCameraCommons.activateMain | |||||
| deactivateMain = iCameraCommons.deactivateMain | |||||
| // endregion | |||||
| // region controls | |||||
| // todo: move orbit to a plugin maybe? so that its not forced | |||||
| private _controlsCtors = new Map<string, TControlsCtor>([['orbit', (object, domElement)=>{ | |||||
| const controls = new OrbitControls3(object, domElement ? !domElement.ownerDocument ? domElement.documentElement : domElement : document.body) | |||||
| // this._controls.enabled = false | |||||
| // this._controls.listenToKeyEvents(window as any) // optional // todo: this breaks keyboard events in UI like cursor left/right, make option for this | |||||
| // this._controls.enableKeys = true | |||||
| controls.screenSpacePanning = true | |||||
| return controls | |||||
| }]]) | |||||
| setControlsCtor(key: string, ctor: TControlsCtor, replace = false): void { | |||||
| if (!replace && this._controlsCtors.has(key)) { | |||||
| console.error(key + ' already exists.') | |||||
| return | |||||
| } | |||||
| this._controlsCtors.set(key, ctor) | |||||
| } | |||||
| removeControlsCtor(key: string): void { | |||||
| this._controlsCtors.delete(key) | |||||
| } | |||||
| private _controlsChanged = ()=>{ | |||||
| if (this._controls && this._controls.target) this.refreshTarget(undefined, false) | |||||
| this.setDirty({change: 'controls'}) | |||||
| } | |||||
| private _initCameraControls() { | |||||
| const mode = this.controlsMode | |||||
| this._controls = this._controlsCtors.get(mode)?.(this, this._canvas) ?? undefined | |||||
| if (!this._controls && mode !== '') console.error('Unable to create controls with mode ' + mode + '. Are you missing a plugin?') | |||||
| this._controls?.addEventListener('change', this._controlsChanged) | |||||
| this._currentControlsMode = this._controls ? mode : '' | |||||
| // todo maybe set target like this: | |||||
| // if (this._controls) this._controls.target = this.target | |||||
| } | |||||
| private _disposeCameraControls() { | |||||
| if (this._controls) { | |||||
| if (this._controls.target === this.target) this._controls.target = new Vector3() // just in case | |||||
| this._controls?.removeEventListener('change', this._controlsChanged) | |||||
| this._controls?.dispose() | |||||
| } | |||||
| this._currentControlsMode = '' | |||||
| this._controls = undefined | |||||
| } | |||||
| refreshCameraControls(setDirty = true): void { | |||||
| if (!this._controlsCtors) return // class not initialized | |||||
| if (this._controls) { | |||||
| if (this._currentControlsMode !== this.controlsMode || this !== this._controls.object) { // in-case camera changed or mode changed | |||||
| this._disposeCameraControls() | |||||
| this._initCameraControls() | |||||
| } | |||||
| } else { | |||||
| this._initCameraControls() | |||||
| } | |||||
| // todo: only for orbit control like controls? | |||||
| if (this._controls) { | |||||
| const ce = this.interactionsEnabled | |||||
| this._controls.enabled = ce | |||||
| if (ce) this.up.copy(Object3D.DEFAULT_UP) | |||||
| } | |||||
| if (setDirty) this.setDirty() | |||||
| this.refreshUi() | |||||
| } | |||||
| // endregion | |||||
| // region serialization | |||||
| /** | |||||
| * Serializes this camera with controls to JSON. | |||||
| * @param meta - metadata for serialization | |||||
| * @param baseOnly - Calls only super.toJSON, does internal three.js serialization. Set it to true only if you know what you are doing. | |||||
| */ | |||||
| toJSON(meta?: any, baseOnly = false): any { | |||||
| if (baseOnly) return super.toJSON(meta) | |||||
| // todo add camOptions for backwards compatibility? | |||||
| return ThreeSerialization.Serialize(this, meta, true) | |||||
| } | |||||
| fromJSON(data: any, meta?: any): this | null { | |||||
| if (data.camOptions) { | |||||
| // todo | |||||
| console.error('todo: old file camOptions') | |||||
| } | |||||
| if (data.aspect === 'auto') { | |||||
| data.aspect = this.aspect | |||||
| this.autoAspect = true | |||||
| } | |||||
| // if (data.cameraObject) this._camera.fromJSON(data.cameraObject) | |||||
| // todo: add check for OrbitControls being not deserialized(inited properly) if it doesn't exist yet (if it is not inited properly) | |||||
| // console.log(JSON.parse(JSON.stringify(data))) | |||||
| ThreeSerialization.Deserialize(data, this, meta, true) | |||||
| this.setDirty({change: 'deserialize'}) | |||||
| return this | |||||
| } | |||||
| // endregion | |||||
| // region utils/others | |||||
| // for shader prop updater | |||||
| private _positionWorld = new Vector3() | |||||
| updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this { | |||||
| material.uniforms.cameraPositionWorld?.value?.copy(this._positionWorld) | |||||
| material.uniforms.cameraNearFar?.value?.set(this.near, this.far) | |||||
| if (material.uniforms.projection) material.uniforms.projection.value = this.projectionMatrix // todo: rename to projectionMatrix2? | |||||
| material.defines.PERSPECTIVE_CAMERA = this.type === 'PerspectiveCamera' ? '1' : '0' | |||||
| // material.defines.ORTHOGRAPHIC_CAMERA = this.type === 'OrthographicCamera' ? '1' : '0' // todo | |||||
| return this | |||||
| } | |||||
| dispose(): void { | |||||
| this._disposeCameraControls() | |||||
| // todo: anything else? | |||||
| // iObjectCommons.dispose and dispatch event dispose is called automatically because of updateObject3d | |||||
| } | |||||
| // endregion | |||||
| // region ui | |||||
| private _camUi: UiObjectConfig[] = [ | |||||
| ...generateUiConfig(this), | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Min Near', | |||||
| getValue: () => this.userData.minNearPlane ?? 0.2, | |||||
| setValue: (v) => this.userData.minNearPlane = v, | |||||
| onChange: () => this.setDirty(), | |||||
| }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Max Far', | |||||
| getValue: () => this.userData.maxFarPlane ?? 1000, | |||||
| setValue: (v) => this.userData.maxFarPlane = v, | |||||
| onChange: () => this.setDirty(), | |||||
| }, | |||||
| ()=>({ // because _controlsCtors can change | |||||
| type: 'dropdown', | |||||
| label: 'Controls Mode', | |||||
| property: [this, 'controlsMode'], | |||||
| children: ['', 'orbit', ...this._controlsCtors.keys()].map(v=>({label: v === '' ? 'none' : v, value:v})), | |||||
| onChange: () => this.refreshCameraControls(), | |||||
| }), | |||||
| ] | |||||
| uiConfig: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Camera', | |||||
| limitedUi: true, | |||||
| children: [ | |||||
| ...this._camUi, | |||||
| // todo hack for zoom in and out for now. | |||||
| ()=>(this._controls as OrbitControls3)?.zoomIn ? { | |||||
| type: 'button', | |||||
| label: 'Zoom in', | |||||
| value: ()=>{ | |||||
| (this._controls as OrbitControls3)?.zoomIn(1) | |||||
| }, | |||||
| } : {}, | |||||
| ()=>(this._controls as OrbitControls3)?.zoomOut ? { | |||||
| type: 'button', | |||||
| label: 'Zoom out', | |||||
| value: ()=>{ | |||||
| (this._controls as OrbitControls3)?.zoomOut(1) | |||||
| }, | |||||
| } : {}, | |||||
| ()=>this._controls?.uiConfig, | |||||
| ], | |||||
| } | |||||
| // endregion | |||||
| // region deprecated/old | |||||
| @onChange((k: string, v: boolean)=>{ | |||||
| if (!v) console.warn('Setting camera invisible is not supported', k, v) | |||||
| }) | |||||
| visible: boolean | |||||
| get isActiveCamera(): boolean { | |||||
| return this.isMainCamera | |||||
| } | |||||
| /** | |||||
| * @deprecated use `<T>camera.controls` instead | |||||
| */ | |||||
| getControls<T extends ICameraControls>(): T|undefined { | |||||
| return this._controls as any as T | |||||
| } | |||||
| /** | |||||
| * @deprecated use `this` instead | |||||
| */ | |||||
| get cameraObject(): this { | |||||
| return this | |||||
| } | |||||
| /** | |||||
| * @deprecated use `this` instead | |||||
| */ | |||||
| get modelObject(): this { | |||||
| return this | |||||
| } | |||||
| /** | |||||
| * @deprecated | |||||
| * @param setDirty | |||||
| */ | |||||
| targetUpdated(setDirty = true): void { | |||||
| if (setDirty) this.setDirty() | |||||
| } | |||||
| // setCameraOptions<T extends Partial<IPerspectiveCameraOptions | IOrthographicCameraOptions>>(value: T, setDirty = true): void { | |||||
| // const ops: any = {...value} | |||||
| // | |||||
| // this._refreshCameraOptions(false) | |||||
| // this.refreshCameraControls(false) | |||||
| // if (setDirty) this.setDirty() | |||||
| // } | |||||
| // not to be used | |||||
| // private _changeType(setDirty = true) { | |||||
| // // let cam = this._camera.modelObject | |||||
| // | |||||
| // // change of type, not supported now. | |||||
| // // if (this._options.type !== cam.type) { | |||||
| // // const cam2 = this._options.type === 'PerspectiveCamera' ? new PerspectiveCamera() : new OrthographicCamera() | |||||
| // // cam2.name = this._camera.name | |||||
| // // cam2.near = this._camera.modelObject.near | |||||
| // // cam2.far = this._camera.modelObject.far | |||||
| // // cam2.zoom = this._camera.modelObject.zoom | |||||
| // // cam2.scale.copy(this._camera.modelObject.scale) | |||||
| // // | |||||
| // // const isActive = this._isMainCamera | |||||
| // // if (isActive) this.deactivateMain() | |||||
| // // this._camera = this._setCameraObject(cam2) | |||||
| // // cam = this._camera.modelObject | |||||
| // // if (isActive) this.activateMain() | |||||
| // // this._camera.modelObject.updateProjectionMatrix() | |||||
| // // } | |||||
| // | |||||
| // // this._nearFarChanged() // this updates projection matrix todo: move to setDirty | |||||
| // | |||||
| // if (setDirty) this.setDirty() | |||||
| // } | |||||
| // private _cameraObjectUpdate = (e: any)=>{ | |||||
| // this.setDirty(e) | |||||
| // } | |||||
| // private _setCameraObject(cam: OrthographicCamera | PerspectiveCamera) { | |||||
| // if (this._camera) this._camera.removeEventListener('objectUpdate', this._cameraObjectUpdate) | |||||
| // this._camera = setupIModel(cam as any) | |||||
| // this._camera.addEventListener('objectUpdate', this._cameraObjectUpdate) | |||||
| // return this._camera | |||||
| // } | |||||
| // for ortho | |||||
| // private _frustumSize: number | undefined = undefined | |||||
| // | |||||
| // get frustumSize(): number | undefined { | |||||
| // return this._frustumSize | |||||
| // } | |||||
| // | |||||
| // set frustumSize(value: number | undefined) { | |||||
| // this._frustumSize = value | |||||
| // if (value !== undefined) { | |||||
| // cam.top = value / 2 | |||||
| // cam.bottom = -value / 2 | |||||
| // cam.left = aspect * value / 2 | |||||
| // cam.right = -aspect * value / 2 | |||||
| // } | |||||
| // this.setDirty() | |||||
| // } | |||||
| // endregion | |||||
| // region inherited type fixes | |||||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | |||||
| traverse: (callback: (object: IObject3D) => void) => void | |||||
| traverseVisible: (callback: (object: IObject3D) => void) => void | |||||
| traverseAncestors: (callback: (object: IObject3D) => void) => void | |||||
| getObjectById: (id: number) => IObject3D | undefined | |||||
| getObjectByName: (name: string) => IObject3D | undefined | |||||
| getObjectByProperty: (name: string, value: string) => IObject3D | undefined | |||||
| copy: (source: ICamera|Camera, recursive?: boolean, distanceFromTarget?: number) => this | |||||
| clone: (recursive?: boolean) => this | |||||
| add: (...object: IObject3D[]) => this | |||||
| remove: (...object: IObject3D[]) => this | |||||
| dispatchEvent: (event: ICameraEvent) => void | |||||
| parent: IObject3D | null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| } | |||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry' | |||||
| export const iGeometryCommons = { | |||||
| setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void { | |||||
| this.dispatchEvent({bubbleToObject: true, ...options, type: 'geometryUpdate', geometry: this}) // this sets sceneUpdate in root scene | |||||
| this.refreshUi() | |||||
| }, | |||||
| refreshUi: function(this: IGeometry) { | |||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| }, | |||||
| upgradeGeometry: upgradeGeometry, | |||||
| makeUiConfig: function(this: IGeometry): UiObjectConfig { | |||||
| if (this.uiConfig) return this.uiConfig | |||||
| return { | |||||
| label: 'Geometry', | |||||
| type: 'folder', | |||||
| children: [ | |||||
| { | |||||
| type: 'input', | |||||
| property: [this, 'uuid'], | |||||
| disabled: true, | |||||
| }, | |||||
| // { | |||||
| // type: 'input', | |||||
| // property: [this, 'name'], | |||||
| // }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Create uv2 from uv', | |||||
| value: () => { | |||||
| if (this.hasAttribute('uv2')) { | |||||
| if (!confirm('uv2 already exists, replace with uv data?')) return | |||||
| } | |||||
| this.setAttribute('uv2', this.getAttribute('uv')) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Remove vertex color attribute', | |||||
| hidden: () => !this.hasAttribute('color'), | |||||
| value: () => { | |||||
| if (!this.hasAttribute('color')) { | |||||
| prompt('No color attribute found') | |||||
| return | |||||
| } | |||||
| if (!confirm('Remove color attribute?')) return | |||||
| this.deleteAttribute('color') | |||||
| }, | |||||
| }, | |||||
| // { | |||||
| // type: 'button', | |||||
| // label: 'Invert eigen vectors', | |||||
| // value: () => { | |||||
| // console.log(geometry) | |||||
| // const offsets = geometry.userData.normalsCaptureOffsets | |||||
| // if (!offsets) return | |||||
| // const m = offsets.offsetMatrix as Matrix4 | |||||
| // console.log(offsets.offsetMatrix.toArray()) | |||||
| // console.log(m.determinant()) | |||||
| // | |||||
| // const m1 = new Matrix4().makeRotationX(Math.PI / 2) | |||||
| // m.multiply(m1) | |||||
| // | |||||
| // console.log(m.determinant()) | |||||
| // offsets.offsetMatrixInv.copy(m).invert() | |||||
| // console.log(offsets.offsetMatrix.toArray()) | |||||
| // }, | |||||
| // }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Mesh count', | |||||
| getValue: () => this.appliedMeshes?.size ?? 0, | |||||
| disabled: true, | |||||
| }, | |||||
| ], | |||||
| } | |||||
| }, | |||||
| } | |||||
| export function upgradeGeometry(this: IGeometry) { | |||||
| if (this.assetType === 'geometry') return // already upgraded | |||||
| if (!this.isBufferGeometry) { | |||||
| console.error('Material is not a this', this) | |||||
| return | |||||
| } | |||||
| this.assetType = 'geometry' | |||||
| if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty | |||||
| if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi | |||||
| if (!this.appliedMeshes) this.appliedMeshes = new Set() | |||||
| if (!this.userData) this.userData = {} | |||||
| this.uiConfig = iGeometryCommons.makeUiConfig.call(this) | |||||
| // todo: dispose uiconfig on geometry dispose | |||||
| // todo: add serialization? | |||||
| } |
| export {PerspectiveCamera2} from './camera/PerspectiveCamera2' | |||||
| export {ExtendedShaderMaterial} from './material/ExtendedShaderMaterial' | |||||
| export {PhysicalMaterial, type PhysicalMaterialEventTypes, MeshStandardMaterial2} from './material/PhysicalMaterial' | |||||
| export {ShaderMaterial2} from './material/ShaderMaterial2' | |||||
| export {UnlitMaterial, type UnlitMaterialEventTypes, MeshBasicMaterial2} from './material/UnlitMaterial' | |||||
| export {iObjectCommons} from './object/iObjectCommons' | |||||
| export {iCameraCommons} from './object/iCameraCommons' | |||||
| export {iGeometryCommons} from './geometry/iGeometryCommons' | |||||
| export {iMaterialCommons} from './material/iMaterialCommons' | |||||
| export {upgradeTexture} from './ITexture' | |||||
| export {upgradeWebGLRenderer} from './IRenderer' | |||||
| export {RootScene} from './object/RootScene' | |||||
| export type {ICameraControls, TControlsCtor} from './camera/ICameraControls' | |||||
| export type {ICamera, ICameraEvent, ICameraEventTypes, ICameraUserData, TCameraControlsMode, ICameraSetDirtyOptions} from './ICamera' | |||||
| export type {IGeometry, IGeometryUserData, IGeometryEvent, IGeometryEventTypes, IGeometrySetDirtyOptions} from './IGeometry' | |||||
| export type {IMaterial, IMaterialEvent, IMaterialEventTypes, IMaterialParameters, IMaterialUserData, IMaterialSetDirtyOptions, IMaterialTemplate, IMaterialGenerator} from './IMaterial' | |||||
| export type {IObject3D, IObject3DEvent, IObjectSetDirtyOptions, IObjectProcessor, IObject3DEventTypes, IObject3DUserData} from './IObject' | |||||
| export type {IRenderManager, IRenderManagerOptions, IWebGLRenderer, IRenderManagerEventTypes, IAnimationLoopEvent, TThreeRendererMode, TThreeRendererModeUserData, IRenderManagerUpdateEvent, IRenderManagerEvent} from './IRenderer' | |||||
| export type {IScene, ISceneEvent, ISceneEventTypes, ISceneSetDirtyOptions, AddObjectOptions, ISceneUserData, IWidget} from './IScene' | |||||
| export type {ITexture, ITextureUserData} from './ITexture' |
| import {ShaderMaterial2} from './ShaderMaterial2' | |||||
| import {getTexelDecoding2} from '../../three/utils/encoding' | |||||
| import { | |||||
| BufferGeometry, | |||||
| Camera, | |||||
| ColorSpace, | |||||
| IUniform, | |||||
| LinearSRGBColorSpace, | |||||
| Object3D, | |||||
| Scene, | |||||
| Shader, | |||||
| ShaderMaterialParameters, | |||||
| Texture, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| export class ExtendedShaderMaterial extends ShaderMaterial2 { | |||||
| declare ['constructor']: (typeof ExtendedShaderMaterial) & (typeof ShaderMaterial2) | |||||
| textures: {colorSpace: ColorSpace, id: string}[] = [] | |||||
| constructor(parameters: ShaderMaterialParameters, textureIds: string[]) { | |||||
| super(parameters) | |||||
| this.setTextureIds(textureIds) | |||||
| } | |||||
| setTextureIds(ids: string[]) { | |||||
| if (this.textures.map(t=>t.id).join(';') !== ids.join(';')) { | |||||
| this.textures = ids.map(t=>({id: t, colorSpace: LinearSRGBColorSpace})) | |||||
| this.setDirty() | |||||
| } | |||||
| } | |||||
| private _setUniformTexSize(uniform?: IUniform, t?: Texture) { | |||||
| if (!t || !uniform) return | |||||
| const w = t.image?.width ?? 512 | |||||
| const h = t.image?.height ?? 512 | |||||
| const last = uniform.value | |||||
| if (!last.isVector2) console.warn('uniform is not a Vector2') | |||||
| if (last && Math.abs(last.x - w) + Math.abs(last.y - h) > 0.1) { | |||||
| last.x = w; last.y = h | |||||
| this.uniformsNeedUpdate = true | |||||
| } | |||||
| } | |||||
| onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| this._setUniformTexSize(this.uniforms.screenSize, renderer.getRenderTarget()?.texture) | |||||
| for (const item of this.textures) { | |||||
| const textureID = item.id | |||||
| const t = this.uniforms[textureID]?.value | |||||
| if (t) { | |||||
| this._setUniformTexSize(this.uniforms[textureID + 'Size'], t) | |||||
| if (t.colorSpace !== item.colorSpace) { | |||||
| item.colorSpace = t.colorSpace | |||||
| this.needsUpdate = true | |||||
| } | |||||
| } | |||||
| } | |||||
| super.onBeforeRender(renderer, scene, camera, geometry, object) | |||||
| } | |||||
| onBeforeCompile(s: Shader, renderer: WebGLRenderer) { | |||||
| s.fragmentShader = this.textures | |||||
| .map(t=>`uniform sampler2D ${t.id}; \n` | |||||
| + getTexelDecoding2(t.id ?? 'input', t.colorSpace ?? LinearSRGBColorSpace)).join('\n') | |||||
| + s.fragmentShader | |||||
| super.onBeforeCompile(s, renderer) | |||||
| } | |||||
| customProgramCacheKey(): string { | |||||
| return super.customProgramCacheKey() + this.textures.map(t=>t.id + t.colorSpace).join(';') | |||||
| } | |||||
| } |
| import {IMaterial} from '../IMaterial' | |||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| import {makeSamplerUi} from '../../ui/image-ui' | |||||
| import { | |||||
| AdditiveBlending, | |||||
| AlwaysDepth, | |||||
| BackSide, | |||||
| Blending, | |||||
| DepthModes, | |||||
| DoubleSide, | |||||
| EqualDepth, | |||||
| FrontSide, | |||||
| GreaterDepth, | |||||
| GreaterEqualDepth, | |||||
| LessDepth, | |||||
| LessEqualDepth, | |||||
| MultiplyBlending, | |||||
| NeverDepth, | |||||
| NoBlending, | |||||
| NormalBlending, | |||||
| NormalMapTypes, | |||||
| NotEqualDepth, | |||||
| ObjectSpaceNormalMap, | |||||
| Side, | |||||
| SubtractiveBlending, | |||||
| TangentSpaceNormalMap, | |||||
| } from 'three' | |||||
| import {downloadBlob} from 'ts-browser-helpers' | |||||
| import {PhysicalMaterial} from './PhysicalMaterial' | |||||
| export const iMaterialUI = { | |||||
| base: (material: IMaterial): UiObjectConfig[] => [ | |||||
| { | |||||
| type: 'input', | |||||
| property: [material, 'name'], | |||||
| }, | |||||
| // { | |||||
| // type: 'monitor', | |||||
| // property: [material, 'uuid'], | |||||
| // }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'wireframe'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'vertexColors'], | |||||
| }, | |||||
| { | |||||
| type: 'color', | |||||
| property: [material, 'color'], | |||||
| }, | |||||
| material.flatShading !== undefined ? { | |||||
| type: 'checkbox', | |||||
| property: [material, 'flatShading'], | |||||
| } : {}, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'map'], | |||||
| }, | |||||
| makeSamplerUi(material, 'map'), | |||||
| ], | |||||
| blending: (material: IMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Blending', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'opacity'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'transparent'], | |||||
| onChange: material.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'dropdown', | |||||
| property: [material, 'depthFunc'], | |||||
| children: ([ | |||||
| ['Never', NeverDepth], | |||||
| ['Always', AlwaysDepth], | |||||
| ['Less', LessDepth], | |||||
| ['LessEqual', LessEqualDepth], | |||||
| ['Equal', EqualDepth], | |||||
| ['GreaterEqual', GreaterEqualDepth], | |||||
| ['Greater', GreaterDepth], | |||||
| ['NotEqual', NotEqualDepth], | |||||
| ] as [string, DepthModes][]).map(value => ({ | |||||
| label: value[0], | |||||
| value: value[1], | |||||
| })), | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'depthTest'], | |||||
| onChange: material.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'depthWrite'], | |||||
| onChange: material.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'alphaTest'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| property: [material, 'dithering'], | |||||
| }, | |||||
| { | |||||
| type: 'dropdown', | |||||
| label: 'Blending', | |||||
| property: [material, 'blending'], | |||||
| children: ([ | |||||
| ['None', NoBlending], | |||||
| ['Normal', NormalBlending], | |||||
| ['Additive', AdditiveBlending], | |||||
| ['Subtractive', SubtractiveBlending], | |||||
| ['Multiply', MultiplyBlending], | |||||
| ] as [string, Blending][]).map(value => ({ | |||||
| label: value[0], | |||||
| value: value[1], | |||||
| })), | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'alphaMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'alphaMap'), | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Render to Gbuffer', | |||||
| // hidden: ()=>!material.transparent && material.transmission < 0.001, | |||||
| getValue: ()=>material.userData.renderToGBuffer === true, | |||||
| setValue: (v: boolean)=>{ | |||||
| material.userData.renderToGBuffer = v ? v : undefined | |||||
| material.setDirty() | |||||
| }, | |||||
| }, | |||||
| material.isPhysicalMaterial ? { | |||||
| type: 'checkbox', | |||||
| label: 'Inverse AlphaMap', | |||||
| hidden: ()=>!material.transparent, | |||||
| getValue: ()=>material.userData.inverseAlphaMap === true, | |||||
| setValue: (v: boolean)=>{ | |||||
| material.userData.inverseAlphaMap = v ? v : undefined | |||||
| material.setDirty() | |||||
| }, | |||||
| } : {}, | |||||
| ], | |||||
| } | |||||
| ), | |||||
| polygonOffset: (material: IMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Polygon Offset', | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Polygon Offset', | |||||
| property: [material, 'polygonOffset'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Polygon Offset Factor', | |||||
| bounds: [-10, 10], | |||||
| property: [material, 'polygonOffsetFactor'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| label: 'Polygon Offset Units', | |||||
| bounds: [-10, 10], | |||||
| property: [material, 'polygonOffsetUnits'], | |||||
| }, | |||||
| ], | |||||
| } | |||||
| ), | |||||
| aoLightMap: (material: IMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'AO/Lightmap', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 2], | |||||
| property: [material, 'aoMapIntensity'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'aoMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'aoMap'), | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 2], | |||||
| property: [material, 'lightMapIntensity'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'lightMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'lightMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| misc: (material: IMaterial): UiObjectConfig[] => [ | |||||
| { | |||||
| type: 'dropdown', | |||||
| label: 'Side', | |||||
| property: [material, 'side'], | |||||
| children: ([ | |||||
| ['Front', FrontSide], | |||||
| ['Back', BackSide], | |||||
| ['Double', DoubleSide], | |||||
| ] as [string, Side][]).map(value => ({ | |||||
| label: value[0], | |||||
| value: value[1], | |||||
| })), | |||||
| }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Mesh count', | |||||
| getValue: ()=>material.appliedMeshes.size || 0, | |||||
| disabled: true, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: `Download ${material.constructor.TypeSlug}}`, | |||||
| value: ()=>{ | |||||
| const blob = new Blob([JSON.stringify(material.toJSON(), null, 2)], {type: 'application/json'}) | |||||
| downloadBlob(blob, `unlit-material.${material.constructor.TypeSlug}`) | |||||
| }, | |||||
| }, | |||||
| ()=>material.materialExtensions?.map(v=>v.getUiConfig?.(material, material.uiConfig?.uiRefresh)).filter(v=>v), | |||||
| ], | |||||
| roughMetal: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Rough/Metal', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'roughness'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'metalness'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'roughnessMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'roughnessMap'), | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'metalnessMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'metalnessMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| bumpNormal: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Bump/Normal', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 0.2], | |||||
| stepSize: 0.001, | |||||
| property: [material, 'bumpScale'], | |||||
| hidden: ()=>!material.bumpMap, | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'bumpMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'bumpMap'), | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'normalMap'], | |||||
| }, | |||||
| { | |||||
| type: 'vec2', | |||||
| property: [material, 'normalScale'], | |||||
| hidden: ()=>!material.normalMap, | |||||
| }, | |||||
| { | |||||
| type: 'dropdown', | |||||
| hidden: ()=>!material.normalMap, | |||||
| property: [material, 'normalMapType'], | |||||
| children: ([ | |||||
| ['TangentSpace', TangentSpaceNormalMap], | |||||
| ['ObjectSpace', ObjectSpaceNormalMap], | |||||
| ] as [string, NormalMapTypes][]).map(value => ({ | |||||
| label: value[0], | |||||
| value: value[1], | |||||
| })), | |||||
| }, | |||||
| makeSamplerUi(material, 'normalMap'), | |||||
| { | |||||
| type: 'input', | |||||
| property: [material, 'displacementScale'], | |||||
| hidden: ()=>!material.displacementMap, | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'displacementMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'displacementMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| emission: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Emission', | |||||
| children: [ | |||||
| { | |||||
| type: 'color', | |||||
| property: [material, 'emissive'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 10], | |||||
| property: [material, 'emissiveIntensity'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'emissiveMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'emissiveMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| transmission: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Refraction', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'reflectivity'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'transmission'], | |||||
| limitedUi: true, | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| stepSize: 0.001, | |||||
| property: [material, 'thickness'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'transmissionMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'transmissionMap'), | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'thicknessMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'thicknessMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| clearcoat: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Clearcoat', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'clearcoat'], | |||||
| }, | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| hidden: ()=>material.clearcoat < 0.001, | |||||
| property: [material, 'clearcoatRoughness'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'clearcoatMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'clearcoatMap'), | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'clearcoatRoughness'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'clearcoatRoughnessMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'clearcoatRoughnessMap'), | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'clearcoatNormalMap'], | |||||
| }, | |||||
| { | |||||
| type: 'vec2', | |||||
| property: [material, 'clearcoatNormalScale'], | |||||
| hidden: ()=>!material.clearcoatNormalMap, | |||||
| }, | |||||
| makeSamplerUi(material, 'clearcoatNormalMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| sheen: (material: PhysicalMaterial): UiObjectConfig => ( | |||||
| { | |||||
| type: 'folder', | |||||
| label: 'Sheen', | |||||
| children: [ | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'sheen'], | |||||
| }, | |||||
| { | |||||
| type: 'color', | |||||
| hidden: ()=>material.sheen < 0.001, | |||||
| property: [material, 'sheenColor'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'sheenColorMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'sheenColorMap'), | |||||
| { | |||||
| type: 'slider', | |||||
| bounds: [0, 1], | |||||
| property: [material, 'sheenRoughness'], | |||||
| }, | |||||
| { | |||||
| type: 'image', | |||||
| property: [material, 'sheenRoughnessMap'], | |||||
| }, | |||||
| makeSamplerUi(material, 'sheenRoughnessMap'), | |||||
| ], | |||||
| } | |||||
| ), | |||||
| } |
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| import { | |||||
| BufferGeometry, | |||||
| Camera, | |||||
| Color, | |||||
| IUniform, | |||||
| Material, | |||||
| MeshPhysicalMaterial, | |||||
| MeshPhysicalMaterialParameters, | |||||
| Object3D, | |||||
| Scene, | |||||
| Shader, | |||||
| TangentSpaceNormalMap, | |||||
| Vector2, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {shaderReplaceString} from '../../utils/shader-helpers' | |||||
| import { | |||||
| IMaterial, | |||||
| IMaterialEvent, | |||||
| IMaterialEventTypes, | |||||
| IMaterialGenerator, | |||||
| IMaterialParameters, | |||||
| IMaterialTemplate, | |||||
| } from '../IMaterial' | |||||
| import {SerializationMetaType, ThreeSerialization} from '../../utils/serialization' | |||||
| import {MaterialExtender, MaterialExtension} from '../../materials' | |||||
| import {iMaterialCommons, threeMaterialPropList} from './iMaterialCommons' | |||||
| import {IObject3D} from '../IObject' | |||||
| import {ITexture} from '../ITexture' | |||||
| import {iMaterialUI} from './IMaterialUi' | |||||
| export type PhysicalMaterialEventTypes = IMaterialEventTypes | '' | |||||
| export class PhysicalMaterial extends MeshPhysicalMaterial<IMaterialEvent, PhysicalMaterialEventTypes> implements IMaterial<IMaterialEvent, PhysicalMaterialEventTypes> { | |||||
| declare ['constructor']: typeof PhysicalMaterial | |||||
| public static readonly TypeSlug = 'pmat' | |||||
| public static readonly TYPE = 'PhysicalMaterial' // not using .type because it is used by three.js | |||||
| assetType = 'material' as const | |||||
| public readonly isPhysicalMaterial = true | |||||
| public materialExtensions: MaterialExtension[] = [] | |||||
| readonly appliedMeshes: Set<IObject3D> = new Set() | |||||
| readonly setDirty = iMaterialCommons.setDirty | |||||
| clone(): this {return iMaterialCommons.clone(super.clone).call(this)} | |||||
| generator?: IMaterialGenerator | |||||
| map: ITexture | null = null | |||||
| alphaMap: ITexture | null = null | |||||
| roughnessMap: ITexture | null = null | |||||
| metalnessMap: ITexture | null = null | |||||
| normalMap: ITexture | null = null | |||||
| bumpMap: ITexture | null = null | |||||
| displacementMap: ITexture | null = null | |||||
| constructor({customMaterialExtensions, ...parameters}: MeshPhysicalMaterialParameters & IMaterialParameters = {}) { | |||||
| super(parameters) | |||||
| this.fog = false | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| if (customMaterialExtensions) this.registerMaterialExtensions(customMaterialExtensions) | |||||
| iMaterialCommons.upgradeMaterial.call(this) | |||||
| } | |||||
| // region Material Extension | |||||
| extraUniformsToUpload: Record<string, IUniform> = {} | |||||
| registerMaterialExtensions = iMaterialCommons.registerMaterialExtensions | |||||
| unregisterMaterialExtensions = iMaterialCommons.unregisterMaterialExtensions | |||||
| customProgramCacheKey(): string { | |||||
| return super.customProgramCacheKey() + MaterialExtender.CacheKeyForExtensions(this, this.materialExtensions) + this.userData.inverseAlphaMap | |||||
| } | |||||
| onBeforeCompile(shader: Shader, renderer: WebGLRenderer): void { // shader is not Shader but WebglUniforms.getParameters return value type so includes defines | |||||
| const f = [ | |||||
| ['vec3 totalDiffuse = ', 'afterModulation'], | |||||
| ['#include <aomap_fragment>', 'beforeModulation'], | |||||
| ['#include <lights_physical_fragment>', 'beforeAccumulation'], | |||||
| ['#include <clipping_planes_fragment>', 'mainStart'], | |||||
| ] | |||||
| const v = [ | |||||
| ['#include <uv_vertex>', 'mainStart'], | |||||
| ] | |||||
| for (const vElement of v) shader.vertexShader = shaderReplaceString(shader.vertexShader, vElement[0], '#glMarker ' + vElement[1] + '\n' + vElement[0]) | |||||
| for (const fElement of f) shader.fragmentShader = shaderReplaceString(shader.fragmentShader, fElement[0], '#glMarker ' + fElement[1] + '\n' + fElement[0]) | |||||
| iMaterialCommons.onBeforeCompile.call(this, shader, renderer) | |||||
| ;(shader as any).defines && ((shader as any).defines.INVERSE_ALPHAMAP = this.userData.inverseAlphaMap ? 1 : 0) | |||||
| super.onBeforeCompile(shader, renderer) | |||||
| } | |||||
| onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onBeforeRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onBeforeRender.call(this, renderer, scene, camera, geometry, object) | |||||
| const t = this.userData.inverseAlphaMap ? 1 : 0 | |||||
| if (t !== this.defines.INVERSE_ALPHAMAP) { | |||||
| this.defines.INVERSE_ALPHAMAP = t | |||||
| this.needsUpdate = true | |||||
| } | |||||
| } | |||||
| onAfterRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onAfterRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onAfterRender.call(this, renderer, scene, camera, geometry, object) | |||||
| } | |||||
| // endregion | |||||
| // region UI Config | |||||
| // todo dispose ui config | |||||
| uiConfig: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Physical Material', | |||||
| uuid: 'MPM2_' + this.uuid, | |||||
| expanded: true, | |||||
| children: [ | |||||
| ...iMaterialUI.base(this), | |||||
| iMaterialUI.blending(this), | |||||
| iMaterialUI.polygonOffset(this), | |||||
| iMaterialUI.aoLightMap(this), | |||||
| iMaterialUI.roughMetal(this), | |||||
| iMaterialUI.bumpNormal(this), | |||||
| iMaterialUI.emission(this), | |||||
| iMaterialUI.transmission(this), | |||||
| iMaterialUI.clearcoat(this), | |||||
| iMaterialUI.sheen(this), | |||||
| ...iMaterialUI.misc(this), | |||||
| ], | |||||
| } | |||||
| // endregion UI Config | |||||
| // region Serialization | |||||
| /** | |||||
| * Sets the values of this material based on the values of the passed material or an object with material properties | |||||
| * The input is expected to be a valid material or a deserialized material parameters object(including the deserialized userdata) | |||||
| * @param parameters - material or material parameters object | |||||
| * @param allowInvalidType - if true, the type of the oldMaterial is not checked. Objects without type are always allowed. | |||||
| * @param clearCurrentUserData - if undefined, then depends on material.isMaterial. if true, the current userdata is cleared before setting the new values, because it can have data which wont be overwritten if not present in the new material. | |||||
| */ | |||||
| setValues(parameters: Material|(MeshPhysicalMaterialParameters&{type?:string}), allowInvalidType = true, clearCurrentUserData: boolean|undefined = undefined): this { | |||||
| if (!parameters) return this | |||||
| if (parameters.type && !allowInvalidType && !['MeshPhysicalMaterial', 'MeshStandardMaterial', 'MeshStandardMaterial2', this.constructor.TYPE].includes(parameters.type)) { | |||||
| console.error('Material type is not supported:', parameters.type) | |||||
| return this | |||||
| } | |||||
| // Blender exporter used to export a scalar. See three.js:#7459 | |||||
| if (typeof (<any>parameters).normalScale === 'number') { | |||||
| (<any>parameters).normalScale = [(<any>parameters).normalScale, (<any>parameters).normalScale] | |||||
| } | |||||
| if (clearCurrentUserData === undefined) clearCurrentUserData = (<Material>parameters).isMaterial | |||||
| if (clearCurrentUserData) this.userData = {} | |||||
| iMaterialCommons.setValues(super.setValues).call(this, parameters) | |||||
| this.userData.uuid = this.uuid // just in case | |||||
| return this | |||||
| } | |||||
| copy(source: Material|any): this { | |||||
| return this.setValues(source, false) | |||||
| } | |||||
| /** | |||||
| * Serializes this material to JSON. | |||||
| * @param meta - metadata for serialization | |||||
| * @param _internal - Calls only super.toJSON, does internal three.js serialization and @serialize tags. Set it to true only if you know what you are doing. This is used in Serialization->serializer->material | |||||
| */ | |||||
| toJSON(meta?: SerializationMetaType, _internal = false): any { | |||||
| if (_internal) return { | |||||
| ...super.toJSON(meta), | |||||
| ...ThreeSerialization.Serialize(this, meta, true), // this will serialize the properties of this class(like defined with @serialize and @serialize attribute) | |||||
| } | |||||
| return ThreeSerialization.Serialize(this, meta, false) // this will call toJSON again, but with baseOnly=true, that's why we set isThis to false. | |||||
| } | |||||
| /** | |||||
| * Deserializes the material from JSON. | |||||
| * Textures should be loaded and in meta.textures before calling this method. | |||||
| * todo - needs to be tested | |||||
| * @param data | |||||
| * @param meta | |||||
| * @param _internal | |||||
| */ | |||||
| fromJSON(data: any, meta?: SerializationMetaType, _internal = false): this | null { | |||||
| if (_internal) { | |||||
| ThreeSerialization.Deserialize(data, this, meta, true) | |||||
| return this.setValues(data) | |||||
| } | |||||
| ThreeSerialization.Deserialize(data, this, meta, false) | |||||
| return this | |||||
| } | |||||
| // endregion | |||||
| static readonly MaterialProperties = { | |||||
| // keep updated with properties in MeshStandardMaterial.js | |||||
| ...threeMaterialPropList, | |||||
| color: new Color(0xffffff), | |||||
| roughness: 1, | |||||
| metalness: 0, | |||||
| map: null, | |||||
| lightMap: null, | |||||
| lightMapIntensity: 1, | |||||
| aoMap: null, | |||||
| aoMapIntensity: 1, | |||||
| emissive: '#000000', | |||||
| emissiveIntensity: 1, | |||||
| emissiveMap: null, | |||||
| bumpMap: null, | |||||
| bumpScale: 1, | |||||
| normalMap: null, | |||||
| normalMapType: TangentSpaceNormalMap, | |||||
| normalScale: new Vector2(1, 1), | |||||
| displacementMap: null, | |||||
| displacementScale: 1, | |||||
| displacementBias: 0, | |||||
| roughnessMap: null, | |||||
| metalnessMap: null, | |||||
| alphaMap: null, | |||||
| envMap: null, | |||||
| envMapIntensity: 1, | |||||
| // refractionRatio: 0, | |||||
| wireframe: false, | |||||
| wireframeLinewidth: 1, | |||||
| wireframeLinecap: 'round', | |||||
| wireframeLinejoin: 'round', | |||||
| flatShading: false, | |||||
| fog: true, | |||||
| // skinning: false, | |||||
| // vertexTangents: false, //removed from threejs | |||||
| // morphTargets: false, | |||||
| // morphNormals: false, | |||||
| // GLTF Extensions // todo: supported anywhere? | |||||
| // glossiness: 0, | |||||
| // glossinessMap: null, | |||||
| // specularColor: new Color(0), | |||||
| // specularColorMap: null, | |||||
| // keep updated with properties in MeshPhysicalMaterial.js | |||||
| clearcoat: 0, | |||||
| clearcoatMap: null, | |||||
| clearcoatRoughness: 0, | |||||
| clearcoatRoughnessMap: null, | |||||
| clearcoatNormalScale: new Vector2(1, 1), | |||||
| clearcoatNormalMap: null, | |||||
| reflectivity: 0.5, // because this is used in Material.js->toJSON and fromJSON instead of ior | |||||
| iridescenceMap: null, | |||||
| iridescenceIOR: 1.3, | |||||
| iridescenceThicknessRange: [100, 400], | |||||
| iridescenceThicknessMap: null, | |||||
| sheen: 0, | |||||
| sheenColor: new Color(0x000000), | |||||
| sheenColorMap: null, | |||||
| sheenRoughness: 1.0, | |||||
| sheenRoughnessMap: null, | |||||
| transmission: 0, | |||||
| transmissionMap: null, | |||||
| thickness: 0, | |||||
| thicknessMap: null, | |||||
| attenuationDistance: Infinity, | |||||
| attenuationColor: new Color(1, 1, 1), | |||||
| specularIntensity: 1.0, | |||||
| specularIntensityMap: null, | |||||
| specularColor: new Color(1, 1, 1), | |||||
| specularColorMap: null, | |||||
| } | |||||
| static MaterialTemplate: IMaterialTemplate<PhysicalMaterial, Partial<typeof PhysicalMaterial.MaterialProperties>> = { | |||||
| materialType: PhysicalMaterial.TYPE, | |||||
| name: 'physical', | |||||
| typeSlug: PhysicalMaterial.TypeSlug, | |||||
| alias: ['standard', 'physical', PhysicalMaterial.TYPE, PhysicalMaterial.TypeSlug, 'MeshStandardMaterial', 'MeshStandardMaterial2', 'MeshPhysicalMaterial'], | |||||
| params: { | |||||
| color: new Color(1, 1, 1), | |||||
| }, | |||||
| generator: (params) => { | |||||
| return new PhysicalMaterial(params) | |||||
| }, | |||||
| } | |||||
| } | |||||
| export class MeshStandardMaterial2 extends PhysicalMaterial { | |||||
| constructor(parameters?: MeshPhysicalMaterialParameters) { | |||||
| super(parameters) | |||||
| console.error('MeshStandardMaterial2 is deprecated, use UnlitMaterial instead') | |||||
| } | |||||
| } |
| import { | |||||
| BufferGeometry, | |||||
| Camera, | |||||
| IUniform, | |||||
| Material, | |||||
| Object3D, | |||||
| Scene, | |||||
| Shader, | |||||
| ShaderMaterial, | |||||
| ShaderMaterialParameters, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {IMaterial, IMaterialEvent, IMaterialEventTypes, IMaterialParameters} from '../IMaterial' | |||||
| import {MaterialExtender, MaterialExtension} from '../../materials' | |||||
| import {iMaterialCommons, threeMaterialPropList} from './iMaterialCommons' | |||||
| export class ShaderMaterial2<E extends IMaterialEvent = IMaterialEvent, ET = IMaterialEventTypes> extends ShaderMaterial<E, ET> implements IMaterial<E, ET> { | |||||
| declare ['constructor']: typeof ShaderMaterial2 | |||||
| static readonly TypeSlug = 'shaderMat' | |||||
| static readonly TYPE = 'ShaderMaterial2' | |||||
| static readonly MaterialProperties = { | |||||
| ...threeMaterialPropList, | |||||
| fragmentShader: '', | |||||
| vertexShader: '', | |||||
| uniforms: {}, | |||||
| defines: {}, | |||||
| extensions: {}, | |||||
| isRawShaderMaterial: false, | |||||
| uniformsGroups: {}, | |||||
| wireframe: false, | |||||
| wireframeLinewidth: 1, | |||||
| clipping: false, | |||||
| lights: false, | |||||
| fog: false, | |||||
| glslVersion: null, | |||||
| defaultAttributeValues: {}, | |||||
| } | |||||
| assetType = 'material' as const | |||||
| public readonly isAShaderMaterial = true | |||||
| public materialExtensions: MaterialExtension[] = [] | |||||
| readonly appliedMeshes: Set<any> = new Set() | |||||
| readonly setDirty = iMaterialCommons.setDirty | |||||
| readonly isRawShaderMaterial: boolean | |||||
| type: 'ShaderMaterial' | 'RawShaderMaterial' = 'ShaderMaterial' | |||||
| constructor({customMaterialExtensions, ...parameters}: ShaderMaterialParameters & IMaterialParameters = {}, isRawShaderMaterial = false) { | |||||
| super(parameters) | |||||
| this.isRawShaderMaterial = isRawShaderMaterial | |||||
| if (isRawShaderMaterial) { | |||||
| this.type = 'RawShaderMaterial' | |||||
| } | |||||
| if (customMaterialExtensions) this.registerMaterialExtensions(customMaterialExtensions) | |||||
| iMaterialCommons.upgradeMaterial.call(this) | |||||
| } | |||||
| // region Material Extension | |||||
| extraUniformsToUpload: Record<string, IUniform> = {} | |||||
| registerMaterialExtensions = iMaterialCommons.registerMaterialExtensions | |||||
| unregisterMaterialExtensions = iMaterialCommons.unregisterMaterialExtensions | |||||
| customProgramCacheKey(): string { | |||||
| return super.customProgramCacheKey() + MaterialExtender.CacheKeyForExtensions(this, this.materialExtensions) + this.userData.inverseAlphaMap | |||||
| } | |||||
| onBeforeCompile(shader: Shader, renderer: WebGLRenderer): void { // shader is not Shader but WebglUniforms.getParameters return value type so includes defines | |||||
| iMaterialCommons.onBeforeCompile.call(this, shader, renderer) | |||||
| super.onBeforeCompile(shader, renderer) | |||||
| } | |||||
| onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onBeforeRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onBeforeRender.call(this, renderer, scene, camera, geometry, object) | |||||
| } | |||||
| onAfterRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onAfterRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onAfterRender.call(this, renderer, scene, camera, geometry, object) | |||||
| } | |||||
| // endregion | |||||
| /** | |||||
| * Sets the values of this material based on the values of the passed material or an object with material properties | |||||
| * The input is expected to be a valid material or a deserialized material parameters object(including the deserialized userdata) | |||||
| * @param parameters - material or material parameters object | |||||
| */ | |||||
| setValues(parameters: Material|(ShaderMaterialParameters)): this { | |||||
| return iMaterialCommons.setValues(super.setValues).call(this, parameters) | |||||
| } | |||||
| toJSON(_?: any): any { // todo make abstract? | |||||
| throw new Error('Method not supported for this material.') | |||||
| } | |||||
| fromJSON(_: any, _2?: any): this | null { // todo make abstract? | |||||
| throw new Error('Method not supported for this material.') | |||||
| } | |||||
| /** | |||||
| * @deprecated use this directly | |||||
| */ | |||||
| get materialObject() { | |||||
| return this | |||||
| } | |||||
| } |
| import { | |||||
| BufferGeometry, | |||||
| Camera, | |||||
| Color, | |||||
| IUniform, | |||||
| Material, | |||||
| MeshBasicMaterial, | |||||
| MeshBasicMaterialParameters, | |||||
| MultiplyOperation, | |||||
| Object3D, | |||||
| Scene, | |||||
| Shader, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| import { | |||||
| IMaterial, | |||||
| IMaterialEvent, | |||||
| IMaterialEventTypes, | |||||
| IMaterialGenerator, | |||||
| IMaterialParameters, | |||||
| IMaterialTemplate, | |||||
| } from '../IMaterial' | |||||
| import {MaterialExtender, MaterialExtension} from '../../materials' | |||||
| import {shaderReplaceString} from '../../utils/shader-helpers' | |||||
| import {SerializationMetaType, ThreeSerialization} from '../../utils/serialization' | |||||
| import {ITexture} from '../ITexture' | |||||
| import {iMaterialCommons, threeMaterialPropList} from './iMaterialCommons' | |||||
| import {IObject3D} from '../IObject' | |||||
| import {iMaterialUI} from './IMaterialUi' | |||||
| export type UnlitMaterialEventTypes = IMaterialEventTypes | '' | |||||
| export class UnlitMaterial extends MeshBasicMaterial<IMaterialEvent, UnlitMaterialEventTypes> implements IMaterial<IMaterialEvent, UnlitMaterialEventTypes> { | |||||
| declare ['constructor']: typeof UnlitMaterial | |||||
| public static readonly TypeSlug = 'bmat' | |||||
| public static readonly TYPE = 'UnlitMaterial' // not using .type because it is used by three.js | |||||
| assetType = 'material' as const | |||||
| public readonly isUnlitMaterial = true | |||||
| public materialExtensions: MaterialExtension[] = [] | |||||
| readonly appliedMeshes: Set<IObject3D> = new Set() | |||||
| readonly setDirty = iMaterialCommons.setDirty | |||||
| clone(): this {return iMaterialCommons.clone(super.clone).call(this)} | |||||
| generator?: IMaterialGenerator | |||||
| envMap: ITexture | null = null | |||||
| constructor({customMaterialExtensions, ...parameters}: MeshBasicMaterialParameters & IMaterialParameters = {}) { | |||||
| super(parameters) | |||||
| this.fog = false | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| if (customMaterialExtensions) this.registerMaterialExtensions(customMaterialExtensions) | |||||
| iMaterialCommons.upgradeMaterial.call(this) | |||||
| } | |||||
| // region Material Extension | |||||
| extraUniformsToUpload: Record<string, IUniform> = {} | |||||
| registerMaterialExtensions = iMaterialCommons.registerMaterialExtensions | |||||
| unregisterMaterialExtensions = iMaterialCommons.unregisterMaterialExtensions | |||||
| customProgramCacheKey(): string { | |||||
| return super.customProgramCacheKey() + MaterialExtender.CacheKeyForExtensions(this, this.materialExtensions) + this.userData.inverseAlphaMap | |||||
| } | |||||
| onBeforeCompile(shader: Shader, renderer: WebGLRenderer): void { // shader is not Shader but WebglUniforms.getParameters return value type so includes defines | |||||
| const f = [ | |||||
| ['vec3 outgoingLight = ', 'afterModulation'], // added markers before found substring | |||||
| ['#include <aomap_fragment>', 'beforeModulation'], | |||||
| ['ReflectedLight reflectedLight = ', 'beforeAccumulation'], | |||||
| ['#include <clipping_planes_fragment>', 'mainStart'], | |||||
| ] | |||||
| const v = [ | |||||
| ['#include <uv_vertex>', 'mainStart'], | |||||
| ] | |||||
| for (const vElement of v) shader.vertexShader = shaderReplaceString(shader.vertexShader, vElement[0], '#glMarker ' + vElement[1] + '\n' + vElement[0]) | |||||
| for (const vElement of f) shader.fragmentShader = shaderReplaceString(shader.fragmentShader, vElement[0], '#glMarker ' + vElement[1] + '\n' + vElement[0]) | |||||
| iMaterialCommons.onBeforeCompile.call(this, shader, renderer) | |||||
| // ;(shader as any).defines.INVERSE_ALPHAMAP = this.userData.inverseAlphaMap ? 1 : 0 // todo | |||||
| super.onBeforeCompile(shader, renderer) | |||||
| } | |||||
| onBeforeRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onBeforeRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onBeforeRender.call(this, renderer, scene, camera, geometry, object) | |||||
| // const t = this.userData.inverseAlphaMap ? 1 : 0 // todo | |||||
| // if (t !== this.defines.INVERSE_ALPHAMAP) { | |||||
| // this.defines.INVERSE_ALPHAMAP = t | |||||
| // this.needsUpdate = true | |||||
| // } | |||||
| } | |||||
| onAfterRender(renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, object: Object3D): void { | |||||
| super.onAfterRender(renderer, scene, camera, geometry, object) | |||||
| iMaterialCommons.onAfterRender.call(this, renderer, scene, camera, geometry, object) | |||||
| } | |||||
| // endregion | |||||
| // region Serialization | |||||
| /** | |||||
| * Sets the values of this material based on the values of the passed material or an object with material properties | |||||
| * The input is expected to be a valid material or a deserialized material parameters object(including the deserialized userdata) | |||||
| * @param parameters - material or material parameters object | |||||
| * @param allowInvalidType - if true, the type of the oldMaterial is not checked. Objects without type are always allowed. | |||||
| * @param clearCurrentUserData - if undefined, then depends on material.isMaterial. if true, the current userdata is cleared before setting the new values, because it can have data which wont be overwritten if not present in the new material. | |||||
| */ | |||||
| setValues(parameters: Material|(MeshBasicMaterialParameters&{type?:string}), allowInvalidType = true, clearCurrentUserData: boolean|undefined = undefined): this { | |||||
| if (!parameters) return this | |||||
| if (parameters.type && !allowInvalidType && !['MeshBasicMaterial', 'MeshBasicMaterial2', this.constructor.TYPE].includes(parameters.type)) { | |||||
| console.error('Material type is not supported:', parameters.type) | |||||
| return this | |||||
| } | |||||
| if (clearCurrentUserData === undefined) clearCurrentUserData = (<Material>parameters).isMaterial | |||||
| if (clearCurrentUserData) this.userData = {} | |||||
| iMaterialCommons.setValues(super.setValues).call(this, parameters) | |||||
| this.userData.uuid = this.uuid // just in case | |||||
| return this | |||||
| } | |||||
| copy(source: Material|any): this { | |||||
| return this.setValues(source, false) | |||||
| } | |||||
| /** | |||||
| * Serializes this material to JSON. | |||||
| * @param meta - metadata for serialization | |||||
| * @param _internal - Calls only super.toJSON, does internal three.js serialization and @serialize tags. Set it to true only if you know what you are doing. This is used in Serialization->serializer->material | |||||
| */ | |||||
| toJSON(meta?: SerializationMetaType, _internal = false): any { | |||||
| if (_internal) return { | |||||
| ...super.toJSON(meta), | |||||
| ...ThreeSerialization.Serialize(this, meta, true), // this will serialize the properties of this class(like defined with @serialize and @serialize attribute) | |||||
| } | |||||
| return ThreeSerialization.Serialize(this, meta, false) // this will call toJSON again, but with baseOnly=true, that's why we set isThis to false. | |||||
| } | |||||
| /** | |||||
| * Deserializes the material from JSON. | |||||
| * Textures should be loaded and in meta.textures before calling this method. | |||||
| * todo - needs to be tested | |||||
| * @param data | |||||
| * @param meta | |||||
| * @param _internal | |||||
| */ | |||||
| fromJSON(data: any, meta?: SerializationMetaType, _internal = false): this | null { | |||||
| if (_internal) { | |||||
| ThreeSerialization.Deserialize(data, this, meta, true) | |||||
| return this.setValues(data) | |||||
| } | |||||
| ThreeSerialization.Deserialize(data, this, meta, false) | |||||
| return this | |||||
| } | |||||
| // endregion | |||||
| // region UI Config | |||||
| // todo dispose ui config | |||||
| uiConfig: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: 'Unlit Material', | |||||
| uuid: 'MBM2_' + this.uuid, | |||||
| expanded: true, | |||||
| children: [ | |||||
| ...iMaterialUI.base(this), | |||||
| iMaterialUI.blending(this), | |||||
| iMaterialUI.polygonOffset(this), | |||||
| iMaterialUI.aoLightMap(this), | |||||
| ...iMaterialUI.misc(this), | |||||
| ], | |||||
| } | |||||
| // endregion UI Config | |||||
| // Class properties can also be listed with annotations like @serialize or @property | |||||
| static readonly MaterialProperties = { | |||||
| ...threeMaterialPropList, | |||||
| color: new Color(0xffffff), | |||||
| map: null, | |||||
| lightMap: null, | |||||
| lightMapIntensity: 1, | |||||
| aoMap: null, | |||||
| aoMapIntensity: 1, | |||||
| specularMap: null, | |||||
| alphaMap: null, | |||||
| envMap: null, | |||||
| combine: MultiplyOperation, | |||||
| envMapIntensity: 1, | |||||
| reflectivity: 1, | |||||
| refractionRatio: 0.98, | |||||
| wireframe: false, | |||||
| wireframeLinewidth: 1, | |||||
| wireframeLinecap: 'round', | |||||
| wireframeLinejoin: 'round', | |||||
| skinning: false, | |||||
| fog: true, | |||||
| } | |||||
| static MaterialTemplate: IMaterialTemplate<UnlitMaterial, Partial<typeof UnlitMaterial.MaterialProperties>> = { | |||||
| materialType: UnlitMaterial.TYPE, | |||||
| name: 'unlit', | |||||
| typeSlug: UnlitMaterial.TypeSlug, | |||||
| alias: ['basic', 'unlit', UnlitMaterial.TYPE, UnlitMaterial.TypeSlug, 'MeshBasicMaterial', 'MeshBasicMaterial2'], | |||||
| params: { | |||||
| color: new Color(1, 1, 1), | |||||
| }, | |||||
| generator: (params) => { | |||||
| return new UnlitMaterial(params) | |||||
| }, | |||||
| } | |||||
| } | |||||
| export class MeshBasicMaterial2 extends UnlitMaterial { | |||||
| constructor(parameters?: MeshBasicMaterialParameters) { | |||||
| super(parameters) | |||||
| console.error('MeshBasicMaterial2 is deprecated, use UnlitMaterial instead') | |||||
| } | |||||
| } |
| import { | |||||
| AddEquation, | |||||
| AlwaysStencilFunc, | |||||
| ColorManagement, | |||||
| FrontSide, | |||||
| KeepStencilOp, | |||||
| LessEqualDepth, | |||||
| Material, | |||||
| MaterialParameters, | |||||
| NormalBlending, | |||||
| OneMinusSrcAlphaFactor, | |||||
| Scene, | |||||
| Shader, | |||||
| SrcAlphaFactor, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {copyProps} from 'ts-browser-helpers' | |||||
| import {copyMaterialUserData} from '../../utils/serialization' | |||||
| import {MaterialExtender, MaterialExtension} from '../../materials' | |||||
| import {IScene} from '../IScene' | |||||
| import {IMaterial, IMaterialSetDirtyOptions} from '../IMaterial' | |||||
| /** | |||||
| * Map of all material properties and their default values in three.js - Material.js | |||||
| * This is used to copy properties and serialize/deserialize them. | |||||
| * @note: Upgrade note: keep updated from three.js/src/Material.js:22 | |||||
| */ | |||||
| export const threeMaterialPropList = { | |||||
| // uuid: '', // DONT COPY, should remain commented | |||||
| name: '', | |||||
| blending: NormalBlending, | |||||
| side: FrontSide, | |||||
| vertexColors: false, | |||||
| opacity: 1, | |||||
| transparent: false, | |||||
| blendSrc: SrcAlphaFactor, | |||||
| blendDst: OneMinusSrcAlphaFactor, | |||||
| blendEquation: AddEquation, | |||||
| blendSrcAlpha: null, | |||||
| blendDstAlpha: null, | |||||
| blendEquationAlpha: null, | |||||
| depthFunc: LessEqualDepth, | |||||
| depthTest: true, | |||||
| depthWrite: true, | |||||
| stencilWriteMask: 0xff, | |||||
| stencilFunc: AlwaysStencilFunc, | |||||
| stencilRef: 0, | |||||
| stencilFuncMask: 0xff, | |||||
| stencilFail: KeepStencilOp, | |||||
| stencilZFail: KeepStencilOp, | |||||
| stencilZPass: KeepStencilOp, | |||||
| stencilWrite: false, | |||||
| clippingPlanes: null, | |||||
| clipIntersection: false, | |||||
| clipShadows: false, | |||||
| shadowSide: null, | |||||
| colorWrite: true, | |||||
| precision: null, | |||||
| polygonOffset: false, | |||||
| polygonOffsetFactor: 0, | |||||
| polygonOffsetUnits: 0, | |||||
| dithering: false, | |||||
| alphaToCoverage: false, | |||||
| premultipliedAlpha: false, | |||||
| forceSinglePass: false, | |||||
| visible: true, | |||||
| toneMapped: true, | |||||
| userData: {}, | |||||
| // wireframeLinecap: 'round', | |||||
| // wireframeLinejoin: 'round', | |||||
| alphaTest: 0, | |||||
| // fog: true, | |||||
| } | |||||
| export const iMaterialCommons = { | |||||
| threeMaterialPropList, | |||||
| setDirty: function(this: IMaterial, options?: IMaterialSetDirtyOptions): void { | |||||
| this.needsUpdate = true | |||||
| this.dispatchEvent({bubbleToObject: true, ...options, type: 'materialUpdate', material: this}) // this sets sceneUpdate in root scene | |||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| }, | |||||
| setValues: (superSetValues: Material['setValues']): IMaterial['setValues'] => | |||||
| function(this: IMaterial, parameters: Material | (MaterialParameters & {type?: string})): IMaterial { | |||||
| // legacy check for old color management(non-sRGB) in material.setValues todo: move this to Material.fromJSON | |||||
| const legacyColors = (parameters as any)?.metadata && (parameters as any)?.metadata.version <= 4.5 | |||||
| const lastColorManagementEnabled = ColorManagement.enabled | |||||
| if (legacyColors) ColorManagement.enabled = false | |||||
| const propList = this.constructor.MaterialProperties | |||||
| const params: any = !propList ? {...parameters} : copyProps(parameters, {} as any, Array.from(Object.keys(propList))) | |||||
| // remove undefined values | |||||
| for (const key of Object.keys(params)) if (params[key] === undefined) delete params[key] | |||||
| const userData = params.userData | |||||
| delete params.userData | |||||
| // todo: can migrate to @serialize for properties which have UI etc and use super.setValues for the rest like threeMaterialPropList | |||||
| superSetValues.call(this, params) | |||||
| if (userData) copyMaterialUserData(this.userData, userData) | |||||
| if (legacyColors) ColorManagement.enabled = lastColorManagementEnabled | |||||
| this.setDirty?.() | |||||
| return this | |||||
| }, | |||||
| clone: (superClone: Material<any, any>['clone']): IMaterial['clone'] => | |||||
| function(this: IMaterial): IMaterial { | |||||
| if (!this.userData.cloneId) { | |||||
| this.userData.cloneId = '0' | |||||
| } | |||||
| if (!this.userData.cloneCount) { | |||||
| this.userData.cloneCount = 0 | |||||
| } | |||||
| this.userData.cloneCount += 1 | |||||
| const material: IMaterial = this.generator?.({})?.setValues(this, false) ?? superClone.call(this) | |||||
| material.userData.cloneId = material.userData.cloneId + '_' + this.userData.cloneCount | |||||
| material.userData.cloneCount = 0 | |||||
| material.name = material.name + '_' + material.userData.cloneId | |||||
| return material | |||||
| }, | |||||
| registerMaterialExtensions: function(this: IMaterial, customMaterialExtensions: MaterialExtension[]): void { | |||||
| MaterialExtender.RegisterExtensions(this, customMaterialExtensions) | |||||
| }, | |||||
| unregisterMaterialExtensions: function(this: IMaterial, customMaterialExtensions: MaterialExtension[]): void { | |||||
| MaterialExtender.UnregisterExtensions(this, customMaterialExtensions) | |||||
| }, | |||||
| onBeforeCompile: function(this: IMaterial, shader: Shader, renderer: WebGLRenderer): void { | |||||
| if (this.materialExtensions) MaterialExtender.ApplyMaterialExtensions(this, shader, this.materialExtensions, renderer) | |||||
| this.dispatchEvent({type: 'beforeCompile', shader, renderer}) | |||||
| shader.fragmentShader = shader.fragmentShader.replaceAll('#glMarker', '// ') | |||||
| shader.vertexShader = shader.vertexShader.replaceAll('#glMarker', '// ') | |||||
| }, | |||||
| onBeforeRender: function(this: IMaterial, renderer, scene: Scene & Partial<IScene>, camera, geometry, object) { | |||||
| if (this.envMapIntensity !== undefined && !this.userData.separateEnvMapIntensity && scene.envMapIntensity !== undefined) { | |||||
| this.userData.__envIntensity = this.envMapIntensity | |||||
| this.envMapIntensity = scene.envMapIntensity | |||||
| } | |||||
| if (this.defines && this.envMap !== undefined && scene.fixedEnvMapDirection !== undefined) { | |||||
| if (scene.fixedEnvMapDirection) { | |||||
| if (!this.defines.FIX_ENV_DIRECTION) { | |||||
| this.defines.FIX_ENV_DIRECTION = '1' | |||||
| this.needsUpdate = true | |||||
| } | |||||
| } else if (this.defines.FIX_ENV_DIRECTION !== undefined) { | |||||
| delete this.defines.FIX_ENV_DIRECTION | |||||
| this.needsUpdate = true | |||||
| } | |||||
| } | |||||
| this.dispatchEvent({type: 'beforeRender', renderer, scene, camera, geometry, object}) | |||||
| } as IMaterial['onBeforeRender'], | |||||
| onAfterRender: function(this: IMaterial, renderer, scene: Scene & Partial<IScene>, camera, geometry, object) { | |||||
| if (this.userData.__envIntensity !== undefined) { | |||||
| this.envMapIntensity = this.userData.__envIntensity | |||||
| delete this.userData.__envIntensity | |||||
| } | |||||
| this.dispatchEvent({type: 'afterRender', renderer, scene, camera, geometry, object}) | |||||
| } as IMaterial['onAfterRender'], | |||||
| upgradeMaterial: upgradeMaterial, | |||||
| // todo; | |||||
| } as const | |||||
| /** | |||||
| * Convert a standard three.js {@link Material} to {@link IMaterial} | |||||
| */ | |||||
| export function upgradeMaterial(this: IMaterial): IMaterial { | |||||
| if (!this.isMaterial) { | |||||
| console.error('Material is not a material', this) | |||||
| return this | |||||
| } | |||||
| if (!this.setDirty) this.setDirty = iMaterialCommons.setDirty | |||||
| if (!this.appliedMeshes) this.appliedMeshes = new Set() | |||||
| if (!this.userData) this.userData = {} | |||||
| this.userData.uuid = this.uuid | |||||
| // legacy | |||||
| if (!this.userData.setDirty) this.userData.setDirty = (e: any) => { | |||||
| console.warn('userData.setDirty is deprecated. Use setDirty instead.') | |||||
| this.setDirty(e) | |||||
| } | |||||
| if (this.assetType === 'material') return this // already upgraded | |||||
| this.assetType = 'material' | |||||
| this.setValues = iMaterialCommons.setValues(this.setValues) | |||||
| this.clone = iMaterialCommons.clone(this.clone) | |||||
| // todo: add uiconfig, serialization, other stuff from UnlitMaterial? | |||||
| // dispose uiconfig etc. on dispose | |||||
| return this | |||||
| } |
| import {IObject3D} from '../IObject' | |||||
| import {IUiConfigContainer, UiObjectConfig} from 'uiconfig.js' | |||||
| import {ICamera} from '../ICamera' | |||||
| export function makeIObject3DUiConfig(this: IObject3D, isMesh?:boolean): UiObjectConfig { | |||||
| if (!this) return {} | |||||
| if (this.uiConfig) return this.uiConfig | |||||
| const config: UiObjectConfig = { | |||||
| type: 'folder', | |||||
| label: this.name || 'unnamed', | |||||
| expanded: true, | |||||
| limitedUi: true, | |||||
| children: [ | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Visible', | |||||
| property: [this, 'visible'], | |||||
| limitedUi: true, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Pick/Focus', | |||||
| value: ()=>{ | |||||
| // todo instead of dispatching, make a IObject3D.select function | |||||
| this.dispatchEvent({type: 'select', ui: true, value: this, bubbleToParent: true, focusCamera: true}) | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Pick Parent', | |||||
| hidden: ()=>!this.parent, | |||||
| value: ()=>{ | |||||
| const parent = this.parent | |||||
| if (parent) { | |||||
| parent.dispatchEvent({type: 'select', ui: true, bubbleToParent: true, value: parent}) | |||||
| } | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Name', | |||||
| property: [this, 'name'], | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Casts Shadow', | |||||
| hidden: () => !(this as any).isMesh, | |||||
| property: [this, 'castShadow'], | |||||
| // onChange: this.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Receive Shadow', | |||||
| hidden: () => !(this as any).isMesh, | |||||
| property: [this, 'receiveShadow'], | |||||
| // onChange: this.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Frustum culled', | |||||
| property: [this, 'frustumCulled'], | |||||
| // onChange: this.setDirty, | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Position', | |||||
| property: [this, 'position'], | |||||
| limitedUi: true, | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Rotation', | |||||
| property: [this, 'rotation'], | |||||
| limitedUi: true, | |||||
| }, | |||||
| { | |||||
| type: 'vec3', | |||||
| label: 'Scale', | |||||
| property: [this, 'scale'], | |||||
| }, | |||||
| { | |||||
| type: 'input', | |||||
| label: 'Render Order', | |||||
| property: [this, 'renderOrder'], | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Auto Scale', | |||||
| hidden: ()=>!this.autoScale, | |||||
| prompt: ['Auto Scale Radius: Object will be scaled to the given radius', this.userData.autoScaleRadius || '2', true], | |||||
| value: (res: string|null)=>{ | |||||
| if (!res) return | |||||
| const rad = parseFloat(res) | |||||
| if (Math.abs(rad) > 0) this.autoScale?.(rad) | |||||
| }, | |||||
| }, | |||||
| // { | |||||
| // type: 'button', | |||||
| // label: 'Auto Center', | |||||
| // value: ()=>{ | |||||
| // autoCenterObject3D(object) | |||||
| // }, | |||||
| // }, | |||||
| this.userData.license !== undefined ? { | |||||
| type: 'input', | |||||
| label: 'License/Credits', | |||||
| property: [this.userData, 'license'], | |||||
| limitedUi: true, | |||||
| } : {}, | |||||
| ], | |||||
| } | |||||
| if (this.isMesh && isMesh !== false) { | |||||
| // todo: move to make mesh ui function? | |||||
| const ui = [ | |||||
| // morph targets | |||||
| ()=>{ | |||||
| const dict = Object.entries(this.morphTargetDictionary || {}) | |||||
| return dict.length ? { | |||||
| label: 'Morph Targets', | |||||
| type: 'folder', | |||||
| children: dict.map(([name, i])=>({ | |||||
| type: 'slider', | |||||
| label: name, | |||||
| bounds: [0, 1], | |||||
| stepSize: 0.0001, | |||||
| property: [this.morphTargetInfluences, i as any], | |||||
| onChange: (e: any)=>{ | |||||
| this.setDirty?.({refreshScene: e.last, frameFade: false, refreshUi: false}) | |||||
| }, | |||||
| })), | |||||
| } : undefined | |||||
| }, | |||||
| // geometry | |||||
| ()=>(this.geometry as IUiConfigContainer)?.uiConfig, | |||||
| // material(s) | |||||
| ()=>Array.isArray(this.material) ? this.material.length < 1 ? undefined : { | |||||
| label: 'Materials', | |||||
| type: 'folder', | |||||
| children: (this.material as IUiConfigContainer[]).map((a)=>a?.uiConfig).filter(a=>a), | |||||
| } : (this.material as IUiConfigContainer)?.uiConfig, | |||||
| ] | |||||
| ;(config.children as UiObjectConfig[]).push(...ui) | |||||
| } | |||||
| // todo: if we are replacing all the cameras in the scene, is this even required? | |||||
| if (this.isCamera) { | |||||
| // todo: move to make camera ui function? | |||||
| const ui: UiObjectConfig[] = [ | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Set View', | |||||
| value: ()=>{ | |||||
| // todo: call setView on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'setView', ui: true, camera: this as ICamera}) | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Activate main', | |||||
| hidden: ()=>(this as ICamera)?.isMainCamera, | |||||
| value: ()=>{ | |||||
| // todo: call activateMain on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'activateMain', ui: true, camera: this as ICamera}) | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'button', | |||||
| label: 'Deactivate main', | |||||
| hidden: ()=>!(this as ICamera)?.isMainCamera, | |||||
| value: ()=>{ | |||||
| // todo: call activateMain on the camera, which will dispatch the event | |||||
| (this as ICamera).dispatchEvent({type: 'activateMain', ui: true, camera: undefined}) | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| { | |||||
| type: 'checkbox', | |||||
| label: 'Auto LookAt Target', | |||||
| getValue: ()=>(this as ICamera).userData.autoLookAtTarget ?? false, | |||||
| setValue: (v)=>{ | |||||
| (this as ICamera).userData.autoLookAtTarget = v | |||||
| config.uiRefresh?.(true, 'postFrame') | |||||
| }, | |||||
| }, | |||||
| ] | |||||
| ;(config.children as UiObjectConfig[]).push(...ui) | |||||
| } | |||||
| // todo: lights? | |||||
| // todo: issue when selected object is moved to picking from SceneUI | |||||
| // (config.children as UiObjectConfig[]).push(makeHierarchyUi(object)) | |||||
| this.uiConfig = config | |||||
| return config | |||||
| } | |||||
| // function makeHierarchyUi(object: Object3D, root?: Object3D): UiObjectConfig { | |||||
| // const dispatch = ()=>(root || object).dispatchEvent({type: 'select', ui: true, value: object}) | |||||
| // if (object.children.length === 0) return { | |||||
| // type: 'button', | |||||
| // label: 'Select ' + (object.name || 'unnamed'), | |||||
| // // limitedUi: true, | |||||
| // value: dispatch, | |||||
| // } | |||||
| // return { | |||||
| // type: 'folder', | |||||
| // label: 'Select ' + (object.name || 'unnamed'), | |||||
| // // limitedUi: true, | |||||
| // children: object.children.map((child)=>makeHierarchyUi(child, root || object)), | |||||
| // value: dispatch, | |||||
| // onExpand: dispatch, | |||||
| // } | |||||
| // } |
| import { | |||||
| Color, | |||||
| EquirectangularReflectionMapping, | |||||
| EventListener, | |||||
| IUniform, | |||||
| Object3D, | |||||
| Scene, | |||||
| UVMapping, | |||||
| Vector3, | |||||
| } from 'three' | |||||
| import {IObject3D, IObjectProcessor} from '../IObject' | |||||
| import {type ICamera} from '../ICamera' | |||||
| import {Box3B} from '../../three/math/Box3B' | |||||
| import {AnyOptions, onChange, serialize} from 'ts-browser-helpers' | |||||
| import {PerspectiveCamera2} from '../camera/PerspectiveCamera2' | |||||
| import {ThreeSerialization} from '../../utils/serialization' | |||||
| import {ITexture} from '../ITexture' | |||||
| import {AddObjectOptions, IScene, ISceneEvent, ISceneEventTypes, ISceneSetDirtyOptions} from '../IScene' | |||||
| import {iObjectCommons} from './iObjectCommons' | |||||
| import {RootSceneImportResult} from '../../assetmanager/IAssetImporter' | |||||
| export class RootScene extends Scene<ISceneEvent, ISceneEventTypes> implements IScene<ISceneEvent, ISceneEventTypes> { | |||||
| isRootScene = true | |||||
| assetType = 'model' as const | |||||
| // private _processors = new ObjectProcessorMap<'environment' | 'background'>() | |||||
| // private _sceneObjects: ISceneObject[] = [] | |||||
| private _mainCamera: ICamera | null = null | |||||
| /** | |||||
| * The root object where all imported objects are added. | |||||
| */ | |||||
| readonly modelRoot: IObject3D | |||||
| /** | |||||
| * The default camera in the scene | |||||
| */ | |||||
| @serialize() readonly defaultCamera: ICamera | |||||
| /** | |||||
| * The intensity for the environment light. | |||||
| */ | |||||
| @onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty | |||||
| @serialize() envMapIntensity = 1 | |||||
| /** | |||||
| * Fixed direction environment reflections irrespective of camera position. | |||||
| */ | |||||
| @onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty | |||||
| @serialize() fixedEnvMapDirection = false | |||||
| /** | |||||
| * The intensity for the environment light. | |||||
| */ | |||||
| @onChange(RootScene.prototype.setDirty) // todo: fix options that get passed to setDirty | |||||
| @serialize() backgroundIntensity = 1 | |||||
| // private _environmentLight?: IEnvironmentLight | |||||
| // required just because we don't want activeCamera to be null. | |||||
| private _dummyCam = new PerspectiveCamera2('') as ICamera | |||||
| get mainCamera(): ICamera { | |||||
| return this._mainCamera || this._dummyCam | |||||
| } | |||||
| set mainCamera(camera: ICamera | undefined) { | |||||
| const cam = this.mainCamera | |||||
| if (!camera) camera = this.defaultCamera | |||||
| if (cam === camera) return | |||||
| if (cam) { | |||||
| cam.deactivateMain(undefined, true) | |||||
| cam.removeEventListener('cameraUpdate', this._mainCameraUpdate) | |||||
| } | |||||
| if (camera) { | |||||
| camera.activateMain(undefined, true) | |||||
| camera.addEventListener('cameraUpdate', this._mainCameraUpdate) | |||||
| this._mainCamera = camera | |||||
| } else { | |||||
| this._mainCamera = null | |||||
| } | |||||
| this.dispatchEvent({type: 'activeCameraChange', lastCamera: cam, camera}) // deprecated | |||||
| this.dispatchEvent({type: 'mainCameraChange', lastCamera: cam, camera}) | |||||
| this.setDirty() | |||||
| } | |||||
| addEventListener<T extends ISceneEventTypes>(type: T, listener: EventListener<ISceneEvent, T, this>): void { | |||||
| if (type === 'activeCameraChange') console.error('activeCameraChange is deprecated. Use mainCameraChange instead.') | |||||
| if (type === 'activeCameraUpdate') console.error('activeCameraUpdate is deprecated. Use mainCameraUpdate instead.') | |||||
| if (type === 'sceneMaterialUpdate') console.error('sceneMaterialUpdate is deprecated. Use materialUpdate instead.') | |||||
| if (type === 'update') console.error('update is deprecated. Use sceneUpdate instead.') | |||||
| super.addEventListener(type, listener) | |||||
| } | |||||
| /** | |||||
| * Create a scene instance. This is done automatically in the {@link ThreeViewer} and must not be created separately. | |||||
| * @param camera | |||||
| * @param objectProcessor | |||||
| */ | |||||
| constructor(camera: ICamera, objectProcessor?: IObjectProcessor) { | |||||
| super() | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| iObjectCommons.upgradeObject3D.call(this, undefined, objectProcessor) | |||||
| // this is called from parentDispatch since scene is a parent. | |||||
| this.addEventListener('materialUpdate', ()=>this.dispatchEvent({type: 'sceneMaterialUpdate'})) | |||||
| this.addEventListener('objectUpdate', this.refreshScene) | |||||
| this.defaultCamera = camera | |||||
| this.modelRoot = new Object3D() as IObject3D | |||||
| this.modelRoot.userData.rootSceneModelRoot = true | |||||
| this.modelRoot.name = 'Scene' // for the UI | |||||
| // this.modelRoot.addEventListener('update', this.setDirty) // todo: where was this dispatched from/used ? | |||||
| // eslint-disable-next-line deprecation/deprecation | |||||
| this.add(this.modelRoot as any) | |||||
| // this.addSceneObject(this.modelRoot as any, {addToRoot: true, autoScale: false}) | |||||
| // this.addSceneObject(this.defaultCamera, {addToRoot: true}) | |||||
| // eslint-disable-next-line deprecation/deprecation | |||||
| this.add(this.defaultCamera) | |||||
| this.mainCamera = this.defaultCamera | |||||
| // this.boxHelper = new Box3Helper(this.getBounds()) | |||||
| // this.boxHelper.userData.bboxVisible = false | |||||
| // this.boxHelper.visible = false | |||||
| // this.add(this.boxHelper) | |||||
| } | |||||
| /** | |||||
| * Add a widget (non-physical/interactive) object to the scene. like gizmos, ui components etc. | |||||
| * @param model | |||||
| * @param options | |||||
| */ | |||||
| // addWidget(model: IWidget, options: AnyOptions = {}): void { | |||||
| // if (model.assetType !== 'widget') { | |||||
| // console.warn('Invalid asset type for ', model, ', adding anyway') | |||||
| // } | |||||
| // this.add(model.modelObject) | |||||
| // | |||||
| // // todo: dispatch event, add event listeners, etc | |||||
| // } | |||||
| /** | |||||
| * Add any processed object to the scene. | |||||
| * @param imported | |||||
| * @param options | |||||
| */ | |||||
| addObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T { | |||||
| if (!imported) return imported | |||||
| if (!imported.isObject3D) { | |||||
| console.error('Invalid object, cannot add to scene.', imported) | |||||
| return imported | |||||
| } | |||||
| this._addObject3D(<IObject3D>imported, options) | |||||
| this.dispatchEvent({type: 'addSceneObject', object: <IObject3D>imported}) | |||||
| return imported | |||||
| } | |||||
| /** | |||||
| * Load model root scene exported to GLTF format. Used internally by {@link ThreeViewer.addSceneObject}. | |||||
| * @param obj | |||||
| * @param options | |||||
| */ | |||||
| loadModelRoot(obj: RootSceneImportResult, options?: AddObjectOptions) { | |||||
| if (!obj.userData?.rootSceneModelRoot) { | |||||
| console.error('Invalid model root scene object. Trying to add anyway.', obj) | |||||
| } | |||||
| if (obj.userData) { | |||||
| // todo deep merge all userdata? | |||||
| if (obj.userData.__importData) | |||||
| this.modelRoot.userData.__importData = { | |||||
| ...this.modelRoot.userData.__importData, | |||||
| ...obj.userData.__importData, | |||||
| } | |||||
| if (obj.userData.gltfAsset) { | |||||
| this.modelRoot.userData.__gltfAsset = { // todo: merge values? | |||||
| ...this.modelRoot.userData.__gltfAsset, | |||||
| ...obj.userData.gltfAsset, | |||||
| } | |||||
| } | |||||
| if (obj.userData.gltfExtras) | |||||
| this.modelRoot.userData.__gltfExtras = { | |||||
| ...this.modelRoot.userData.__gltfExtras, | |||||
| ...obj.userData.gltfExtras, | |||||
| } | |||||
| } | |||||
| if (obj.userData?.gltfAsset?.copyright) obj.children.forEach(c => !c.userData.license && (c.userData.license = obj.userData.gltfAsset?.copyright)) | |||||
| if (obj.animations) { | |||||
| if (!this.modelRoot.animations) this.modelRoot.animations = [] | |||||
| for (const animation of obj.animations) { | |||||
| if (this.modelRoot.animations.includes(animation)) continue | |||||
| this.modelRoot.animations.push(animation) | |||||
| } | |||||
| } | |||||
| obj.children.forEach(c=>this.addObject(c, options)) | |||||
| } | |||||
| private _addObject3D(model: IObject3D|null, {autoCenter = false, autoScale = false, autoScaleRadius = 2., addToRoot = false, license}: AddObjectOptions = {}): void { | |||||
| const obj = model | |||||
| if (!obj) { | |||||
| console.error('Invalid object, cannot add to scene.') | |||||
| return | |||||
| } | |||||
| // eslint-disable-next-line deprecation/deprecation | |||||
| if (addToRoot) this.add(obj) | |||||
| else this.modelRoot.add(obj) | |||||
| if (autoCenter && !obj.userData.isCentered) { | |||||
| obj.autoCenter?.() | |||||
| } else { | |||||
| obj.userData.isCentered = true // mark as centered, so that autoCenter is not called again when file is reloaded. | |||||
| } | |||||
| if (autoScale && !obj.userData.autoScaled) { | |||||
| obj.autoScale?.(obj.userData.autoScaleRadius || autoScaleRadius) | |||||
| } else { | |||||
| obj.userData.autoScaled = true // mark as auto-scaled, so that autoScale is not called again when file is reloaded. | |||||
| } | |||||
| if (license) obj.userData.license = [obj.userData.license, license].filter(v=>v).join(', ') | |||||
| this.setDirty({refreshScene: true}) | |||||
| } | |||||
| clearSceneModels(dispose = false): void { | |||||
| if (dispose) this.disposeSceneModels() | |||||
| this.modelRoot.clear() | |||||
| this.modelRoot.children = [] | |||||
| this.setDirty({refreshScene: true}) | |||||
| } | |||||
| disposeSceneModels(setDirty = true) { | |||||
| [...this.modelRoot.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||||
| this.modelRoot.clear() | |||||
| if (setDirty) this.setDirty({refreshScene: true}) | |||||
| } | |||||
| @serialize() | |||||
| @onChange(RootScene.prototype._onEnvironmentChange.name) // cannot do this as updateShadow in shadowBaker resets it every frame temporarily, todo: why was that needed? | |||||
| public environment: ITexture | null = null | |||||
| private _onEnvironmentChange() { | |||||
| // console.warn('environment changed') | |||||
| if (this.environment?.mapping === UVMapping) { | |||||
| this.environment.mapping = EquirectangularReflectionMapping // for PMREMGenerator | |||||
| this.environment.needsUpdate = true | |||||
| } | |||||
| this.dispatchEvent({type: 'environmentChanged', environment: this.environment}) | |||||
| this.setDirty({refreshScene: true, geometryChanged: false}) | |||||
| } | |||||
| private _onBackgroundChange() { | |||||
| this.dispatchEvent({type: 'backgroundChanged', background: this.background, backgroundColor: this.backgroundColor}) | |||||
| this.setDirty({refreshScene: true, geometryChanged: false}) | |||||
| } | |||||
| @serialize() | |||||
| @onChange(RootScene.prototype._onBackgroundChange) | |||||
| public background: null | Color | ITexture | 'environment' = null | |||||
| @serialize() | |||||
| @onChange(RootScene.prototype._onBackgroundChange) | |||||
| public backgroundColor: Color | null = null // read in three.js WebGLBackground | |||||
| /** | |||||
| * @deprecated Use addSceneObject | |||||
| */ | |||||
| add(...object: Object3D[]): this { | |||||
| super.add(...object) | |||||
| // this._onSceneUpdate() // this is not needed, since it will be bubbled up from the object3d and we will get event objectUpdate | |||||
| return this | |||||
| } | |||||
| setBackgroundColor(color: string | number | Color | null) { | |||||
| this.backgroundColor = color ? new Color(color) : null | |||||
| } | |||||
| /** | |||||
| * Mark the scene dirty, and force render in the next frame. | |||||
| * @param options - set sceneUpdate to true to to mark that any object transformations have changed. It might trigger effects like frame fade depening on plugins. | |||||
| * @returns {this} | |||||
| */ | |||||
| setDirty(options?: ISceneSetDirtyOptions): this { // todo;;; | |||||
| if (options?.sceneUpdate) { | |||||
| console.warn('sceneUpdate is deprecated, use refreshScene instead.') | |||||
| options.refreshScene = true | |||||
| } | |||||
| if (options?.refreshScene) { | |||||
| this.refreshScene(options) | |||||
| } else { | |||||
| this.dispatchEvent({type: 'update'}) // todo remove | |||||
| iObjectCommons.setDirty.call(this, {...options, scene: this}) | |||||
| } // this sets dirty in the viewer | |||||
| return this | |||||
| } | |||||
| private _mainCameraUpdate = () => { | |||||
| this.setDirty({refreshScene: false}) | |||||
| this.refreshActiveCameraNearFar() | |||||
| this.dispatchEvent({type: 'mainCameraUpdate'}) // this sets dirty in the viewer | |||||
| this.dispatchEvent({type: 'activeCameraUpdate'}) // deprecated | |||||
| } | |||||
| // cached values | |||||
| private _sceneBounds: Box3B = new Box3B | |||||
| private _sceneBoundingRadius = 0 | |||||
| /** | |||||
| * For visualizing the scene bounds. API incomplete. | |||||
| * @type {Box3Helper} | |||||
| */ | |||||
| // readonly boxHelper: Box3Helper | |||||
| refreshScene(event?: Partial<ISceneEvent> & ISceneSetDirtyOptions): this { | |||||
| if (event && event.type === 'objectUpdate' && event.object === this) return this // ignore self | |||||
| if (event?.sceneUpdate === false || event?.refreshScene === false) return this.setDirty(event) // so that it doesn't trigger frame fade, shadow refresh etc | |||||
| // console.warn(event) | |||||
| this.refreshActiveCameraNearFar() | |||||
| this._sceneBounds = this.getBounds(false, true) | |||||
| // this.boxHelper?.boxHelper?.copy?.(this._sceneBounds) | |||||
| this._sceneBoundingRadius = this._sceneBounds.getSize(new Vector3()).length() / 2. | |||||
| this.dispatchEvent({...event, type: 'sceneUpdate', hierarchyChanged: ['addedToParent', 'removedFromParent'].includes(event?.change || '')}) | |||||
| iObjectCommons.setDirty.call(this, event) | |||||
| return this | |||||
| } | |||||
| refreshUi = iObjectCommons.refreshUi | |||||
| /** | |||||
| * Dispose the scene and clear all resources. | |||||
| * @warn Not fully implemented yet, just clears the scene. | |||||
| */ | |||||
| dispose(): void { | |||||
| this.disposeSceneModels(); | |||||
| [...this.children].forEach(child => child.dispose ? child.dispose() : child.removeFromParent()) | |||||
| this.clear() | |||||
| // todo: dispose more stuff? | |||||
| this.environment?.dispose() | |||||
| if ((this.background as ITexture)?.isTexture) (this.background as ITexture)?.dispose?.() | |||||
| this.environment = null | |||||
| this.background = null | |||||
| return | |||||
| } | |||||
| /** | |||||
| * Find objects by name exact match in the complete hierarchy. | |||||
| * @param name - name | |||||
| * @param parent - optional root node to start search from | |||||
| * @returns Array of found objects | |||||
| */ | |||||
| public findObjectsByName(name: string, parent?: IObject3D): IObject3D[] { | |||||
| const o: IObject3D[] = []; | |||||
| (parent ?? this).traverse(object => { | |||||
| if (object.name === name) o.push(object) | |||||
| }) | |||||
| return o | |||||
| } | |||||
| /** | |||||
| * Returns the bounding box of the scene model root. | |||||
| * @param precise | |||||
| * @param ignoreInvisible | |||||
| * @returns {Box3B} | |||||
| */ | |||||
| getBounds(precise = false, ignoreInvisible = true): Box3B { | |||||
| // See bboxVisible in userdata in Box3B | |||||
| return new Box3B().expandByObject(this, precise, ignoreInvisible) | |||||
| } | |||||
| /** | |||||
| * Refreshes the scene active camera near far values, based on the scene bounding box. | |||||
| * This is called automatically every time the camera is updated. | |||||
| */ | |||||
| refreshActiveCameraNearFar(): void { | |||||
| const camera = this.mainCamera as ICamera | |||||
| if (!camera) return | |||||
| if (camera.userData.autoNearFar === false) { | |||||
| camera.near = camera.userData.minNearPlane ?? 0.2 | |||||
| camera.far = camera.userData.maxFarPlane ?? 1000 | |||||
| return | |||||
| } | |||||
| // todo check if this takes too much time with large scenes(when moving the camera and not animating), but we also need to support animations | |||||
| const bbox = this.getBounds(false) // todo: can we use this._sceneBounds or will it have some issue with animation? | |||||
| const pos = camera.getWorldPosition(new Vector3()).sub(bbox.getCenter(new Vector3())) | |||||
| const radius = 1.5 * bbox.getSize(new Vector3()).length() / 2. | |||||
| const dist = pos.length() | |||||
| const near = Math.max(camera.userData.minNearPlane ?? 0.2, dist - radius) | |||||
| const far = Math.min(Math.max(near + 1, dist + radius), camera.cameraObject.userData.maxFarPlane ?? 1000) | |||||
| camera.near = near | |||||
| camera.far = far | |||||
| // camera.near = 3 | |||||
| // camera.far = 20 | |||||
| } | |||||
| updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this { | |||||
| if (material.uniforms.sceneBoundingRadius) material.uniforms.sceneBoundingRadius.value = this._sceneBoundingRadius | |||||
| else console.warn('BaseRenderer: no uniform: frameCount') | |||||
| return this | |||||
| } | |||||
| /** | |||||
| * @deprecated | |||||
| * Sets the camera pointing towards the object at a specific distance. | |||||
| * @param rootObject - The object to point at. | |||||
| * @param centerOffset - The distance offset from the object to point at. | |||||
| * @param targetOffset - The distance offset for the target from the center of object to point at. | |||||
| * @param options - Not used yet. | |||||
| */ | |||||
| resetCamera(rootObject:Object3D|undefined = undefined, centerOffset = new Vector3(1, 1, 1), targetOffset = new Vector3(0, 0, 0)): void { | |||||
| if (this._mainCamera) { | |||||
| this.matrixWorldNeedsUpdate = true | |||||
| this.updateMatrixWorld(true) | |||||
| const bounds = rootObject ? new Box3B().expandByObject(rootObject, true, true) : this.getBounds(true) | |||||
| const center = bounds.getCenter(new Vector3()) | |||||
| const radius = bounds.getSize(new Vector3()).length() * 0.5 | |||||
| center.add(targetOffset.clone().multiplyScalar(radius)) | |||||
| this._mainCamera.position = new Vector3( // todo: for nested cameras? | |||||
| center.x + centerOffset.x * radius, | |||||
| center.y + centerOffset.y * radius, | |||||
| center.z + centerOffset.z * radius, | |||||
| ) | |||||
| this._mainCamera.target = center | |||||
| // this.scene.mainCamera.controls?.targetOffset.set(0, 0, 0) | |||||
| this.setDirty() | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Serialize the scene properties | |||||
| * @param meta | |||||
| * @returns {any} | |||||
| */ | |||||
| toJSON(meta?: any): any { | |||||
| const o = ThreeSerialization.Serialize(this, meta, true) | |||||
| // console.log(o) | |||||
| return o | |||||
| } | |||||
| /** | |||||
| * Deserialize the scene properties | |||||
| * @param json - object from {@link toJSON} | |||||
| * @param meta | |||||
| * @returns {this<TCamera>} | |||||
| */ | |||||
| fromJSON(json: any, meta?: any): this { | |||||
| const env = json.environment | |||||
| if (env !== undefined) { | |||||
| this.environment = ThreeSerialization.Deserialize(env, this.environment, meta, false) | |||||
| delete json.environment | |||||
| } | |||||
| ThreeSerialization.Deserialize(json, this, meta, true) | |||||
| json.environment = env | |||||
| return this | |||||
| } | |||||
| /** | |||||
| * Minimum Camera near plane | |||||
| * @deprecated - use camera.userData.minNearPlane instead | |||||
| */ | |||||
| get minNearDistance(): number { | |||||
| console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead') | |||||
| return this.mainCamera.userData.minNearPlane ?? 0.02 | |||||
| } | |||||
| /** | |||||
| * @deprecated - use camera.userData.minNearPlane instead | |||||
| */ | |||||
| set minNearDistance(value: number) { | |||||
| console.error('minNearDistance is deprecated. Use camera.userData.minNearPlane instead') | |||||
| if (this.mainCamera) | |||||
| this.mainCamera.userData.minNearPlane = value | |||||
| } | |||||
| /** | |||||
| * @deprecated | |||||
| */ | |||||
| get activeCamera(): ICamera { | |||||
| console.error('activeCamera is deprecated. Use mainCamera instead.') | |||||
| return this.mainCamera | |||||
| } | |||||
| /** | |||||
| * @deprecated | |||||
| */ | |||||
| set activeCamera(camera: ICamera | undefined) { | |||||
| console.error('activeCamera is deprecated. Use mainCamera instead.') | |||||
| this.mainCamera = camera | |||||
| } | |||||
| /** | |||||
| * Get the threejs scene object | |||||
| * @deprecated | |||||
| */ | |||||
| get modelObject(): this { | |||||
| return this as any | |||||
| } | |||||
| // region inherited type fixes | |||||
| // re-declaring from IObject3D because: https://github.com/microsoft/TypeScript/issues/16936 | |||||
| traverse: (callback: (object: IObject3D) => void) => void | |||||
| traverseVisible: (callback: (object: IObject3D) => void) => void | |||||
| traverseAncestors: (callback: (object: IObject3D) => void) => void | |||||
| getObjectById: (id: number) => IObject3D | undefined | |||||
| getObjectByName: (name: string) => IObject3D | undefined | |||||
| getObjectByProperty: (name: string, value: string) => IObject3D | undefined | |||||
| copy: (source: IObject3D, recursive?: boolean) => this | |||||
| clone: (recursive?: boolean) => this | |||||
| remove: (...object: IObject3D[]) => this | |||||
| dispatchEvent: (event: ISceneEvent) => void | |||||
| parent: null | |||||
| children: IObject3D[] | |||||
| // endregion | |||||
| // region deprecated | |||||
| // /** | |||||
| // * Set the scene environment map, this will be processed with PMREM automatically later. | |||||
| // * @param asset | |||||
| // * @returns {void} | |||||
| // */ | |||||
| // public setEnvironment(asset: ITexture|null|undefined): void { | |||||
| // if (!asset) { | |||||
| // // eslint-disable-next-line deprecation/deprecation | |||||
| // this.environment = null | |||||
| // this._onEnvironmentChange() | |||||
| // return | |||||
| // } | |||||
| // if (!asset.isTexture) { | |||||
| // console.error('Unknown Environment type', asset) | |||||
| // return | |||||
| // } | |||||
| // if (asset.mapping === UVMapping) { | |||||
| // asset.mapping = EquirectangularReflectionMapping // for PMREMGenerator | |||||
| // asset.needsUpdate = true | |||||
| // } | |||||
| // // eslint-disable-next-line deprecation/deprecation | |||||
| // this.environment = asset | |||||
| // // eslint-disable-next-line deprecation/deprecation | |||||
| // // this.background = texture // for testing. | |||||
| // this._onEnvironmentChange() | |||||
| // } | |||||
| // | |||||
| // /** | |||||
| // * Get the current scene environment map | |||||
| // * @returns {ITexture<Texture>} | |||||
| // */ | |||||
| // getEnvironment(): ITexture | null { | |||||
| // return this.environment || null | |||||
| // } | |||||
| /** | |||||
| * Add any processed scene object to the scene. | |||||
| * @deprecated renamed to {@link addObject} | |||||
| * @param imported | |||||
| * @param options | |||||
| */ | |||||
| addSceneObject<T extends IObject3D|Object3D = IObject3D>(imported: T, options?: AddObjectOptions): T { | |||||
| return this.addObject(imported, options) | |||||
| } | |||||
| /** | |||||
| * Equivalent to setDirty({refreshScene: true}), dispatches 'sceneUpdate' event with the specified options. | |||||
| * @deprecated use refreshScene | |||||
| * @param options | |||||
| */ | |||||
| updateScene(options?: AnyOptions): this { | |||||
| console.warn('updateScene is deprecated. Use refreshScene instead') | |||||
| return this.refreshScene(options || {}) | |||||
| } | |||||
| /** | |||||
| * @deprecated renamed to {@link clearSceneModels} | |||||
| */ | |||||
| removeSceneModels() { | |||||
| this.clearSceneModels() | |||||
| } | |||||
| // endregion | |||||
| } |
| import {iObjectCommons} from './iObjectCommons' | |||||
| import {Camera, Vector3} from 'three' | |||||
| import type {ICamera, ICameraEvent, ICameraSetDirtyOptions} from '../ICamera' | |||||
| export const iCameraCommons = { | |||||
| setDirty: function(this: ICamera, options?: ICameraSetDirtyOptions): void { | |||||
| // console.log('target', target, this._controls, this._camera) | |||||
| if (this.controls && this.controls.target && this.target !== this.controls.target) { | |||||
| this.controls.target.copy(this.target) | |||||
| // this.controls.update() // this should be done automatically postFrame | |||||
| } | |||||
| if (!this.controls || !this.controls.enabled) { | |||||
| if (this.userData.autoLookAtTarget) { | |||||
| this.lookAt(this.target) | |||||
| } | |||||
| } | |||||
| this.dispatchEvent({...options, type: 'update'}) // does not bubble | |||||
| this.dispatchEvent({...options, type: 'cameraUpdate', bubbleToParent: true}) // this sets dirty in the viewer | |||||
| iObjectCommons.setDirty.call(this, options) | |||||
| }, | |||||
| activateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | |||||
| if (!_internal) return this.dispatchEvent({ | |||||
| type: 'activateMain', ...options, | |||||
| camera: this, | |||||
| bubbleToParent: true, | |||||
| }) // this will be used by RootScene to deactivate other cameras and activate this one | |||||
| if (this.userData.__isMainCamera) return | |||||
| this.userData.__isMainCamera = true | |||||
| this.userData.__lastScale = this.scale.clone() | |||||
| this.scale.divide(this.getWorldScale(new Vector3())) // make unit scale, for near far and all | |||||
| if (_refresh) { | |||||
| this.refreshCameraControls(false) | |||||
| } | |||||
| this.setDirty({change: 'activateMain', ...options}) | |||||
| // console.log({...this._camera.modelObject.position}) | |||||
| }, | |||||
| deactivateMain: function(this: ICamera, options: Partial<ICameraEvent> = {}, _internal = false, _refresh = true): void { | |||||
| if (!_internal) return this.dispatchEvent({ | |||||
| type: 'activateMain', ...options, | |||||
| camera: null, | |||||
| bubbleToParent: true, | |||||
| }) // this will be used by RootScene to deactivate other cameras and activate this one | |||||
| if (!this.userData.__isMainCamera) return | |||||
| this.userData.__isMainCamera = false // or delete? | |||||
| if (this.userData.__lastScale) { | |||||
| this.scale.copy(this.userData.__lastScale) | |||||
| delete this.userData.__lastScale | |||||
| } | |||||
| if (_refresh) { | |||||
| this.refreshCameraControls(false) | |||||
| } | |||||
| this.setDirty({change: 'deactivateMain', ...options}) | |||||
| }, | |||||
| refreshUi: function(this: ICamera) { | |||||
| // todo | |||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| }, | |||||
| refreshTarget: function(this: ICamera, distanceFromTarget = 4, setDirty = true) { | |||||
| if (this.controls?.enabled && this.controls.target) { | |||||
| if (this.controls.target !== this.target) this.target.copy(this.controls.target) | |||||
| } else { | |||||
| // this.cameraObject.updateWorldMatrix(true, false) | |||||
| this.getWorldDirection(this.target) | |||||
| // .transformDirection(this.cameraObject.matrixWorldInverse) | |||||
| // .multiplyScalar(distanceFromTarget).add(this._position) | |||||
| .multiplyScalar(distanceFromTarget).add(this.getWorldPosition(new Vector3())) | |||||
| // if (this.cameraObject.parent) this.cameraObject.parent.worldToLocal(this._target) | |||||
| } | |||||
| if (setDirty) this.setDirty({change: 'target'}) | |||||
| }, | |||||
| upgradeCamera: upgradeCamera, | |||||
| copy: (superCopy: ICamera['copy']): ICamera['copy'] => | |||||
| function(this: ICamera, camera: ICamera | Camera, recursive?, distanceFromTarget?, ...args): ICamera { | |||||
| if (!camera.isCamera) { | |||||
| console.error('ICamera.copy: camera is not a Camera', camera) | |||||
| return this | |||||
| } | |||||
| superCopy.call(this, camera, recursive, ...args) | |||||
| this.position.copy(this.worldToLocal(camera.getWorldPosition(new Vector3()))) | |||||
| if ((<ICamera>camera).target?.isVector3) this.target.copy((<ICamera>camera).target) | |||||
| else { | |||||
| const minDistance = (this.controls as any).minDistance ?? distanceFromTarget ?? 4 | |||||
| camera.getWorldDirection(this.target).multiplyScalar(minDistance).add(this.getWorldPosition(new Vector3())) | |||||
| } | |||||
| this.setDirty() | |||||
| return this | |||||
| }, | |||||
| } | |||||
| function upgradeCamera(this: ICamera) { | |||||
| if (this.assetType === 'camera') return // already upgraded | |||||
| if (!this.isCamera) { | |||||
| console.error('Object is not a camera', this) | |||||
| return | |||||
| } | |||||
| iObjectCommons.upgradeObject3D.call(this) | |||||
| this.copy = iCameraCommons.copy(this.copy) | |||||
| if (!this.target) this.target = new Vector3() | |||||
| if (!this.refreshTarget) this.refreshTarget = iCameraCommons.refreshTarget | |||||
| if (!this.activateMain) this.activateMain = iCameraCommons.activateMain | |||||
| if (!this.deactivateMain) this.deactivateMain = iCameraCommons.deactivateMain | |||||
| if (!this.refreshUi) this.refreshUi = iCameraCommons.refreshUi | |||||
| if (!this.setDirty) this.setDirty = iCameraCommons.setDirty | |||||
| // if (!this.controlsMode) this.controlsMode = '' | |||||
| this.assetType = 'camera' | |||||
| // todo uiconfig, anything else? | |||||
| } |
| import {Event, Mesh, Vector3} from 'three' | |||||
| import {IMaterial, IMaterialEvent} from '../IMaterial' | |||||
| import {objectHasOwn} from 'ts-browser-helpers' | |||||
| import {IObject3D, IObject3DEvent, IObjectProcessor, IObjectSetDirtyOptions} from '../IObject' | |||||
| import {copyObject3DUserData} from '../../utils/serialization' | |||||
| import {IGeometry, IGeometryEvent} from '../IGeometry' | |||||
| import {Box3B} from '../../three' | |||||
| import {makeIObject3DUiConfig} from './IObjectUi' | |||||
| import {upgradeGeometry} from '../geometry/iGeometryCommons' | |||||
| import {iMaterialCommons} from '../material/iMaterialCommons' | |||||
| export const iObjectCommons = { | |||||
| setDirty: function(this: IObject3D, options?: IObjectSetDirtyOptions): void { | |||||
| this.dispatchEvent({bubbleToParent: true, ...options, type: 'objectUpdate', object: this}) // this sets sceneUpdate in root scene | |||||
| if (options?.refreshUi !== false) this.refreshUi?.() | |||||
| // console.log('object update') | |||||
| }, | |||||
| upgradeObject3D: upgradeObject3D, | |||||
| makeUiConfig: makeIObject3DUiConfig, | |||||
| autoCenter: function<T extends IObject3D>(this: T, setDirty = true): T { | |||||
| const bb = new Box3B().expandByObject(this, true, true) | |||||
| const center = bb.getCenter(new Vector3()) | |||||
| this.position.sub(center) | |||||
| this.updateMatrix() | |||||
| this.userData.autoCentered = true | |||||
| this.userData.isCentered = true | |||||
| if (setDirty) this.setDirty({change: 'autoCenter'}) | |||||
| return this | |||||
| }, | |||||
| autoScale: function<T extends IObject3D>(this: T, autoScaleRadius?: number, isCentered?: boolean, setDirty = true): T { | |||||
| const bbox = new Box3B().expandByObject(this, true, true) | |||||
| const radius = bbox.getSize(new Vector3()).length() * 0.5 | |||||
| if (autoScaleRadius === undefined) { | |||||
| autoScaleRadius = this.userData.autoScaleRadius || 1 | |||||
| } | |||||
| const scale = autoScaleRadius / radius | |||||
| // this.scale.multiplyScalar(20 / radius) | |||||
| if (isFinite(scale)) { // NaN when radius is 0 | |||||
| if (this.userData.pseudoCentered) { | |||||
| this.children.forEach(child => { | |||||
| child.scale.multiplyScalar(scale) | |||||
| }) | |||||
| } else | |||||
| this.scale.multiplyScalar(scale) | |||||
| if (isCentered || this.userData.isCentered) this.position.multiplyScalar(scale) | |||||
| this.traverse((obj)=>{ | |||||
| const l = obj as any | |||||
| if (l.isLight && l.shadow?.camera?.right) { | |||||
| l.shadow.camera.right *= scale | |||||
| l.shadow.camera.left *= scale | |||||
| l.shadow.camera.top *= scale | |||||
| l.shadow.camera.bottom *= scale | |||||
| obj.setDirty() | |||||
| } | |||||
| if (l.isCamera && l.right) { | |||||
| l.right *= scale | |||||
| l.left *= scale | |||||
| l.top *= scale | |||||
| l.bottom *= scale | |||||
| obj.setDirty() | |||||
| } | |||||
| }) | |||||
| this.userData.autoScaled = true | |||||
| this.userData.autoScaleRadius = autoScaleRadius | |||||
| if (setDirty) this.setDirty({change: 'autoScale'}) | |||||
| } | |||||
| return this | |||||
| }, | |||||
| eventCallbacks: { | |||||
| onAddedToParent: function(this: IObject3D, e: Event): void { | |||||
| // added to some parent | |||||
| const root = this.parent?.parentRoot ?? this.parent | |||||
| if (!this.objectProcessor && root?.objectProcessor) { // this is added so that when an upgraded(not processed) object is added to the scene, it will be processed by the scene processor | |||||
| this.traverse(o=>{ | |||||
| o.objectProcessor = root.objectProcessor | |||||
| o.objectProcessor?.processObject(o) | |||||
| }) | |||||
| } | |||||
| if (root !== this.parentRoot) { | |||||
| this.traverse(o=>{ | |||||
| o.parentRoot = root | |||||
| }) | |||||
| } | |||||
| this.setDirty?.({...e, change: 'addedToParent'}) | |||||
| }, | |||||
| onRemovedFromParent: function(this: IObject3D, e: Event): void { | |||||
| // removed from some parent | |||||
| this.setDirty?.({...e, change: 'removedFromParent'}) | |||||
| if (this.parentRoot !== undefined) { | |||||
| this.traverse(o=>{ | |||||
| o.parentRoot = undefined | |||||
| }) | |||||
| } | |||||
| }, | |||||
| onMaterialUpdate: function(this: IObject3D, e: IMaterialEvent<'materialUpdate'>): void { | |||||
| if (!e.bubbleToObject) return | |||||
| this.dispatchEvent({bubbleToParent: true, ...e, object: this, material: e.target}) | |||||
| }, | |||||
| onGeometryUpdate: function(this: IObject3D, e: IGeometryEvent<'geometryUpdate'>): void { | |||||
| if (!e.bubbleToObject) return | |||||
| this.dispatchEvent({bubbleToParent: true, ...e, object: this, geometry: e.geometry}) | |||||
| }, | |||||
| }, | |||||
| initMaterial: function(this: IObject3D): void { | |||||
| if (objectHasOwn(this, '_currentMaterial')) return | |||||
| this._currentMaterial = null | |||||
| const currentMaterial = this.material | |||||
| delete this.material | |||||
| Object.defineProperty(this, 'material', { | |||||
| get: iObjectCommons.getMaterial, | |||||
| set: iObjectCommons.setMaterial, | |||||
| }) | |||||
| Object.defineProperty(this, 'materials', { | |||||
| get: iObjectCommons.getMaterials, | |||||
| set: iObjectCommons.setMaterials, | |||||
| }) | |||||
| // this is called initially in Material manager from process model below, not required here... | |||||
| // todo: shouldnt be called from there. maybe check if material is upgraded before | |||||
| if (currentMaterial && !Array.isArray(currentMaterial) && !currentMaterial.assetType) { | |||||
| console.error('todo: initMaterial: material not upgraded') | |||||
| } | |||||
| this.material = currentMaterial | |||||
| // Legacy | |||||
| if (!(this as any).setMaterial) { | |||||
| (this as any).setMaterial = (m: IMaterial | IMaterial[]| undefined)=>{ | |||||
| const mats = this.material | |||||
| console.error('setMaterial is deprecated, use material property directly') | |||||
| this.material = m | |||||
| return mats | |||||
| } | |||||
| } | |||||
| // Legacy | |||||
| if (this.userData.setMaterial) console.error('userData.setMaterial already defined') | |||||
| this.userData.setMaterial = (m: any)=>{ | |||||
| console.error('userData.setMaterial is deprecated, use setMaterial directly') | |||||
| this.material = m | |||||
| } | |||||
| }, | |||||
| getMaterial: function(this: IObject3D): IMaterial | IMaterial[] | undefined { | |||||
| return this._currentMaterial || undefined | |||||
| }, | |||||
| getMaterials: function(this: IObject3D): IMaterial[] { | |||||
| return !this._currentMaterial ? [] : Array.isArray(this._currentMaterial) ? [...this._currentMaterial] : [this._currentMaterial] | |||||
| }, | |||||
| setMaterial: function(this: IObject3D, material: IMaterial | IMaterial[] | undefined) { | |||||
| const imats = (Array.isArray(material) ? material : [material]).filter(v=>v) | |||||
| if (this.material == imats || imats.length === 1 && this.material === imats[0]) return [] | |||||
| // todo: check by uuid? | |||||
| // Remove old material listeners | |||||
| const mats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!] | |||||
| for (const mat of mats) { | |||||
| if (!mat) continue | |||||
| this._onMaterialUpdate && mat.removeEventListener('materialUpdate', this._onMaterialUpdate) | |||||
| if (mat.appliedMeshes) { | |||||
| mat.appliedMeshes.delete(this) | |||||
| if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false) | |||||
| mat.dispose() // this will dispose textures(if they are idle) if the material is registered in the material manager | |||||
| } | |||||
| } | |||||
| const materials = [] | |||||
| for (const mat of imats) { | |||||
| // const mat = material?.materialObject | |||||
| if (!mat) continue | |||||
| if (!mat.assetType) { | |||||
| console.error('Material not upgraded') | |||||
| iMaterialCommons.upgradeMaterial.call(mat) | |||||
| } | |||||
| materials.push(mat) | |||||
| if (mat) { | |||||
| this._onMaterialUpdate && mat.addEventListener('materialUpdate', this._onMaterialUpdate) | |||||
| mat.appliedMeshes.add(this) | |||||
| } | |||||
| } | |||||
| this._currentMaterial = !materials.length ? null : materials.length !== 1 ? materials : materials[0] || null | |||||
| this.dispatchEvent({type: 'materialChanged', material, oldMaterial: mats, object: this, bubbleToParent: true}) | |||||
| this.refreshUi() | |||||
| }, | |||||
| setMaterials: function(this: IObject3D, materials: IMaterial[]) { | |||||
| this.material = materials || undefined | |||||
| }, | |||||
| initGeometry: function(this: IObject3D): void { | |||||
| const currentGeometry = this.geometry | |||||
| this._currentGeometry = null | |||||
| delete this.geometry | |||||
| Object.defineProperty(this, 'geometry', { | |||||
| get: iObjectCommons.getGeometry, | |||||
| set: iObjectCommons.setGeometry, | |||||
| }) | |||||
| this.geometry = currentGeometry | |||||
| // Legacy | |||||
| if (!(this as any).setGeometry) { | |||||
| (this as any).setGeometry = (geometry: IGeometry) =>{ | |||||
| const geom = this.geometry | |||||
| console.error('setGeometry is deprecated, use geometry property directly') | |||||
| this.geometry = geometry | |||||
| return geom | |||||
| } | |||||
| } | |||||
| // Legacy | |||||
| if (this.userData.setGeometry) console.error('userData.setGeometry already defined') | |||||
| this.userData.setGeometry = (g: any)=>{ | |||||
| console.error('userData.setGeometry is deprecated, use setGeometry directly') | |||||
| this.geometry = g | |||||
| } | |||||
| }, | |||||
| getGeometry: function(this: IObject3D&Mesh): IGeometry | undefined { | |||||
| return this._currentGeometry || undefined | |||||
| }, | |||||
| setGeometry: function(this: IObject3D&Mesh, geometry: IGeometry | undefined): void { | |||||
| const geom = this.geometry || undefined | |||||
| // todo: check by uuid? | |||||
| if (geom === geometry) return | |||||
| if (geom) { | |||||
| this._onGeometryUpdate && geom.removeEventListener('geometryUpdate', this._onGeometryUpdate) | |||||
| if (geom.appliedMeshes) { | |||||
| geom.appliedMeshes.delete(this) | |||||
| if (geom.userData && geom.appliedMeshes.size === 0 && geom.userData.disposeOnIdle !== false) geom.dispose() | |||||
| } | |||||
| } | |||||
| if (geometry) { | |||||
| if (!geometry.assetType) { | |||||
| // console.error('Geometry not upgraded') | |||||
| upgradeGeometry.call(geometry) | |||||
| } | |||||
| } | |||||
| this._currentGeometry = geometry || null | |||||
| if (geometry) { | |||||
| this.updateMorphTargets() | |||||
| this._onGeometryUpdate && geometry.addEventListener('geometryUpdate', this._onGeometryUpdate) | |||||
| geometry.appliedMeshes.add(this) | |||||
| } | |||||
| this.dispatchEvent({type: 'geometryChanged', geometry, oldGeometry: geom, bubbleToParent: true}) | |||||
| this.refreshUi() | |||||
| }, | |||||
| refreshUi: function(this: IObject3D): void { | |||||
| this.uiConfig?.uiRefresh?.(true, 'postFrame', 1) | |||||
| }, | |||||
| dispatchEvent: (superDispatch: IObject3D['dispatchEvent']) => | |||||
| function(this: IObject3D, event: IObject3DEvent): void { | |||||
| if (event.bubbleToParent || this.userData?.__autoBubbleToParentEvents?.includes(event.type)) { | |||||
| // console.log('parent dispatch', e, this.parentRoot, this.parent) | |||||
| const pRoot = this.parentRoot || this.parent | |||||
| if (this.parentRoot !== this) pRoot?.dispatchEvent(event) | |||||
| } | |||||
| superDispatch.call(this, event) | |||||
| }, | |||||
| clone: (superClone: IObject3D['clone']): IObject3D['clone'] => | |||||
| function(this: IObject3D, ...args): IObject3D { | |||||
| const userData = this.userData | |||||
| this.userData = {} | |||||
| const clone: any = superClone.call(this, ...args) | |||||
| this.userData = userData | |||||
| copyObject3DUserData(clone.userData, userData) // todo: do same for this.toJSON() | |||||
| const objParent = this.parentRoot || undefined | |||||
| if (objParent && objParent.assetType !== 'model') { | |||||
| console.warn('Cloning an IObject with a parent that is not an \'model\' is not supported') | |||||
| } | |||||
| iObjectCommons.upgradeObject3D.call(clone, objParent, this.objectProcessor) | |||||
| clone.userData.cloneParent = this.uuid | |||||
| return clone | |||||
| }, | |||||
| copy: (superCopy: IObject3D['copy']): IObject3D['copy'] => | |||||
| function(this: IObject3D, source: IObject3D, ...args): IObject3D { | |||||
| const userData = source.userData | |||||
| source.userData = {} | |||||
| const t: any = superCopy.call(this, source, ...args) | |||||
| source.userData = userData | |||||
| copyObject3DUserData(this.userData, source) // todo: do same for object.toJSON() | |||||
| return t | |||||
| }, | |||||
| add: (superAdd: IObject3D['add']): IObject3D['add'] => | |||||
| function(this: IObject3D, ...args): IObject3D { | |||||
| for (const a of args) iObjectCommons.upgradeObject3D.call(a, this.parentRoot || this, this.objectProcessor) | |||||
| return superAdd.call(this, ...args) | |||||
| }, | |||||
| dispose: (superDispose?: IObject3D['dispose']) => | |||||
| function(this: IObject3D): void { | |||||
| this.dispatchEvent({type: 'dispose', bubbleToParent: false}) | |||||
| if (this.__disposed) { | |||||
| console.warn('Object already disposed', this) | |||||
| return | |||||
| } | |||||
| this.__disposed = true | |||||
| // this is first so that the leaf children are removed from parent first, removed event will be fired depth first | |||||
| for (const c of [...this.children]) c?.dispose?.() | |||||
| this.children = [] | |||||
| if (this.parent) this.removeFromParent() | |||||
| delete this.parentRoot | |||||
| // safeSetProperty(this, 'modelObject', undefined, true) // in-case modelObject is just a getter. | |||||
| this.userData = {} // todo: clear only our userdata and maybe any private variables? | |||||
| // this.uiConfig?.dispose?.() // todo: make uiConfig.dispose | |||||
| this.uiConfig = undefined | |||||
| superDispose?.call(this) | |||||
| }, | |||||
| } | |||||
| /** | |||||
| * Converts three.js Object3D to IObject3D, setup object events, adds utility methods, and runs objectProcessor. | |||||
| * @param parent | |||||
| * @param objectProcessor | |||||
| */ | |||||
| function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void { // parent is the root Object3DModel. | |||||
| if (!this) return | |||||
| // console.log('upgradeObject3D', this, parent, objectProcessor) | |||||
| if (this.__disposed) { | |||||
| console.warn('re-init/re-add disposed object, things might not work as intended', this) | |||||
| delete this.__disposed | |||||
| } | |||||
| if (!this.userData) this.userData = {} | |||||
| this.userData.uuid = this.uuid | |||||
| // not checking assetType but custom var __objectSetup because its required in types sometimes, check PerspectiveCamera2 | |||||
| // if (this.assetType) return | |||||
| if (this.userData.__objectSetup) return | |||||
| this.userData.__objectSetup = true | |||||
| if (!this.objectProcessor) this.objectProcessor = objectProcessor || this.parent?.objectProcessor || parent?.objectProcessor | |||||
| if (!this.userData.__autoBubbleToParentEvents) this.userData.__autoBubbleToParentEvents = ['select'] | |||||
| // Event bubbling. todo: set bubbleToParent in these events when dispatched from child and remove from here? | |||||
| if (this.isCamera) this.userData.__autoBubbleToParentEvents.push('activateMain', 'setView') | |||||
| if (this.isLight) this.assetType = 'light' | |||||
| else if (this.isCamera) this.assetType = 'camera' | |||||
| else this.assetType = 'model' | |||||
| if (parent) this.parentRoot = parent | |||||
| const oldFunctions = { | |||||
| dispatchEvent: this.dispatchEvent, | |||||
| clone: this.clone, | |||||
| copy: this.copy, | |||||
| add: this.add, | |||||
| dispose: this.dispose, | |||||
| } | |||||
| this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required? | |||||
| // typed because of type-checking | |||||
| this.dispatchEvent = iObjectCommons.dispatchEvent(this.dispatchEvent) | |||||
| this.dispose = iObjectCommons.dispose(this.dispose) | |||||
| this.clone = iObjectCommons.clone(this.clone) | |||||
| this.copy = iObjectCommons.copy(this.copy) // todo: do same for object.toJSON() | |||||
| this.add = iObjectCommons.add(this.add) | |||||
| if (!this.setDirty) this.setDirty = iObjectCommons.setDirty | |||||
| if (!this.refreshUi) this.refreshUi = iObjectCommons.refreshUi | |||||
| if (!this.autoScale) this.autoScale = iObjectCommons.autoScale | |||||
| if (!this.autoCenter) this.autoCenter = iObjectCommons.autoCenter | |||||
| // fired from Object3D.js | |||||
| this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | |||||
| this.addEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) | |||||
| this.addEventListener('dispose', ()=>{ | |||||
| this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent) | |||||
| this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent) | |||||
| }) | |||||
| if ((this.isMesh || this.isLine) && !this.userData.__meshSetup) { | |||||
| this.userData.__meshSetup = true | |||||
| this._onMaterialUpdate = (e: IMaterialEvent) => iObjectCommons.eventCallbacks.onMaterialUpdate.call(this, e) | |||||
| this._onGeometryUpdate = (e: IGeometryEvent) => iObjectCommons.eventCallbacks.onGeometryUpdate.call(this, e) | |||||
| // Material, Geometry prop init | |||||
| iObjectCommons.initMaterial.call(this) | |||||
| iObjectCommons.initGeometry.call(this) | |||||
| // from GLTFObject3DExtrasExtension | |||||
| if (!this.userData.__keepShadowDef) { | |||||
| this.castShadow = true | |||||
| this.receiveShadow = true | |||||
| this.userData.__keepShadowDef = true | |||||
| } | |||||
| this.addEventListener('dispose', ()=>{ | |||||
| if (this.material) { | |||||
| // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!] | |||||
| this.material = undefined // this will dispose material if not used by other meshes | |||||
| // delete this.material | |||||
| // for (const oldMat of oldMats) { | |||||
| // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose() | |||||
| // } | |||||
| } | |||||
| if (this.geometry) { | |||||
| // const oldGeom = this.geometry | |||||
| this.geometry = undefined // this will dispose geometry if not used by other meshes | |||||
| // delete this.geometry | |||||
| // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose() | |||||
| } | |||||
| delete this._onMaterialUpdate | |||||
| delete this._onGeometryUpdate | |||||
| }) | |||||
| } | |||||
| if (!this.uiConfig && (this.assetType === 'model' || this.assetType === 'camera')) { | |||||
| // todo: lights/other types? | |||||
| iObjectCommons.makeUiConfig.call(this) | |||||
| } | |||||
| // todo: serialization? | |||||
| const children = [...this.children] | |||||
| for (const c of children) upgradeObject3D.call(c, this) | |||||
| // region Legacy | |||||
| if (this.userData.dispose) console.warn('userData.dispose already defined') | |||||
| this.userData.dispose = () => { | |||||
| console.warn('userData.dispose is deprecated, use dispose directly') | |||||
| this.dispose?.() | |||||
| } | |||||
| if (!this.modelObject) { | |||||
| Object.defineProperty(this, 'modelObject', { | |||||
| get: ()=>{ | |||||
| console.error('modelObject is deprecated, use object directly') | |||||
| return this | |||||
| }, | |||||
| }) | |||||
| } | |||||
| if (!this.userData.setDirty) | |||||
| this.userData.setDirty = (e: any)=>{ | |||||
| console.error('object.userData.setDirty is deprecated, use object.setDirty directly') | |||||
| this.setDirty?.(e) | |||||
| } | |||||
| // endregion | |||||
| // if (!this.objectProcessor) console.warn('objectProcessor not set for', this) | |||||
| // else | |||||
| this.objectProcessor?.processObject(this) | |||||
| } | |||||
| declare module '*.txt' { | |||||
| const content: string | |||||
| export default content | |||||
| } | |||||
| declare module '*.glsl' { | |||||
| const content: string | |||||
| export default content | |||||
| } | |||||
| declare module '*.vert' { | |||||
| const content: string | |||||
| export default content | |||||
| } | |||||
| declare module '*.frag' { | |||||
| const content: string | |||||
| export default content | |||||
| } | |||||
| declare module '*.module.scss' { | |||||
| const content: any | |||||
| export default content | |||||
| export const stylesheet: string | |||||
| } | |||||
| declare module '*.module.css' { | |||||
| const content: any | |||||
| export default content | |||||
| export const stylesheet: string | |||||
| } | |||||
| declare module '*.css' { | |||||
| const content: string | |||||
| export default content | |||||
| } | |||||
| // export {} | |||||
| // hack for typedoc | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| // declare type OffscreenCanvas = HTMLCanvasElement |
| export * from './viewer/index' | |||||
| export * from './three/Threejs' | |||||
| export * from './three/index' | |||||
| export * from './core/index' | |||||
| export * from './assetmanager/index' | |||||
| export * from './utils/index' | |||||
| export * from './plugins/index' | |||||
| export * from './postprocessing/index' | |||||
| export * from './materials/index' | |||||
| export * from './rendering/index' | |||||
| export {_testStart, _testFinish} from './testing/testing' | |||||
| export {autoCenterObject3D, autoScaleObject3D} from './three/utils/object-transform' |
| import {IMaterial, IMaterialUserData} from '../core' | |||||
| import {getOrCall} from 'ts-browser-helpers' | |||||
| import {shaderReplaceString} from '../utils/shader-helpers' | |||||
| import {Object3D, Shader, WebGLRenderer} from 'three' | |||||
| import {MaterialExtension} from './MaterialExtension' | |||||
| import {generateUUID} from '../three/utils/misc' | |||||
| export class MaterialExtender { | |||||
| static VoidMain = 'void main()' | |||||
| static ApplyMaterialExtensions(material: IMaterial, shader: Shader, materialExtensions: MaterialExtension[], renderer: WebGLRenderer) { | |||||
| for (const materialExtension of materialExtensions) { | |||||
| this.ApplyMaterialExtension(material, shader, materialExtension, renderer) | |||||
| } | |||||
| } | |||||
| static ApplyMaterialExtension(material: IMaterial, shader: Shader, materialExtension: MaterialExtension, renderer: WebGLRenderer) { | |||||
| // Add parsFragmentSnippet just before void main in fragment shader | |||||
| let a = getOrCall(materialExtension.parsFragmentSnippet, renderer, material) ?? '' | |||||
| if (a.length) { | |||||
| shader.fragmentShader = shaderReplaceString(shader.fragmentShader, this.VoidMain, '\n' + a + '\n', {prepend: true}) | |||||
| } | |||||
| // Add parsVertexSnippet just before void main in vertex shader | |||||
| a = getOrCall(materialExtension.parsVertexSnippet, renderer, material) ?? '' | |||||
| if (a.length) { | |||||
| shader.vertexShader = shaderReplaceString(shader.vertexShader, this.VoidMain, '\n' + a + '\n', {prepend: true}) | |||||
| } | |||||
| // Add extra uniforms | |||||
| if (materialExtension.extraUniforms) { | |||||
| shader.uniforms = Object.assign(shader.uniforms, materialExtension.extraUniforms) | |||||
| } | |||||
| // Add extra defines and set needsUpdate to true if needed | |||||
| if (materialExtension.extraDefines) | |||||
| updateMaterialDefines(materialExtension.extraDefines, material) | |||||
| // Call shaderExtender if defined | |||||
| materialExtension.shaderExtender?.(shader, material, renderer) | |||||
| // Save last shader so that it can be used to check if shader has changed in extensions | |||||
| material.lastShader = shader | |||||
| } | |||||
| static CacheKeyForExtensions(material: IMaterial, materialExtensions: MaterialExtension[]): string { | |||||
| let r = '' | |||||
| for (const materialExtension of materialExtensions) { | |||||
| r += this.CacheKeyForExtension(material, materialExtension) | |||||
| } | |||||
| return r | |||||
| } | |||||
| static CacheKeyForExtension(material: IMaterial, materialExtension: MaterialExtension): string { | |||||
| let r = '' | |||||
| if (materialExtension.computeCacheKey) r += getOrCall(materialExtension.computeCacheKey, material) | |||||
| if (materialExtension.extraDefines) r += Object.values(materialExtension.extraDefines).join('') | |||||
| return r | |||||
| } | |||||
| static RegisterExtensions(material: IMaterial, customMaterialExtensions?: MaterialExtension[]): MaterialExtension[] { | |||||
| const exts = [] | |||||
| if (!Array.isArray(material.materialExtensions)) material.materialExtensions = [] | |||||
| if (customMaterialExtensions) | |||||
| for (const ext of customMaterialExtensions) { | |||||
| if (!ext.isCompatible || !ext.isCompatible(material) || material.materialExtensions.includes(ext)) continue | |||||
| else exts.push(ext) | |||||
| if (!ext.uuid) ext.uuid = generateUUID() | |||||
| if (!ext.__setDirty) ext.__setDirty = ()=>{ | |||||
| if (!ext.updateVersion) ext.updateVersion = 0 | |||||
| ext.updateVersion++ | |||||
| } | |||||
| if (!ext.setDirty) ext.setDirty = ext.__setDirty | |||||
| } | |||||
| material.materialExtensions = [...material.materialExtensions || [], ...exts] | |||||
| if (!(material as any).__ext_beforeRenderListen) { | |||||
| (material as any).__ext_beforeRenderListen = true | |||||
| material.addEventListener('beforeRender', materialBeforeRender) | |||||
| } | |||||
| if (!(material as any).__ext_afterRenderListen) { | |||||
| (material as any).__ext_afterRenderListen = true | |||||
| material.addEventListener('afterRender', materialAfterRender) | |||||
| } | |||||
| return exts | |||||
| } | |||||
| static UnregisterExtensions(material: IMaterial, customMaterialExtensions?: MaterialExtension[]) { | |||||
| if (customMaterialExtensions) { | |||||
| material.materialExtensions = material.materialExtensions?.filter((v)=>!customMaterialExtensions.includes(v)) || [] | |||||
| } | |||||
| if (!material.materialExtensions?.length) { | |||||
| material.removeEventListener('beforeRender', materialBeforeRender) | |||||
| material.removeEventListener('afterRender', materialAfterRender) | |||||
| ;(material as any).__ext_beforeRenderListen = false | |||||
| ;(material as any).__ext_afterRenderListen = false | |||||
| } | |||||
| } | |||||
| } | |||||
| function updateMaterialDefines(defines: any, material: IMaterial) { | |||||
| if (!material.defines) { | |||||
| console.warn('Material does not have defines', material) // todo: check when material.defines is undefined | |||||
| material.defines = {} | |||||
| } | |||||
| let flag = false | |||||
| const entries = Object.entries(defines) | |||||
| for (const [key, val] of entries) { | |||||
| if (val === undefined) { | |||||
| if (material.defines[key] !== undefined) { | |||||
| delete material.defines[key] | |||||
| flag = true | |||||
| } | |||||
| } else if (material.defines[key] !== val) { | |||||
| material.defines[key] = val | |||||
| flag = true | |||||
| } | |||||
| } | |||||
| if (flag) material.needsUpdate = true | |||||
| } | |||||
| function materialBeforeRender({target, object, renderer}:{object?: Object3D, renderer?: WebGLRenderer, target: IMaterial}) { | |||||
| const material = target | |||||
| if (!material || !object || !renderer) throw new Error('Invalid material, object or renderer') | |||||
| if (!material.materialExtensions) return | |||||
| for (const value of material.materialExtensions) { | |||||
| value.onObjectRender?.(object, material, renderer) | |||||
| if ((material as any).lastShader) { | |||||
| const updater = getOrCall(value.updaters) || [] | |||||
| for (const v2 of updater) v2 && v2.updateShaderProperties((material as any).lastShader) | |||||
| } | |||||
| const udVersion: keyof IMaterialUserData = '_' + value.uuid + '_version' as any | |||||
| if (value.updateVersion !== material.userData[udVersion]) { | |||||
| material.userData[udVersion] = value.updateVersion | |||||
| material.needsUpdate = true | |||||
| } | |||||
| } | |||||
| } | |||||
| function materialAfterRender({target, object, renderer}:{object?: Object3D, renderer?: WebGLRenderer, target: IMaterial}) { | |||||
| const material = target | |||||
| if (!material || !object || !renderer) throw new Error('Invalid material, object or renderer') | |||||
| if (!material.materialExtensions) return | |||||
| for (const value of material.materialExtensions) { | |||||
| value.onAfterRender?.(object, material, renderer) | |||||
| } | |||||
| } |
| import {IUniform, Object3D, Shader, WebGLRenderer} from 'three' | |||||
| import {IMaterial} from '../core' | |||||
| import {UiObjectConfig} from 'uiconfig.js' | |||||
| /** | |||||
| * Material extension interface | |||||
| * This is used to extend a three.js material satisfying the IMaterial interface, with extra uniforms, defines, shader code, etc. | |||||
| */ | |||||
| export interface MaterialExtension{ | |||||
| /** | |||||
| * Extra uniforms to copy to material | |||||
| */ | |||||
| extraUniforms?: {[uniform: string]: IUniform}; | |||||
| /** | |||||
| * Extra defines to copy to material | |||||
| */ | |||||
| extraDefines?: Record<string, number|string>; | |||||
| /** | |||||
| * Custom callback to extend/modify/replace shader code and other shader properties | |||||
| * @param shader | |||||
| * @param material | |||||
| * @param renderer | |||||
| */ | |||||
| shaderExtender?: (shader: Shader, material: IMaterial, renderer: WebGLRenderer) => void, | |||||
| /** | |||||
| * Extra code to add to the top of the fragment shader | |||||
| * Value can be a string or a function that returns a string | |||||
| */ | |||||
| parsFragmentSnippet?: string | ((renderer?: WebGLRenderer, material?:IMaterial)=>string), | |||||
| /** | |||||
| * Extra code to add to the top of the vertex shader | |||||
| * Value can be a string or a function that returns a string | |||||
| */ | |||||
| parsVertexSnippet?: string | ((renderer?: WebGLRenderer, material?:IMaterial)=>string), | |||||
| // customCacheKey?: string, // same as computeCacheKey | |||||
| /** | |||||
| * Custom cache key to use for this material extension. | |||||
| * A different cache key will cause the shader to be recompiled. | |||||
| * Check three.js docs for more info. | |||||
| * Value can be a string or a function that returns a string | |||||
| * This will only be checked if `material.needsUpdate` is `true`, not on every render. | |||||
| */ | |||||
| computeCacheKey?: string | ((material: IMaterial) => string) | |||||
| /** | |||||
| * Custom callback to run code before the material is rendered | |||||
| * Executes from `material.onBeforeRender` for each material for each object it's rendered on. | |||||
| * @param object | |||||
| * @param material | |||||
| * @param renderer | |||||
| */ | |||||
| onObjectRender?: (object: Object3D, material: IMaterial, renderer: WebGLRenderer) => void | |||||
| /** | |||||
| * Custom callback to run code after the material is rendered | |||||
| * Executes from `material.onAfterRender` for each material for each object it's rendered on. | |||||
| * @param object | |||||
| * @param material | |||||
| * @param renderer | |||||
| */ | |||||
| onAfterRender?: (object: Object3D, material: IMaterial, renderer: WebGLRenderer) => void | |||||
| /** | |||||
| * Function to check if this material extension is compatible with the given material. | |||||
| * If not compatible, the material extension will not be applied. | |||||
| * This is only checked when the extension is registered. | |||||
| * @param material | |||||
| */ | |||||
| isCompatible: (material: IMaterial) => boolean | |||||
| /** | |||||
| * List of shader properties updaters to run on the material. | |||||
| * | |||||
| */ | |||||
| updaters?: IShaderPropertiesUpdater[]|(()=>IShaderPropertiesUpdater[]) | |||||
| /** | |||||
| * Function to return the UI config for this material extension. | |||||
| * This is called once when the material extension is registered. | |||||
| * @param material | |||||
| */ | |||||
| getUiConfig?: (material: IMaterial, refreshUi: UiObjectConfig['uiRefresh']) => UiObjectConfig | undefined | |||||
| updateVersion?: number | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| __setDirty?: () => void // set by MaterialExtender, this increments updateVersion, which ends up calling needsUpdate on all the materials with this extension | |||||
| uuid?: string | |||||
| setDirty?: ()=>void // this is set automatically if does not exists. calls __setDirty for all materials. //todo: also refresh UI. | |||||
| } | |||||
| export interface IShaderPropertiesUpdater { | |||||
| updateShaderProperties(material: {defines: Record<string, string | number | undefined>, uniforms: {[name: string]: IUniform}}): this; | |||||
| } |
| export {MaterialExtender} from './MaterialExtender' | |||||
| export type {MaterialExtension, IShaderPropertiesUpdater} from './MaterialExtension' |
| import {IPassID, IPipelinePass} from '../../postprocessing' | |||||
| import {ISerializedConfig, ThreeViewer} from '../../viewer' | |||||
| import {AnyFunction, serialize} from 'ts-browser-helpers' | |||||
| import {SerializationMetaType} from '../../utils/serialization' | |||||
| import {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||||
| export abstract class PipelinePassPlugin<T extends IPipelinePass, TPassId extends IPassID, TEvent extends string, TViewer extends ThreeViewer=ThreeViewer> extends AViewerPluginSync<TEvent, TViewer> { | |||||
| abstract passId: TPassId | |||||
| @serialize('pass') | |||||
| protected _pass?: T | |||||
| abstract createPass(v:TViewer):T | |||||
| /** | |||||
| * This function is called every frame before composer render, if this pass is being used in the pipeline | |||||
| * @param _ | |||||
| * @protected | |||||
| */ | |||||
| protected _beforeRender(): boolean {return this._pass?.enabled && this.enabled || false} | |||||
| private _enabledTemp = true // to save enabled state when pass is not yet created | |||||
| @serialize() | |||||
| get enabled(): boolean { | |||||
| return this._pass?.enabled || this._enabledTemp | |||||
| } | |||||
| set enabled(value: boolean) { | |||||
| if (this._pass) this._pass.enabled = value | |||||
| this._enabledTemp = value | |||||
| } | |||||
| constructor() { | |||||
| super() | |||||
| this._beforeRender = this._beforeRender.bind(this) | |||||
| } | |||||
| onAdded(viewer: TViewer): void { | |||||
| super.onAdded(viewer) | |||||
| this._pass = this.createPass(viewer) | |||||
| this._pass.onDirty?.push(viewer.setDirty) | |||||
| this._pass.beforeRender = wrapThisFunction(this._beforeRender, this._pass.beforeRender) | |||||
| viewer.renderManager.registerPass(this._pass) | |||||
| this.enabled = this._enabledTemp | |||||
| } | |||||
| onRemove(viewer: TViewer): void { | |||||
| if (this._pass) viewer.renderManager.unregisterPass(this._pass) | |||||
| this._pass?.dispose?.() | |||||
| this._pass = undefined | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| get pass(): T | undefined { | |||||
| return this._pass | |||||
| } | |||||
| toJSON(meta?: SerializationMetaType): ISerializedConfig&{pass?: any} { | |||||
| return super.toJSON(meta) | |||||
| } | |||||
| fromJSON(data: ISerializedConfig&{pass?: any}, meta?: SerializationMetaType): this|null|Promise<this|null> { | |||||
| return super.fromJSON(data, meta) | |||||
| } | |||||
| } | |||||
| function wrapThisFunction<T extends AnyFunction, T2>(f1: ()=>void, f2?: T): T { | |||||
| return function(this: T2, ...args: Parameters<T>) { | |||||
| f1() | |||||
| return f2 && f2.call(this, ...args) | |||||
| } as T | |||||
| } |
| export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin' | |||||
| export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin' | |||||
| export {PipelinePassPlugin} from './base/PipelinePassPlugin' | |||||
| export {RenderTargetPreviewPlugin} from './ui/RenderTargetPreviewPlugin' |
| import { | |||||
| BasicDepthPacking, | |||||
| Color, | |||||
| IUniform, | |||||
| MeshDepthMaterial, | |||||
| NoBlending, | |||||
| Texture, | |||||
| TextureDataType, | |||||
| UnsignedByteType, | |||||
| WebGLRenderTarget, | |||||
| } from 'three' | |||||
| import {GBufferRenderPass} from '../../postprocessing' | |||||
| import {ThreeViewer} from '../../viewer' | |||||
| import {IShaderPropertiesUpdater} from '../../materials' | |||||
| import {PipelinePassPlugin} from '../base/PipelinePassPlugin' | |||||
| export type DepthBufferPluginEventTypes = '' | |||||
| // type DepthBufferPluginTarget = WebGLMultipleRenderTargets | WebGLRenderTarget | |||||
| export type DepthBufferPluginTarget = WebGLRenderTarget | |||||
| export type DepthBufferPluginPass = GBufferRenderPass<'depth', DepthBufferPluginTarget> | |||||
| export class DepthBufferPlugin | |||||
| extends PipelinePassPlugin<DepthBufferPluginPass, 'depth', DepthBufferPluginEventTypes> | |||||
| implements IShaderPropertiesUpdater { | |||||
| readonly passId = 'depth' | |||||
| public static readonly PluginType = 'DepthBufferPlugin' | |||||
| private _depthTarget?: DepthBufferPluginTarget | |||||
| private _depthTexture?: Texture | |||||
| readonly material: MeshDepthMaterial = new MeshDepthMaterial({ | |||||
| depthPacking: BasicDepthPacking, | |||||
| blending: NoBlending, | |||||
| }) | |||||
| // private _gbufferPass?: IFilter<GBufferRenderPass<WebGLMultipleRenderTargets> | |||||
| createPass(v: ThreeViewer) { | |||||
| const target = v.renderManager.createTarget<DepthBufferPluginTarget>( | |||||
| { | |||||
| depthBuffer: true, | |||||
| samples: v.renderManager.composerTarget.samples || 0, | |||||
| type: this.bufferType, | |||||
| // magFilter: NearestFilter, | |||||
| // minFilter: NearestFilter, | |||||
| // generateMipmaps: false, | |||||
| // encoding: LinearEncoding, | |||||
| }) | |||||
| target.texture.name = 'depthBuffer' | |||||
| this._depthTexture = target.texture | |||||
| this._depthTarget = target | |||||
| if (this.isPrimaryGBuffer) v.renderManager.gbufferTarget = target | |||||
| this.material.userData.isGBufferMaterial = true | |||||
| const pass = new GBufferRenderPass('depth', target, this.material, new Color(0, 0, 0), 1) | |||||
| pass.before = ['render'] | |||||
| pass.after = [] | |||||
| pass.required = ['render'] | |||||
| return pass | |||||
| } | |||||
| constructor( | |||||
| public readonly bufferType: TextureDataType = UnsignedByteType, | |||||
| public readonly isPrimaryGBuffer = false | |||||
| ) { | |||||
| super() | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | |||||
| if (this._depthTarget) { | |||||
| viewer.renderManager.disposeTarget(this._depthTarget) | |||||
| this._depthTarget = undefined | |||||
| } | |||||
| return super.onRemove(viewer) | |||||
| } | |||||
| getTarget() { | |||||
| return this._depthTarget | |||||
| } | |||||
| updateShaderProperties(material: {defines: Record<string, string | number | undefined>; uniforms: {[p: string]: IUniform}}): this { | |||||
| if (material.uniforms.tDepth) material.uniforms.tDepth.value = this._depthTexture ?? undefined | |||||
| else this._viewer?.console.warn('BaseRenderer: no uniform: tDepth') | |||||
| return this | |||||
| } | |||||
| } | |||||
| #RenderTargetPreviewPluginContainer{ | |||||
| position: absolute; | |||||
| left: 0; | |||||
| bottom: 0; | |||||
| width: 100%; | |||||
| z-index: 1000; | |||||
| display: flex; | |||||
| flex-wrap: wrap; | |||||
| flex-direction: row; | |||||
| gap: 5px; | |||||
| padding: 5px; | |||||
| pointer-events: none; | |||||
| height: auto; | |||||
| } | |||||
| .RenderTargetPreviewPluginTarget{ | |||||
| position: relative; | |||||
| width: 200px; | |||||
| height: 200px; | |||||
| } | |||||
| .RenderTargetPreviewPluginCollapsed{ | |||||
| height: 25px; | |||||
| } | |||||
| .RenderTargetPreviewPluginTargetHeader{ | |||||
| position: absolute; | |||||
| top: 0; | |||||
| left: 0; | |||||
| right: 0; | |||||
| background: rgba(0,0,0,0.5); | |||||
| color: white; | |||||
| padding: 2px; | |||||
| font-size: 16px; | |||||
| height: 20px; | |||||
| font-family: monospace; | |||||
| text-align: center; | |||||
| pointer-events: auto; | |||||
| cursor: pointer; | |||||
| } | |||||
| .RenderTargetPreviewPluginTargetHeader::after{ | |||||
| content: '-'; | |||||
| position: absolute; | |||||
| right: 2px; | |||||
| width: 20px; | |||||
| height: 20px; | |||||
| line-height: 16px; | |||||
| } | |||||
| .RenderTargetPreviewPluginCollapsed .RenderTargetPreviewPluginTargetHeader::after{ | |||||
| content: '+'; | |||||
| line-height: 20px; | |||||
| } |
| import {ThreeViewer} from '../../viewer' | |||||
| import {IRenderTarget} from '../../rendering' | |||||
| import {createDiv, createStyles, getOrCall, onChange, ValOrFunc} from 'ts-browser-helpers' | |||||
| import {Vector4} from 'three' | |||||
| import styles from './RenderTargetPreviewPlugin.css' | |||||
| import {AViewerPluginSync} from '../../viewer/AViewerPlugin' | |||||
| export class RenderTargetPreviewPlugin <TEvent extends string> extends AViewerPluginSync<TEvent> { | |||||
| static readonly PluginType = 'RenderTargetPreviewPlugin' | |||||
| @onChange(RenderTargetPreviewPlugin.prototype.refreshUi) enabled = true | |||||
| toJSON: any = null | |||||
| mainDiv: HTMLDivElement = createDiv({id: 'RenderTargetPreviewPluginContainer'}) | |||||
| stylesheet?: HTMLStyleElement | |||||
| constructor() { | |||||
| super() | |||||
| } | |||||
| targetBlocks: { | |||||
| target: ValOrFunc<IRenderTarget|undefined> | |||||
| name: string | |||||
| visible: boolean | |||||
| transparent: boolean | |||||
| originalColorSpace: boolean | |||||
| div: HTMLDivElement | |||||
| }[] = [] | |||||
| onAdded(viewer: ThreeViewer): void { | |||||
| super.onAdded(viewer) | |||||
| viewer.addEventListener('postRender', this._postRender) | |||||
| this.stylesheet = createStyles(styles, viewer.container) | |||||
| this.refreshUi() | |||||
| } | |||||
| onRemove(viewer: ThreeViewer): void { | |||||
| viewer.removeEventListener('postRender', this._postRender) | |||||
| this.stylesheet?.remove() | |||||
| this.stylesheet = undefined | |||||
| this.refreshUi() | |||||
| super.onRemove(viewer) | |||||
| } | |||||
| private _postRender = () => { | |||||
| if (!this._viewer) return | |||||
| for (const target of this.targetBlocks) { | |||||
| if (!target.visible) return | |||||
| const rt = getOrCall(target.target) | |||||
| if (!rt) { | |||||
| // todo draw white or pink | |||||
| continue | |||||
| } | |||||
| const rect = target.div.getBoundingClientRect() | |||||
| const tex = rt.texture | |||||
| const canvasRect = this._viewer.canvas.getBoundingClientRect() | |||||
| rect.x = rect.x - canvasRect.x | |||||
| rect.y = canvasRect.height + canvasRect.y - rect.y - rect.height | |||||
| if (Array.isArray(tex)) { | |||||
| // todo support multi target | |||||
| console.warn('todo: multi target') | |||||
| continue | |||||
| } | |||||
| this._viewer.renderManager.blit(null, { | |||||
| source: tex, | |||||
| clear: !target.transparent, | |||||
| respectColorSpace: !target.originalColorSpace, | |||||
| viewport: new Vector4(rect.x, rect.y, rect.width, rect.height), | |||||
| }) | |||||
| } | |||||
| } | |||||
| addTarget(target: ValOrFunc<IRenderTarget|undefined>, name: string, transparent = false, originalColorSpace = false, visible = true): this { | |||||
| const div = document.createElement('div') | |||||
| const targetDef = {target, name, transparent, div, originalColorSpace, visible} | |||||
| div.classList.add('RenderTargetPreviewPluginTarget') | |||||
| const header = document.createElement('div') | |||||
| header.classList.add('RenderTargetPreviewPluginTargetHeader') | |||||
| header.innerText = name | |||||
| header.onclick = () => { | |||||
| targetDef.visible = !targetDef.visible | |||||
| if (!targetDef.visible) div.classList.add('RenderTargetPreviewPluginCollapsed') | |||||
| else div.classList.remove('RenderTargetPreviewPluginCollapsed') | |||||
| this._viewer?.setDirty() | |||||
| } | |||||
| div.appendChild(header) | |||||
| this.mainDiv.appendChild(div) | |||||
| this.targetBlocks.push(targetDef) | |||||
| this.refreshUi() | |||||
| return this | |||||
| } | |||||
| removeTarget(target: ValOrFunc<IRenderTarget|undefined>): this { | |||||
| const index = this.targetBlocks.findIndex(t => t.target === target) | |||||
| if (index >= 0) { | |||||
| const t = this.targetBlocks[index] | |||||
| this.targetBlocks.splice(index, 1) | |||||
| t.div.remove() | |||||
| } | |||||
| this.refreshUi() | |||||
| return this | |||||
| } | |||||
| refreshUi(): void { | |||||
| if (!this.mainDiv) return | |||||
| if (!this._viewer) { | |||||
| if (this.mainDiv.parentElement) this.mainDiv.remove() | |||||
| this.mainDiv.style.display = 'none' | |||||
| this.mainDiv.style.zIndex = '1000' | |||||
| return | |||||
| } | |||||
| if (!this.mainDiv.parentElement) this._viewer.container?.appendChild(this.mainDiv) | |||||
| this.mainDiv.style.display = this.enabled ? 'flex' : 'none' | |||||
| this.mainDiv.style.zIndex = parseInt(this._viewer.canvas.style.zIndex || '0') + 1 + '' | |||||
| } | |||||
| dispose() { | |||||
| for (const target of this.targetBlocks) { | |||||
| this.removeTarget(target.target) | |||||
| } | |||||
| super.dispose() | |||||
| } | |||||
| } |
| import {EffectComposer} from 'three/examples/jsm/postprocessing/EffectComposer.js' | |||||
| import {WebGLRenderer, WebGLRenderTarget} from 'three' | |||||
| import {ExtendedCopyPass} from './ExtendedCopyPass' | |||||
| export class EffectComposer2 extends EffectComposer { | |||||
| copyPass2 = new ExtendedCopyPass() | |||||
| constructor(renderer: WebGLRenderer, renderTarget: WebGLRenderTarget) { | |||||
| super(renderer, renderTarget) | |||||
| } | |||||
| setPixelRatio(pixelRatio: number, updateSize = true): void { | |||||
| const t = this.setSize | |||||
| if (!updateSize) this.setSize = ()=>{return} | |||||
| super.setPixelRatio(pixelRatio) | |||||
| if (!updateSize) this.setSize = t | |||||
| } | |||||
| } |
| import {UniformsUtils} from 'three' | |||||
| import {CopyShader} from 'three/examples/jsm/shaders/CopyShader.js' | |||||
| import {glsl} from 'ts-browser-helpers' | |||||
| import {ExtendedShaderPass} from './ExtendedShaderPass' | |||||
| export class ExtendedCopyPass extends ExtendedShaderPass { | |||||
| constructor() { | |||||
| super({ | |||||
| uniforms: UniformsUtils.clone(CopyShader.uniforms), | |||||
| vertexShader: CopyShader.vertexShader, | |||||
| fragmentShader: glsl` | |||||
| uniform float opacity; | |||||
| varying vec2 vUv; | |||||
| void main() { | |||||
| gl_FragColor = tDiffuseTexelToLinear(texture2D(tDiffuse, vUv)) * opacity; | |||||
| #include <encodings_fragment> | |||||
| } | |||||
| `, | |||||
| }, 'tDiffuse') | |||||
| } | |||||
| } |
| import {IPipelinePass} from './Pass' | |||||
| import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js' | |||||
| import { | |||||
| CanvasTexture, | |||||
| Color, | |||||
| HalfFloatType, | |||||
| LinearFilter, | |||||
| Material, | |||||
| NoColorSpace, | |||||
| RGBAFormat, | |||||
| UnsignedByteType, | |||||
| WebGLMultipleRenderTargets, | |||||
| WebGLRenderTarget, | |||||
| } from 'three' | |||||
| import {uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {GenericBlendTexturePass} from './GenericBlendTexturePass' | |||||
| import {IRenderTarget} from '../rendering' | |||||
| import {ICamera, IRenderManager, IScene, IWebGLRenderer} from '../core' | |||||
| import {ViewerRenderManager} from '../viewer' | |||||
| export class ExtendedRenderPass extends RenderPass implements IPipelinePass<'render'> { | |||||
| readonly isExtendedRenderPass = true | |||||
| @uiToggle('Enabled') @serialize() enabled = true | |||||
| readonly passId = 'render' | |||||
| private _blendPass: GenericBlendTexturePass | |||||
| readonly renderManager: ViewerRenderManager | |||||
| private _doTransmissionFix = true | |||||
| blurTransmissionTarget = true | |||||
| preserveTransparentTarget = false | |||||
| private _transparentTarget?: IRenderTarget | |||||
| get transparentTarget(): IRenderTarget { | |||||
| if (!this._transparentTarget) { | |||||
| this._transparentTarget = this.renderManager.getTempTarget({ | |||||
| sizeMultiplier: 1, | |||||
| samples: this.renderManager.composerTarget.samples || 0, | |||||
| colorSpace: NoColorSpace, | |||||
| type: this.renderManager.renderer.extensions.has('EXT_color_buffer_half_float') ? HalfFloatType : UnsignedByteType, | |||||
| format: RGBAFormat, | |||||
| minFilter: LinearFilter, | |||||
| magFilter: LinearFilter, | |||||
| depthBuffer: false, | |||||
| }) | |||||
| } | |||||
| return this._transparentTarget | |||||
| } | |||||
| private _releaseTransparentTarget() { | |||||
| if (this._transparentTarget) | |||||
| this.renderManager.releaseTempTarget(this._transparentTarget) | |||||
| this._transparentTarget = undefined | |||||
| } | |||||
| canvasTexture: CanvasTexture | |||||
| constructor(renderManager: ViewerRenderManager, overrideMaterial?: Material, clearColor = new Color(0, 0, 0), clearAlpha = 0) { | |||||
| super(undefined, undefined, overrideMaterial, clearColor, clearAlpha) | |||||
| this.renderManager = renderManager | |||||
| // this.canvasTexture = new CanvasTexture(renderManager.renderer.domElement) | |||||
| this._blendPass = new GenericBlendTexturePass({}, 'c = vec4(a.rgb * (1. - b.a) + b.rgb * b.a, 1.);') | |||||
| this.setDirty = this.setDirty.bind(this) | |||||
| } | |||||
| render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets|WebGLRenderTarget|null, readBuffer?: WebGLMultipleRenderTargets|WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) { | |||||
| if (!this.enabled) return | |||||
| let needsSwap = false | |||||
| renderer.userData.mainRenderPass = true | |||||
| if (!this._doTransmissionFix && !this.renderManager.rgbm) { | |||||
| super.render(renderer, writeBuffer || null, readBuffer, deltaTime, maskActive) | |||||
| this.needsSwap = needsSwap | |||||
| renderer.userData.mainRenderPass = undefined | |||||
| return | |||||
| } | |||||
| const ud = renderer.userData | |||||
| if (!ud) console.error('threejs is not patched?') | |||||
| const useGBufferDepth = (this.renderManager.zPrepass || !this.renderManager.depthBuffer) && this.renderManager.gbufferTarget | |||||
| let depthRenderBuffer: WebGLRenderbuffer | undefined = undefined | |||||
| if (useGBufferDepth) { | |||||
| const gbuffer = this.renderManager.gbufferTarget | |||||
| if (gbuffer) { | |||||
| const renderBufferProps = renderer.properties.get(gbuffer) | |||||
| depthRenderBuffer = renderBufferProps.__webglDepthRenderbuffer || renderBufferProps.__webglDepthbuffer | |||||
| } | |||||
| if (!depthRenderBuffer) { | |||||
| console.warn('No depth/gbuffer present for zPrepass.') | |||||
| } | |||||
| } | |||||
| let renderFn = ()=> { | |||||
| // @ts-expect-error patched three.js RenderPass to accept depthBuffer | |||||
| super.render(renderer, undefined, readBuffer, deltaTime, maskActive, depthRenderBuffer) // read is write in super.render (RenderPass) | |||||
| } | |||||
| if (!this.renderManager.rgbm) { | |||||
| // Opaque + Transparent | |||||
| { | |||||
| const curClear = this.clear | |||||
| const curClearDepth = renderer.autoClearDepth | |||||
| renderer.autoClearDepth = !useGBufferDepth | |||||
| this.clear = true | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: true, | |||||
| backgroundRender: true, | |||||
| opaqueRender: true, | |||||
| transparentRender: true, | |||||
| transmissionRender: false, | |||||
| }, renderFn) | |||||
| this.clear = curClear | |||||
| renderer.autoClearDepth = curClearDepth | |||||
| } | |||||
| // Transmissive | |||||
| { | |||||
| const source = !readBuffer ? undefined : Array.isArray(readBuffer.texture) ? readBuffer.texture[0] : readBuffer.texture | |||||
| // todo: first check if any transmissive object is there to use this buffer | |||||
| this.renderManager.blit(writeBuffer, {clear: true, source}) | |||||
| // viewer.renderer.blit(writeBuffer.texture as any, readBuffer as any, {}) | |||||
| // super.render(renderer, undefined as any, writeBuffer, deltaTime, maskActive); // copy read to write buffer | |||||
| const curClear = this.clear | |||||
| this.clear = false | |||||
| // don't need this clear is already false | |||||
| // const curClearDepth = renderer.autoClearDepth | |||||
| // renderer.autoClearDepth = false | |||||
| ud.transmissionRenderTarget = writeBuffer | |||||
| ud.blurTransmissionTarget = this.blurTransmissionTarget | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: false, | |||||
| backgroundRender: false, | |||||
| opaqueRender: false, | |||||
| transparentRender: false, | |||||
| transmissionRender: true, | |||||
| }, renderFn) | |||||
| ud.blurTransmissionTarget = undefined | |||||
| ud.transmissionRenderTarget = undefined | |||||
| // renderer.autoClearDepth = curClearDepth | |||||
| this.clear = curClear | |||||
| } | |||||
| needsSwap = false | |||||
| } else if (this.renderManager.rgbm) { | |||||
| needsSwap = false | |||||
| const renderToScreen = this.renderToScreen | |||||
| if (renderToScreen && !writeBuffer) { | |||||
| console.error('ExtendedRenderPass: renderToScreen is true but writeBuffer is not set, which is required for rgbm') | |||||
| } | |||||
| this.renderToScreen = false // for super RenderPass.render | |||||
| if (!renderer.info.autoReset) throw 'renderer.info.autoReset must be true' | |||||
| // Opaque | |||||
| { | |||||
| const curClearDepth = renderer.autoClearDepth | |||||
| renderer.autoClearDepth = !useGBufferDepth | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: true, | |||||
| backgroundRender: true, | |||||
| opaqueRender: true, | |||||
| transparentRender: false, | |||||
| transmissionRender: false, | |||||
| }, renderFn) | |||||
| renderer.autoClearDepth = curClearDepth | |||||
| } | |||||
| if (!useGBufferDepth && readBuffer) { | |||||
| const renderBufferProps2 = renderer.properties.get(readBuffer) | |||||
| depthRenderBuffer = renderBufferProps2.__webglDepthRenderbuffer || renderBufferProps2.__webglDepthbuffer | |||||
| } | |||||
| renderFn = ()=> { | |||||
| // @ts-expect-error patched three.js RenderPass to accept depthBuffer | |||||
| super.render(renderer, undefined, this.transparentTarget, deltaTime, maskActive, depthRenderBuffer) | |||||
| } | |||||
| // Transparent | |||||
| { | |||||
| const curClear = this.clear | |||||
| const curClearDepth = renderer.autoClearDepth | |||||
| renderer.autoClearDepth = false | |||||
| this.clear = true | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: false, | |||||
| backgroundRender: false, | |||||
| opaqueRender: false, | |||||
| transparentRender: true, | |||||
| transmissionRender: false, | |||||
| }, renderFn) | |||||
| this.clear = curClear | |||||
| renderer.autoClearDepth = curClearDepth | |||||
| } | |||||
| if (renderer.info.render.calls > 0) { | |||||
| this._blendPass.uniforms.tDiffuse2.value = this.transparentTarget.texture | |||||
| this._blendPass.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | |||||
| needsSwap = true | |||||
| } | |||||
| // Transmission | |||||
| { | |||||
| const curClear = this.clear | |||||
| this.clear = false // it is cleared in transparent pass above even if no object is rendered | |||||
| // const curClearDepth = renderer.autoClearDepth | |||||
| // renderer.autoClearDepth = false | |||||
| ud.transmissionRenderTarget = needsSwap ? writeBuffer : readBuffer | |||||
| ud.blurTransmissionTarget = this.blurTransmissionTarget | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: false, | |||||
| backgroundRender: false, | |||||
| opaqueRender: false, | |||||
| transparentRender: false, | |||||
| transmissionRender: true, | |||||
| }, renderFn) | |||||
| ud.blurTransmissionTarget = undefined | |||||
| ud.transmissionRenderTarget = undefined | |||||
| // renderer.autoClearDepth = curClearDepth | |||||
| this.clear = curClear | |||||
| } | |||||
| if (renderer.info.render.calls > 0) { | |||||
| // console.log('missive blit', renderer.info.render.frame) | |||||
| this._blendPass.uniforms.tDiffuse2.value = this.transparentTarget.texture | |||||
| this._blendPass.render(renderer, writeBuffer, readBuffer, deltaTime, maskActive) | |||||
| needsSwap = true | |||||
| } | |||||
| if (renderToScreen) { | |||||
| this.renderToScreen = true | |||||
| const tex = needsSwap ? writeBuffer?.texture : readBuffer?.texture | |||||
| const source = Array.isArray(tex) ? tex[0] : tex | |||||
| source && this.renderManager.blit(undefined, { | |||||
| source, respectColorSpace: true, | |||||
| }) | |||||
| // needsSwap = false | |||||
| } | |||||
| } | |||||
| if (!this.preserveTransparentTarget) | |||||
| this._releaseTransparentTarget() | |||||
| this.needsSwap = needsSwap | |||||
| renderer.userData.mainRenderPass = undefined | |||||
| } | |||||
| public onDirty: (()=>void)[] = [] | |||||
| dispose() { | |||||
| this._releaseTransparentTarget() | |||||
| this.onDirty = [] | |||||
| this.scene = undefined | |||||
| this.camera = undefined | |||||
| super.dispose?.() | |||||
| } | |||||
| setDirty() { | |||||
| this.onDirty.forEach(v=>v()) | |||||
| } | |||||
| beforeRender(scene: IScene, camera: ICamera, _: IRenderManager): void { | |||||
| this.scene = scene | |||||
| this.camera = camera | |||||
| } | |||||
| // legacy | |||||
| /** | |||||
| * @deprecated renamed to {@link isExtendedRenderPass} | |||||
| */ | |||||
| get isRenderPass2() { | |||||
| console.error('isRenderPass2 is deprecated, use isExtendedRenderPass instead') | |||||
| return true | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @deprecated renamed to {@link ExtendedRenderPass} | |||||
| */ | |||||
| export class RenderPass2 extends ExtendedRenderPass { | |||||
| constructor(...args: ConstructorParameters<typeof ExtendedRenderPass>) { | |||||
| console.error('RenderPass2 is deprecated, use ExtendedRenderPass instead') | |||||
| super(...args) | |||||
| } | |||||
| } | |||||
| import {IPass} from './Pass' | |||||
| import {ShaderPass} from 'three/examples/jsm/postprocessing/ShaderPass.js' | |||||
| import {ExtendedShaderMaterial, IWebGLRenderer, ShaderMaterial2} from '../core' | |||||
| import {Shader, WebGLMultipleRenderTargets, WebGLRenderTarget} from 'three' | |||||
| import {uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {IShaderPropertiesUpdater} from '../materials' | |||||
| export class ExtendedShaderPass extends ShaderPass implements IPass { | |||||
| public static readonly DEFAULT_TEX_ID = 'tDiffuse' | |||||
| material!: ShaderMaterial2 | |||||
| overrideReadBuffer: WebGLRenderTarget|null = null | |||||
| readonly isExtendedShaderPass = true | |||||
| // private _textureIDs: string[] | |||||
| @uiToggle('Enabled') @serialize() enabled = true | |||||
| constructor(shader: Shader|ShaderMaterial2, ...textureID: string[]) { | |||||
| super( | |||||
| (<ShaderMaterial2>shader).isMaterial ? <ShaderMaterial2>shader : new ExtendedShaderMaterial(<Shader>shader, textureID), | |||||
| textureID.length < 1 ? ExtendedShaderPass.DEFAULT_TEX_ID : textureID[0]) | |||||
| } | |||||
| render(renderer: IWebGLRenderer, writeBuffer?: WebGLMultipleRenderTargets|WebGLRenderTarget|null, readBuffer?: WebGLMultipleRenderTargets|WebGLRenderTarget, deltaTime?: number, maskActive?: boolean) { | |||||
| if (!this.enabled) return | |||||
| super.render(renderer, writeBuffer || null, this.overrideReadBuffer || readBuffer, deltaTime, maskActive) | |||||
| } | |||||
| updateShaderProperties(updater?: (IShaderPropertiesUpdater|undefined) | (IShaderPropertiesUpdater|undefined)[]) { | |||||
| if (!updater) return | |||||
| if (!Array.isArray(updater)) updater = [updater] | |||||
| updater.forEach(value => value?.updateShaderProperties(this.material)) | |||||
| } | |||||
| public onDirty: (()=>void)[] = [] | |||||
| dispose() { | |||||
| this.material?.dispose?.() | |||||
| this.fsQuad?.dispose?.() | |||||
| this.onDirty = [] | |||||
| } | |||||
| setDirty() { | |||||
| this.onDirty.forEach(v=>v()) | |||||
| } | |||||
| // legacy | |||||
| /** | |||||
| * @deprecated renamed to {@link isExtendedShaderPass} | |||||
| */ | |||||
| get isShaderPass2() { | |||||
| console.error('isShaderPass2 is deprecated, use isExtendedShaderPass instead') | |||||
| return true | |||||
| } | |||||
| } | |||||
| /** | |||||
| * @deprecated renamed to {@link ExtendedShaderPass} | |||||
| */ | |||||
| export class ShaderPass2 extends ExtendedShaderPass { | |||||
| constructor(shader: Shader|ShaderMaterial2, ...textureID: string[]) { | |||||
| console.error('ShaderPass2 is renamed to ExtendedShaderPass') | |||||
| super(shader, ...textureID) | |||||
| } | |||||
| } | |||||
| import {Color, Material, WebGLMultipleRenderTargets, WebGLRenderTarget} from 'three' | |||||
| import {RenderPass} from 'three/examples/jsm/postprocessing/RenderPass.js' | |||||
| import {IPassID, IPipelinePass} from './Pass' | |||||
| import {ICamera, IMaterial, IRenderManager, IScene, IWebGLRenderer, PhysicalMaterial} from '../core' | |||||
| export class GBufferRenderPass<TP extends IPassID, T extends WebGLMultipleRenderTargets | WebGLRenderTarget> extends RenderPass implements IPipelinePass<TP> { // todo: extend from jittered? | |||||
| readonly isGBufferRenderPass = true | |||||
| scene?: IScene | |||||
| before?: IPassID[] | |||||
| after?: IPassID[] | |||||
| required?: IPassID[] | |||||
| constructor(public readonly passId: TP, public target: T, material: Material, clearColor: Color = new Color(1, 1, 1), clearAlpha = 1) { | |||||
| super(undefined, undefined, material, clearColor, clearAlpha) | |||||
| } | |||||
| private _transparentMats = new Set<IMaterial>() | |||||
| private _transmissiveMats = new Set<[IMaterial, number]>() | |||||
| /** | |||||
| * Renders to {@link target} | |||||
| * @param renderer | |||||
| * @param _ - this is ignored | |||||
| * @param _1 - this is ignored | |||||
| * @param deltaTime | |||||
| * @param maskActive | |||||
| */ | |||||
| render(renderer: IWebGLRenderer, _?: WebGLRenderTarget|null, _1?: WebGLRenderTarget|WebGLMultipleRenderTargets, deltaTime?: number, maskActive?: boolean) { | |||||
| if (!this.scene || !this.camera) return | |||||
| const t = renderer.getRenderTarget() | |||||
| const activeCubeFace = renderer.getActiveCubeFace() | |||||
| const activeMipLevel = renderer.getActiveMipmapLevel() | |||||
| const preprocessMaterial = (material: IMaterial) => { | |||||
| const renderToGBuffer = material.userData.renderToGBuffer === undefined ? material.userData.renderToDepth : material.userData.renderToGBuffer | |||||
| if ( | |||||
| material.transparent && material.userData.renderToDepth || // transparent and render to gbuffer | |||||
| !material.transparent && !material.transmission && renderToGBuffer === false // opaque and dont render to gbuffer | |||||
| ) { | |||||
| this._transparentMats.add(material) | |||||
| material.transparent = !material.transparent | |||||
| // material.needsUpdate = true | |||||
| } | |||||
| if ( | |||||
| material.transmission && | |||||
| Math.abs(material.transmission || 0) > 0 && renderToGBuffer // transmission and render to gbuffer | |||||
| ) { | |||||
| this._transmissiveMats.add([material, material.transmission]) | |||||
| material.transmission = 0 | |||||
| // material.needsUpdate = true | |||||
| } | |||||
| } | |||||
| this.scene.traverse(({material}) => { | |||||
| if (!material) return | |||||
| if (Array.isArray(material)) material.forEach(preprocessMaterial) | |||||
| else preprocessMaterial(material) | |||||
| }) | |||||
| // todo; copy double sided, check with post processing | |||||
| renderer.renderWithModes({ | |||||
| shadowMapRender: false, | |||||
| backgroundRender: false, | |||||
| opaqueRender: true, | |||||
| transparentRender: false, | |||||
| transmissionRender: false, | |||||
| mainRenderPass: false, | |||||
| }, ()=> super.render(renderer, null, this.target, deltaTime as any, maskActive as any)) // here this.target is the write-buffer, variable writeBuffer is ignored | |||||
| this._transparentMats.forEach(m => m.transparent = !m.transparent) | |||||
| this._transparentMats.clear() | |||||
| this._transmissiveMats.forEach(([m, tr]: [PhysicalMaterial, number]) => m.transmission = tr) | |||||
| this._transmissiveMats.clear() | |||||
| renderer.setRenderTarget(t, activeCubeFace, activeMipLevel) | |||||
| } | |||||
| beforeRender(scene: IScene, camera: ICamera, _: IRenderManager): void { | |||||
| this.scene = scene | |||||
| this.camera = camera | |||||
| } | |||||
| } |
| import {IUniform} from 'three/src/renderers/shaders/UniformsLib' | |||||
| import {Texture} from 'three' | |||||
| import {CopyShader} from 'three/examples/jsm/shaders/CopyShader.js' | |||||
| import {ExtendedShaderPass} from './ExtendedShaderPass' | |||||
| import {IPass} from './Pass' | |||||
| import {glsl} from 'ts-browser-helpers' | |||||
| export class GenericBlendTexturePass extends ExtendedShaderPass implements IPass { | |||||
| constructor(uniforms: {[uniform: string]: IUniform}, blendFunc = 'c = a + b;', extraFrag = '', texture?: Texture) { | |||||
| super({ | |||||
| vertexShader: CopyShader.vertexShader, | |||||
| fragmentShader: glsl` | |||||
| varying vec2 vUv; | |||||
| ${extraFrag} | |||||
| void blend(in vec4 a, in vec4 b, inout vec4 c){ | |||||
| ${blendFunc} | |||||
| } | |||||
| void main() { | |||||
| vec4 texel = vec4(0); | |||||
| blend(tDiffuseTexelToLinear ( texture2D( tDiffuse, vUv ) ), tDiffuse2TexelToLinear ( texture2D( tDiffuse2, vUv ) ), texel); | |||||
| texel = clamp(texel, vec4(0), vec4(8)); | |||||
| gl_FragColor = texel; | |||||
| #include <encodings_fragment> | |||||
| } | |||||
| `, | |||||
| uniforms: { | |||||
| 'tDiffuse': {value: null}, | |||||
| 'tDiffuse2': {value: texture}, | |||||
| ...uniforms, | |||||
| }, | |||||
| }, 'tDiffuse', 'tDiffuse2') | |||||
| this.clear = false | |||||
| this.needsSwap = true | |||||
| } | |||||
| } |
| import {IDisposable} from 'ts-browser-helpers' | |||||
| import {IUniform} from 'three' | |||||
| import {Pass} from 'three/examples/jsm/postprocessing/Pass.js' | |||||
| import {IShaderPropertiesUpdater, MaterialExtension} from '../materials' | |||||
| import {ICamera, IRenderManager, IScene} from '../core' | |||||
| export type IPassID = 'render' | 'screen' | string | |||||
| export interface IPass<Tid extends IPassID = IPassID> extends Pass, IDisposable { | |||||
| uniforms?: {[name: string]: IUniform} | |||||
| updateShaderProperties?: (updater?: (IShaderPropertiesUpdater|undefined) | (IShaderPropertiesUpdater|undefined)[])=>void | |||||
| materialExtension?: MaterialExtension | |||||
| dirty?: boolean // isDirty (optional) | |||||
| setDirty?(): void | |||||
| onDirty?: (()=>void)[]; | |||||
| passId?: Tid; | |||||
| } | |||||
| export interface IPipelinePass<Tid extends IPassID = IPassID> extends IPass<Tid> { | |||||
| readonly passId: Tid | |||||
| after?: IPassID[] | |||||
| before?: IPassID[] | |||||
| required?: IPassID[] | |||||
| beforeRender?(scene: IScene, camera: ICamera, renderManager: IRenderManager): void; | |||||
| onRegister?(renderer: IRenderManager): void; | |||||
| onUnregister?(renderer: IRenderManager): void; | |||||
| } |
| import {ExtendedShaderPass} from './ExtendedShaderPass' | |||||
| import {Shader} from 'three' | |||||
| import {ShaderMaterial2} from '../core' | |||||
| import {CopyShader} from 'three/examples/jsm/shaders/CopyShader.js' | |||||
| import {IPassID, IPipelinePass} from './Pass' | |||||
| export type TViewerScreenShaderFrag = string | [string, string] | {pars?: string, main: string} | |||||
| export type TViewerScreenShader = TViewerScreenShaderFrag | Shader | ShaderMaterial2 | |||||
| export class ScreenPass extends ExtendedShaderPass implements IPipelinePass<'screen'> { | |||||
| readonly passId = 'screen' | |||||
| after: IPassID[] = ['render'] | |||||
| required: IPassID[] = ['render'] | |||||
| constructor(shader: TViewerScreenShader, ...textureID: string[]) { | |||||
| super( | |||||
| (<any>shader)?.fragmentShader || (<ShaderMaterial2>shader)?.isShaderMaterial ? <Shader|ShaderMaterial2>shader : | |||||
| makeScreenShader(shader), | |||||
| ...textureID.length ? textureID : ['tDiffuse']) | |||||
| } | |||||
| } | |||||
| function makeScreenShader(shader: string | [string, string] | {pars?: string; main: string} | Shader | ShaderMaterial2) { | |||||
| const c = { | |||||
| ...CopyShader, | |||||
| fragmentShader: ` | |||||
| varying vec2 vUv; | |||||
| ${Array.isArray(shader) ? shader[0] : (<any>shader)?.pars || ''} | |||||
| void main() { | |||||
| gl_FragColor = tDiffuseTexelToLinear (texture2D(tDiffuse, vUv)); | |||||
| ${Array.isArray(shader) ? shader[1] : typeof shader === 'string' ? shader : (shader as any)?.main || ''} | |||||
| gl_FragColor = LinearTosRGB(gl_FragColor); | |||||
| }`, | |||||
| uniforms: { | |||||
| tDiffuse: {value: null}, | |||||
| }, | |||||
| } | |||||
| return c | |||||
| } | |||||
| export {ExtendedRenderPass} from './ExtendedRenderPass' | |||||
| export {ExtendedShaderPass} from './ExtendedShaderPass' | |||||
| export {ExtendedCopyPass} from './ExtendedCopyPass' | |||||
| export {GenericBlendTexturePass} from './GenericBlendTexturePass' | |||||
| export {GBufferRenderPass} from './GBufferRenderPass' | |||||
| export {ScreenPass} from './ScreenPass' | |||||
| export {sortPasses} from './sortPasses' | |||||
| export {EffectComposer2} from './EffectComposer2' | |||||
| export type {IPassID, IPipelinePass, IPass} from './Pass' | |||||
| export type {TViewerScreenShader, TViewerScreenShaderFrag} from './ScreenPass' |
| import {IPassID, IPipelinePass} from './Pass' | |||||
| import {includesAll} from 'ts-browser-helpers' | |||||
| export function sortPasses(ps: IPipelinePass<IPassID>[]) { | |||||
| const pipeline: IPassID[] = [] | |||||
| const dict: Record<IPassID, {after: IPassID[], before: IPassID[], dependencies: Set<IPassID>}> = {} | |||||
| for (const pass of ps) { | |||||
| if (!pass.enabled || !pass.passId) continue | |||||
| dict[pass.passId] = { | |||||
| after: pass.after ?? [], | |||||
| before: pass.before ?? [], | |||||
| dependencies: new Set(pass.required ?? []), | |||||
| } | |||||
| } | |||||
| for (const [passId, pass] of Object.entries(dict)) { | |||||
| const optional = new Set([...pass.after, ...pass.before]) | |||||
| pass.dependencies.forEach(v => optional.has(v) && optional.delete(v)) | |||||
| optional.forEach(value => { | |||||
| const dPass = dict[value] | |||||
| if (!dPass) return | |||||
| if (dPass.dependencies.has(passId)) { | |||||
| console.error('cyclic', passId, value) | |||||
| throw 'Cyclic dependency' | |||||
| } | |||||
| pass.dependencies.add(value) | |||||
| }) | |||||
| } | |||||
| // eslint-disable-next-line no-constant-condition | |||||
| while (true) { | |||||
| let updated = false | |||||
| const entries = [...Object.entries(dict)] | |||||
| for (const [passId, pass] of entries) { | |||||
| if (pipeline.includes(passId)) continue | |||||
| if (includesAll(pipeline, pass.dependencies.values())) { | |||||
| const afterIndex = Math.max(-1, ...pass.after.map(v => pipeline.indexOf(v))) | |||||
| const beforeIndex = Math.min(pipeline.length, ...pass.before.map(v => { | |||||
| const k = pipeline.indexOf(v) | |||||
| return k < 0 ? pipeline.length : k | |||||
| })) | |||||
| if (afterIndex >= beforeIndex) { | |||||
| console.error(pass, ps, pipeline, afterIndex, beforeIndex) | |||||
| throw 'Not possible' | |||||
| } | |||||
| pipeline.splice(pass.after.length > 0 ? afterIndex + 1 : beforeIndex, 0, passId) | |||||
| // console.log(pipeline, passId, afterIndex, beforeIndex) | |||||
| updated = true | |||||
| delete dict[passId] | |||||
| } | |||||
| } | |||||
| if (Object.keys(dict).length < 1) break | |||||
| if (!updated) { | |||||
| console.error(entries, dict, pipeline) | |||||
| throw 'Not possible 2' | |||||
| break | |||||
| } | |||||
| } | |||||
| // console.log('Refreshed Pipeline:', pipeline) | |||||
| return pipeline | |||||
| } |
| import { | |||||
| IUniform, | |||||
| NoColorSpace, | |||||
| NoToneMapping, | |||||
| PCFShadowMap, | |||||
| ShaderMaterial, | |||||
| Texture, | |||||
| Vector2, | |||||
| Vector4, | |||||
| WebGLRenderer, | |||||
| WebGLRenderTarget, | |||||
| } from 'three' | |||||
| import {IPassID, IPipelinePass, sortPasses} from '../postprocessing' | |||||
| import {IRenderTarget} from './RenderTarget' | |||||
| import {EffectComposer2} from '../postprocessing/EffectComposer2' | |||||
| import {RenderTargetManager} from './RenderTargetManager' | |||||
| import {IShaderPropertiesUpdater} from '../materials' | |||||
| import { | |||||
| IRenderManager, | |||||
| IRenderManagerEvent, | |||||
| IRenderManagerEventTypes, | |||||
| type IRenderManagerOptions, | |||||
| IRenderManagerUpdateEvent, | |||||
| IScene, | |||||
| IWebGLRenderer, | |||||
| upgradeWebGLRenderer, | |||||
| } from '../core' | |||||
| import {onChange, serializable, serialize} from 'ts-browser-helpers' | |||||
| @serializable('RenderManager') | |||||
| export class RenderManager extends RenderTargetManager<IRenderManagerEvent, IRenderManagerEventTypes> implements IShaderPropertiesUpdater, IRenderManager { | |||||
| private readonly _isWebGL2: boolean | |||||
| private readonly _composer: EffectComposer2 | |||||
| private readonly _context: WebGLRenderingContext | |||||
| private readonly _renderSize = new Vector2(512, 512) // this is updated automatically. | |||||
| protected readonly _renderer: IWebGLRenderer<this> | |||||
| private _renderScale = 1. | |||||
| private _passes: IPipelinePass[] = [] | |||||
| private _pipeline: IPassID[] = [] | |||||
| private _passesNeedsUpdate = true | |||||
| private _frameCount = 0 | |||||
| private _lastTime = 0 | |||||
| private _totalFrameCount = 0 | |||||
| public static readonly POWER_PREFERENCE: 'high-performance' | 'low-power' | 'default' = 'high-performance' | |||||
| get renderer() {return this._renderer} | |||||
| /** | |||||
| * Use total frame count, if this is set to true, then frameCount won't be reset when the viewer is set to dirty. | |||||
| * Which will generate different random numbers for each frame during postprocessing steps. With TAA set properly, this will give a smoother result. | |||||
| */ | |||||
| @serialize() stableNoise = false | |||||
| public frameWaitTime = 0 // time to wait before next frame // used by canvas recorder //todo/ | |||||
| protected _dirty = true | |||||
| /** | |||||
| * Set autoBuildPipeline = false to be able to set the pipeline manually. | |||||
| */ | |||||
| @onChange(RenderManager.prototype.rebuildPipeline) | |||||
| public autoBuildPipeline = true | |||||
| rebuildPipeline(setDirty = true): void { | |||||
| this._passesNeedsUpdate = true | |||||
| if (setDirty) this._updated({change: 'rebuild'}) | |||||
| } | |||||
| /** | |||||
| * Regenerates the render pipeline by resolving dependencies and sorting the passes. | |||||
| * This is called automatically when the passes are changed. | |||||
| */ | |||||
| private _refreshPipeline(): IPassID[] { | |||||
| if (!this.autoBuildPipeline) return this._pipeline | |||||
| const ps = this._passes | |||||
| return this._pipeline = sortPasses(ps) | |||||
| } | |||||
| private _animationLoop(time: number, frame?:XRFrame) { | |||||
| const deltaTime = time - this._lastTime | |||||
| this._lastTime = time | |||||
| this.frameWaitTime -= deltaTime | |||||
| if (this.frameWaitTime > 0) return | |||||
| this.frameWaitTime = 0 | |||||
| this.dispatchEvent({type: 'animationLoop', deltaTime, time, renderer: this._renderer, xrFrame: frame}) | |||||
| } | |||||
| constructor({canvas, alpha = true, targetOptions}:IRenderManagerOptions) { | |||||
| super() | |||||
| this._animationLoop = this._animationLoop.bind(this) | |||||
| // this._xrPreAnimationLoop = this._xrPreAnimationLoop.bind(this) | |||||
| this._renderSize = new Vector2(canvas.clientWidth, canvas.clientHeight) | |||||
| this._renderer = this._initWebGLRenderer(canvas, alpha) | |||||
| this._context = this._renderer.getContext() | |||||
| this._isWebGL2 = this._renderer.capabilities.isWebGL2 | |||||
| this.resetShadows() | |||||
| const composerTarget = this.createTarget<WebGLRenderTarget>(targetOptions, false) | |||||
| composerTarget.texture.name = 'EffectComposer.rt1' | |||||
| this._composer = new EffectComposer2(this._renderer, composerTarget) | |||||
| // if (animationLoop) this.addEventListener('animationLoop', animationLoop) // todo: from viewer | |||||
| } | |||||
| protected _initWebGLRenderer(canvas: HTMLCanvasElement, alpha: boolean): IWebGLRenderer<this> { | |||||
| const renderer = new WebGLRenderer({ | |||||
| canvas, | |||||
| antialias: false, | |||||
| alpha, | |||||
| premultipliedAlpha: false, // todo: see this, maybe use this with rgbm mode. | |||||
| preserveDrawingBuffer: true, | |||||
| powerPreference: RenderManager.POWER_PREFERENCE, | |||||
| }) | |||||
| renderer.useLegacyLights = false | |||||
| renderer.setAnimationLoop(this._animationLoop) | |||||
| renderer.onContextLost = (event: WebGLContextEvent) => { | |||||
| this.dispatchEvent({type: 'contextLost', event}) | |||||
| } | |||||
| renderer.onContextRestore = () => { | |||||
| // console.log('restored') | |||||
| this.dispatchEvent({type: 'contextRestored'}) | |||||
| } | |||||
| renderer.setSize(this._renderSize.width, this._renderSize.height, false) | |||||
| renderer.setPixelRatio(this._renderScale) | |||||
| renderer.toneMapping = NoToneMapping | |||||
| renderer.toneMappingExposure = 1 | |||||
| renderer.outputColorSpace = NoColorSpace // or SRGBColorSpace | |||||
| renderer.shadowMap.enabled = true | |||||
| renderer.shadowMap.type = PCFShadowMap // use? THREE.PCFShadowMap. dont use VSM if need ground: https://github.com/mrdoob/three.js/issues/17473 | |||||
| // renderer.shadowMap.type = BasicShadowMap // use? THREE.PCFShadowMap. dont use VSM if need ground: https://github.com/mrdoob/three.js/issues/17473 | |||||
| renderer.shadowMap.autoUpdate = false | |||||
| return upgradeWebGLRenderer.call(renderer, this) | |||||
| } | |||||
| setSize(width?: number, height?: number, force = false) { | |||||
| if (!force && | |||||
| (width ? Math.abs(width - this._renderSize.width) : 0) + | |||||
| (height ? Math.abs(height - this._renderSize.height) : 0) < 0.1 | |||||
| ) return | |||||
| if (width) this._renderSize.width = width | |||||
| if (height) this._renderSize.height = height | |||||
| if (!this.webglRenderer.xr.enabled) { | |||||
| this._renderer.setSize(this._renderSize.width, this._renderSize.height, false) | |||||
| this._renderer.setPixelRatio(this._renderScale) | |||||
| } | |||||
| this._composer.setPixelRatio(this._renderScale, false) | |||||
| this._composer.setSize(this._renderSize.width, this._renderSize.height) | |||||
| this._resizeTracedTargets() | |||||
| // console.log('setSize', {...this._renderSize}, this._trackedTargets.length) | |||||
| this.dispatchEvent({type: 'resize'}) | |||||
| this._updated({change: 'size', data: this._renderSize.toArray()}) | |||||
| this.reset() | |||||
| } | |||||
| // render(scene: RenderScene): void { | |||||
| // const camera = scene.activeCamera | |||||
| // const activeScene = scene.activeScene | |||||
| // if(!camera) return | |||||
| // this._renderer.render(scene.threeScene, camera) | |||||
| // // todo gizmos | |||||
| // } | |||||
| render(scene: IScene): void { | |||||
| if (this._passesNeedsUpdate) { | |||||
| this._refreshPipeline() | |||||
| this.refreshPasses() | |||||
| } | |||||
| for (const pass of this._passes) { | |||||
| if (pass.enabled) pass.beforeRender?.(scene, scene.mainCamera, this) | |||||
| } | |||||
| this._composer.render() | |||||
| this._frameCount += 1 | |||||
| this._totalFrameCount += 1 | |||||
| this._dirty = false | |||||
| } | |||||
| get needsRender(): boolean { | |||||
| this._dirty = this._dirty || this._passes.findIndex(value => value.dirty) >= 0 // todo: check for enabled passes only. | |||||
| return this._dirty | |||||
| } | |||||
| setDirty(reset = false): void { | |||||
| this._dirty = true | |||||
| if (reset) this.reset() | |||||
| // do NOT call _updated from here. | |||||
| } | |||||
| reset(): void { | |||||
| this._frameCount = 0 | |||||
| this._dirty = true | |||||
| // do NOT call _updated from here. | |||||
| } | |||||
| resetShadows(): void { | |||||
| this._renderer.shadowMap.needsUpdate = true | |||||
| } | |||||
| refreshPasses(): void { | |||||
| if (!this._passesNeedsUpdate) return | |||||
| this._passesNeedsUpdate = false | |||||
| const p = [] | |||||
| for (const passId of this._pipeline) { | |||||
| const a = this._passes.find(value => value.passId === passId) | |||||
| if (!a) { | |||||
| console.warn('Unable to find pass: ', passId) | |||||
| continue | |||||
| } | |||||
| p.push(a) | |||||
| } | |||||
| [...this._composer.passes].forEach(p1=>this._composer.removePass(p1)) | |||||
| p.forEach(p1=>this._composer.addPass(p1)) | |||||
| this._updated({change: 'passRefresh'}) | |||||
| } | |||||
| dispose(): void { | |||||
| super.dispose() | |||||
| this._renderer.dispose() | |||||
| } | |||||
| updateShaderProperties(material: {defines: Record<string, string|number|undefined>, uniforms: {[name: string]: IUniform}}): this { | |||||
| // if (material.uniforms.currentFrameCount) material.uniforms.currentFrameCount.value = this.frameCount | |||||
| if (!this.stableNoise) { | |||||
| if (material.uniforms.frameCount) material.uniforms.frameCount.value = this._totalFrameCount | |||||
| else console.warn('BaseRenderer: no uniform: frameCount') | |||||
| } else { | |||||
| if (material.uniforms.frameCount) material.uniforms.frameCount.value = this.frameCount | |||||
| else console.warn('BaseRenderer: no uniform: frameCount') | |||||
| } | |||||
| return this | |||||
| } | |||||
| // region Passes | |||||
| registerPass(pass: IPipelinePass, replaceId = true): void { | |||||
| if (replaceId) { | |||||
| for (const pass1 of [...this._passes]) { | |||||
| if (pass.passId === pass1.passId) this.unregisterPass(pass1) | |||||
| } | |||||
| } | |||||
| this._passes.push(pass) | |||||
| pass.onRegister?.(this) | |||||
| this.rebuildPipeline(false) | |||||
| this._updated({change: 'registerPass', pass}) | |||||
| } | |||||
| unregisterPass(pass: IPipelinePass): void { | |||||
| const i = this._passes.indexOf(pass) | |||||
| if (i >= 0) { | |||||
| pass.onUnregister?.(this) | |||||
| this._passes.splice(i, 1) | |||||
| this.rebuildPipeline(false) | |||||
| this._updated({change: 'unregisterPass', pass}) | |||||
| } | |||||
| } | |||||
| renderTargetToDataUrl(target: WebGLRenderTarget, mimeType = 'image/png', quality = 90): string { | |||||
| const buffer = new Uint8Array(target.width * target.height * 4) | |||||
| this._renderer.readRenderTargetPixels(target, 0, 0, target.width, target.height, buffer) | |||||
| const canvas = document.createElement('canvas') | |||||
| canvas.width = target.width | |||||
| canvas.height = target.height | |||||
| const ctx = canvas.getContext('2d') | |||||
| if (!ctx) throw new Error('Unable to get 2d context') | |||||
| const imageData = ctx.createImageData(target.width, target.height, {colorSpace: ['display-p3', 'srgb'].includes(target.texture.colorSpace) ? <PredefinedColorSpace>target.texture.colorSpace : undefined}) | |||||
| imageData.data.set(buffer) | |||||
| ctx.putImageData(imageData, 0, 0) | |||||
| const string = canvas.toDataURL(mimeType, quality) | |||||
| canvas.remove() | |||||
| return string | |||||
| } | |||||
| // endregion | |||||
| // region Getters and Setters | |||||
| get frameCount(): number { | |||||
| return this._frameCount | |||||
| } | |||||
| get totalFrameCount(): number { | |||||
| return this._totalFrameCount | |||||
| } | |||||
| set pipeline(value: IPassID[]) { | |||||
| this._pipeline = value | |||||
| if (this.autoBuildPipeline) { | |||||
| console.warn('BaseRenderer: pipeline changed, but autoBuildPipeline is true. This will not have any effect.') | |||||
| } | |||||
| this.rebuildPipeline() | |||||
| } | |||||
| get pipeline(): IPassID[] { | |||||
| return this._pipeline | |||||
| } | |||||
| get composer(): EffectComposer2 { | |||||
| return this._composer | |||||
| } | |||||
| get passes(): IPipelinePass[] { | |||||
| return this._passes | |||||
| } | |||||
| get isWebGL2(): boolean { | |||||
| return this._isWebGL2 | |||||
| } | |||||
| get composerTarget(): IRenderTarget { | |||||
| return this._composer.renderTarget1 | |||||
| } | |||||
| get composerTarget2(): IRenderTarget { | |||||
| return this._composer.renderTarget2 | |||||
| } | |||||
| get renderSize(): Vector2 { | |||||
| return this._renderSize | |||||
| } | |||||
| get renderScale(): number { | |||||
| return this._renderScale | |||||
| } | |||||
| set renderScale(value: number) { | |||||
| if (value !== this._renderScale) { | |||||
| this._renderScale = value | |||||
| this.setSize(undefined, undefined, true) | |||||
| } | |||||
| } | |||||
| get context(): WebGLRenderingContext { | |||||
| return this._context | |||||
| } | |||||
| get webglRenderer(): WebGLRenderer { | |||||
| return this._renderer | |||||
| } | |||||
| @serialize() | |||||
| get useLegacyLights(): boolean { | |||||
| return this._renderer.useLegacyLights | |||||
| } | |||||
| set useLegacyLights(v: boolean) { | |||||
| this._renderer.useLegacyLights = v | |||||
| this._updated({change: 'useLegacyLights', data: v}) | |||||
| this.resetShadows() | |||||
| } | |||||
| get clock() { | |||||
| return this._composer.clock | |||||
| } | |||||
| // endregion | |||||
| // region Events Dispatch | |||||
| private _updated(data?: Partial<IRenderManagerUpdateEvent>) { | |||||
| this.dispatchEvent({...data, type: 'update'}) | |||||
| } | |||||
| // endregion | |||||
| // / TODO | |||||
| blit(destination: IRenderTarget|undefined|null, {source, viewport, material, clear = true, respectColorSpace = false}: {source?: Texture, viewport?: Vector4, material?: ShaderMaterial, clear?: boolean, respectColorSpace?: boolean} = {}): void { | |||||
| const copyPass = !respectColorSpace ? this._composer.copyPass : this._composer.copyPass2 | |||||
| const {renderToScreen, material: oldMaterial, uniforms: oldUniforms, clear: oldClear} = copyPass | |||||
| if (material) { | |||||
| copyPass.material = material | |||||
| } | |||||
| const oldViewport = this._renderer.getViewport(new Vector4()) | |||||
| const oldScissor = this._renderer.getScissor(new Vector4()) | |||||
| const oldScissorTest = this._renderer.getScissorTest() | |||||
| const oldAutoClear = this._renderer.autoClear | |||||
| const oldTarget = this._renderer.getRenderTarget() | |||||
| if (viewport) this._renderer.setViewport(viewport) | |||||
| if (viewport) this._renderer.setScissor(viewport) | |||||
| if (viewport) this._renderer.setScissorTest(true) | |||||
| this._renderer.autoClear = false | |||||
| copyPass.uniforms = copyPass.material.uniforms | |||||
| copyPass.renderToScreen = false | |||||
| copyPass.clear = clear | |||||
| this._renderer.renderWithModes({ | |||||
| sceneRender: true, | |||||
| opaqueRender: true, | |||||
| shadowMapRender: false, | |||||
| backgroundRender: false, | |||||
| transparentRender: true, | |||||
| transmissionRender: false, | |||||
| }, ()=>{ | |||||
| copyPass.render(this._renderer, <WebGLRenderTarget>destination || null, {texture: source} as any, 0, false) | |||||
| }) | |||||
| copyPass.renderToScreen = renderToScreen | |||||
| copyPass.clear = oldClear | |||||
| copyPass.material = oldMaterial | |||||
| copyPass.uniforms = oldUniforms | |||||
| this._renderer.autoClear = oldAutoClear | |||||
| if (viewport) this._renderer.setViewport(oldViewport) | |||||
| if (viewport) this._renderer.setScissor(oldScissor) | |||||
| if (viewport) this._renderer.setScissorTest(oldScissorTest) | |||||
| this._renderer.setRenderTarget(oldTarget) // todo: active cubeface etc | |||||
| } | |||||
| // clearColor({r, g, b, a, target, depth = true, stencil = true}: | |||||
| // {r?: number, g?: number, b?: number, a?: number, target?: IRenderTarget, depth?: boolean, stencil?: boolean}): void { | |||||
| // const color = this._renderer.getClearColor(new Color()) | |||||
| // const alpha = this._renderer.getClearAlpha() | |||||
| // this._renderer.setClearAlpha(a ?? alpha) | |||||
| // this._renderer.setClearColor(new Color(r ?? color.r, g ?? color.g, b ?? color.b)) | |||||
| // const lastTarget = this._renderer.getRenderTarget() | |||||
| // const activeCubeFace = this._renderer.getActiveCubeFace() | |||||
| // const activeMipLevel = this._renderer.getActiveMipmapLevel() | |||||
| // this._renderer.setRenderTarget((target as WebGLRenderTarget) ?? null) | |||||
| // this._renderer.clear(true, depth, stencil) | |||||
| // this._renderer.setRenderTarget(lastTarget, activeCubeFace, activeMipLevel) | |||||
| // this._renderer.setClearColor(color) | |||||
| // this._renderer.setClearAlpha(alpha) | |||||
| // } | |||||
| /** | |||||
| * @deprecated use renderScale instead | |||||
| */ | |||||
| get displayCanvasScaling() { | |||||
| console.error('displayCanvasScaling is deprecated, use renderScale instead') | |||||
| return this.renderScale | |||||
| } | |||||
| /** | |||||
| * @deprecated use renderScale instead | |||||
| */ | |||||
| set displayCanvasScaling(value) { | |||||
| console.error('displayCanvasScaling is deprecated, use renderScale instead') | |||||
| this.renderScale = value | |||||
| } | |||||
| } |
| import { | |||||
| ColorSpace, | |||||
| EventDispatcher, | |||||
| MagnificationTextureFilter, | |||||
| MinificationTextureFilter, | |||||
| Texture, | |||||
| TextureDataType, | |||||
| } from 'three' | |||||
| export interface IRenderTarget extends EventDispatcher { | |||||
| texture: Texture | Texture[] | |||||
| sizeMultiplier?: number | |||||
| isTemporary?: boolean | |||||
| targetKey?: string // for caching. | |||||
| clone(trackTarget?: boolean): this | |||||
| setSize(width: number, height: number, depth?: number): void; | |||||
| dispose(): void; | |||||
| samples: number | |||||
| } | |||||
| export interface CreateRenderTargetOptions { | |||||
| sizeMultiplier?: number, | |||||
| size?: {width: number, height: number}, | |||||
| generateMipmaps?: boolean, | |||||
| samples?: number, | |||||
| minFilter?: MinificationTextureFilter | |||||
| magFilter?: MagnificationTextureFilter | |||||
| colorSpace?: ColorSpace | |||||
| type?: TextureDataType | |||||
| format?: number | |||||
| depthBuffer?: boolean | |||||
| depthTexture?: boolean | |||||
| textureCount?: number | |||||
| } | |||||
| export function createRenderTargetKey(op: CreateRenderTargetOptions = {}): string { | |||||
| // colorSpace is in key because of ext_sRGB | |||||
| return [op.sizeMultiplier, op.samples, op.colorSpace, op.type, op.format, op.depthBuffer, op.depthTexture, op.size?.width, op.size?.height].join(';') | |||||
| } |
| import {Class} from 'ts-browser-helpers' | |||||
| import {createRenderTargetKey, CreateRenderTargetOptions, IRenderTarget} from './RenderTarget' | |||||
| import { | |||||
| BaseEvent, | |||||
| DepthTexture, | |||||
| EventDispatcher, | |||||
| LinearFilter, | |||||
| LinearMipMapLinearFilter, | |||||
| NoColorSpace, | |||||
| RGBAFormat, | |||||
| Texture, | |||||
| UnsignedByteType, | |||||
| Vector2, | |||||
| WebGLCubeRenderTarget, | |||||
| WebGLMultipleRenderTargets, | |||||
| WebGLRenderTarget, | |||||
| WebGLRenderTargetOptions, | |||||
| } from 'three' | |||||
| export abstract class RenderTargetManager<E extends BaseEvent = BaseEvent, ET extends string = string> extends EventDispatcher<E, ET> { | |||||
| abstract isWebGL2: boolean | |||||
| abstract readonly renderSize: Vector2 | |||||
| abstract renderScale: number | |||||
| private _trackedTargets: IRenderTarget[] = [] | |||||
| private _trackedTempTargets: IRenderTarget[] = [] | |||||
| private _releasedTempTargets: Record<string, IRenderTarget[]> = {} | |||||
| readonly maxTempPerKey = 5 | |||||
| protected constructor() { | |||||
| super() | |||||
| this._processNewTarget = this._processNewTarget.bind(this) | |||||
| this._processNewTempTarget = this._processNewTempTarget.bind(this) | |||||
| this.trackTarget = this.trackTarget.bind(this) | |||||
| this.disposeTarget = this.disposeTarget.bind(this) | |||||
| this.createTarget = this.createTarget.bind(this) | |||||
| this.createTargetCustom = this.createTargetCustom.bind(this) | |||||
| } | |||||
| trackTarget(target: IRenderTarget) { | |||||
| this._trackedTargets.push(target) | |||||
| } | |||||
| removeTrackedTarget(target: IRenderTarget) { | |||||
| const ind = this._trackedTargets.indexOf(target) | |||||
| if (ind >= 0) | |||||
| this._trackedTargets.splice(ind, 1) | |||||
| } | |||||
| createTarget<T extends IRenderTarget = IRenderTarget>({ | |||||
| sizeMultiplier = undefined, | |||||
| samples = 0, | |||||
| colorSpace = NoColorSpace, | |||||
| type = UnsignedByteType, | |||||
| format = RGBAFormat, | |||||
| depthBuffer = true, | |||||
| depthTexture = false, | |||||
| size = undefined, | |||||
| textureCount = 1, | |||||
| ...op | |||||
| }: CreateRenderTargetOptions = {}, trackTarget = true): T { | |||||
| if (!this.isWebGL2) samples = 0 | |||||
| if (sizeMultiplier !== undefined && size !== undefined) | |||||
| console.error('Both sizeMultiplier and size are defined. sizeMultiplier will be ignored.') | |||||
| size = size || this.renderSize.clone().multiplyScalar(this.renderScale * (sizeMultiplier = sizeMultiplier || 1)) | |||||
| size.width = Math.floor(size.width) | |||||
| size.height = Math.floor(size.height) | |||||
| const depthTex = depthTexture ? new DepthTexture(size.width, size.height, UnsignedByteType) : undefined | |||||
| const target = this.createTargetCustom<T>(textureCount > 1 ? { | |||||
| width: size.width, | |||||
| height: size.height, | |||||
| count: textureCount, | |||||
| } : size, | |||||
| {samples, colorSpace, type, format, depthBuffer, depthTexture: depthTex}, | |||||
| textureCount > 1 ? WebGLMultipleRenderTargets as any : WebGLRenderTarget) | |||||
| this._processNewTarget(target, sizeMultiplier, trackTarget) | |||||
| this._setTargetOptions(target, op) | |||||
| return target | |||||
| } | |||||
| disposeTarget(target: IRenderTarget): void { | |||||
| if (!target) return | |||||
| if (target.isTemporary) return this.releaseTempTarget(target) | |||||
| this.removeTrackedTarget(target) | |||||
| target.dispose() | |||||
| } | |||||
| getTempTarget(op: CreateRenderTargetOptions = {}): IRenderTarget { | |||||
| const key = createRenderTargetKey(op) | |||||
| let target: IRenderTarget | undefined | |||||
| if (this._releasedTempTargets[key]?.length) target = this._releasedTempTargets[key].pop() | |||||
| if (!target) { | |||||
| target = this.createTarget(op) | |||||
| this._processNewTempTarget(target, key) | |||||
| } else { | |||||
| this._setTargetOptions(target, op) | |||||
| } | |||||
| return target | |||||
| } | |||||
| releaseTempTarget(target: IRenderTarget): void { | |||||
| const key = target.targetKey | |||||
| if (!key || !target.isTemporary) { | |||||
| throw 'Not a temp target' | |||||
| } | |||||
| if (this._releasedTempTargets[key].length > this.maxTempPerKey) { | |||||
| target.dispose() | |||||
| this._trackedTempTargets.splice(this._trackedTempTargets.indexOf(target), 1) | |||||
| } else this._releasedTempTargets[key].push(target) | |||||
| } | |||||
| createTargetCustom<T extends IRenderTarget>({ | |||||
| width, | |||||
| height, | |||||
| count, | |||||
| }: {width: number, height: number, count?: number}, options: WebGLRenderTargetOptions = {}, clazz?: Class<T>): T { | |||||
| const processNewTarget = this._processNewTarget | |||||
| let size = [width, height] | |||||
| if (count && count > 1) size.push(count) | |||||
| if (clazz?.prototype === WebGLCubeRenderTarget.prototype) { // todo: check for subclass also of WebGLCubeRenderTarget | |||||
| if (width !== height) throw 'Width and height of cube render target must be equal' | |||||
| size = [width] | |||||
| } | |||||
| options = { | |||||
| format: RGBAFormat, | |||||
| minFilter: LinearFilter, | |||||
| magFilter: LinearFilter, | |||||
| generateMipmaps: false, | |||||
| type: UnsignedByteType, | |||||
| colorSpace: NoColorSpace, | |||||
| ...options, | |||||
| } | |||||
| const params = [...size, options] | |||||
| return new class RenderTarget extends ((clazz as any as Class<WebGLRenderTarget>) ?? WebGLRenderTarget) implements IRenderTarget { | |||||
| isTemporary?: boolean | |||||
| sizeMultiplier?: number | |||||
| constructor(...ps: any[]) { | |||||
| super(...ps) | |||||
| if (Array.isArray(this.texture)) { | |||||
| this.texture.forEach(t => { | |||||
| t.colorSpace = options.colorSpace | |||||
| t.toJSON = () => ({}) | |||||
| }) | |||||
| } else { | |||||
| this.texture.toJSON = () => ({}) // so that it doesn't get serialized | |||||
| } | |||||
| } | |||||
| setSize(w: number, h: number, depth?: number) { | |||||
| super.setSize(Math.floor(w), Math.floor(h), depth) | |||||
| // console.log('setSize', w, h, depth) | |||||
| return this | |||||
| } | |||||
| clone(trackTarget = true): any { | |||||
| if (this.isTemporary) throw 'Cloning temporary render targets not supported' | |||||
| if (Array.isArray(this.texture)) throw 'Cloning multiple render targets not supported' | |||||
| // Note: todo: webgl render target.clone messes up the texture, by not copying isRenderTargetTexture prop and maybe some other stuff. So its better to just create a new one | |||||
| const cloned = super.clone() as IRenderTarget | |||||
| const tex = cloned.texture | |||||
| if (Array.isArray(tex)) tex.forEach(t => t.isRenderTargetTexture = true) | |||||
| else tex.isRenderTargetTexture = true | |||||
| return processNewTarget(cloned, this.sizeMultiplier || 1, trackTarget) | |||||
| } | |||||
| }(...params) as any as T | |||||
| } | |||||
| dispose() { | |||||
| this._trackedTargets.forEach(t=>t.dispose()) | |||||
| Object.values(this._trackedTempTargets).forEach(t=>t.dispose()) | |||||
| this._trackedTargets = [] | |||||
| this._releasedTempTargets = {} | |||||
| this._trackedTempTargets = [] | |||||
| } | |||||
| protected _resizeTracedTargets() { | |||||
| this._trackedTargets.forEach(v=>{ | |||||
| const target = v as any as WebGLRenderTarget | |||||
| const multiplier = (target as any).sizeMultiplier | |||||
| if (multiplier) { | |||||
| const s = this.renderSize.clone().multiplyScalar(this.renderScale * multiplier) | |||||
| target.setSize(Math.floor(s.width), Math.floor(s.height)) | |||||
| } | |||||
| }) | |||||
| } | |||||
| private _processNewTempTarget(target: IRenderTarget, key: string): IRenderTarget { | |||||
| target.isTemporary = true | |||||
| target.targetKey = key | |||||
| if (this._releasedTempTargets[key] === undefined) this._releasedTempTargets[key] = [] | |||||
| this._trackedTempTargets.push(target) | |||||
| return target | |||||
| } | |||||
| private _setTargetOptions(target: IRenderTarget, op: CreateRenderTargetOptions) { | |||||
| const tex = target.texture | |||||
| for (const t of Array.isArray(tex) ? tex : [tex]) | |||||
| this._setTargetTextureOptions(t, op) | |||||
| } | |||||
| private _setTargetTextureOptions(texture: Texture, op: CreateRenderTargetOptions) { | |||||
| texture.minFilter = op.minFilter ?? LinearFilter | |||||
| texture.magFilter = op.magFilter ?? LinearFilter | |||||
| texture.generateMipmaps = op.generateMipmaps ?? false | |||||
| if (texture.generateMipmaps && texture.minFilter === LinearFilter) // todo: check if this is needed for magFilter | |||||
| texture.minFilter = LinearMipMapLinearFilter | |||||
| if (!texture.generateMipmaps && texture.minFilter === LinearMipMapLinearFilter) | |||||
| texture.minFilter = LinearFilter | |||||
| } | |||||
| private _processNewTarget(target: IRenderTarget, sizeMultiplier: number | undefined, trackTarget: boolean): IRenderTarget { | |||||
| if (sizeMultiplier !== undefined) target.sizeMultiplier = sizeMultiplier | |||||
| if (trackTarget) this.trackTarget(target) | |||||
| return target | |||||
| } | |||||
| } |
| export {RenderManager} from './RenderManager' | |||||
| export {RenderTargetManager} from './RenderTargetManager' | |||||
| export {createRenderTargetKey} from './RenderTarget' | |||||
| export type {IRenderTarget, CreateRenderTargetOptions} from './RenderTarget' |
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| export function _testFinish(): void { | |||||
| window.dispatchEvent(new Event('threepipe-test-finished')) | |||||
| } | |||||
| // eslint-disable-next-line @typescript-eslint/naming-convention | |||||
| export function _testStart(): void { | |||||
| window.dispatchEvent(new Event('threepipe-test-started')) | |||||
| } |
| export {WebGLArrayRenderTarget} from 'three' | |||||
| export {WebGL3DRenderTarget} from 'three' | |||||
| export {WebGLMultipleRenderTargets} from 'three' | |||||
| export {WebGLCubeRenderTarget} from 'three' | |||||
| export {WebGLRenderTarget} from 'three' | |||||
| export {WebGLRenderer} from 'three' | |||||
| export {WebGL1Renderer} from 'three' | |||||
| export {ShaderLib} from 'three' | |||||
| export {UniformsLib} from 'three' | |||||
| export {UniformsUtils} from 'three' | |||||
| export {ShaderChunk} from 'three' | |||||
| export {FogExp2} from 'three' | |||||
| export {Fog} from 'three' | |||||
| export {Scene} from 'three' | |||||
| export {Sprite} from 'three' | |||||
| export {LOD} from 'three' | |||||
| export {SkinnedMesh} from 'three' | |||||
| export {Skeleton} from 'three' | |||||
| export {Bone} from 'three' | |||||
| export {Mesh} from 'three' | |||||
| export {InstancedMesh} from 'three' | |||||
| export {LineSegments} from 'three' | |||||
| export {LineLoop} from 'three' | |||||
| export {Line} from 'three' | |||||
| export {Points} from 'three' | |||||
| export {Group} from 'three' | |||||
| export {VideoTexture} from 'three' | |||||
| export {FramebufferTexture} from 'three' | |||||
| export {Source} from 'three' | |||||
| export {DataTexture} from 'three' | |||||
| export {DataArrayTexture} from 'three' | |||||
| export {Data3DTexture} from 'three' | |||||
| export {CompressedTexture} from 'three' | |||||
| export {CompressedArrayTexture} from 'three' | |||||
| export {CubeTexture} from 'three' | |||||
| export {CanvasTexture} from 'three' | |||||
| export {DepthTexture} from 'three' | |||||
| export {Texture} from 'three' | |||||
| export {BoxGeometry, | |||||
| CapsuleGeometry, | |||||
| CircleGeometry, | |||||
| ConeGeometry, | |||||
| CylinderGeometry, | |||||
| DodecahedronGeometry, | |||||
| EdgesGeometry, | |||||
| ExtrudeGeometry, | |||||
| IcosahedronGeometry, | |||||
| LatheGeometry, | |||||
| OctahedronGeometry, | |||||
| PlaneGeometry, | |||||
| PolyhedronGeometry, | |||||
| RingGeometry, | |||||
| ShapeGeometry, | |||||
| SphereGeometry, | |||||
| TetrahedronGeometry, | |||||
| TorusGeometry, | |||||
| TorusKnotGeometry, | |||||
| TubeGeometry, | |||||
| WireframeGeometry} from 'three' | |||||
| export { | |||||
| ShadowMaterial, | |||||
| SpriteMaterial, | |||||
| RawShaderMaterial, | |||||
| ShaderMaterial, | |||||
| PointsMaterial, | |||||
| MeshPhysicalMaterial, | |||||
| MeshStandardMaterial, | |||||
| MeshPhongMaterial, | |||||
| MeshToonMaterial, | |||||
| MeshNormalMaterial, | |||||
| MeshLambertMaterial, | |||||
| MeshDepthMaterial, | |||||
| MeshDistanceMaterial, | |||||
| MeshBasicMaterial, | |||||
| MeshMatcapMaterial, | |||||
| LineDashedMaterial, | |||||
| LineBasicMaterial, | |||||
| Material, | |||||
| } from 'three' | |||||
| export {AnimationLoader} from 'three' | |||||
| export {CompressedTextureLoader} from 'three' | |||||
| export {CubeTextureLoader} from 'three' | |||||
| export {DataTextureLoader} from 'three' | |||||
| export {TextureLoader} from 'three' | |||||
| export {ObjectLoader} from 'three' | |||||
| export {MaterialLoader} from 'three' | |||||
| export {BufferGeometryLoader} from 'three' | |||||
| export {DefaultLoadingManager, LoadingManager} from 'three' | |||||
| export {ImageLoader} from 'three' | |||||
| export {ImageBitmapLoader} from 'three' | |||||
| export {FileLoader} from 'three' | |||||
| export {Loader} from 'three' | |||||
| export {LoaderUtils} from 'three' | |||||
| export {Cache} from 'three' | |||||
| export {AudioLoader} from 'three' | |||||
| export {SpotLight} from 'three' | |||||
| export {PointLight} from 'three' | |||||
| export {RectAreaLight} from 'three' | |||||
| export {HemisphereLight} from 'three' | |||||
| export {HemisphereLightProbe} from 'three' | |||||
| export {DirectionalLight} from 'three' | |||||
| export {AmbientLight} from 'three' | |||||
| export {AmbientLightProbe} from 'three' | |||||
| export {Light} from 'three' | |||||
| export {LightProbe} from 'three' | |||||
| export {StereoCamera} from 'three' | |||||
| export {PerspectiveCamera} from 'three' | |||||
| export {OrthographicCamera} from 'three' | |||||
| export {CubeCamera} from 'three' | |||||
| export {ArrayCamera} from 'three' | |||||
| export {Camera} from 'three' | |||||
| export {AudioListener} from 'three' | |||||
| export {PositionalAudio} from 'three' | |||||
| export {AudioContext} from 'three' | |||||
| export {AudioAnalyser} from 'three' | |||||
| export {Audio} from 'three' | |||||
| export {VectorKeyframeTrack} from 'three' | |||||
| export {StringKeyframeTrack} from 'three' | |||||
| export {QuaternionKeyframeTrack} from 'three' | |||||
| export {NumberKeyframeTrack} from 'three' | |||||
| export {ColorKeyframeTrack} from 'three' | |||||
| export {BooleanKeyframeTrack} from 'three' | |||||
| export {PropertyMixer} from 'three' | |||||
| export {PropertyBinding} from 'three' | |||||
| export {KeyframeTrack} from 'three' | |||||
| export {AnimationUtils} from 'three' | |||||
| export {AnimationObjectGroup} from 'three' | |||||
| export {AnimationMixer} from 'three' | |||||
| export {AnimationClip} from 'three' | |||||
| export {Uniform} from 'three' | |||||
| export {UniformsGroup} from 'three' | |||||
| export {InstancedBufferGeometry} from 'three' | |||||
| export {BufferGeometry} from 'three' | |||||
| export {InterleavedBufferAttribute} from 'three' | |||||
| export {InstancedInterleavedBuffer} from 'three' | |||||
| export {InterleavedBuffer} from 'three' | |||||
| export {InstancedBufferAttribute} from 'three' | |||||
| export {GLBufferAttribute} from 'three' | |||||
| export { | |||||
| Float64BufferAttribute, | |||||
| Float32BufferAttribute, | |||||
| Float16BufferAttribute, | |||||
| Uint32BufferAttribute, | |||||
| Int32BufferAttribute, | |||||
| Uint16BufferAttribute, | |||||
| Int16BufferAttribute, | |||||
| Uint8ClampedBufferAttribute, | |||||
| Uint8BufferAttribute, | |||||
| Int8BufferAttribute, | |||||
| BufferAttribute, | |||||
| } from 'three' | |||||
| export {Object3D} from 'three' | |||||
| export {Raycaster} from 'three' | |||||
| export {Layers} from 'three' | |||||
| export {EventDispatcher} from 'three' | |||||
| export {Clock} from 'three' | |||||
| export {QuaternionLinearInterpolant} from 'three' | |||||
| export {LinearInterpolant} from 'three' | |||||
| export {DiscreteInterpolant} from 'three' | |||||
| export {CubicInterpolant} from 'three' | |||||
| export {Interpolant} from 'three' | |||||
| export {Triangle} from 'three' | |||||
| export {MathUtils} from 'three' | |||||
| export {Spherical} from 'three' | |||||
| export {Cylindrical} from 'three' | |||||
| export {Plane} from 'three' | |||||
| export {Frustum} from 'three' | |||||
| export {Sphere} from 'three' | |||||
| export {Ray} from 'three' | |||||
| export {Matrix4} from 'three' | |||||
| export {Matrix3} from 'three' | |||||
| export {Box3} from 'three' | |||||
| export {Box2} from 'three' | |||||
| export {Line3} from 'three' | |||||
| export {Euler} from 'three' | |||||
| export {Vector4} from 'three' | |||||
| export {Vector3} from 'three' | |||||
| export {Vector2} from 'three' | |||||
| export {Quaternion} from 'three' | |||||
| export {Color} from 'three' | |||||
| export {ColorManagement} from 'three' | |||||
| export {SphericalHarmonics3} from 'three' | |||||
| export {SpotLightHelper} from 'three' | |||||
| export {SkeletonHelper} from 'three' | |||||
| export {PointLightHelper} from 'three' | |||||
| export {HemisphereLightHelper} from 'three' | |||||
| export {GridHelper} from 'three' | |||||
| export {PolarGridHelper} from 'three' | |||||
| export {DirectionalLightHelper} from 'three' | |||||
| export {CameraHelper} from 'three' | |||||
| export {BoxHelper} from 'three' | |||||
| export {Box3Helper} from 'three' | |||||
| export {PlaneHelper} from 'three' | |||||
| export {ArrowHelper} from 'three' | |||||
| export {AxesHelper} from 'three' | |||||
| export {ArcCurve} from 'three' | |||||
| export {CatmullRomCurve3} from 'three' | |||||
| export {CubicBezierCurve} from 'three' | |||||
| export {CubicBezierCurve3} from 'three' | |||||
| export {EllipseCurve} from 'three' | |||||
| export {LineCurve} from 'three' | |||||
| export {LineCurve3} from 'three' | |||||
| export {QuadraticBezierCurve} from 'three' | |||||
| export {QuadraticBezierCurve3} from 'three' | |||||
| export {SplineCurve} from 'three' | |||||
| export {Shape} from 'three' | |||||
| export {Path} from 'three' | |||||
| export {ShapePath} from 'three' | |||||
| export {CurvePath} from 'three' | |||||
| export {Curve} from 'three' | |||||
| export {DataUtils} from 'three' | |||||
| export {ImageUtils} from 'three' | |||||
| export {ShapeUtils} from 'three' | |||||
| export {PMREMGenerator} from 'three' | |||||
| export {WebGLUtils} from 'three' | |||||
| export * from 'three/src/constants.js' | |||||
| export * from 'three/examples/jsm/libs/fflate.module.js' |
| import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js' | |||||
| import {IUiConfigContainer, uiInput, UiObjectConfig, uiPanelContainer, uiToggle} from 'uiconfig.js' | |||||
| import {serialize} from 'ts-browser-helpers' | |||||
| import {ICameraControls} from '../../core' | |||||
| export type TOrbitControlsEvents = 'change' | 'end' | 'start' | |||||
| @uiPanelContainer('Orbit Controls') | |||||
| export class OrbitControls3 extends OrbitControls implements IUiConfigContainer, ICameraControls<TOrbitControlsEvents> { | |||||
| uiConfig?: UiObjectConfig<void, 'panel'> | |||||
| @serialize() type = 'OrbitControls' | |||||
| @uiToggle() enabled = true | |||||
| @uiToggle() @serialize() dollyZoom = false | |||||
| @uiToggle() @serialize() enableDamping = true | |||||
| @uiInput() @serialize() dampingFactor = 0.08 | |||||
| @uiToggle() @serialize() autoRotate = false | |||||
| @uiInput() @serialize() autoRotateSpeed = 2.0 | |||||
| @uiToggle() @serialize() enableZoom = true | |||||
| @uiInput() @serialize() zoomSpeed = 0.15 | |||||
| @uiInput() @serialize() maxZoomSpeed = 0.20 | |||||
| @uiToggle() @serialize() enableRotate = true | |||||
| @uiInput() @serialize() rotateSpeed = 2.0 | |||||
| @uiToggle() @serialize() enablePan = true | |||||
| @uiInput() @serialize() panSpeed = 1.0 | |||||
| @uiInput() @serialize() autoPushTarget = false | |||||
| @uiInput() @serialize() autoPullTarget = false | |||||
| @uiInput() @serialize() minDistance = 0.35 | |||||
| @uiInput() @serialize() maxDistance = 1000 | |||||
| @uiInput() @serialize() minZoom = 0.01 | |||||
| @uiInput() @serialize() maxZoom = 1000 | |||||
| @uiInput() @serialize() minPolarAngle = 0 | |||||
| @uiInput() @serialize() maxPolarAngle = Math.PI | |||||
| @uiInput() @serialize() minAzimuthAngle = -10000 // should be -Infinity but this breaks the UI | |||||
| @uiInput() @serialize() maxAzimuthAngle = 10000 | |||||
| // @uiToggle() | |||||
| @serialize() screenSpacePanning = true | |||||
| // @uiInput() | |||||
| @serialize() keyPanSpeed = 7.0 | |||||
| throttleUpdate = 60 // throttle to 60 updates per second (implemented in OrbitControls.js.update() method) | |||||
| zoomIn(delta: number) { | |||||
| // @ts-expect-error not in ts | |||||
| super.zoomIn(delta) | |||||
| } | |||||
| zoomOut(delta: number) { | |||||
| // @ts-expect-error not in ts | |||||
| super.zoomOut(delta) | |||||
| } | |||||
| } |
| export {OrbitControls3, type TOrbitControlsEvents} from './controls/OrbitControls3' | |||||
| export {Box3B} from './math/Box3B' | |||||
| export * from './utils/index' |