Преглед изворни кода

Core viewer framework

master
Palash Bansal пре 3 година
комит
e0537c3a36
No account linked to committer's email address
100 измењених фајлова са 26128 додато и 0 уклоњено
  1. 6
    0
      .eslintignore
  2. 210
    0
      .eslintrc.cjs
  3. 55
    0
      .github/workflows/deploy-pages.yml
  4. 64
    0
      .gitignore
  5. 8
    0
      .idea/.gitignore
  6. 6
    0
      .idea/inspectionProfiles/Project_Default.xml
  7. 6
    0
      .idea/jsLinters/eslint.xml
  8. 8
    0
      .idea/modules.xml
  9. 14
    0
      .idea/threepipe.iml
  10. 6
    0
      .idea/vcs.xml
  11. 1
    0
      .npmignore
  12. 21
    0
      LICENSE
  13. 93
    0
      Readme.md
  14. 13810
    0
      package-lock.json
  15. 123
    0
      package.json
  16. 0
    0
      plugins/index.ts
  17. 84
    0
      rollup.config.mjs
  18. 136
    0
      src/assetmanager/AssetExporter.ts
  19. 562
    0
      src/assetmanager/AssetImporter.ts
  20. 436
    0
      src/assetmanager/AssetManager.ts
  21. 27
    0
      src/assetmanager/IAsset.ts
  22. 159
    0
      src/assetmanager/IAssetImporter.ts
  23. 33
    0
      src/assetmanager/IExporter.ts
  24. 20
    0
      src/assetmanager/IImporter.ts
  25. 37
    0
      src/assetmanager/Importer.ts
  26. 319
    0
      src/assetmanager/MaterialManager.ts
  27. 136
    0
      src/assetmanager/export/GLTFExporter2.ts
  28. 258
    0
      src/assetmanager/export/GLTFWriter2.ts
  29. 8
    0
      src/assetmanager/export/SimpleJSONExporter.ts
  30. 8
    0
      src/assetmanager/export/SimpleTextExporter.ts
  31. 4
    0
      src/assetmanager/export/index.ts
  32. 62
    0
      src/assetmanager/gltf/GLTFLightExtrasExtension.ts
  33. 277
    0
      src/assetmanager/gltf/GLTFMaterialExtrasExtension.ts
  34. 102
    0
      src/assetmanager/gltf/GLTFMaterialsAlphaMapExtension.ts
  35. 109
    0
      src/assetmanager/gltf/GLTFMaterialsBumpMapExtension.ts
  36. 104
    0
      src/assetmanager/gltf/GLTFMaterialsDisplacementMapExtension.ts
  37. 109
    0
      src/assetmanager/gltf/GLTFMaterialsLightMapExtension.ts
  38. 72
    0
      src/assetmanager/gltf/GLTFObject3DExtrasExtension.ts
  39. 290
    0
      src/assetmanager/gltf/GLTFViewerConfigExtension.ts
  40. 8
    0
      src/assetmanager/gltf/index.ts
  41. 73
    0
      src/assetmanager/import/DRACOLoader2.ts
  42. 106
    0
      src/assetmanager/import/GLTFLoader2.ts
  43. 33
    0
      src/assetmanager/import/JSONMaterialLoader.ts
  44. 586
    0
      src/assetmanager/import/MTLLoader2.ts
  45. 939
    0
      src/assetmanager/import/OBJLoader2.ts
  46. 85
    0
      src/assetmanager/import/RGBEPNGLoader.ts
  47. 18
    0
      src/assetmanager/import/SimpleJSONLoader.ts
  48. 18
    0
      src/assetmanager/import/ZipLoader.ts
  49. 8
    0
      src/assetmanager/import/index.ts
  50. 14
    0
      src/assetmanager/index.ts
  51. 114
    0
      src/core/ICamera.ts
  52. 35
    0
      src/core/IGeometry.ts
  53. 131
    0
      src/core/IMaterial.ts
  54. 199
    0
      src/core/IObject.ts
  55. 135
    0
      src/core/IRenderer.ts
  56. 108
    0
      src/core/IScene.ts
  57. 27
    0
      src/core/ITexture.ts
  58. 24
    0
      src/core/camera/ICameraControls.ts
  59. 471
    0
      src/core/camera/PerspectiveCamera2.ts
  60. 96
    0
      src/core/geometry/iGeometryCommons.ts
  61. 20
    0
      src/core/index.ts
  62. 75
    0
      src/core/material/ExtendedShaderMaterial.ts
  63. 454
    0
      src/core/material/IMaterialUi.ts
  64. 316
    0
      src/core/material/PhysicalMaterial.ts
  65. 111
    0
      src/core/material/ShaderMaterial2.ts
  66. 233
    0
      src/core/material/UnlitMaterial.ts
  67. 201
    0
      src/core/material/iMaterialCommons.ts
  68. 215
    0
      src/core/object/IObjectUi.ts
  69. 588
    0
      src/core/object/RootScene.ts
  70. 111
    0
      src/core/object/iCameraCommons.ts
  71. 461
    0
      src/core/object/iObjectCommons.ts
  72. 36
    0
      src/global.d.ts
  73. 13
    0
      src/index.ts
  74. 145
    0
      src/materials/MaterialExtender.ts
  75. 98
    0
      src/materials/MaterialExtension.ts
  76. 2
    0
      src/materials/index.ts
  77. 71
    0
      src/plugins/base/PipelinePassPlugin.ts
  78. 4
    0
      src/plugins/index.ts
  79. 87
    0
      src/plugins/pipeline/DepthBufferPlugin.ts
  80. 49
    0
      src/plugins/ui/RenderTargetPreviewPlugin.css
  81. 126
    0
      src/plugins/ui/RenderTargetPreviewPlugin.ts
  82. 19
    0
      src/postprocessing/EffectComposer2.ts
  83. 21
    0
      src/postprocessing/ExtendedCopyPass.ts
  84. 321
    0
      src/postprocessing/ExtendedRenderPass.ts
  85. 70
    0
      src/postprocessing/ExtendedShaderPass.ts
  86. 87
    0
      src/postprocessing/GBufferRenderPass.ts
  87. 36
    0
      src/postprocessing/GenericBlendTexturePass.ts
  88. 32
    0
      src/postprocessing/Pass.ts
  89. 45
    0
      src/postprocessing/ScreenPass.ts
  90. 10
    0
      src/postprocessing/index.ts
  91. 61
    0
      src/postprocessing/sortPasses.ts
  92. 440
    0
      src/rendering/RenderManager.ts
  93. 39
    0
      src/rendering/RenderTarget.ts
  94. 222
    0
      src/rendering/RenderTargetManager.ts
  95. 4
    0
      src/rendering/index.ts
  96. 8
    0
      src/testing/testing.ts
  97. 220
    0
      src/three/Threejs.ts
  98. 63
    0
      src/three/controls/OrbitControls3.ts
  99. 3
    0
      src/three/index.ts
  100. 0
    0
      src/three/math/Box3B.ts

+ 6
- 0
.eslintignore Прегледај датотеку

node_modules
dist
public
config
libs
docs

+ 210
- 0
.eslintrc.cjs Прегледај датотеку

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' },
},
],
}

+ 55
- 0
.github/workflows/deploy-pages.yml Прегледај датотеку

# 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


+ 64
- 0
.gitignore Прегледај датотеку

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

+ 8
- 0
.idea/.gitignore Прегледај датотеку

# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

+ 6
- 0
.idea/inspectionProfiles/Project_Default.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>

+ 6
- 0
.idea/jsLinters/eslint.xml Прегледај датотеку

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="EslintConfiguration">
<option name="fix-on-save" value="true" />
</component>
</project>

+ 8
- 0
.idea/modules.xml Прегледај датотеку

<?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>

+ 14
- 0
.idea/threepipe.iml Прегледај датотеку

<?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>

+ 6
- 0
.idea/vcs.xml Прегледај датотеку

<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

+ 1
- 0
.npmignore Прегледај датотеку

*.backup

+ 21
- 0
LICENSE Прегледај датотеку

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.

+ 93
- 0
Readme.md Прегледај датотеку

# ThreePipe

A new way to work with three.js, 3D models and rendering on the web.

[ThreePipe](https://threepipe.org/) &mdash;
[Github](https://github.com/repalash/threepipe) &mdash;
[Examples](https://threepipe.org/examples/) &mdash;
[Docs](https://threepipe.org/docs/) &mdash;
[WebGi](https://webgi.xyz/docs/)

[![License: MIT](https://img.shields.io/badge/License-MIT-g.svg)](https://opensource.org/licenses/MIT)
[![Discord Server](https://img.shields.io/discord/956788102473584660?label=Discord&logo=discord)](https://discord.gg/apzU8rUWxY)
[![NPM Package](https://img.shields.io/npm/v/threepipe.svg)](https://www.npmjs.com/package/threepipe)
[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/repalash.svg?style=social&label=Follow%20%40repalash)](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/) &mdash; [Docs](https://webgi.xyz/docs/)

[![Twitter](https://img.shields.io/twitter/url/https/twitter.com/repalash.svg?style=social&label=Follow%20%40pixotronics)](https://twitter.com/pixotronics)

## Contributing
Contributions to ThreePipe are welcome and encouraged! Feel free to open issues and pull requests on the GitHub repository.

+ 13810
- 0
package-lock.json
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 123
- 0
package.json Прегледај датотеку

{
"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"
]
}

+ 0
- 0
plugins/index.ts Прегледај датотеку


+ 84
- 0
rollup.config.mjs Прегледај датотеку

// 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.
},
})
]
}

+ 136
- 0
src/assetmanager/AssetExporter.ts Прегледај датотеку

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
}


}

+ 562
- 0
src/assetmanager/AssetImporter.ts Прегледај датотеку

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


}

+ 436
- 0
src/assetmanager/AssetManager.ts Прегледај датотеку

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
}

+ 27
- 0
src/assetmanager/IAsset.ts Прегледај датотеку

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[];
}

+ 159
- 0
src/assetmanager/IAssetImporter.ts Прегледај датотеку

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[]>

}


+ 33
- 0
src/assetmanager/IExporter.ts Прегледај датотеку

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>
}

+ 20
- 0
src/assetmanager/IImporter.ts Прегледај датотеку

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;
}

+ 37
- 0
src/assetmanager/Importer.ts Прегледај датотеку

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
}
}

+ 319
- 0
src/assetmanager/MaterialManager.ts Прегледај датотеку

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
}
}


+ 136
- 0
src/assetmanager/export/GLTFExporter2.ts Прегледај датотеку

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)
},
})
}
}

+ 258
- 0
src/assetmanager/export/GLTFWriter2.ts Прегледај датотеку

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

}
}

+ 8
- 0
src/assetmanager/export/SimpleJSONExporter.ts Прегледај датотеку

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'})
}
}


+ 8
- 0
src/assetmanager/export/SimpleTextExporter.ts Прегледај датотеку

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'})
}
}

+ 4
- 0
src/assetmanager/export/index.ts Прегледај датотеку

export {GLTFExporter2, type GLTFExporter2Options} from './GLTFExporter2'
export {GLTFWriter2} from './GLTFWriter2'
export {SimpleJSONExporter} from './SimpleJSONExporter'
export {SimpleTextExporter} from './SimpleTextExporter'

+ 62
- 0
src/assetmanager/gltf/GLTFLightExtrasExtension.ts Прегледај датотеку

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
}
},
})
}

+ 277
- 0
src/assetmanager/gltf/GLTFMaterialExtrasExtension.ts Прегледај датотеку

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)
},
})
}

+ 102
- 0
src/assetmanager/gltf/GLTFMaterialsAlphaMapExtension.ts Прегледај датотеку

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}

+ 109
- 0
src/assetmanager/gltf/GLTFMaterialsBumpMapExtension.ts Прегледај датотеку

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}

+ 104
- 0
src/assetmanager/gltf/GLTFMaterialsDisplacementMapExtension.ts Прегледај датотеку

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}

+ 109
- 0
src/assetmanager/gltf/GLTFMaterialsLightMapExtension.ts Прегледај датотеку

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}

+ 72
- 0
src/assetmanager/gltf/GLTFObject3DExtrasExtension.ts Прегледај датотеку

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
}
},
})
}

+ 290
- 0
src/assetmanager/gltf/GLTFViewerConfigExtension.ts Прегледај датотеку

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
}

+ 8
- 0
src/assetmanager/gltf/index.ts Прегледај датотеку

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'

+ 73
- 0
src/assetmanager/import/DRACOLoader2.ts Прегледај датотеку

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;')?.()
}

}

+ 106
- 0
src/assetmanager/import/GLTFLoader2.ts Прегледај датотеку

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])
}}
}
}


}


+ 33
- 0
src/assetmanager/import/JSONMaterialLoader.ts Прегледај датотеку

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)
}
}

+ 586
- 0
src/assetmanager/import/MTLLoader2.ts Прегледај датотеку

/* 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};

+ 939
- 0
src/assetmanager/import/OBJLoader2.ts Прегледај датотеку

/* 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 };

+ 85
- 0
src/assetmanager/import/RGBEPNGLoader.ts Прегледај датотеку

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
}



+ 18
- 0
src/assetmanager/import/SimpleJSONLoader.ts Прегледај датотеку

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)
}
}


+ 18
- 0
src/assetmanager/import/ZipLoader.ts Прегледај датотеку

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)
}

}

+ 8
- 0
src/assetmanager/import/index.ts Прегледај датотеку

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'

+ 14
- 0
src/assetmanager/index.ts Прегледај датотеку

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'

+ 114
- 0
src/core/ICamera.ts Прегледај датотеку

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


+ 35
- 0
src/core/IGeometry.ts Прегледај датотеку

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}



+ 131
- 0
src/core/IMaterial.ts Прегледај датотеку

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
}

+ 199
- 0
src/core/IObject.ts Прегледај датотеку

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

}

+ 135
- 0
src/core/IRenderer.ts Прегледај датотеку

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

+ 108
- 0
src/core/IScene.ts Прегледај датотеку

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

}

+ 27
- 0
src/core/ITexture.ts Прегледај датотеку

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

}

+ 24
- 0
src/core/camera/ICameraControls.ts Прегледај датотеку

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

+ 471
- 0
src/core/camera/PerspectiveCamera2.ts Прегледај датотеку

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

}



+ 96
- 0
src/core/geometry/iGeometryCommons.ts Прегледај датотеку

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?
}

+ 20
- 0
src/core/index.ts Прегледај датотеку

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'

+ 75
- 0
src/core/material/ExtendedShaderMaterial.ts Прегледај датотеку

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(';')
}

}

+ 454
- 0
src/core/material/IMaterialUi.ts Прегледај датотеку

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'),
],
}
),
}

+ 316
- 0
src/core/material/PhysicalMaterial.ts Прегледај датотеку

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')
}
}

+ 111
- 0
src/core/material/ShaderMaterial2.ts Прегледај датотеку

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
}
}

+ 233
- 0
src/core/material/UnlitMaterial.ts Прегледај датотеку

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')
}
}

+ 201
- 0
src/core/material/iMaterialCommons.ts Прегледај датотеку

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
}

+ 215
- 0
src/core/object/IObjectUi.ts Прегледај датотеку

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,
// }
// }

+ 588
- 0
src/core/object/RootScene.ts Прегледај датотеку

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
}

+ 111
- 0
src/core/object/iCameraCommons.ts Прегледај датотеку

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?
}

+ 461
- 0
src/core/object/iObjectCommons.ts Прегледај датотеку

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)

}


+ 36
- 0
src/global.d.ts Прегледај датотеку

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

+ 13
- 0
src/index.ts Прегледај датотеку

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'

+ 145
- 0
src/materials/MaterialExtender.ts Прегледај датотеку

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)
}
}

+ 98
- 0
src/materials/MaterialExtension.ts Прегледај датотеку

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;
}

+ 2
- 0
src/materials/index.ts Прегледај датотеку

export {MaterialExtender} from './MaterialExtender'
export type {MaterialExtension, IShaderPropertiesUpdater} from './MaterialExtension'

+ 71
- 0
src/plugins/base/PipelinePassPlugin.ts Прегледај датотеку

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
}

+ 4
- 0
src/plugins/index.ts Прегледај датотеку

export {DepthBufferPlugin} from './pipeline/DepthBufferPlugin'
export type {DepthBufferPluginEventTypes, DepthBufferPluginPass, DepthBufferPluginTarget} from './pipeline/DepthBufferPlugin'
export {PipelinePassPlugin} from './base/PipelinePassPlugin'
export {RenderTargetPreviewPlugin} from './ui/RenderTargetPreviewPlugin'

+ 87
- 0
src/plugins/pipeline/DepthBufferPlugin.ts Прегледај датотеку

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
}

}


+ 49
- 0
src/plugins/ui/RenderTargetPreviewPlugin.css Прегледај датотеку

#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;
}

+ 126
- 0
src/plugins/ui/RenderTargetPreviewPlugin.ts Прегледај датотеку

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()
}

}

+ 19
- 0
src/postprocessing/EffectComposer2.ts Прегледај датотеку

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
}

}

+ 21
- 0
src/postprocessing/ExtendedCopyPass.ts Прегледај датотеку

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')
}
}

+ 321
- 0
src/postprocessing/ExtendedRenderPass.ts Прегледај датотеку

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)
}
}


+ 70
- 0
src/postprocessing/ExtendedShaderPass.ts Прегледај датотеку

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)
}
}


+ 87
- 0
src/postprocessing/GBufferRenderPass.ts Прегледај датотеку

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
}

}

+ 36
- 0
src/postprocessing/GenericBlendTexturePass.ts Прегледај датотеку

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
}

}

+ 32
- 0
src/postprocessing/Pass.ts Прегледај датотеку

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;
}

+ 45
- 0
src/postprocessing/ScreenPass.ts Прегледај датотеку

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
}


+ 10
- 0
src/postprocessing/index.ts Прегледај датотеку

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'

+ 61
- 0
src/postprocessing/sortPasses.ts Прегледај датотеку

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
}

+ 440
- 0
src/rendering/RenderManager.ts Прегледај датотеку

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
}

}

+ 39
- 0
src/rendering/RenderTarget.ts Прегледај датотеку

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(';')
}

+ 222
- 0
src/rendering/RenderTargetManager.ts Прегледај датотеку

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
}

}

+ 4
- 0
src/rendering/index.ts Прегледај датотеку

export {RenderManager} from './RenderManager'
export {RenderTargetManager} from './RenderTargetManager'
export {createRenderTargetKey} from './RenderTarget'
export type {IRenderTarget, CreateRenderTargetOptions} from './RenderTarget'

+ 8
- 0
src/testing/testing.ts Прегледај датотеку

// 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'))
}

+ 220
- 0
src/three/Threejs.ts Прегледај датотеку

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'

+ 63
- 0
src/three/controls/OrbitControls3.ts Прегледај датотеку

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)
}

}

+ 3
- 0
src/three/index.ts Прегледај датотеку

export {OrbitControls3, type TOrbitControlsEvents} from './controls/OrbitControls3'
export {Box3B} from './math/Box3B'
export * from './utils/index'

+ 0
- 0
src/three/math/Box3B.ts Прегледај датотеку


Неке датотеке нису приказане због велике количине промена

Loading…
Откажи
Сачувај