├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── main.yml.disabled │ └── size.yml.disabled ├── .gitignore ├── .husky └── pre-commit ├── .prettierignore ├── LICENSE ├── README.md ├── ci └── publish-gh-pages.js ├── examples ├── simple │ ├── .gitignore │ ├── README.md │ └── index.html └── webpack_simple │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── src │ ├── index.js │ └── index.template │ └── webpack.config.js ├── package.json ├── src ├── DDDViewerConfig.ts ├── SceneViewer.ts ├── ViewerState.ts ├── camera │ ├── BaseCameraController.ts │ ├── FreeCameraController.ts │ ├── GeolocationCameraController.ts │ ├── OrbitCameraController.ts │ ├── PanningCameraController.ts │ ├── WalkCameraController.ts │ └── WalkCollideCameraController.ts ├── core │ ├── DDDObjectRef.ts │ └── ScenePosition.ts ├── format │ ├── DDDFormat.ts │ └── GeoJSON.ts ├── index.ts ├── layers │ ├── Base3DLayer.ts │ ├── DDD3DLayer.ts │ ├── GeoJson3DLayer.ts │ ├── GeoTile3DLayer.ts │ ├── LayerManager.ts │ └── OverlayLayer.ts ├── loading │ └── QueueLoader.ts ├── process │ ├── ViewerProcess.ts │ ├── ViewerProcessManager.ts │ ├── anim │ │ ├── AnimationProcess.ts │ │ ├── CameraMoveAnimationProcess.ts │ │ ├── DateTimeAnimationProcess.ts │ │ └── TextAnimationProcess.ts │ └── sequencer │ │ └── ViewerSequencer.ts └── render │ ├── environment │ └── DefaultEnvironment.ts │ ├── materials │ ├── SkyboxMaterial.ts │ ├── TerrainMaterial.ts │ └── TextMaterial.ts │ ├── pipeline │ └── DefaultRenderPipeline.ts │ └── skybox │ ├── CubemapSkybox.ts │ ├── DynamicSkybox.ts │ └── Skybox.ts ├── test └── blah.test.ts.disabled ├── tsconfig.json └── tsdx.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2015: true, 5 | //"commonjs": true 6 | }, 7 | ignorePatterns: ['**/ddd-viewer/**/*.js', '**/ddd-viewer/**/*.ts'], 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/eslint-recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaVersion: 12, 16 | sourceType: 'module', 17 | }, 18 | plugins: ['@typescript-eslint'], 19 | rules: { 20 | indent: ['error', 4], 21 | quotes: ['error', 'double'], 22 | semi: ['error', 'always'], 23 | 'object-curly-spacing': ['error', 'always'], 24 | 'array-bracket-spacing': ['error', 'always'], 25 | 'no-console': 'off', 26 | '@typescript-eslint/no-non-null-assertion': 'off', 27 | '@typescript-eslint/no-inferrable-types': 'off', 28 | '@typescript-eslint/no-empty-function': 'off', 29 | '@typescript-eslint/no-unused-vars': 'warn', 30 | "@typescript-eslint/no-explicit-any": "off", 31 | "no-mixed-spaces-and-tabs": "off" 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: ddd-viewer build 2 | run-name: Full build has been triggered by (${{ github.actor }}) 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | jobs: 8 | build: 9 | name: Build job on Node ${{ matrix.node }} and ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | continue-on-error: true 12 | strategy: 13 | matrix: 14 | node: [ '18.0', '20.0'] 15 | os: [ubuntu-latest, windows-latest, macOS-latest] 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v2 19 | 20 | - name: Use Node ${{ matrix.node }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node }} 24 | 25 | # - name: Remove lock files under windows systems 26 | # run: | 27 | # if exist "./package-lock.json" del "./package-lock.json" 28 | # if exist "./yarn-lock.json" del "./yarn-lock.json" 29 | 30 | - name: Remove lock files under non-windows systems 31 | run: | 32 | rm -rf ./package-lock.json 33 | rm -rf ./yarn-lock.json 34 | if: matrix.os != 'windows-latest' 35 | 36 | - name: Install dependencies 37 | run: | 38 | yarn 39 | 40 | - name: Lint 41 | run: yarn lint 42 | 43 | # - name: Audit 44 | # run: yarn audit 45 | 46 | - name: Test 47 | run: yarn test --ci --coverage --maxWorkers=2 48 | 49 | - name: Build 50 | env: 51 | NODE_OPTIONS: '--max_old_space_size=8192' 52 | run: yarn build 53 | 54 | - name: Deploy GitHub pages 55 | run: | 56 | mkdir -p ./examples/simple/lib/ 57 | cp dist/ddd-viewer.umd.production.min.js examples/simple/lib/ddd-viewer.umd.production.min.js 58 | node ci/publish-gh-pages.js 59 | if: matrix.os != 'ubuntu-latest' && matrix.node == '20.0' -------------------------------------------------------------------------------- /.github/workflows/main.yml.disabled: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /.github/workflows/size.yml.disabled: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | node_modules 4 | /dist 5 | /.settings/ 6 | .project 7 | .pydevproject 8 | 9 | .nyc_output 10 | /coverage/ 11 | /tests/e2e/reports/ 12 | selenium-debug.log 13 | 14 | # local env files 15 | .env.local 16 | .env.*.local 17 | 18 | # Log files 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Editor directories and files 24 | .idea 25 | .vscode 26 | *.suo 27 | *.ntvs* 28 | *.njsproj 29 | *.sln 30 | *.sw* 31 | 32 | *.log 33 | 34 | package-lock.json 35 | yarn.lock 36 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jose Juan Montes and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![ddd-viewer build](https://github.com/blackyale/ddd-viewer/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/blackyale/ddd-viewer/actions/workflows/build.yml) 2 | 3 | # DDD-Viewer 4 | 5 |

6 | 11 |

12 | 13 | > DDD-Viewer library for viewing DDD and OpenStreetMap data in 3D. 14 | 15 | 16 | ## Overview 17 | 18 | DDD-Viewer is an HTML5 library for 3D visualization of scenes generated by the [DDD tool](https://github.com/jjmontesl/ddd). 19 | 20 | In particular, DDD-Viewer allows to render and interact with 3D maps generated from [OpenStreetMap](https://www.openstreetmap.org/) and other datasouces. 21 | 22 | DDD-Viewer is a TypeScript library based on [BabylonJS](https://www.babylonjs.com/). 23 | 24 | You can embed DDD-Viewer scenes in your website or use it to build a custom application. 25 | 26 | *NOTE that this project is currently experimental and the data formats and library are expected to change.* 27 | 28 | 29 | ## Examples 30 | 31 | - DDD-Viewer-App at [3dsmaps.com](https://3dsmaps.com). 32 | - The [examples](examples/) directory contains simple usage examples. 33 | 34 | *TODO: Add 1-2 screenshots* 35 | 36 | 37 | ## Using the library 38 | 39 | DDD-Viewer is meant to be easily used as a standalone viewer for websites or 40 | as a library from 3rd party projects. 41 | 42 | Note that if serving your own copy of DDD-Viewer, you may wish to point your users to your own 43 | assets. See the [Configuration] and the [Assets] sections below for further information. 44 | 45 | 46 | ### From vanilla Javascript, with no bundler (\ tag) 47 | 48 | *TODO* 49 | 50 | You can find an example in the [simple UMD example](examples/simple) directory. 51 | 52 | ### From a node project (TypeScript / Webpack) 53 | 54 | *TODO* 55 | 56 | ### Configuration 57 | 58 | *TODO* 59 | 60 | 61 | ## Building the library 62 | 63 | You only need to build the library if you wish to make changes to it 64 | (otherwise you can use the bundled libraries or include *ddd-viewer* through *npm*). 65 | 66 | **Download / install** 67 | 68 | Clone the `ddd-viewer` repository and install JS/TS dependencies through `npm`: 69 | 70 | git clone -c core.autocrlf=false https://github.com/jjmontesl/ddd-viewer.git 71 | 72 | Enter the `ddd-viewer` directory (`cd ddd-viewer`) and install dependencies: 73 | 74 | npm install 75 | 76 | **Npm scripts** 77 | 78 | - `npm run dev`: Development mode (watches source files and compiles when changes are made) 79 | - `npm run build`: Builds the library 80 | 85 | 86 | *TODO* 87 | 88 | 89 | ## Data / Assets 90 | 91 | ### Textures and object library 92 | 93 | Currently, in order to render the primary DDD OSM 3D data you need to make some 3D and graphics assets available. 94 | 95 | ### 3D Map Tiles 96 | 97 | 98 | ### Downloading tiles locally for development or publishing 99 | 100 | There is an [assets download tool](https://github.com/jjmontesl/ddd-viewer-app/tree/master/tools/downloader) 101 | which you can find in the related repository for ddd-viewer-app. 102 | 103 | Please download assets and 3D tiles locally during development and for publishing, as 3dsmaps.com server 104 | has very limited resources. 105 | 106 | ## Questions / Support / Contributing 107 | 108 | Please use the [Issue Tracker]() to report issues or if you have any questions 109 | about DDD-Viewer usage. 110 | 111 | Please get in touch if you wish to contribute to the library. Have a look at the 112 | issue tracker to know about the tasks in the roadmap if you are interested. 113 | 114 | You can follow [@3dsmaps](https://twitter.com/3dsmaps) on Twitter to stay up to date. 115 | 116 | (Leave a ⭐️ if you like this project!) 117 | 118 | 119 | ## License 120 | 121 | This project is licensed under the MIT License. 122 | 123 | Copyright (c) 2021 Jose Juan Montes and contributors 124 | 125 | See LICENSE file for further information. 126 | 127 | -------------------------------------------------------------------------------- /ci/publish-gh-pages.js: -------------------------------------------------------------------------------- 1 | var ghpages = require('gh-pages'); 2 | 3 | ghpages.publish('examples/simple', function(err) {}); -------------------------------------------------------------------------------- /examples/simple/.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /examples/simple/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | This an easy example of how to embed ddd-viewer into a simple html. 4 | 5 | 6 | ## Building the example 7 | 8 | **Using** 9 | 10 | For using this example you need to build `ddd-viewer.umd.production.min.js` and place it to the `lib/` folder. 11 | 12 | Open `index.html` in your favourite browser. 13 | 14 | Check the [project's README](../../README.md) for more information. 15 | 16 | 17 | ## How do we use the library? 18 | 19 | Import the UMD build into your `` element with a script tag: 20 | ```html 21 | 22 | 23 | 24 | ``` 25 | 26 | We used Tailwind CSS library for presentation, but it`s not necessary for the viewer. 27 | 28 | We have to setup our html with a canvas element that is going to be our container for the 3D scene rendering. 29 | 30 | ```html 31 |
32 |

Simple Example DDD-Viewer

33 | 34 |
35 | ``` 36 | 37 | We have to initialize the viewer passing it the configuration and the layers we want to show up: 38 | 39 | ```js 40 | function initViewer() { 41 | 42 | var dddviewer = window['ddd-viewer']; 43 | 44 | const dddConfig = { 45 | "defaultCoords": [-8.723, 42.238], 46 | //"defaultCoords": [-8.4069793, 43.3861094], 47 | "tileUrlBase": "https://3dsmaps.com/cache/ddd_http/", 48 | "assetsUrlbase": "https://3dsmaps.com/assets/", 49 | "materialsTextureSet": "default256", 50 | "geolocation": false 51 | } 52 | 53 | const canvas = document.getElementById('ddd-scene'); 54 | const sceneViewer = new dddviewer.SceneViewer(canvas, dddConfig); 55 | 56 | const layerDddOsm3d = new dddviewer.GeoTile3DLayer(); 57 | sceneViewer.layerManager.addLayer(layerDddOsm3d); 58 | 59 | // Events 60 | //window.addEventListener('resize', this.resize); 61 | 62 | } 63 | ``` 64 | 65 | Finally you have to call the library when the page loads: 66 | 67 | ```js 68 | window.addEventListener("load", () => { 69 | initViewer(); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /examples/simple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DDD-Viewer Simple Example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

Simple Example DDD-Viewer

24 | 25 |
26 | 27 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /examples/webpack_simple/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | package-lock.json -------------------------------------------------------------------------------- /examples/webpack_simple/README.md: -------------------------------------------------------------------------------- 1 | ## Overview 2 | 3 | Webpack Simple is an incorporated ddd-viewer in one page with webpack. 4 | 5 | The example of Webpack Simple is perfect if you are thinking of incorporating the library into a project with webpack. 6 | 7 | ## Building the example 8 | 9 | **Installation** 10 | 11 | 1. Go to webpack simple with the terminal. 12 | 13 | 2. First you can init the draft with: 14 | 15 | `npm init` 16 | 17 | 3. After you you have to install the dependencies with: 18 | 19 | `npm install` 20 | 21 | ***NOTE:*** 22 | 23 | *The library is now experimental, so it is not uploaded to npm, as seen in the package.json:* 24 | 25 | `"ddd-viewer": "file:../../ddd-viewer",` 26 | 27 | You have to make a local npm to be able to use the library then run this line from npm: 28 | 29 | `npm link ddd-viewer` 30 | 31 | 4. You will have the project ready now to run the server: 32 | 33 | `npm run serve` 34 | 35 | ## How do we use the library? 36 | 37 | You import the scene and the tiles: 38 | 39 | ```js 40 | import { SceneViewer } from 'ddd-viewer'; 41 | 42 | `import { GeoTile3DLayer } from 'ddd-viewer'; 43 | ``` 44 | 45 | And after you init the viewer: 46 | 47 | ```js 48 | export function initViewer() { 49 | 50 | var dddviewer = window['ddd-viewer']; 51 | 52 | const dddConfig = { 53 | "defaultCoords": [-8.723, 42.238], 54 | //"defaultCoords": [-8.4069793, 43.3861094], 55 | "tileUrlBase": "https://3dsmaps.com/cache/ddd_http/", 56 | "assetsUrlbase": "https://3dsmaps.com/assets/", 57 | "materialsTextureSet": "default256", 58 | "geolocation": false 59 | } 60 | 61 | const canvas = document.getElementById('ddd-scene'); 62 | const sceneViewer = new SceneViewer(canvas, dddConfig); 63 | 64 | const layerDddOsm3d = new GeoTile3DLayer(); 65 | sceneViewer.layerManager.addLayer("ddd-osm-3d", layerDddOsm3d); 66 | 67 | } 68 | ``` 69 | 70 | Finally you have to call the library when the page loads: 71 | 72 | ```js 73 | window.addEventListener("load", () => { 74 | initViewer(); 75 | }); 76 | ``` -------------------------------------------------------------------------------- /examples/webpack_simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack_simple", 3 | "version": "1.0.0", 4 | "description": "DDD(3Ds) Viewer Webpack Example", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "serve": "webpack serve", 8 | "build": "webpack build" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "ddd-viewer": "file:../../ddd-viewer", 14 | "webpack": "^5.58.0", 15 | "webpack-cli": "^4.8.0" 16 | }, 17 | "devDependencies": { 18 | "html-webpack-plugin": "^4.5.2", 19 | "webpack-dev-server": "^4.3.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/webpack_simple/src/index.js: -------------------------------------------------------------------------------- 1 | import { SceneViewer } from 'ddd-viewer'; 2 | import { GeoTile3DLayer } from 'ddd-viewer'; 3 | 4 | export function initViewer() { 5 | 6 | var dddviewer = window['ddd-viewer']; 7 | 8 | const dddConfig = { 9 | "defaultCoords": [-8.723, 42.238], 10 | //"defaultCoords": [-8.4069793, 43.3861094], 11 | "tileUrlBase": "https://3dsmaps.com/cache/ddd_http/", 12 | "tileUrlSuffix": "", 13 | "assetsUrlbase": "https://3dsmaps.com/assets/", 14 | "materialsTextureSet": "default256", 15 | "geolocation": false 16 | } 17 | 18 | const canvas = document.getElementById('ddd-scene'); 19 | const sceneViewer = new SceneViewer(canvas, dddConfig); 20 | 21 | const layerDddOsm3d = new GeoTile3DLayer(); 22 | sceneViewer.layerManager.addLayer(layerDddOsm3d); 23 | 24 | // Events 25 | //window.addEventListener('resize', this.resize); 26 | 27 | } 28 | 29 | window.addEventListener("load", () => { 30 | initViewer(); 31 | }); -------------------------------------------------------------------------------- /examples/webpack_simple/src/index.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | DDD-Viewer Simple Example 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |

Webpack Example DDD-Viewer

26 | 27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/webpack_simple/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/index.js', 6 | mode: 'development', 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'webpack_simple.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.m?js/, 15 | resolve: { 16 | fullySpecified: false 17 | } 18 | }, 19 | ] 20 | }, 21 | plugins: [new HtmlWebpackPlugin({template: './src/index.template'})], 22 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ddd-viewer", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "author": "Jose Juan Montes", 6 | "contributors": [ 7 | "Farvell" 8 | ], 9 | "description": "DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes", 10 | "main": "dist/ddd-viewer.esm.js", 11 | "typings": "dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "engines": { 16 | "node": ">=10" 17 | }, 18 | "scripts": { 19 | "dev": "barrelsby -d src --delete && tsdx watch --format esm --target web", 20 | "build-esm": "barrelsby -d src --delete && tsdx build --format esm --target web", 21 | "build-umd": "barrelsby -d src --delete && tsdx build --max_old_space_size=8192 --format umd --target web", 22 | "build": "barrelsby -d src --delete && tsdx build --max_old_space_size=8192 --format esm,umd --target web", 23 | "barrelsby": "barrelsby -d src --delete", 24 | "doc": "barrelsby -d src --delete && npx typedoc --out doc/api src/index.ts", 25 | "test": "tsdx test --passWithNoTests", 26 | "lint": "eslint . --ext .ts --fix", 27 | "_prepare": "tsdx build --format esm,umd --target web", 28 | "size": "size-limit", 29 | "analyze": "size-limit --why" 30 | }, 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "npx lint-staged" 34 | } 35 | }, 36 | "module": "dist/ddd-viewer.esm.js", 37 | "size-limit": [ 38 | { 39 | "path": "dist/ddd-viewer.cjs.production.min.js", 40 | "limit": "10 KB" 41 | }, 42 | { 43 | "path": "dist/ddd-viewer.esm.js", 44 | "limit": "10 KB" 45 | } 46 | ], 47 | "devDependencies": { 48 | "@rollup/plugin-node-resolve": "^15.2.1", 49 | "@size-limit/preset-small-lib": "^9.0.0", 50 | "@types/ol": "^6.5.1", 51 | "@types/proj4": "^2.5.0", 52 | "@types/suncalc": "^1.8.0", 53 | "@typescript-eslint/eslint-plugin": "^6.7.2", 54 | "@typescript-eslint/parser": "^6.7.2", 55 | "barrelsby": "^2.8.1", 56 | "eslint": "^8.50.0", 57 | "gh-pages": "^6.0.0", 58 | "husky": "^7.0.1", 59 | "lint-staged": "^14.0.1", 60 | "ol": "^6.5.0", 61 | "prettier": "^3.0.3", 62 | "proj4": "^2.7.2", 63 | "size-limit": "^9.0.0", 64 | "suncalc": "^1.8.0", 65 | "tsdx": "^0.14.1", 66 | "tslib": "^2.2.0", 67 | "typedoc": "^0.25.1", 68 | "typescript": "^4.3.2" 69 | }, 70 | "dependencies": { 71 | "@babylonjs/core": "5.0.0-rc.4", 72 | "@babylonjs/loaders": "5.0.0-rc.4", 73 | "@babylonjs/materials": "5.0.0-rc.4", 74 | "earcut": "^2.2.2" 75 | }, 76 | "lint-staged": { 77 | "*.ts": [ 78 | "prettier -w", 79 | "eslint --fix" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DDDViewerConfig.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | /** 8 | * This object represents ddd-viewer configuration, and is used to pass 9 | * options to the main object (DDDViewer) constructor. 10 | */ 11 | class DDDViewerConfig { 12 | defaultCoords: number[] | null = [ -8.726, 42.233 ]; // [0.0, 0.0]; 13 | 14 | // TODO: Move this to layer configuration (?) 15 | tileUrlBase: string = "/cache/ddd_http/"; 16 | tileUrlSuffix: string = ""; 17 | 18 | assetsUrlbase: string = "/assets/"; 19 | 20 | materialsTextureSet: string | null = "default256"; 21 | materialsSplatmap: number | null = null; // 256 22 | 23 | //sceneGroundTextureOverrideUrl: string | null = null; 24 | 25 | timeScale: number = 0.0; 26 | 27 | moveSpeeds: number[] = [ 2.0, 5.0, 10.0 ]; 28 | 29 | sceneTileDrawDistanceDefault: number = 1; 30 | } 31 | 32 | export { DDDViewerConfig }; 33 | -------------------------------------------------------------------------------- /src/ViewerState.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { DDDViewerConfig } from "./DDDViewerConfig"; 8 | 9 | /** 10 | * Holds DDDViewer global state like viewer position, date/time, configuration... 11 | * Some internal values are also stored here for convenience (FPS, drawcalls, mobile detection...). 12 | * This object must be JSON-serializable. 13 | */ 14 | class ViewerState { 15 | 16 | mapVisible = true; 17 | sceneVisible = false; 18 | dddConfig: DDDViewerConfig; 19 | isMobile = false; 20 | positionTileZoomLevel = 11; 21 | positionWGS84 = [ -8.726, 42.233 ]; // [0.0, 0.0]; 22 | 23 | // Position in scene, in engine coordinates (elevation is Y) 24 | positionScene = [ 0, 0, 0 ]; 25 | positionGroundHeight: number = 150.0; 26 | positionTerrainElevation = 0; 27 | positionHeading = 0.0; 28 | positionTilt = 0.0; 29 | positionName: string | null = null; 30 | positionDate: Date = new Date(); 31 | positionDateSeconds: number = this.positionDate.getTime() / 1000; 32 | geolocationEnabled = false; 33 | timeScale = 24 * 2; // 24 * 2 = 48x faster (1 day = 30 min) 34 | 35 | // TODO: These nodes are instrumented: remove selectedMesh from here and use ids. 36 | // TODO: Try removing this and this.sceneViewer id still used 37 | sceneSelectedMesh = null; 38 | sceneSelectedMeshId: string | null = null; 39 | 40 | sceneFPS: number = 0; 41 | sceneDrawCalls: number = 0; 42 | sceneTriangles: number = 0; 43 | 44 | sceneShadowsEnabled = false; 45 | sceneTextsEnabled = false; 46 | scenePostprocessingEnabled = false; 47 | scenePickingEnabled = true; 48 | sceneViewModeShow = true; 49 | sceneSelectedShowNormals = true; 50 | sceneTileDrawDistance = 1; 51 | sceneMoveSpeed = 5; 52 | sceneEnvironmentProbe = 16; // 16; // null to use a static environment (should be associated to the skybox, but it's currently fixed) 53 | sceneSkybox = "/textures/TropicalSunnyDay"; // /textures/skybox/clouds1/clouds1 // "@dynamic"; // ""/textures/TropicalSunnyDay"; 54 | 55 | // TODO: This shall be a per-layer setting 56 | sceneGroundTextureOverrideUrl: string | null = null; 57 | 58 | sceneTitleText:string | null = null; 59 | 60 | /** 61 | * 3dsmaps DDD tile server supports on-demand generation. When a tile that's not available is enqueued, it responds 62 | * with information about the job status. This array contains enqueued tiles status. 63 | */ 64 | remoteQueueJobsStatus: any[] = []; 65 | 66 | 67 | constructor(dddConfig: DDDViewerConfig, initialCoords: number[] | null = null) { 68 | 69 | this.dddConfig = dddConfig; 70 | 71 | if (dddConfig.defaultCoords) { 72 | this.positionWGS84 = dddConfig.defaultCoords; 73 | } 74 | 75 | if (initialCoords) { 76 | this.positionWGS84 = initialCoords; 77 | } 78 | 79 | const shadowsEnabled = localStorage.getItem( "dddSceneShadowsEnabled" ); 80 | this.sceneShadowsEnabled = shadowsEnabled ? JSON.parse( shadowsEnabled ) : this.sceneShadowsEnabled; 81 | 82 | const textsEnabled = localStorage.getItem( "dddSceneTextsEnabled" ); 83 | this.sceneTextsEnabled = textsEnabled ? JSON.parse( textsEnabled ) : this.sceneTextsEnabled; 84 | 85 | this.sceneTileDrawDistance = dddConfig.sceneTileDrawDistanceDefault; 86 | 87 | // Start time 88 | this.positionDate.setHours( 11 ); 89 | this.positionDate.setMinutes( 0 ); 90 | 91 | } 92 | } 93 | 94 | export { ViewerState }; 95 | -------------------------------------------------------------------------------- /src/camera/BaseCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | 10 | /** 11 | * A Camera and Input controller. 12 | * 13 | * This allows controlling the main camera, which is the main interface for viewing. 14 | * Controllers process input devices if they wish to respond to user input. 15 | * 16 | * Client code may use custom controllers or disable these controllers 17 | * and position the camera manually. 18 | */ 19 | abstract class BaseCameraController { 20 | 21 | protected dddViewer: SceneViewer; 22 | 23 | constructor(dddViewer: SceneViewer) { 24 | 25 | // Reference to DDDViewer 26 | this.dddViewer = dddViewer; 27 | 28 | // Babylon camera which we are controlling 29 | //this.camera = dddViewer.camera; 30 | } 31 | 32 | protected getCamera(): Camera { 33 | return this.dddViewer.camera; 34 | } 35 | 36 | abstract update(deltaTime: number): void; 37 | 38 | abstract activate(): void; 39 | 40 | } 41 | 42 | export { BaseCameraController }; -------------------------------------------------------------------------------- /src/camera/FreeCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera, UniversalCamera, Vector3 } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | 11 | /** 12 | * DDD Viewer base layer class. 13 | */ 14 | class FreeCameraController extends BaseCameraController { 15 | 16 | fixMinHeight = false; 17 | 18 | update(deltaTime: number): void { 19 | // Fix viewer to floor 20 | if (this.fixMinHeight) { 21 | const terrainElevation = this.dddViewer.viewerState.positionTerrainElevation; 22 | if (terrainElevation && this.dddViewer.camera.position.y < ( terrainElevation + 1.0 )) { 23 | this.getCamera().position.y = terrainElevation + 1.0; 24 | } 25 | } 26 | } 27 | 28 | activate(): void { 29 | if (this.dddViewer.camera) { 30 | this.dddViewer.camera.customRenderTargets.length = 0; //4 = []; 31 | this.dddViewer.camera.detachControl(); 32 | this.dddViewer.camera.dispose(); 33 | } 34 | 35 | //console.debug("Creating free camera."); 36 | const camera = new UniversalCamera( "Camera", Vector3.Zero(), this.dddViewer.scene ); 37 | camera.minZ = 1.0; // 0.1; 38 | camera.maxZ = 4500; 39 | camera.angularSensibility = 500.0; 40 | camera.touchAngularSensibility = 1000.0; 41 | //camera.touchMoveSensibility = 1.0; 42 | camera.inertia = 0.0; 43 | camera.keysUp.push( 87 ); 44 | camera.keysDown.push( 83 ); 45 | camera.keysLeft.push( 65 ); 46 | camera.keysRight.push( 68 ); 47 | camera.keysUpward.push( 69 ); 48 | camera.keysDownward.push( 81 ); 49 | camera.attachControl( this.dddViewer.engine.getRenderingCanvas(), true ); 50 | camera.fov = 40.0 * ( Math.PI / 180.0 ); // 35.0 might be GM, 45.8... is default // 35 51 | const positionScene = this.dddViewer.wgs84ToScene( this.dddViewer.viewerState.positionWGS84 ); 52 | camera.position = new Vector3( positionScene[0], this.dddViewer.viewerState.positionGroundHeight + this.dddViewer.viewerState.positionTerrainElevation + 1, positionScene[2]); 53 | camera.rotation = new Vector3(( 90.0 - this.dddViewer.viewerState.positionTilt ) * ( Math.PI / 180.0 ), this.dddViewer.viewerState.positionHeading * ( Math.PI / 180.0 ), 0.0 ); 54 | //camera.cameraRotation = new Vector2(/* (90.0 - this.viewerState.positionTilt) * (Math.PI / 180.0) */ 0, this.viewerState.positionHeading * (Math.PI / 180.0)); 55 | this.dddViewer.camera = camera; 56 | this.dddViewer.setMoveSpeed( this.dddViewer.viewerState.sceneMoveSpeed ); 57 | 58 | this.dddViewer.updateRenderTargets(); 59 | if (this.dddViewer.shadowGenerator) { 60 | this.dddViewer.shadowGenerator.splitFrustum(); 61 | } 62 | } 63 | 64 | } 65 | 66 | export { FreeCameraController }; -------------------------------------------------------------------------------- /src/camera/GeolocationCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | 11 | /** 12 | * DDD Viewer base layer class. 13 | */ 14 | class GeolocationCameraController extends BaseCameraController { 15 | update(deltaTime: number): void {} 16 | 17 | activate(): void {} 18 | 19 | /* 20 | geolocationPosition( enabled ) { 21 | 22 | //this.selectCameraFree(); 23 | //this.walkMode = true; 24 | //this.camera.detachControl(); 25 | 26 | /- 27 | this.app.$getLocation({enableHighAccuracy: true}).then(coordinates => { 28 | console.log(coordinates); 29 | let altitude = coordinates.altitude !== null ? coordinates.altitude : 2.0; 30 | let scenePos = this.wgs84ToScene([coordinates.lng, coordinates.lat, altitude]); 31 | //console.log(scenePos); 32 | this.camera.position.x = scenePos[0]; 33 | this.camera.position.y = altitude; 34 | this.camera.position.z = scenePos[2]; 35 | 36 | let heading = coordinates.heading; 37 | if (heading) { 38 | this.sceneViewer.viewerState.positionHeading = heading; 39 | let rotation = new Vector3((90.0 - this.sceneViewer.viewerState.positionTilt) * (Math.PI / 180.0), this.sceneViewer.viewerState.positionHeading * (Math.PI / 180.0), 0.0); 40 | this.camera.rotation = rotation; 41 | } 42 | }); 43 | -/ 44 | 45 | this.viewerState.geolocationEnabled = enabled; 46 | 47 | if ( enabled ) { 48 | 49 | const that = this; 50 | 51 | // Enable geolocation 52 | this.selectCameraFree(); 53 | 54 | //this._geolocationWatchId = this.app.$watchLocation({enableHighAccuracy: true, maximumAge: 5}).then(coordinates => { 55 | this.app.$getLocation({ enableHighAccuracy: true, maximumAge: 5 }).then(( coords ) => { that.onDeviceLocation( coords ); }); 56 | 57 | // Compass 58 | this._onDeviceOrientation = function( e ) { that.onDeviceOrientation( e ); }; 59 | this._onDeviceOrientation.bind( that ); 60 | const isIOS = false; 61 | if ( isIOS ) { 62 | DeviceOrientationEvent.requestPermission().then(( response ) => { 63 | if ( response === "granted" ) { 64 | window.addEventListener( "deviceorientation", this._onDeviceOrientation ); 65 | } else { 66 | alert( "Compass usage permission not granted." ); 67 | } 68 | }).catch(() => alert( "Compass not supported." )); 69 | } else { 70 | window.addEventListener( "deviceorientationabsolute", this._onDeviceOrientation ); 71 | } 72 | 73 | } else { 74 | 75 | // Disable geolocation 76 | 77 | this.viewerState.geolocationEnabled = false; 78 | if ( this._geolocationWatchId !== null ) { 79 | this.app.$clearLocationWatch( this._geolocationWatchId ); 80 | this._geolocationWatchId = null; 81 | } 82 | window.removeEventListener( "deviceorientationabsolute", this._onDeviceOrientation ); 83 | window.removeEventListener( "deviceorientation", this._onDeviceOrientation ); 84 | this._onDeviceOrientation = null; 85 | } 86 | 87 | } 88 | 89 | onDeviceLocation( coordinates ) { 90 | //console.log(coordinates); 91 | if ( coordinates ) { 92 | 93 | const altitude = coordinates.altitude !== null ? coordinates.altitude : 2.0; 94 | if ( this.walkMode ) { altitude.y = 2.5; } 95 | 96 | this.viewerState.positionWGS84 = [ coordinates.lng, coordinates.lat, altitude ]; 97 | const scenePos = this.wgs84ToScene( this.viewerState.positionWGS84 ); 98 | this.viewerState.positionScene = scenePos; 99 | 100 | this.camera.position.x = scenePos[0]; 101 | this.camera.position.y = altitude; 102 | this.camera.position.z = scenePos[2]; 103 | 104 | /- 105 | let heading = coordinates.heading; 106 | if (heading !== null && !isNaN(heading)) { 107 | this.viewerState.positionHeading = heading; 108 | let rotation = new Vector3((90.0 - this.viewerState.positionTilt) * (Math.PI / 180.0), this.viewerState.positionHeading * (Math.PI / 180.0), 0.0); 109 | this.camera.rotation = rotation; 110 | //console.debug(heading); 111 | } 112 | -/ 113 | } 114 | 115 | if ( this.viewerState.geolocationEnabled ) { 116 | const that = this; 117 | setTimeout( function() { 118 | that.app.$getLocation({ enableHighAccuracy: true, maximumAge: 5 }).then(( coords ) => { that.onDeviceLocation( coords ); }); 119 | }, 1000 ); 120 | } 121 | 122 | } 123 | */ 124 | 125 | /** 126 | * From: https://www.w3.org/TR/orientation-event/ 127 | */ 128 | /* 129 | getQuaternion( alpha, beta, gamma ) { 130 | 131 | var degtorad = Math.PI / 180; // Degree-to-Radian conversion 132 | 133 | var _x = beta ? beta * degtorad : 0; // beta value 134 | var _y = gamma ? gamma * degtorad : 0; // gamma value 135 | var _z = alpha ? alpha * degtorad : 0; // alpha value 136 | 137 | var cX = Math.cos( _x/2 ); 138 | var cY = Math.cos( _y/2 ); 139 | var cZ = Math.cos( _z/2 ); 140 | var sX = Math.sin( _x/2 ); 141 | var sY = Math.sin( _y/2 ); 142 | var sZ = Math.sin( _z/2 ); 143 | 144 | // 145 | // ZXY quaternion construction. 146 | // 147 | 148 | var w = cX * cY * cZ - sX * sY * sZ; 149 | var x = sX * cY * cZ - cX * sY * sZ; 150 | var y = cX * sY * cZ + sX * cY * sZ; 151 | var z = cX * cY * sZ + sX * sY * cZ; 152 | 153 | //return [ w, x, y, z ]; 154 | return new Quaternion(x, y, z, w); 155 | } 156 | */ 157 | 158 | /* 159 | onDeviceOrientation( e ) { 160 | 161 | //let rotation = Quaternion.FromEulerAngles(e.alpha * Math.PI / 180.0, e.beta * Math.PI / 180.0, e.gamma * Math.PI / 180.0); 162 | //let forward = Vector3.Forward().rotateByQuaternionToRef(rotation, new Vector3()); 163 | //let heading = Math.atan2(forward.y, forward.x) * 180.0 / Math.PI; 164 | //alert(heading); 165 | 166 | let heading = e.webkitCompassHeading || Math.abs( e.alpha - 360 ); 167 | 168 | if ( heading !== null && !isNaN( heading )) { 169 | 170 | heading = ( heading ) % 360.0; 171 | this.viewerState.positionHeading = heading; 172 | 173 | let tilt = e.webkitCompassTilt || Math.abs( e.beta - 360 ); 174 | if ( tilt !== null && !isNaN( tilt )) { 175 | this.viewerState.positionTilt = ( - tilt ); 176 | } 177 | 178 | const tiltRotation = ( 90.0 - this.viewerState.positionTilt ) * ( Math.PI / 180.0 ); 179 | if ( tiltRotation < 0 ) { tilt = Math.PI * 2 - tiltRotation; } 180 | const rotation = new Vector3( tiltRotation, this.viewerState.positionHeading * ( Math.PI / 180.0 ), 0.0 ); 181 | //let rotation = new Vector3(Math.PI / 2 + -e.beta * Math.PI / 180.0, -e.alpha * Math.PI / 180.0, e.gamma * Math.PI / 180.0 ); 182 | this.camera!.rotation = rotation; 183 | //console.debug(heading); 184 | } 185 | //compassCircle.style.transform = `translate(-50%, -50%) rotate(${-compass}deg)`; 186 | } 187 | */ 188 | } 189 | 190 | export { GeolocationCameraController }; 191 | -------------------------------------------------------------------------------- /src/camera/OrbitCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { ArcRotateCamera, BoundingInfo, Camera, Mesh, Vector3 } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | 11 | /** 12 | * DDD Viewer base layer class. 13 | */ 14 | class OrbitCameraController extends BaseCameraController { 15 | 16 | update(deltaTime: number): void { 17 | // Fix viewer to floor 18 | const terrainElevation = this.dddViewer.viewerState.positionTerrainElevation; 19 | if (terrainElevation && this.dddViewer.camera.position.y < ( terrainElevation + 1.0 )) { 20 | this.getCamera().position.y = terrainElevation + 1.0; 21 | } 22 | } 23 | 24 | activate(): void { 25 | 26 | let targetCoords = Vector3.Zero(); 27 | if ( this.dddViewer.selectedObject ) { 28 | const boundingBox: BoundingInfo = this.dddViewer.getBoundsRecursively( this.dddViewer.selectedObject!.mesh ); 29 | //targetCoords = this.selectedMesh.absolutePosition; 30 | const minWorld = boundingBox.minimum; 31 | const maxWorld = boundingBox.maximum; 32 | targetCoords = new Vector3(( minWorld.x + maxWorld.x ) / 2, ( minWorld.y + maxWorld.y ) / 2, ( minWorld.z + maxWorld.z ) / 2 ); 33 | } 34 | 35 | let distance = 75.0; 36 | if ( this.dddViewer.camera ) { 37 | distance = Vector3.Distance( this.dddViewer.camera.position, targetCoords ); 38 | 39 | this.dddViewer.camera.customRenderTargets.length = 0; // = []; 40 | 41 | this.dddViewer.camera.detachControl(); 42 | this.dddViewer.camera.dispose(); 43 | } 44 | 45 | console.debug( "Creating orbit camera pointing to: " + targetCoords ); 46 | 47 | const camera = new ArcRotateCamera( "Camera", -( 90 + this.dddViewer.viewerState.positionHeading ) * Math.PI / 180.0, this.dddViewer.viewerState.positionTilt * Math.PI / 180.0, distance, targetCoords, this.dddViewer.scene ); 48 | camera.attachControl( this.dddViewer.engine.getRenderingCanvas(), true ); 49 | camera.minZ = 0.5; // 1.0; // 0.1; 50 | //camera.maxZ = 2500; // Automatic? see focusOn() 51 | camera.lowerRadiusLimit = 15; 52 | camera.upperRadiusLimit = 1000; 53 | camera.upperBetaLimit = Math.PI; // /2; // Math.PI / 2 = limit to flat view 54 | camera.panningSensibility = 50.0; // 0.5; 55 | //camera.angularSensibility = 50.0; 56 | //camera.inertia = 0.10; 57 | 58 | //camera.multiTouchPanning = false; 59 | //camera.multiTouchPanAndZoom = false; 60 | //camera.pinchZoom = true; 61 | 62 | camera.useNaturalPinchZoom = true; 63 | camera.fov = 35.0 * ( Math.PI / 180.0 ); 64 | this.dddViewer.camera = camera; 65 | 66 | this.dddViewer.updateRenderTargets(); 67 | if (this.dddViewer.shadowGenerator) { 68 | this.dddViewer.shadowGenerator.splitFrustum(); 69 | } 70 | } 71 | 72 | } 73 | 74 | export { OrbitCameraController }; -------------------------------------------------------------------------------- /src/camera/PanningCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | 11 | /** 12 | * DDD Viewer base layer class. 13 | */ 14 | class PanningCameraController extends BaseCameraController { 15 | 16 | update(deltaTime: number): void { 17 | 18 | } 19 | 20 | activate(): void { 21 | 22 | } 23 | 24 | } 25 | 26 | export { PanningCameraController }; -------------------------------------------------------------------------------- /src/camera/WalkCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera, Vector3 } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | import { FreeCameraController } from "./FreeCameraController"; 11 | 12 | /** 13 | * DDD Viewer base layer class. 14 | */ 15 | class WalkCameraController extends FreeCameraController { 16 | 17 | sceneCameraWalkHeight = 1.75; // 2.0; 18 | 19 | //falling = false; 20 | 21 | update(deltaTime: number): void { 22 | // Fix viewer to floor 23 | const terrainElevation = this.dddViewer.viewerState.positionTerrainElevation; 24 | if ( terrainElevation !== null && this.dddViewer.camera ) { 25 | this.getCamera().position.y = terrainElevation + this.sceneCameraWalkHeight; // 3.0; 26 | } 27 | } 28 | 29 | activate(): void { 30 | super.activate(); 31 | //this.walkMode = true; 32 | this.dddViewer.camera!.inertia = 0.2; // 0.0; 33 | this.dddViewer.setMoveSpeed( this.dddViewer.viewerState.sceneMoveSpeed ); 34 | } 35 | 36 | } 37 | 38 | export { WalkCameraController }; -------------------------------------------------------------------------------- /src/camera/WalkCollideCameraController.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera, Vector3 } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { BaseCameraController } from "./BaseCameraController"; 10 | import { WalkCameraController } from "./WalkCameraController"; 11 | 12 | /** 13 | * DDD Viewer base layer class. 14 | */ 15 | class WalkCollideCameraController extends WalkCameraController { 16 | 17 | velocity = new Vector3(); 18 | 19 | gravity = -9.8; 20 | 21 | falling = false; 22 | 23 | fallStartDistance = 0.5; 24 | 25 | update(deltaTime: number): void { 26 | // Fix viewer to floor 27 | const terrainElevation = this.dddViewer.viewerState.positionTerrainElevation; 28 | if ( terrainElevation !== null && this.dddViewer.camera ) { 29 | const currentHeight = this.getCamera().position.y; 30 | const baseHeight = terrainElevation + this.sceneCameraWalkHeight; 31 | if (currentHeight > baseHeight + this.fallStartDistance) { 32 | this.falling = true; 33 | } 34 | if (this.falling) { 35 | this.velocity.y += (this.gravity * deltaTime); 36 | this.getCamera().position.addInPlace(this.velocity.scale(deltaTime)); 37 | } 38 | 39 | if (!this.falling || this.getCamera().position.y < baseHeight) { 40 | this.falling = false; 41 | this.velocity.set(0, 0, 0); 42 | this.getCamera().position.y = terrainElevation + this.sceneCameraWalkHeight; 43 | } 44 | 45 | } 46 | } 47 | 48 | } 49 | 50 | export { WalkCollideCameraController }; -------------------------------------------------------------------------------- /src/core/DDDObjectRef.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Mesh, Node } from "@babylonjs/core"; 8 | 9 | /** 10 | * A reference to a "logical" object in the scene. 11 | * 12 | * Like real scene mesh objects, DDDObjectRefs have metadata, a path, parent and children. 13 | * 14 | * This allows to reference sets of triangles in larger objects (eg. if several logical objects were 15 | * batched into a larger mesh at design time or elsewhere). 16 | * 17 | * TODO: Reference the containing layer so each layer can resolve its objects. 18 | */ 19 | class DDDObjectRef { 20 | mesh: Node; 21 | 22 | submeshIdx: number = -1; 23 | 24 | faceIndexStart: number = -1; 25 | faceIndexEnd: number = -1; 26 | 27 | constructor(mesh: Node, submeshIdx: number = -1) { 28 | this.mesh = mesh; 29 | this.submeshIdx = submeshIdx; 30 | 31 | if (this.submeshIdx > -1) { 32 | const metadata = DDDObjectRef.nodeMetadata(mesh); 33 | const indexes = 34 | "ddd:combined:indexes" in metadata 35 | ? metadata["ddd:combined:indexes"] 36 | : metadata["ddd:batch:indexes"]; 37 | if (indexes && submeshIdx in indexes) { 38 | this.faceIndexStart = submeshIdx > 0 ? indexes[submeshIdx - 1][0] : 0; 39 | this.faceIndexEnd = indexes[submeshIdx][0]; 40 | } 41 | } 42 | 43 | /* 44 | metadata = indexes[i][1]; 45 | 46 | // WARN: TODO: this transformation is done in other places 47 | let meshName = null; 48 | if (!combined) { 49 | meshName = pickResult.pickedMesh.id.split("/").pop().replaceAll('#', '_'); // .replaceAll("_", " "); 50 | } else { 51 | meshName = metadata['ddd:path'].split("/").pop().replaceAll('#', '_'); // .replaceAll("_", " "); 52 | } 53 | */ 54 | } 55 | 56 | static nodeMetadata(node: Node) { 57 | if ( 58 | node && 59 | node.metadata && 60 | node.metadata.gltf && 61 | node.metadata.gltf.extras 62 | ) { 63 | return node.metadata.gltf.extras; 64 | } else if (node.metadata !== null) { 65 | return node.metadata; 66 | } else { 67 | return null; 68 | } 69 | } 70 | 71 | static fromMeshFace(mesh: Mesh, faceIndex: number): DDDObjectRef { 72 | //console.debug("Selecting from mesh and face (mesh=" + mesh.id + ", faceIndex=" + faceIndex + ")"); 73 | 74 | const metadata = DDDObjectRef.nodeMetadata(mesh); 75 | 76 | let subIndex = -1; 77 | if (metadata) { 78 | if ( 79 | "ddd:combined:indexes" in metadata || 80 | "ddd:batch:indexes" in metadata 81 | ) { 82 | const indexes = 83 | "ddd:combined:indexes" in metadata 84 | ? metadata["ddd:combined:indexes"] 85 | : metadata["ddd:batch:indexes"]; 86 | // Find triangle in indexes 87 | let prevIndex = -1; 88 | //if (indexes.length > 0) { subIndex = 0; } 89 | for (let i = 0; i < indexes.length; i++) { 90 | if (faceIndex > prevIndex && faceIndex < indexes[i][0]) { 91 | subIndex = i; 92 | break; 93 | } 94 | prevIndex = indexes[i][0]; 95 | } 96 | } 97 | } 98 | 99 | const objectRef = new DDDObjectRef(mesh, subIndex); 100 | return objectRef; 101 | } 102 | 103 | /* 104 | findChildByName(name: string): DDDObjectRef | null { 105 | return null; 106 | } 107 | */ 108 | 109 | getMetadata(): any { 110 | let metadata = DDDObjectRef.nodeMetadata(this.mesh); 111 | if (metadata && this.submeshIdx >= 0) { 112 | const indexes = 113 | "ddd:combined:indexes" in metadata 114 | ? metadata["ddd:combined:indexes"] 115 | : metadata["ddd:batch:indexes"]; 116 | metadata = indexes[this.submeshIdx][1]; 117 | } 118 | return metadata; 119 | } 120 | 121 | static urlId(value: string): string { 122 | let result = value; 123 | if (result) { 124 | result = result.replaceAll("/", "-"); 125 | result = result.replaceAll("#", "_"); 126 | result = result.replaceAll(" ", "_"); 127 | result = encodeURIComponent(result); 128 | result = result.replaceAll("%3A", ":"); 129 | //result = result.replace("/", "_"); 130 | //result = result.replace("#", "_"); 131 | } 132 | return result; 133 | } 134 | 135 | getId(): string { 136 | const metadata = this.getMetadata(); 137 | 138 | let result = this.mesh.id; 139 | if (metadata && "ddd:rpath" in metadata) { 140 | result = metadata["ddd:rpath"]; 141 | } else if (metadata && "ddd:path" in metadata) { 142 | result = metadata["ddd:path"]; 143 | } 144 | return result; 145 | } 146 | 147 | getUrlId(): string { 148 | let result = this.getId(); 149 | result = DDDObjectRef.urlId(result); 150 | return result; 151 | } 152 | 153 | getLabel(): string { 154 | const result = this.getId(); 155 | return result; 156 | } 157 | 158 | getChildren(): DDDObjectRef[] { 159 | const result: DDDObjectRef[] = []; 160 | 161 | //if (this.submeshIdx < 0) { 162 | for (const child of this.mesh.getChildren()) { 163 | result.push(new DDDObjectRef(child)); 164 | } 165 | //} 166 | 167 | const metadata = this.getMetadata(); 168 | if ( 169 | metadata && 170 | ("ddd:combined:indexes" in metadata || "ddd:batch:indexes" in metadata) 171 | ) { 172 | const indexes = 173 | "ddd:combined:indexes" in metadata 174 | ? metadata["ddd:combined:indexes"] 175 | : metadata["ddd:batch:indexes"]; 176 | for (let i = 0; i < indexes.length; i++) { 177 | result.push(new DDDObjectRef(this.mesh, i)); 178 | } 179 | } 180 | 181 | return result; 182 | } 183 | 184 | getParent(): DDDObjectRef | null { 185 | let result = null; 186 | const parent = this.mesh.parent; 187 | if (parent) { 188 | result = new DDDObjectRef(parent); 189 | } 190 | return result; 191 | } 192 | } 193 | 194 | export { DDDObjectRef }; 195 | -------------------------------------------------------------------------------- /src/core/ScenePosition.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | 8 | /** 9 | * ScenePosition represents 10 | */ 11 | class ScenePosition { 12 | 13 | // Position is Lat, Lon, Altitude (currently MSL) 14 | positionWGS84: number[] = [ 0, 0, 0 ]; 15 | 16 | positionTileZoomLevel: number = 0; 17 | 18 | // Ground altitude 19 | positionGroundHeight: number = 0; 20 | 21 | positionHeading: number = 0; 22 | 23 | positionTilt: number = 0; 24 | 25 | } 26 | 27 | export { ScenePosition }; -------------------------------------------------------------------------------- /src/format/DDDFormat.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | 10 | /** 11 | * Manages reading of files in DDD format. DDD files are glTF files 12 | * with custom metadata. This class processes nodes and metadata. 13 | */ 14 | class DDDFormat { 15 | 16 | protected dddViewer: SceneViewer; 17 | 18 | constructor(dddViewer: SceneViewer) { 19 | // Reference to DDDViewer 20 | this.dddViewer = dddViewer; 21 | } 22 | 23 | 24 | 25 | } 26 | 27 | export { DDDFormat }; -------------------------------------------------------------------------------- /src/format/GeoJSON.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | 10 | /** 11 | */ 12 | class GeoJSONFormat { 13 | 14 | protected dddViewer: SceneViewer; 15 | 16 | constructor(dddViewer: SceneViewer) { 17 | // Reference to DDDViewer 18 | this.dddViewer = dddViewer; 19 | } 20 | 21 | } 22 | 23 | export { GeoJSONFormat }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Automatically generated by barrelsby. 3 | */ 4 | 5 | export * from "./DDDViewerConfig"; 6 | export * from "./SceneViewer"; 7 | export * from "./ViewerState"; 8 | export * from "./camera/BaseCameraController"; 9 | export * from "./camera/FreeCameraController"; 10 | export * from "./camera/GeolocationCameraController"; 11 | export * from "./camera/OrbitCameraController"; 12 | export * from "./camera/PanningCameraController"; 13 | export * from "./camera/WalkCameraController"; 14 | export * from "./camera/WalkCollideCameraController"; 15 | export * from "./core/DDDObjectRef"; 16 | export * from "./core/ScenePosition"; 17 | export * from "./format/DDDFormat"; 18 | export * from "./format/GeoJSON"; 19 | export * from "./layers/Base3DLayer"; 20 | export * from "./layers/DDD3DLayer"; 21 | export * from "./layers/GeoJson3DLayer"; 22 | export * from "./layers/GeoTile3DLayer"; 23 | export * from "./layers/LayerManager"; 24 | export * from "./layers/OverlayLayer"; 25 | export * from "./loading/QueueLoader"; 26 | export * from "./process/ViewerProcess"; 27 | export * from "./process/ViewerProcessManager"; 28 | export * from "./process/anim/AnimationProcess"; 29 | export * from "./process/anim/CameraMoveAnimationProcess"; 30 | export * from "./process/anim/DateTimeAnimationProcess"; 31 | export * from "./process/anim/TextAnimationProcess"; 32 | export * from "./process/sequencer/ViewerSequencer"; 33 | export * from "./render/environment/DefaultEnvironment"; 34 | export * from "./render/materials/SkyboxMaterial"; 35 | export * from "./render/materials/TerrainMaterial"; 36 | export * from "./render/materials/TextMaterial"; 37 | export * from "./render/pipeline/DefaultRenderPipeline"; 38 | export * from "./render/skybox/CubemapSkybox"; 39 | export * from "./render/skybox/DynamicSkybox"; 40 | export * from "./render/skybox/Skybox"; 41 | -------------------------------------------------------------------------------- /src/layers/Base3DLayer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { SceneViewer } from "SceneViewer"; 8 | import { LayerManager } from "./LayerManager"; 9 | 10 | /** 11 | * DDD Viewer base layer class. 12 | */ 13 | abstract class Base3DLayer { 14 | 15 | key: string; 16 | visible: boolean = true; 17 | 18 | protected dddViewer: SceneViewer | null; 19 | protected layerManager: LayerManager | null; // TODO: added for backwards compat (for convenience), should be removed 20 | 21 | constructor(key: string) { 22 | this.key = key; 23 | this.visible = true; 24 | 25 | this.dddViewer = null; 26 | this.layerManager = null; 27 | } 28 | 29 | abstract update(deltaTime: number): void; 30 | 31 | abstract clearScene(): void; 32 | 33 | setVisible(visible: boolean): void { 34 | this.visible = visible; 35 | } 36 | 37 | setViewer(dddViewer: SceneViewer | null): void { 38 | this.dddViewer = dddViewer; 39 | this.layerManager = dddViewer ? dddViewer.layerManager : null; 40 | 41 | if (!dddViewer) { 42 | this.clearScene(); 43 | } 44 | } 45 | 46 | } 47 | 48 | export { Base3DLayer }; -------------------------------------------------------------------------------- /src/layers/DDD3DLayer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { 8 | AbstractMesh, 9 | Color3, 10 | Mesh, 11 | MeshBuilder, 12 | Node, 13 | Ray, 14 | StandardMaterial, 15 | Texture, 16 | TransformNode, 17 | Vector3, 18 | } from "@babylonjs/core"; 19 | import { Coordinate } from "ol/coordinate"; 20 | import * as extent from "ol/extent"; 21 | import * as olProj from "ol/proj"; 22 | import { createXYZ, extentFromProjection } from "ol/tilegrid"; 23 | import TileGrid from "ol/tilegrid/TileGrid"; 24 | import { SceneViewer } from "../SceneViewer"; 25 | import { Base3DLayer } from "./Base3DLayer"; 26 | 27 | class DDD3DLayer extends Base3DLayer { 28 | node: Node | null = null; 29 | 30 | constructor(key: string) { 31 | super(key); 32 | console.log("Constructing new DDD3DLayer."); 33 | // TODO: This makes sense here, but is also duplicated on SceneViewer 34 | } 35 | 36 | update(): void {} 37 | 38 | // TODO: Tile coordinates should be made a type or reuse OpenLayers grid coordinates type 39 | loadData(data: string): void { 40 | const objectBlob = new Blob([ data ], { type: "model/gltf-binary" }); 41 | const objectUrl = URL.createObjectURL(objectBlob); 42 | 43 | const layerKey = "model"; 44 | const pivot = new Mesh( 45 | "dddobject_" + layerKey, 46 | this.layerManager!.sceneViewer.scene, 47 | ); 48 | 49 | this.layerManager!.sceneViewer.queueLoader.enqueueLoadModel( 50 | objectUrl, 51 | // onSuccess 52 | (newMeshes: AbstractMesh[], _particleSystems: any, _skeletons: any) => { 53 | //console.log("DDD3DLayer GLB loaded", newMeshes); 54 | 55 | newMeshes.forEach((mesh: AbstractMesh, _i: number) => { 56 | if (this.layerManager!.sceneViewer.shadowGenerator) { 57 | mesh.receiveShadows = true; 58 | if ( 59 | mesh.metadata && 60 | mesh.metadata.gltf.extras && 61 | (mesh.metadata.gltf.extras["ddd:shadows"] === false || 62 | mesh.metadata.gltf.extras["ddd:shadows"] === "false" || 63 | mesh.metadata.gltf.extras["ddd:path"].indexOf("/Areas_") > 0 || 64 | mesh.metadata.gltf.extras["ddd:path"].indexOf("/Ways_") > 0) 65 | ) { 66 | //console.debug("No shadow"); 67 | return; 68 | } 69 | 70 | // TODO: Do this at SceneViewer processMesh level 71 | this.layerManager!.sceneViewer.shadowGenerator.getShadowMap()!.renderList!.push( 72 | mesh, 73 | ); 74 | } 75 | }); 76 | 77 | // Reparent root 78 | (newMeshes[0]).parent = pivot; 79 | this.node = pivot; 80 | 81 | pivot.freezeWorldMatrix(); 82 | 83 | this.layerManager!.sceneViewer.scene.blockfreeActiveMeshesAndRenderingGroups = 84 | true; 85 | this.layerManager!.sceneViewer.processMesh(pivot, pivot); // TODO: Wrong conversion, use Node for "processMesh" 86 | this.layerManager!.sceneViewer.scene.blockfreeActiveMeshesAndRenderingGroups = 87 | false; 88 | 89 | //pivot.occlusionType = AbstractMesh.OCCLUSION_TYPE_OPTIMISTIC; 90 | pivot.freezeWorldMatrix(); 91 | }, 92 | // onError 93 | (_scene: any, _msg: string, ex: any) => { 94 | // eslint-disable-next-line no-console 95 | console.log("Tile model (.glb) loading error: ", ex); 96 | }, 97 | ); 98 | //model.parent = pivot; 99 | } 100 | 101 | clearScene() { 102 | console.log("Deleting DDD3DLayer: " + this.key); 103 | if (this.node) { 104 | //tile.node.setEnabled(false); 105 | this.node.parent = null; 106 | this.node.dispose(); 107 | this.node = null; 108 | } 109 | } 110 | 111 | setVisible(visible: boolean) { 112 | super.setVisible(visible); 113 | if (this.node) this.node.setEnabled(this.visible); 114 | } 115 | } 116 | 117 | export { DDD3DLayer }; 118 | -------------------------------------------------------------------------------- /src/layers/GeoJson3DLayer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractMesh, 3 | Color3, 4 | Mesh, 5 | MeshBuilder, 6 | Node, 7 | Ray, 8 | StandardMaterial, 9 | Texture, 10 | TransformNode, 11 | Vector3, 12 | } from "@babylonjs/core"; 13 | import { Coordinate } from "ol/coordinate"; 14 | import * as extent from "ol/extent"; 15 | import * as olProj from "ol/proj"; 16 | import { createXYZ, extentFromProjection } from "ol/tilegrid"; 17 | import TileGrid from "ol/tilegrid/TileGrid"; 18 | import { SceneViewer } from "../SceneViewer"; 19 | import { Base3DLayer } from "./Base3DLayer"; 20 | 21 | class GeoJsonItem { 22 | properties: any; 23 | } 24 | 25 | class GeoJsonPoint extends GeoJsonItem { 26 | coordsWgs84: Vector3 = Vector3.Zero(); 27 | coordsScene: Vector3 = Vector3.Zero(); 28 | 29 | constructor(coordsWgs84: Vector3) { 30 | super(); 31 | this.coordsWgs84 = coordsWgs84; 32 | } 33 | 34 | /** 35 | * Currently receives a viewer for coordinate transformations 36 | */ 37 | transformCoords(viewer: SceneViewer) { 38 | const csa = viewer.wgs84ToScene(this.coordsWgs84.asArray()); 39 | this.coordsScene = Vector3.FromArray(csa); 40 | } 41 | } 42 | 43 | class GeoJsonLine extends GeoJsonItem { 44 | // TODO: These should be buffers 45 | coordsWgs84: Vector3[] = []; 46 | coordsScene: Vector3[] = []; 47 | 48 | constructor(coordsWgs84: Vector3[]) { 49 | super(); 50 | this.coordsWgs84 = coordsWgs84; 51 | } 52 | 53 | /** 54 | * Currently receives a viewer for coordinate transformations 55 | */ 56 | transformCoords(viewer: SceneViewer) { 57 | this.coordsScene = []; 58 | for (const v of this.coordsWgs84) { 59 | const csa = viewer.wgs84ToScene(v.asArray()); 60 | this.coordsScene.push(Vector3.FromArray(csa)); 61 | } 62 | } 63 | } 64 | 65 | class GeoJson3DLayer extends Base3DLayer { 66 | featuresPoints: GeoJsonPoint[] = []; 67 | featuresLines: GeoJsonLine[] = []; 68 | 69 | altitudeOffset: number = 50; 70 | colorHex: string = "#ff00ff"; 71 | 72 | //private sceneNodes: Mesh[] = []; 73 | private rootNode: TransformNode | null = null; 74 | 75 | private featureMaterial: StandardMaterial | null = null; 76 | 77 | constructor(key: string, geojsonData: any) { 78 | super(key); 79 | setTimeout(() => { 80 | this.loadFromGeoJson(geojsonData); 81 | this.projectFeatures(); 82 | this.updateSceneFromFeatures(); 83 | }, 0); 84 | } 85 | 86 | update(): void {} 87 | 88 | setColor(colorHex: string) { 89 | this.colorHex = colorHex; 90 | 91 | if (this.featureMaterial) { 92 | const color = Color3.FromHexString(colorHex); 93 | //this.featureMaterial.unfreeze(); 94 | this.featureMaterial.diffuseColor = color; 95 | this.featureMaterial.emissiveColor = color; 96 | this.featureMaterial.disableLighting = true; 97 | //this.featureMaterial.freeze(); 98 | } 99 | } 100 | 101 | setVisible(visible: boolean) { 102 | super.setVisible(visible); 103 | if (this.rootNode) this.rootNode.setEnabled(this.visible); 104 | /* 105 | for (let node of this.sceneNodes) { 106 | node.setEnabled(this.visible); 107 | } 108 | */ 109 | } 110 | 111 | setAltitudeOffset(altitudeOffset: number) { 112 | this.altitudeOffset = altitudeOffset; 113 | if (this.rootNode) { 114 | this.rootNode.position.y = this.altitudeOffset; // Apply offset 115 | } 116 | } 117 | 118 | /** 119 | * Processes GeoJSON data (already loaded as Javascript objects) and loads the different features. 120 | */ 121 | loadFromGeoJson(data: any): void { 122 | for (const feature of data["features"]) { 123 | this.loadFeature(feature); 124 | } 125 | } 126 | 127 | loadFeature(feature: any): void { 128 | const properties = feature["properties"]; 129 | const geometry = feature["geometry"]; 130 | 131 | if (geometry["type"] == "Point") { 132 | const lat = geometry["coordinates"][0]; 133 | const lon = geometry["coordinates"][1]; 134 | const alt = 135 | geometry["coordinates"].length > 2 ? geometry["coordinates"][2] : 0; 136 | const geojsonItem = new GeoJsonPoint(new Vector3(lat, lon, alt)); 137 | geojsonItem.properties = properties; 138 | 139 | this.featuresPoints.push(geojsonItem); 140 | } else if (geometry["type"] == "LineString") { 141 | const coords: Vector3[] = []; 142 | for (const coord of geometry["coordinates"]) { 143 | const lat = coord[0]; 144 | const lon = coord[1]; 145 | const alt = coord.length > 2 ? coord[2] : 0; 146 | const v = new Vector3(lat, lon, alt); 147 | coords.push(v); 148 | } 149 | const geojsonItem = new GeoJsonLine(coords); 150 | geojsonItem.properties = properties; 151 | 152 | this.featuresLines.push(geojsonItem); 153 | } else { 154 | console.info("Unknown GeoJSON geometry type: " + geometry["type"]); 155 | } 156 | } 157 | 158 | projectFeatures(): void { 159 | for (const feature of this.featuresPoints) { 160 | feature.transformCoords(this.layerManager!.sceneViewer); 161 | } 162 | for (const feature of this.featuresLines) { 163 | feature.transformCoords(this.layerManager!.sceneViewer); 164 | } 165 | } 166 | 167 | /** 168 | * TODO: This should be a more generic "markers" and "lines" vector visualization facility. 169 | */ 170 | updateSceneFromFeatures(): void { 171 | const sceneViewer = this.layerManager!.sceneViewer; 172 | 173 | // Create material only if it hasn't already been created 174 | if (!this.featureMaterial) { 175 | const featureMaterial = new StandardMaterial( 176 | "featureMaterial", 177 | sceneViewer.scene, 178 | ); 179 | this.featureMaterial = featureMaterial; 180 | } 181 | 182 | if (!this.rootNode) { 183 | this.rootNode = new TransformNode( 184 | "geoJson3DLayer-root", 185 | sceneViewer.scene, 186 | ); 187 | } 188 | 189 | this.setColor(this.colorHex); 190 | 191 | for (const feature of this.featuresPoints) { 192 | const marker = MeshBuilder.CreateSphere( 193 | "pointMarker", 194 | { diameter: 1.5, segments: 3 }, 195 | sceneViewer.scene, 196 | ); 197 | marker.material = this.featureMaterial; 198 | marker.position = feature.coordsScene; 199 | marker.parent = this.rootNode; 200 | //sceneNodes.push(marker); 201 | } 202 | 203 | for (const feature of this.featuresLines) { 204 | const marker = MeshBuilder.CreateLines( 205 | "lineMarker", 206 | { points: feature.coordsScene }, 207 | sceneViewer.scene, 208 | ); 209 | marker.material = this.featureMaterial; 210 | marker.parent = this.rootNode; 211 | //this.sceneNodes.push(marker); 212 | } 213 | 214 | this.setAltitudeOffset(this.altitudeOffset); 215 | } 216 | 217 | clearScene() { 218 | if (this.rootNode) { 219 | this.rootNode.parent = null; 220 | this.rootNode.dispose(); 221 | this.rootNode = null; 222 | } 223 | } 224 | } 225 | 226 | export { GeoJson3DLayer }; 227 | -------------------------------------------------------------------------------- /src/layers/GeoTile3DLayer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { 8 | AbstractMesh, 9 | Color3, 10 | Mesh, 11 | MeshBuilder, 12 | Node, 13 | Ray, 14 | StandardMaterial, 15 | Texture, 16 | TransformNode, 17 | Vector3, 18 | } from "@babylonjs/core"; 19 | import { Coordinate } from "ol/coordinate"; 20 | import * as extent from "ol/extent"; 21 | import * as olProj from "ol/proj"; 22 | import { createXYZ, extentFromProjection } from "ol/tilegrid"; 23 | import TileGrid from "ol/tilegrid/TileGrid"; 24 | import { SceneViewer } from "../SceneViewer"; 25 | import { Base3DLayer } from "./Base3DLayer"; 26 | 27 | class Tile3D { 28 | key: string; 29 | status: string | null; 30 | node: Node | null; 31 | 32 | constructor(key: string) { 33 | this.key = key; 34 | this.status = null; 35 | this.node = null; 36 | } 37 | } 38 | 39 | class GeoTile3D extends Tile3D { 40 | coordsTileGrid: number[] | null; 41 | 42 | constructor(key: string) { 43 | super(key); 44 | this.coordsTileGrid = null; 45 | } 46 | } 47 | 48 | class GeoTile3DLayer extends Base3DLayer { 49 | tiles: { [key: string]: GeoTile3D }; 50 | 51 | groundTextureLayerUrl: string | null = null; 52 | //groundTextureLayerUrl = "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"; // "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"; 53 | //groundTextureLayerUrl = "http://localhost:8090/wmts/ign_ortho/GLOBAL_WEBMERCATOR/{z}/{x}/{y}.jpeg"; // "https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"; 54 | 55 | private _lastHeight = 0; // Used to hack positioning of tiles before height is known 56 | private _lastLoadDynamic = 0; 57 | private _initialHeightSet = false; 58 | 59 | tilesLoadedCount = 0; 60 | tileGrid: TileGrid; 61 | 62 | constructor() { 63 | super("ddd-osm-3d"); // FIXME: key is hardcoded 64 | this.tiles = {}; 65 | // TODO: This makes sense here, but is also duplicated on SceneViewer 66 | this.tileGrid = createXYZ({ 67 | extent: extentFromProjection("EPSG:3857"), 68 | //maxResolution: options.maxResolution, 69 | //maxZoom: options.maxZoom, 70 | //minZoom: options.minZoom, 71 | //tileSize: options.tileSize, 72 | }); 73 | } 74 | 75 | update(): void { 76 | this.updateTilesDynamic(); 77 | } 78 | 79 | /* 80 | * From: https://bartwronski.com/2017/04/13/cull-that-cone/ 81 | */ 82 | testConeSphere( 83 | origin: Vector3, 84 | forward: Vector3, 85 | size: number, 86 | angle: number, 87 | sphereCenter: Vector3, 88 | sphereRadius: number, 89 | ): boolean { 90 | //console.debug(origin, forward, size, angle, sphereCenter, sphereRadius); 91 | const V = sphereCenter.subtract(origin); 92 | const VlenSq = Vector3.Dot(V, V); 93 | const V1len = Vector3.Dot(V, forward); 94 | const distanceClosestPoint = 95 | Math.cos(angle) * Math.sqrt(VlenSq - V1len * V1len) - 96 | V1len * Math.sin(angle); 97 | 98 | const angleCull = distanceClosestPoint > sphereRadius; 99 | const frontCull = V1len > sphereRadius + size; 100 | const backCull = V1len < -sphereRadius; 101 | 102 | return !(angleCull || frontCull || backCull); 103 | } 104 | 105 | updateTilesDynamic(): void { 106 | // loading chunks each 100 frames. Bad performance 107 | this._lastLoadDynamic -= 1; 108 | if (this._lastLoadDynamic > 0) { 109 | return; 110 | } 111 | this._lastLoadDynamic = 1; 112 | 113 | const sceneViewer: SceneViewer = this.layerManager!.sceneViewer; 114 | 115 | const positionWGS84: number[] = ( 116 | this.layerManager?.sceneViewer.positionWGS84() 117 | ); 118 | const coordsWGS84: Coordinate = [ positionWGS84[0], positionWGS84[1] ]; 119 | const coordsUtm: Coordinate = olProj.transform( 120 | coordsWGS84, 121 | "EPSG:4326", 122 | "EPSG:3857", 123 | ); 124 | const tileCoords = this.tileGrid.getTileCoordForCoordAndZ(coordsUtm, 17); 125 | //const tileKey = tileCoords[0] + "/" + tileCoords[1] + "/" + tileCoords[2]; 126 | 127 | // Calculate frustrum (2D) 128 | const frustrumOrigin = sceneViewer.camera!.position.clone(); 129 | //if (this._lastHeight) { frustrumOrigin.y -= this._lastHeight; } // Considers all tiles in a plane centered on last 130 | frustrumOrigin.y = 0; 131 | const frustrumForward = sceneViewer.camera!.getDirection(Vector3.Forward()); 132 | frustrumForward.y = 0; 133 | frustrumForward.normalize(); 134 | const frustrumSize = sceneViewer.viewerState.sceneTileDrawDistance * 300.0; // 1500.0; 135 | const frustrumAngle = sceneViewer.camera!.fov * 2.0; // * (Math.PI / 180.0); // 30.0; 136 | 137 | this.loadTile(tileCoords); // ensure elevation for current tile 138 | 139 | // Project frustrum corners to tiles 140 | 141 | // Calculate tiles inside frustrum 142 | const tiledistWalk = sceneViewer.viewerState.sceneTileDrawDistance + 3; 143 | const tiledistDraw = sceneViewer.viewerState.sceneTileDrawDistance + 0.7; 144 | for (let i = -tiledistWalk; i <= tiledistWalk; i++) { 145 | for (let j = -tiledistWalk; j <= tiledistWalk; j++) { 146 | // Current tile is already enqueued 147 | if (i === 0 && j === 0) { 148 | continue; 149 | } 150 | 151 | if (i * i + j * j > tiledistDraw * tiledistDraw) { 152 | this.disableTile([ 153 | tileCoords[0], 154 | tileCoords[1] + i, 155 | tileCoords[2] + j, 156 | ]); 157 | } else { 158 | const tileCenter = this.tileGrid.getTileCoordCenter([ 159 | tileCoords[0], 160 | tileCoords[1] + i, 161 | tileCoords[2] + j, 162 | ]); 163 | const tileCenterWGS84 = olProj.transform( 164 | tileCenter, 165 | "EPSG:3857", 166 | "EPSG:4326", 167 | ); 168 | const tileCenterScene = 169 | sceneViewer.projection.forward(tileCenterWGS84); 170 | const sphereCenter = new Vector3( 171 | tileCenterScene[0], 172 | 0, 173 | tileCenterScene[1], 174 | ); // TODO: Get median height from tile 175 | const sphereRadius = 230.0 / 2.0; 176 | if ( 177 | this.testConeSphere( 178 | frustrumOrigin, 179 | frustrumForward, 180 | frustrumSize, 181 | frustrumAngle, 182 | sphereCenter, 183 | sphereRadius, 184 | ) 185 | ) { 186 | //console.debug("Loading: ", [tileCoords[0], tileCoords[1] + i, tileCoords[2] + j]) 187 | this.loadTile([ 188 | tileCoords[0], 189 | tileCoords[1] + i, 190 | tileCoords[2] + j, 191 | ]); 192 | } else { 193 | //console.debug("Ignoring: ", [tileCoords[0], tileCoords[1] + i, tileCoords[2] + j]) 194 | this.disableTile([ 195 | tileCoords[0], 196 | tileCoords[1] + i, 197 | tileCoords[2] + j, 198 | ]); 199 | } 200 | } 201 | } 202 | } 203 | 204 | // Sort tiles by distance 205 | 206 | // Enqueue (1 on mobile? 2 on PC?) 207 | 208 | // setEnabled(false) on culled chunks 209 | 210 | // update LOD levels (call lodLevel - remove items, etc) per distance 211 | 212 | /* 213 | for (let i = -1; i <= 1; i++) { 214 | for (let j = -1; j <= 1; j++) { 215 | this.loadTile([tileCoords[0], tileCoords[1] + i, tileCoords[2] + j]); 216 | } 217 | } 218 | */ 219 | } 220 | 221 | disableTile(tileCoords: number[]): void { 222 | const z = tileCoords[0]; 223 | const x = tileCoords[1]; 224 | const y = tileCoords[2]; 225 | const tileKey = z + "/" + x + "/" + y; 226 | 227 | if (!(tileKey in this.tiles)) { 228 | return; 229 | } 230 | 231 | const tile = this.tiles[tileKey]; 232 | if (tile.status !== "loading" && tile.node!.isEnabled(false)) { 233 | tile.node!.setEnabled(false); 234 | tile.node!.parent = null; // TODO: this was not working before (tile.parent did not apply) 235 | } 236 | } 237 | 238 | /** 239 | * Gets tile metadata. 240 | * It does this recursively searching for a "Metadata" named node, as the path exporting root metadata to the root node or scene itself hasn't been found to work. 241 | */ 242 | getTileMetadata(node: Node): any { 243 | /*if (node.id.startsWith("Metadata")) { 244 | return node.metadata.gltf.extras; 245 | }*/ 246 | for (const child of node.getChildren()) { 247 | // Must start with Metadata 248 | if (child.id.indexOf("Metadata") == 0) { 249 | return child.metadata.gltf.extras; 250 | } 251 | } 252 | for (const child of node.getChildren()) { 253 | const md = this.getTileMetadata(child); 254 | if (md !== null) { 255 | return md; 256 | } 257 | } 258 | return null; 259 | } 260 | 261 | // TODO: Tile coordinates should be made a type or reuse OpenLayers grid coordinates type 262 | loadTile(tileCoords: number[]): void { 263 | //console.debug(tileCoords); 264 | const z = tileCoords[0]; 265 | const x = tileCoords[1]; 266 | const y = tileCoords[2]; 267 | const tileKey = z + "/" + x + "/" + y; 268 | 269 | const tileExtent = this.tileGrid.getTileCoordExtent(tileCoords); 270 | const tileCenter = extent.getCenter(tileExtent); 271 | const tileCenterWGS84 = olProj.transform( 272 | tileCenter, 273 | "EPSG:3857", 274 | "EPSG:4326", 275 | ); 276 | //const tileCenterScene = this.layerManager!.sceneViewer.projection.forward( tileCenterWGS84 ); 277 | 278 | const tileExtentMinScene = 279 | this.layerManager!.sceneViewer.projection.forward( 280 | olProj.transform( 281 | extent.getBottomLeft(tileExtent), 282 | "EPSG:3857", 283 | "EPSG:4326", 284 | ), 285 | ); 286 | const tileExtentMaxScene = 287 | this.layerManager!.sceneViewer.projection.forward( 288 | olProj.transform( 289 | extent.getTopRight(tileExtent), 290 | "EPSG:3857", 291 | "EPSG:4326", 292 | ), 293 | ); 294 | const sizeWidth = Math.abs(tileExtentMaxScene[0] - tileExtentMinScene[0]); 295 | const sizeHeight = Math.abs(tileExtentMaxScene[1] - tileExtentMinScene[1]); 296 | 297 | if (tileKey in this.tiles) { 298 | const tile = this.tiles[tileKey]; 299 | if (tile.status !== "loading" && !tile.node!.isEnabled(false)) { 300 | tile.node!.parent = null; // this.layerManager!.sceneViewer.scene; 301 | tile.node!.setEnabled(true); 302 | //tile.node.freezeWorldMatrix(); 303 | } 304 | return; 305 | } else { 306 | this.tiles[tileKey] = new GeoTile3D(tileKey); 307 | this.tiles[tileKey].status = "loading"; 308 | this.tiles[tileKey].coordsTileGrid = tileCoords; 309 | } 310 | 311 | //const tileUrlBase = './scenes/ddd_http_'; 312 | //const tileUrlBase = 'http://localhost:8000/cache/ddd_http/'; 313 | //const tileUrlBase = 'http://' + app.dddConfig.tileUrlBase + ':8000/cache/ddd_http/'; 314 | //const tileUrlBase = 'http://' + location.hostname + '/cache/ddd_http/'; 315 | const tileUrlBase = 316 | this.layerManager!.sceneViewer.viewerState.dddConfig.tileUrlBase; 317 | const tileUrlSuffix = 318 | this.layerManager!.sceneViewer.viewerState.dddConfig.tileUrlSuffix; // e.g. ".uncompressed" 319 | 320 | //const tileUrl = tileUrlBase + z + "/" + x + "/" + y + ".glb"; 321 | const tileUrl = 322 | tileUrlBase + z + "/" + x + "/" + y + tileUrlSuffix + ".glb"; 323 | 324 | //console.debug("Loading: " + tileUrl); 325 | 326 | //const pivot = new TransformNode( "chunk_" + tileKey.replaceAll( "/", "_" ), this.layerManager!.sceneViewer.scene ); // new Mesh("chunk_" + tileKey, this.layerManager.sceneViewer.scene); 327 | const pivot = new Mesh( 328 | "chunk_" + tileKey.replaceAll("/", "_"), 329 | this.layerManager!.sceneViewer.scene, 330 | ); // new Mesh("chunk_" + tileKey, this.layerManager.sceneViewer.scene); 331 | 332 | //let reversePivot = new TransformNode("chunk_reverse_" + tileKey, this.scene); // new Mesh("chunk_" + tileKey, this.scene); 333 | //let rawPivot = new TransformNode("chunk_raw_" + tileKey, this.scene); // new Mesh("chunk_" + tileKey, this.scene); 334 | //reversePivot.scaling = new Vector3(1, 1, -1); // Babylon uses a parent node with this scale to flip glTF models, redone here 335 | //rawPivot.parent = reversePivot; 336 | //reversePivot.parent = pivot; 337 | //pivot.parent = this.scene; 338 | 339 | let marker = this.loadQuadMarker(tileCoords, Color3.Gray()); 340 | this.tiles[tileKey].node = marker; 341 | 342 | this.layerManager!.sceneViewer.queueLoader.enqueueLoadModel( 343 | tileUrl, 344 | // onSuccess 345 | (newMeshes: AbstractMesh[], _particleSystems: any, _skeletons: any) => { 346 | // Ensure tile still exists: 347 | if (!(tileKey in this.tiles)) { 348 | newMeshes[0].parent = null; 349 | newMeshes[0].dispose(); 350 | return; 351 | } 352 | 353 | //console.log("GLB loaded", newMeshes); 354 | 355 | marker.dispose(false, true); 356 | //marker.parent = null; 357 | 358 | let minHeight = Number.POSITIVE_INFINITY; 359 | let maxHeight = Number.NEGATIVE_INFINITY; 360 | newMeshes.forEach((mesh: AbstractMesh, _i: number) => { 361 | if (this.layerManager!.sceneViewer.shadowGenerator) { 362 | mesh.receiveShadows = true; 363 | if ( 364 | mesh.metadata && 365 | mesh.metadata.gltf.extras && 366 | (mesh.metadata.gltf.extras["ddd:shadows"] === false || 367 | mesh.metadata.gltf.extras["ddd:shadows"] === "false" || 368 | mesh.metadata.gltf.extras["ddd:path"].indexOf("/Areas_") > 0 || 369 | mesh.metadata.gltf.extras["ddd:path"].indexOf("/Ways_") > 0) 370 | ) { 371 | //console.debug("No shadow"); 372 | return; 373 | } 374 | 375 | // TODO: Do this at SceneViewer processMesh level 376 | this.layerManager!.sceneViewer.shadowGenerator.getShadowMap()!.renderList!.push( 377 | mesh, 378 | ); 379 | } 380 | 381 | //console.debug(mesh.getBoundingInfo()); 382 | const heightMin = mesh.getBoundingInfo().boundingBox.minimumWorld.y; 383 | if (heightMin < minHeight && heightMin !== 0) { 384 | minHeight = heightMin; 385 | } 386 | const heightMax = mesh.getBoundingInfo().boundingBox.maximumWorld.y; 387 | if (heightMax > maxHeight && heightMax !== 0) { 388 | maxHeight = heightMax; 389 | } 390 | 391 | /* 392 | if(mesh.material) { 393 | if (mesh.id.indexOf("Item") < 0 && mesh.id.indexOf("Building") < 0) { 394 | mesh.material = materialPlane; 395 | } 396 | //mesh.overrideMaterialSideOrientation = Mesh.DOUBLESIDE; 397 | //mesh.updateMeshPositions(); 398 | } 399 | */ 400 | //console.debug(mesh.absolutePosition); 401 | //mesh.position = new Vector3(mesh.position.x, mesh.position.y, -mesh.position.z); 402 | //mesh.updateMeshPositions(); 403 | //mesh.parent = rawPivot; 404 | }); 405 | 406 | // Reparent root 407 | (newMeshes[0]).parent = pivot; 408 | newMeshes[0].id = tileKey.replaceAll("/", "_"); 409 | this.tiles[tileKey].node = pivot; 410 | this.tiles[tileKey].status = "loaded"; 411 | 412 | const tileExtent = this.tileGrid.getTileCoordExtent(tileCoords); 413 | const tileCenter = extent.getCenter(tileExtent); 414 | const tileCenterWGS84 = olProj.transform( 415 | tileCenter, 416 | "EPSG:3857", 417 | "EPSG:4326", 418 | ); 419 | const tileCenterScene = 420 | this.layerManager!.sceneViewer.projection.forward(tileCenterWGS84); 421 | 422 | //let distance = 225.0; 423 | //pivot.position = new Vector3((x - 62360) * distance, 0, -(y - 48539) * distance); 424 | //pivot.scaling = new Vector3(1, 1, -1); 425 | pivot.position = new Vector3(tileCenterScene[0], 0, tileCenterScene[1]); 426 | pivot.rotation = new Vector3(0, Math.PI, 0); 427 | 428 | pivot.freezeWorldMatrix(); 429 | 430 | this.tiles[tileKey].node = pivot; 431 | 432 | this._lastHeight = minHeight; 433 | 434 | this.tilesLoadedCount++; 435 | if (!this._initialHeightSet) { 436 | //console.debug("Repositioning camera height based on terrain height: " + maxHeight); 437 | //that.layerManager.sceneViewer.camera.position.y += maxHeight; 438 | 439 | const ray = new Ray( 440 | new Vector3( 441 | this.layerManager!.sceneViewer.camera!.position.x, 442 | -100.0, 443 | this.layerManager!.sceneViewer.camera!.position.z, 444 | ), 445 | new Vector3(0, 1, 0), 446 | 3000.0, 447 | ); 448 | const pickResult = 449 | this.layerManager!.sceneViewer.scene.pickWithRay(ray); 450 | if ( 451 | pickResult && 452 | pickResult.pickedMesh && 453 | pickResult.pickedMesh.id && 454 | pickResult.pickedMesh.id.indexOf("placeholder_") !== 0 && 455 | pickResult.pickedMesh.id.indexOf("skyBox") !== 0 456 | ) { 457 | //console.debug("Setting height from: " + pickResult.pickedMesh.id); 458 | this._initialHeightSet = true; 459 | this.layerManager!.sceneViewer.camera!.position.y = 460 | pickResult.distance - 100.0; 461 | if ( 462 | this.layerManager!.sceneViewer.viewerState.positionGroundHeight 463 | ) { 464 | this.layerManager!.sceneViewer.camera!.position.y += 465 | this.layerManager!.sceneViewer.viewerState.positionGroundHeight; 466 | } else { 467 | this.layerManager!.sceneViewer.camera!.position.y += 40.0; 468 | } 469 | } else { 470 | //that._tilesLoadedCount--; 471 | //that.layerManager.sceneViewer.camera.position.y += maxHeight; 472 | } 473 | } 474 | 475 | const tileMetadata = this.getTileMetadata(pivot); 476 | //console.debug("Tile metadata: ", tileMetadata); 477 | 478 | // Replace materials, instancing... 479 | pivot.metadata = { 480 | tileCoords: tileCoords, 481 | tileSize: [ sizeWidth, sizeHeight ], 482 | tileInfo: tileMetadata, 483 | }; 484 | 485 | this.layerManager!.sceneViewer.scene.blockfreeActiveMeshesAndRenderingGroups = 486 | true; 487 | this.layerManager!.sceneViewer.processMesh(pivot, pivot); // TODO: Wrong conversion, use Node for "processMesh" 488 | this.layerManager!.sceneViewer.scene.blockfreeActiveMeshesAndRenderingGroups = 489 | false; 490 | 491 | //pivot.occlusionType = AbstractMesh.OCCLUSION_TYPE_OPTIMISTIC; 492 | pivot.freezeWorldMatrix(); 493 | 494 | // TODO: Removed during TS migration, but this is needed to support ground texture replacement 495 | //this.groundTextureLayerProcessNode( tileCoords, pivot ); 496 | 497 | // Check if the selected node is in the recently loaded node 498 | // TODO: Should use a generic notification + object id/naming system 499 | if (this.layerManager!.sceneViewer.viewerState.sceneSelectedMeshId) { 500 | const criteria = { 501 | _node_name: 502 | this.layerManager!.sceneViewer.viewerState.sceneSelectedMeshId, 503 | }; 504 | //console.debug(criteria); 505 | const foundMesh = this.layerManager!.sceneViewer.findNode( 506 | pivot, 507 | criteria, 508 | ); 509 | //console.debug(foundMesh); 510 | if (foundMesh) { 511 | this.layerManager!.sceneViewer.selectMesh(foundMesh, true); 512 | this.layerManager!.sceneViewer.viewerState.sceneSelectedMeshId = 513 | null; // Triggers watchers update 514 | } 515 | } 516 | 517 | /* 518 | this.sceneViewer.selectMesh(pickResult.pickedMesh); 519 | let meshName = pickResult.pickedMesh.id.split("/").pop().replaceAll('#', '_'); // .replaceAll("_", " "); 520 | this.$router.push('/3d/item/' + meshName + '/' + this.sceneViewer.positionString()).catch(()=>{}); 521 | */ 522 | }, 523 | // onError 524 | (_scene: any, _msg: string, ex: any) => { 525 | // eslint-disable-next-line no-console 526 | 527 | //console.debug( "Tile model (.glb) loading error: ", ex ); 528 | 529 | const request = ex.innerError.request; 530 | 531 | if (request != null && request.status === 404) { 532 | // 404 - tile is being generated, show OSM tile as replacement 533 | //console.debug(ex.request); 534 | 535 | marker.dispose(false, true); 536 | marker = this.loadQuadTile(tileCoords); // , Color3.Red() 537 | this.tiles[tileKey].node = marker; // "notfound"; 538 | this.tiles[tileKey].status = "notfound"; 539 | 540 | // Process response, DDD server includes JSON info about tile generation status 541 | try { 542 | const dataView = new DataView(request.response); 543 | // TextDecoder interface is documented at http://encoding.spec.whatwg.org/#interface-textdecoder 544 | const decoder = new TextDecoder("utf-8"); 545 | const decodedString = decoder.decode(dataView); 546 | const responseData = JSON.parse(decodedString); 547 | console.debug(responseData); 548 | this.layerManager!.sceneViewer.viewerState.remoteQueueJobsStatus.push( 549 | responseData, 550 | ); 551 | } catch (e) { 552 | console.error("Could not add queued job to viewer state: " + e); 553 | } 554 | } else { 555 | // Error: colour marker red 556 | console.debug("Tile model (.glb) loading error (not 404): ", ex); 557 | 558 | marker.dispose(false, true); 559 | marker = this.loadQuadTile(tileCoords); // , Color3.Red() 560 | this.tiles[tileKey].node = marker; // "notfound"; 561 | this.tiles[tileKey].status = "error"; 562 | 563 | const color = Color3.Red(); 564 | ((marker).material).emissiveColor = color; 565 | } 566 | }, 567 | ); 568 | //model.parent = pivot; 569 | } 570 | 571 | loadQuadMarker(tileCoords: number[], color: Color3 = Color3.Gray()): Node { 572 | const z = tileCoords[0]; 573 | const x = tileCoords[1]; 574 | const y = tileCoords[2]; 575 | const tileKey = z + "/" + x + "/" + y; 576 | 577 | const tileExtent = this.tileGrid.getTileCoordExtent(tileCoords); 578 | const tileCenter = extent.getCenter(tileExtent); 579 | const tileCenterWGS84 = olProj.transform( 580 | tileCenter, 581 | "EPSG:3857", 582 | "EPSG:4326", 583 | ); 584 | const tileCenterScene = 585 | this.layerManager!.sceneViewer.projection.forward(tileCenterWGS84); 586 | 587 | const tileExtentMinScene = 588 | this.layerManager!.sceneViewer.projection.forward( 589 | olProj.transform( 590 | extent.getBottomLeft(tileExtent), 591 | "EPSG:3857", 592 | "EPSG:4326", 593 | ), 594 | ); 595 | const tileExtentMaxScene = 596 | this.layerManager!.sceneViewer.projection.forward( 597 | olProj.transform( 598 | extent.getTopRight(tileExtent), 599 | "EPSG:3857", 600 | "EPSG:4326", 601 | ), 602 | ); 603 | const sizeWidth = Math.abs(tileExtentMaxScene[0] - tileExtentMinScene[0]); 604 | const sizeHeight = Math.abs(tileExtentMaxScene[1] - tileExtentMinScene[1]); 605 | 606 | const markerName = "Tile " + tileKey; // "placeholder_" + tileKey 607 | const marker = MeshBuilder.CreatePlane( 608 | "placeholder_" + tileKey, 609 | { 610 | width: sizeWidth, 611 | height: sizeHeight, 612 | sideOrientation: Mesh.DOUBLESIDE, 613 | }, 614 | this.layerManager!.sceneViewer.scene, 615 | ); 616 | marker.metadata = { "ddd:title": markerName }; 617 | 618 | marker.position = new Vector3( 619 | tileCenterScene[0], 620 | this._lastHeight, 621 | tileCenterScene[1], 622 | ); 623 | marker.rotation = new Vector3(Math.PI * 0.5, 0, 0); 624 | 625 | //Creation of a repeated textured material 626 | const materialPlane = new StandardMaterial( 627 | "textureTile_" + tileKey, 628 | this.layerManager!.sceneViewer.scene, 629 | ); 630 | //materialPlane.diffuseTexture = new Texture("https://a.tile.openstreetmap.org/" + z + "/" + x + "/" + y + ".png", this.scene); 631 | materialPlane.diffuseColor = color; 632 | materialPlane.specularColor = Color3.Black(); 633 | /* 634 | materialPlane.diffuseTexture.uScale = 1.0 / 225.0; 635 | materialPlane.diffuseTexture.vScale = -1.0 / 225.0; 636 | materialPlane.diffuseTexture.uOffset = -0.5; 637 | materialPlane.diffuseTexture.vOffset = -0.5; 638 | */ 639 | materialPlane.emissiveColor = color; // new Color3(1.0, 1.0, 1.); 640 | materialPlane.disableLighting = true; 641 | materialPlane.backFaceCulling = false; 642 | 643 | marker.material = materialPlane; 644 | 645 | return marker; 646 | } 647 | 648 | loadQuadTile(tileCoords: number[], color = Color3.White()): Node { 649 | const z = tileCoords[0]; 650 | const x = tileCoords[1]; 651 | const y = tileCoords[2]; 652 | const tileKey = z + "/" + x + "/" + y; 653 | 654 | const tileExtent = this.tileGrid.getTileCoordExtent(tileCoords); 655 | const tileCenter = extent.getCenter(tileExtent); 656 | const tileCenterWGS84 = olProj.transform( 657 | tileCenter, 658 | "EPSG:3857", 659 | "EPSG:4326", 660 | ); 661 | const tileCenterScene = 662 | this.layerManager!.sceneViewer.projection.forward(tileCenterWGS84); 663 | 664 | const tileExtentMinScene = 665 | this.layerManager!.sceneViewer.projection.forward( 666 | olProj.transform( 667 | extent.getBottomLeft(tileExtent), 668 | "EPSG:3857", 669 | "EPSG:4326", 670 | ), 671 | ); 672 | const tileExtentMaxScene = 673 | this.layerManager!.sceneViewer.projection.forward( 674 | olProj.transform( 675 | extent.getTopRight(tileExtent), 676 | "EPSG:3857", 677 | "EPSG:4326", 678 | ), 679 | ); 680 | const sizeWidth = Math.abs(tileExtentMaxScene[0] - tileExtentMinScene[0]); 681 | const sizeHeight = Math.abs(tileExtentMaxScene[1] - tileExtentMinScene[1]); 682 | 683 | //console.debug(sizeWidth, sizeHeight); 684 | const markerName = "Tile " + tileKey; // "placeholder_" + tileKey 685 | const marker = MeshBuilder.CreatePlane( 686 | "placeholder_" + tileKey, 687 | { 688 | width: sizeWidth, 689 | height: sizeHeight, 690 | sideOrientation: Mesh.DOUBLESIDE, 691 | }, 692 | this.layerManager!.sceneViewer.scene, 693 | ); 694 | marker.metadata = { "ddd:title": markerName }; 695 | 696 | marker.position = new Vector3( 697 | tileCenterScene[0], 698 | this._lastHeight, 699 | tileCenterScene[1], 700 | ); 701 | marker.rotation = new Vector3(Math.PI * 0.5, 0, 0); 702 | 703 | //Creation of a repeated textured material 704 | const materialPlane = new StandardMaterial( 705 | "textureTile_" + tileKey, 706 | this.layerManager!.sceneViewer.scene, 707 | ); 708 | materialPlane.diffuseTexture = new Texture( 709 | "https://a.tile.openstreetmap.org/" + z + "/" + x + "/" + y + ".png", 710 | this.layerManager!.sceneViewer.scene, 711 | ); 712 | 713 | //if (!color) color = Color3.Black; //new Color3(0, 0, 0); 714 | materialPlane.specularColor = Color3.Black(); 715 | /* 716 | materialPlane.diffuseTexture.uScale = 1.0 / 225.0; 717 | materialPlane.diffuseTexture.vScale = -1.0 / 225.0; 718 | materialPlane.diffuseTexture.uOffset = -0.5; 719 | materialPlane.diffuseTexture.vOffset = -0.5; 720 | */ 721 | materialPlane.emissiveColor = color; // new Color3(1.0, 1.0, 1.); 722 | materialPlane.disableLighting = true; 723 | materialPlane.backFaceCulling = false; 724 | 725 | marker.material = materialPlane; 726 | 727 | return marker; 728 | } 729 | 730 | groundTextureLayerProcessNode(tile: GeoTile3D, node: Node): void { 731 | const tileCoords: number[] = tile.coordsTileGrid; 732 | 733 | let materialGround = null; 734 | 735 | if (this.groundTextureLayerUrl) { 736 | const z = tileCoords[0]; 737 | const x = tileCoords[1]; 738 | const y = tileCoords[2]; 739 | 740 | const tileExtent = this.tileGrid.getTileCoordExtent(tileCoords); 741 | const tileCenter = extent.getCenter(tileExtent); 742 | const tileCenterWGS84 = olProj.transform( 743 | tileCenter, 744 | "EPSG:3857", 745 | "EPSG:4326", 746 | ); 747 | const tileCenterScene = 748 | this.layerManager!.sceneViewer.projection.forward(tileCenterWGS84); 749 | 750 | const tileExtentMinScene = 751 | this.layerManager!.sceneViewer.projection.forward( 752 | olProj.transform( 753 | extent.getBottomLeft(tileExtent), 754 | "EPSG:3857", 755 | "EPSG:4326", 756 | ), 757 | ); 758 | const tileExtentMaxScene = 759 | this.layerManager!.sceneViewer.projection.forward( 760 | olProj.transform( 761 | extent.getTopRight(tileExtent), 762 | "EPSG:3857", 763 | "EPSG:4326", 764 | ), 765 | ); 766 | const sizeWidth = Math.abs(tileExtentMaxScene[0] - tileExtentMinScene[0]); 767 | const sizeHeight = Math.abs( 768 | tileExtentMaxScene[1] - tileExtentMinScene[1], 769 | ); 770 | 771 | // Create material 772 | //console.debug("Creating material for ground texture: " + url); 773 | const tileKey = tileCoords[0] + "/" + tileCoords[1] + "/" + tileCoords[2]; 774 | const url = this.replaceTileCoordsUrl( 775 | tileCoords, 776 | this.groundTextureLayerUrl, 777 | ); 778 | materialGround = new StandardMaterial( 779 | "materialGround_" + tileKey, 780 | this.layerManager!.sceneViewer.scene, 781 | ); 782 | materialGround.roughness = 1.0; 783 | //materialGround.reflectionFresnelParameters = new FresnelParameters(); 784 | materialGround.specularPower = 0.0; 785 | materialGround.diffuseColor = new Color3(0.2, 0.2, 0.2); // Color3.Black(); 786 | materialGround.ambientColor = new Color3(0.07, 0.07, 0.07); // Color3.Black(); 787 | materialGround.specularColor = new Color3(0.05, 0.05, 0.05); // Color3.Black(); 788 | materialGround.emissiveColor = new Color3(0.05, 0.05, 0.05); // Color3.White(); // new Color3(1.0, 1.0, 1.); 789 | //materialGround.disableLighting = true; 790 | //materialGround.backFaceCulling = false; 791 | const materialGroundTexture: Texture = new Texture( 792 | url, 793 | this.layerManager!.sceneViewer.scene, 794 | ); 795 | materialGround.diffuseTexture = materialGroundTexture; 796 | materialGround.ambientTexture = materialGroundTexture; 797 | //materialGround.specularTexture = materialGroundTexture; 798 | materialGround.emissiveTexture = materialGroundTexture; 799 | materialGround.linkEmissiveWithDiffuse = true; 800 | 801 | //materialGroundTexture.uScale = 4 * 1.0 / ( sizeWidth + 0 ); // Force small texture overlap to avoid texture repeating 802 | //materialGroundTexture.vScale = 4 * -1.0 / ( sizeHeight + 1 ); // Force small texture overlap to avoid texture repeating 803 | materialGroundTexture.uScale = ((4 * 1.0) / sizeWidth) * (127 / 128); // Force small texture overlap to avoid texture repeating 804 | materialGroundTexture.vScale = ((4 * -1.0) / sizeHeight) * (127 / 128); // Force small texture overlap to avoid texture repeating 805 | materialGroundTexture.uOffset = -0.5; 806 | materialGroundTexture.vOffset = -0.5; 807 | materialGroundTexture.wrapU = Texture.WRAP_ADDRESSMODE; 808 | materialGroundTexture.wrapV = Texture.WRAP_ADDRESSMODE; 809 | 810 | //materialGround.bumpTexture = materialGround.diffuseTexture; 811 | //materialGround.bumpTexture.uScale = 1.0 / sizeWidth; 812 | //materialGround.bumpTexture.vScale = 1.0 / sizeHeight; 813 | //materialGround.bumpTexture.uOffset = -0.5; 814 | //materialGround.bumpTexture.vOffset = -0.5; 815 | } 816 | 817 | // Assign 818 | const meshes = node.getChildMeshes(); 819 | for (const mesh of meshes) { 820 | if ( 821 | mesh && 822 | mesh.metadata && 823 | mesh.metadata.gltf && 824 | mesh.metadata.gltf.extras 825 | ) { 826 | const metadata = mesh.metadata.gltf.extras; 827 | if ( 828 | metadata["ddd:path"].indexOf("/Areas") > 0 || 829 | metadata["ddd:path"].indexOf("/Ways") > 0 830 | ) { 831 | if (materialGround !== null) { 832 | if (!("_ground_material_original" in mesh.metadata)) { 833 | mesh.metadata["_ground_material_original"] = mesh.material; 834 | } 835 | mesh.material = materialGround; 836 | } else { 837 | if (mesh.metadata["_ground_material_original"]) { 838 | mesh.material = mesh.metadata["_ground_material_original"]; 839 | } 840 | } 841 | } 842 | } 843 | } 844 | } 845 | 846 | groundTextureLayerSetUrl(url: string): void { 847 | // "https://a.tile.openstreetmap.org/" + z + "/" + x + "/" + y + ".png" 848 | //console.debug("Layer setting ground texture layer: " + url); 849 | this.groundTextureLayerUrl = url; 850 | 851 | // Update existing tiles 852 | for (const key in this.tiles) { 853 | const tile = this.tiles[key]; 854 | this.groundTextureLayerProcessNode(tile, tile.node); 855 | } 856 | } 857 | 858 | replaceTileCoordsUrl(tileCoords: number[], url: string): string { 859 | let result = url; 860 | result = result.replace("{z}", tileCoords[0].toString()); 861 | result = result.replace("{x}", tileCoords[1].toString()); 862 | result = result.replace("{y}", tileCoords[2].toString()); 863 | return result; 864 | } 865 | 866 | /* 867 | createTextMaterial( text: string ): StandardMaterial { 868 | 869 | //Create dynamic texture 870 | const texture = new DynamicTexture( "dynamicTexture_text_" + text , { width:512, height:256 }, this.layerManager!.sceneViewer.scene ); 871 | //var textureContext = texture.getContext(); 872 | const font = "bold 44px monospace"; 873 | texture.drawText( "Generating...\nPlease try again later (5+ min).", 75, 135, font, "green", "white", true, true ); 874 | 875 | const material = new StandardMaterial( "Mat" + text, this.layerManager!.sceneViewer.scene ); 876 | material.diffuseTexture = texture; 877 | 878 | return material; 879 | } 880 | */ 881 | 882 | disposeTile(tile: Tile3D) { 883 | console.debug("Disposing tile: " + tile.key); 884 | if (tile.node) { 885 | //tile.node.setEnabled(false); 886 | tile.node.parent = null; 887 | tile.node.dispose(); 888 | tile.node = null; 889 | } 890 | delete this.tiles[tile.key]; 891 | } 892 | 893 | clearScene() { 894 | for (const tileKey in this.tiles) { 895 | const tile = this.tiles[tileKey]; 896 | this.disposeTile(tile); 897 | } 898 | } 899 | } 900 | 901 | export { GeoTile3DLayer }; 902 | -------------------------------------------------------------------------------- /src/layers/LayerManager.ts: -------------------------------------------------------------------------------- 1 | import { SceneViewer } from "../SceneViewer"; 2 | import { Base3DLayer } from "./Base3DLayer"; 3 | import { GeoTile3DLayer } from "./GeoTile3DLayer"; 4 | 5 | 6 | /** 7 | * Manages the list of layers known to DDD Viewer. 8 | */ 9 | class LayerManager { 10 | 11 | sceneViewer: SceneViewer; 12 | layers: { [ key: string ]: Base3DLayer }; 13 | 14 | constructor( sceneViewer: SceneViewer ) { 15 | this.sceneViewer = sceneViewer; 16 | this.layers = {}; 17 | } 18 | 19 | update( deltaTime: number ): void { 20 | for (const key in this.layers) { 21 | // Load tiles dynamically as needed 22 | this.layers[key].update(deltaTime); 23 | } 24 | } 25 | 26 | /** 27 | * Adds a layer to DDD viewer. 28 | * @param key 29 | * @param layer 30 | */ 31 | addLayer(layer: Base3DLayer): void { 32 | layer.setViewer(this.sceneViewer); 33 | this.layers[layer.key] = layer; 34 | } 35 | 36 | removeLayer(layer: Base3DLayer) { 37 | layer.setViewer(null); // have the layer cleanup itself from the scene 38 | delete this.layers[layer.key]; 39 | } 40 | 41 | /** 42 | * Retrieves a layer from DDD viewer managed layers. 43 | * Returns null if the layer does not exist. 44 | */ 45 | getLayer(key: string) { 46 | if (key in this.layers) { 47 | return this.layers[key]; 48 | } 49 | return null; 50 | } 51 | 52 | } 53 | 54 | export { LayerManager }; 55 | -------------------------------------------------------------------------------- /src/layers/OverlayLayer.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbstractMesh, 3 | Color3, 4 | Mesh, 5 | MeshBuilder, 6 | Node, 7 | Ray, 8 | StandardMaterial, 9 | Texture, 10 | TransformNode, 11 | Vector3, 12 | } from "@babylonjs/core"; 13 | import { Coordinate } from "ol/coordinate"; 14 | import * as extent from "ol/extent"; 15 | import * as olProj from "ol/proj"; 16 | import { createXYZ, extentFromProjection } from "ol/tilegrid"; 17 | import TileGrid from "ol/tilegrid/TileGrid"; 18 | import { SceneViewer } from "../SceneViewer"; 19 | import { Base3DLayer } from "./Base3DLayer"; 20 | import { GeoJson3DLayer } from "./GeoJson3DLayer"; 21 | 22 | class OverlayLayer extends Base3DLayer { 23 | sourceLayerKey: string; 24 | 25 | items: HTMLElement[] = []; 26 | 27 | div: HTMLElement | null = null; 28 | 29 | template: string | null = null; 30 | 31 | maxDistance: number = 0; 32 | 33 | maxItems: number = 10; 34 | 35 | occlude: boolean = false; 36 | 37 | constructor(key: string, sourceLayerKey: string) { 38 | super(key); 39 | this.sourceLayerKey = sourceLayerKey; 40 | setTimeout(() => { 41 | this.createOverlayDiv(); 42 | this.updateSceneFromFeatures(); 43 | }, 0); 44 | } 45 | 46 | createOverlayDiv(): void { 47 | // Add an overlay DIV over the 3D canvas 48 | // FIXME: This should be created by the scene viewer, and other divs 49 | const sceneViewer: SceneViewer = this.layerManager!.sceneViewer; 50 | this.div = document.createElement("div"); 51 | sceneViewer.element.appendChild(this.div); 52 | this.div.className = "ddd-layer-overlay"; 53 | 54 | this.div.style.zIndex = "5"; 55 | this.div.style.width = "100%"; 56 | this.div.style.height = "100%"; 57 | this.div.style.position = "absolute"; 58 | this.div.style.top = "0"; 59 | this.div.style.left = "0"; 60 | this.div.style.right = "0"; 61 | this.div.style.bottom = "0"; 62 | this.div.style.pointerEvents = "none"; 63 | } 64 | 65 | resizeOverlayDiv() { 66 | const sceneViewer: SceneViewer = this.layerManager!.sceneViewer; 67 | this.div!.style.width = sceneViewer.canvas.style.width; 68 | this.div!.style.height = sceneViewer.canvas.style.height; 69 | } 70 | 71 | update(): void {} 72 | 73 | setVisible(visible: boolean) { 74 | super.setVisible(visible); 75 | if (this.div) this.div.style.display == "visible" ? "block" : "none"; 76 | /* 77 | for (let node of this.sceneNodes) { 78 | node.setEnabled(this.visible); 79 | } 80 | */ 81 | } 82 | 83 | /** 84 | * Update scene generating a DIV for each feature in the source layer. 85 | */ 86 | updateSceneFromFeatures() { 87 | const sourceLayer = ( 88 | this.layerManager!.getLayer(this.sourceLayerKey)! 89 | ); 90 | for (const feature of sourceLayer.featuresPoints) { 91 | const html = 92 | "
Feature: " + 93 | feature + 94 | "
"; 95 | const featureDiv = document.createElement("div"); 96 | this.div!.appendChild(featureDiv); 97 | featureDiv.outerHTML = html; 98 | } 99 | 100 | /* 101 | for (let feature of this.featuresLines) { 102 | let marker = MeshBuilder.CreateLines("lineMarker", { points: feature.coordsScene }, sceneViewer.scene); 103 | marker.material = this.featureMaterial; 104 | marker.parent = this.rootNode; 105 | //this.sceneNodes.push(marker); 106 | } 107 | */ 108 | } 109 | 110 | clearScene() { 111 | /* 112 | if (this.rootNode) { 113 | this.rootNode.parent = null; 114 | this.rootNode.dispose(); 115 | this.rootNode = null; 116 | } 117 | */ 118 | } 119 | } 120 | 121 | export { OverlayLayer }; 122 | -------------------------------------------------------------------------------- /src/loading/QueueLoader.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { SceneLoader } from "@babylonjs/core"; 4 | import { SceneViewer } from "../SceneViewer"; 5 | //import "babylonjs-loaders"; 6 | 7 | 8 | class QueueLoaderTask { 9 | url?: string; 10 | onSuccess?: any; 11 | onFailure?: any; 12 | } 13 | 14 | class QueueLoader { 15 | 16 | sceneViewer: SceneViewer; 17 | 18 | queue: any[]; 19 | 20 | current: any[]; 21 | 22 | concurrentTasks: number = 2; // 1 on mobile? 2 on PC? 23 | 24 | 25 | constructor( sceneViewer: SceneViewer ) { 26 | this.sceneViewer = sceneViewer; 27 | this.queue = []; 28 | this.current = []; 29 | } 30 | 31 | update(): void { 32 | //loadNext(); 33 | } 34 | 35 | processNext(): void { 36 | if ( this.queue.length < 1 ) { 37 | return; 38 | } 39 | 40 | const task: any = this.queue.pop(); 41 | this.processTask( task ); 42 | } 43 | 44 | enqueueLoadModel( url: string, onSuccess: any, onFailure: any ): void { 45 | const task = { "url": url, "onSuccess": onSuccess, "onFailure": onFailure }; 46 | this.queue.push(task); 47 | if ( this.current.length < this.concurrentTasks ) { 48 | this.processNext(); 49 | } 50 | } 51 | 52 | processTask( task: {[key: string]: any} ): void { 53 | const url: string = task["url"]; 54 | SceneLoader.ImportMesh( null, "", url, this.sceneViewer.scene, 55 | ( newMeshes, particleSystems, skeletons ) => { 56 | this.processNext(); 57 | task.onSuccess( newMeshes, particleSystems, skeletons ); 58 | }, 59 | () => { 60 | }, 61 | ( scene, msg, ex ) => { 62 | task.onFailure( scene, msg, ex ); 63 | this.processNext(); 64 | }, 65 | ".glb" // this is to force format in case a blob URL is being used 66 | ); 67 | } 68 | 69 | //loadResource() { 70 | //} 71 | 72 | } 73 | 74 | export { QueueLoader }; -------------------------------------------------------------------------------- /src/process/ViewerProcess.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { SceneViewer } from "../SceneViewer"; 8 | 9 | /** 10 | * A process that can be running in a DDDViewer instance. 11 | * Processes are updated every frame before drawing the scene. 12 | */ 13 | abstract class ViewerProcess { 14 | 15 | sceneViewer: SceneViewer; 16 | finished: boolean = false; 17 | time: number = 0; 18 | 19 | constructor( sceneViewer: SceneViewer ) { 20 | this.sceneViewer = sceneViewer; 21 | this.finished = false; 22 | } 23 | 24 | update( deltaTime: number ): void { 25 | // TODO: Consider providing an (optional) initialize() lifecycle method for processes (to be run before the first frame) 26 | //if (this.time == 0) initialize(); 27 | this.time += deltaTime; 28 | } 29 | 30 | } 31 | 32 | export { ViewerProcess }; 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/process/ViewerProcessManager.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { SceneViewer } from "../SceneViewer"; 8 | import { ViewerProcess } from "./ViewerProcess"; 9 | 10 | 11 | class ViewerProcessManager { 12 | 13 | sceneViewer: SceneViewer; 14 | currentProcesses: ViewerProcess[]; 15 | 16 | playing = true; 17 | currentTasks = []; 18 | time = 0.0; 19 | 20 | constructor( sceneViewer: SceneViewer ) { 21 | 22 | this.sceneViewer = sceneViewer; 23 | this.currentProcesses = []; 24 | } 25 | 26 | update( deltaTime: number ): void { 27 | 28 | if ( !( this.playing )) { return; } 29 | 30 | this.time += deltaTime; 31 | 32 | // Update all current tasks 33 | for ( const proc of this.currentProcesses ) { 34 | proc.update( deltaTime ); 35 | } 36 | 37 | // Remove finished steps 38 | this.currentProcesses = this.currentProcesses.filter( ( item ) => { return ( ! item.finished ); } ); 39 | 40 | } 41 | 42 | add( process: ViewerProcess ): void { 43 | //console.debug("Adding process: ", process); 44 | 45 | // Sanity check 46 | if (process.sceneViewer != this.sceneViewer) { 47 | throw new Error("Tried to add a ViewerProcess to a ViewerProcessManager which belongs ot a different SceneViewer."); 48 | } 49 | 50 | this.currentProcesses.push( process ); 51 | } 52 | 53 | remove( process: ViewerProcess ): void { 54 | //console.debug("Removing process: ", process); 55 | this.currentProcesses = this.currentProcesses.filter(( item ) => { return ( item !== process ); }); 56 | } 57 | 58 | 59 | } 60 | 61 | export { ViewerProcessManager }; 62 | 63 | -------------------------------------------------------------------------------- /src/process/anim/AnimationProcess.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { EasingFunction, QuadraticEase } from "@babylonjs/core"; 8 | import { SceneViewer } from "../../SceneViewer"; 9 | import { ViewerProcess } from "../ViewerProcess"; 10 | 11 | abstract class AnimationProcess extends ViewerProcess { 12 | 13 | sceneViewer: SceneViewer; 14 | 15 | animTime: number; 16 | time: number; 17 | interpFactor: number; 18 | 19 | easing: EasingFunction; 20 | 21 | /** 22 | * 23 | * @param sceneViewer 24 | * @param animTime if set to null, the Process must manually mark itself as finished 25 | */ 26 | constructor(sceneViewer: SceneViewer, animTime: number | null = null) { 27 | 28 | super(sceneViewer); 29 | 30 | this.sceneViewer = sceneViewer; 31 | 32 | this.time = 0.0; 33 | this.interpFactor = 0.0; // Stored as a class property for convenience of derived classes 34 | 35 | this.animTime = animTime || 0; 36 | 37 | this.easing = new QuadraticEase(); 38 | this.easing.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT); 39 | } 40 | 41 | update( deltaTime: number): void { 42 | 43 | // Avoid calling parent just to update deltaTime, do it here for performance 44 | this.time += deltaTime; 45 | 46 | this.interpFactor = ( this.animTime > 0 ) ? (( this.time ) / ( this.animTime)) : 1.0; 47 | if (this.interpFactor > 1.0) this.interpFactor = 1.0; 48 | 49 | // Ease interpolation 50 | this.interpFactor = this.easing.ease(this.interpFactor); 51 | 52 | if (this.animTime != null && this.time >= this.animTime) { 53 | this.finished = true; 54 | } 55 | } 56 | 57 | } 58 | 59 | export { AnimationProcess }; -------------------------------------------------------------------------------- /src/process/anim/CameraMoveAnimationProcess.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Scalar, TargetCamera, Vector3 } from "@babylonjs/core"; 8 | import { ScenePosition } from "../../core/ScenePosition"; 9 | import { SceneViewer } from "../../SceneViewer"; 10 | import { AnimationProcess } from "./AnimationProcess"; 11 | 12 | class CameraMovementAnimationProcess extends AnimationProcess { 13 | moveStart: ScenePosition; 14 | moveEnd: ScenePosition; 15 | 16 | // Move camera adding a vertical arc which height is a factor of movement distance 17 | moveArcHeightFactor: number = 0.0; // 0.05; 18 | 19 | _mslHeightStart: number | null = null; 20 | _mslHeightEnd: number | null = null; 21 | 22 | constructor( 23 | sceneViewer: SceneViewer, 24 | moveStart: ScenePosition, 25 | moveEnd: ScenePosition, 26 | animTime: number, 27 | ) { 28 | super(sceneViewer, animTime); 29 | 30 | this.moveStart = moveStart; 31 | this.moveEnd = moveEnd; 32 | } 33 | 34 | calculateMslHeights() { 35 | if (!this._mslHeightStart) { 36 | const startPositionScene = this.sceneViewer.wgs84ToScene( 37 | this.moveStart.positionWGS84, 38 | ); 39 | const startPositionSceneVec = new Vector3( 40 | startPositionScene[0], 41 | startPositionScene[1], 42 | startPositionScene[2], 43 | ); 44 | const [ terrainElevation, terrainMesh ] = 45 | this.sceneViewer.elevationMSLFromSceneCoords(startPositionSceneVec); 46 | if (terrainElevation !== null) 47 | this._mslHeightStart = 48 | terrainElevation + this.moveStart.positionGroundHeight; 49 | } 50 | if (!this._mslHeightEnd) { 51 | const endPositionScene = this.sceneViewer.wgs84ToScene( 52 | this.moveEnd.positionWGS84, 53 | ); 54 | const endPositionSceneVec = new Vector3( 55 | endPositionScene[0], 56 | endPositionScene[1], 57 | endPositionScene[2], 58 | ); 59 | const [ terrainElevation, terrainMesh ] = 60 | this.sceneViewer.elevationMSLFromSceneCoords(endPositionSceneVec); 61 | if (terrainElevation !== null) 62 | this._mslHeightEnd = 63 | terrainElevation + this.moveEnd.positionGroundHeight; 64 | } 65 | } 66 | 67 | update(deltaTime: number): void { 68 | super.update(deltaTime); 69 | 70 | // Update camera interpolating between start and end 71 | const move_start = this.moveStart; 72 | const move_end = this.moveEnd; 73 | 74 | const sceneViewer = this.sceneViewer; 75 | 76 | sceneViewer.viewerState.positionWGS84 = [ 77 | Scalar.Lerp( 78 | move_start.positionWGS84[0], 79 | move_end.positionWGS84[0], 80 | this.interpFactor, 81 | ), 82 | Scalar.Lerp( 83 | move_start.positionWGS84[1], 84 | move_end.positionWGS84[1], 85 | this.interpFactor, 86 | ), 87 | ]; 88 | 89 | this.calculateMslHeights(); 90 | 91 | const heightStart = 92 | this._mslHeightStart !== null 93 | ? this._mslHeightStart 94 | : move_start.positionGroundHeight; 95 | const heightEnd = 96 | this._mslHeightEnd !== null 97 | ? this._mslHeightEnd 98 | : move_end.positionGroundHeight + 99 | (this._mslHeightStart !== null 100 | ? this._mslHeightStart - move_start.positionGroundHeight 101 | : 0); 102 | 103 | // Add arc height offset if set 104 | const moveDistance = [ 105 | (move_end.positionWGS84[0] - move_start.positionWGS84[0]) * 111000, 106 | (move_end.positionWGS84[1] - move_start.positionWGS84[1]) * 111000, 107 | ]; 108 | const moveDistanceMag = Math.sqrt( 109 | moveDistance[0] ** 2 + moveDistance[1] ** 2, 110 | ); 111 | const moveArcHeight = moveDistanceMag * this.moveArcHeightFactor; 112 | const moveArcOffset = Math.sin(this.interpFactor * Math.PI) * moveArcHeight; 113 | 114 | const mslHeight = 115 | Scalar.Lerp(heightStart, heightEnd, this.interpFactor) + moveArcOffset; 116 | 117 | sceneViewer.viewerState.positionGroundHeight = 118 | mslHeight - sceneViewer.viewerState.positionTerrainElevation; 119 | sceneViewer.viewerState.positionTilt = Scalar.Lerp( 120 | move_start.positionTilt, 121 | move_end.positionTilt, 122 | this.interpFactor, 123 | ); 124 | 125 | let startHeading = move_start.positionHeading; 126 | const targetHeading = move_end.positionHeading; 127 | if ( 128 | Math.abs(move_end.positionHeading - move_start.positionHeading) > 180.0 129 | ) { 130 | if (move_end.positionHeading - move_start.positionHeading > 0) { 131 | startHeading += 360; 132 | } else { 133 | startHeading -= 360; 134 | } 135 | } 136 | const newPositionHeading = Scalar.LerpAngle( 137 | startHeading, 138 | targetHeading, 139 | this.interpFactor, 140 | ); 141 | sceneViewer.viewerState.positionHeading = 142 | ((newPositionHeading % 360) + 360) % 360; 143 | //sceneViewer.viewerState.positionHeading = 180 / Math.PI * Scalar.LerpAngle(move_start.positionHeading * Math.PI / 180.0, move_end.positionHeading * Math.PI / 180.0, interp_factor); 144 | 145 | const positionScene = sceneViewer.wgs84ToScene( 146 | sceneViewer.viewerState.positionWGS84, 147 | ); 148 | const position = new Vector3(positionScene[0], mslHeight, positionScene[2]); 149 | const rotation = new Vector3( 150 | (90.0 - sceneViewer.viewerState.positionTilt) * (Math.PI / 180.0), 151 | sceneViewer.viewerState.positionHeading * (Math.PI / 180.0), 152 | 0.0, 153 | ); 154 | 155 | sceneViewer.camera!.position = position; 156 | if (sceneViewer.camera instanceof TargetCamera) { 157 | (sceneViewer.camera!).rotation = rotation; 158 | } 159 | } 160 | } 161 | 162 | export { CameraMovementAnimationProcess }; 163 | -------------------------------------------------------------------------------- /src/process/anim/DateTimeAnimationProcess.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | 8 | import { SceneViewer } from "../../SceneViewer"; 9 | import { AnimationProcess } from "./AnimationProcess"; 10 | 11 | class DateTimeAnimationProcess extends AnimationProcess { 12 | 13 | dtStart: Date; 14 | dtEnd: Date; 15 | 16 | constructor( sceneViewer: SceneViewer, dtStart: Date, dtEnd: Date, animTime: number ) { 17 | 18 | super(sceneViewer, animTime); 19 | 20 | this.dtStart = dtStart; 21 | this.dtEnd = dtEnd; 22 | 23 | //console.debug("Datetime anim from " + dtStart + " to " + dtEnd); 24 | console.debug("TODO: Restore missing call sceneViewer.lightSetupFromDatePos();"); 25 | } 26 | 27 | update( deltaTime: number ): void { 28 | 29 | super.update(deltaTime); 30 | 31 | const interpTime = ( this.dtEnd.getTime() / 1000 - this.dtStart.getTime() / 1000 ) * this.interpFactor; 32 | this.sceneViewer.viewerState.positionDate = new Date( this.dtStart.getTime() + interpTime * 1000 ); 33 | 34 | //console.debug("Datetime set by animation to: " + this.sceneViewer.viewerState.positionDate); 35 | 36 | this.sceneViewer.lightSetupFromDatePos(); 37 | } 38 | 39 | } 40 | 41 | export { DateTimeAnimationProcess }; 42 | -------------------------------------------------------------------------------- /src/process/anim/TextAnimationProcess.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D models 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | 8 | import { SceneViewer } from "../../SceneViewer"; 9 | import { AnimationProcess } from "./AnimationProcess"; 10 | 11 | /** 12 | * 13 | */ 14 | class TextAnimationProcess extends AnimationProcess { 15 | 16 | text: string; 17 | 18 | /** 19 | * 20 | * @param text Text to animate. 21 | * @param animTime Animation duration in seconds. 22 | */ 23 | constructor( sceneViewer: SceneViewer, text: string, animTime: number ) { 24 | super( sceneViewer, animTime ); 25 | this.text = text; 26 | } 27 | 28 | update( deltaTime: number ): void { 29 | 30 | super.update( deltaTime ); 31 | 32 | const interpChars = Math.ceil(( this.text.length ) * this.interpFactor ); 33 | const interpText = this.text.substr( 0, interpChars ); 34 | 35 | this.sceneViewer.viewerState.sceneTitleText = interpText; 36 | 37 | if ( this.finished ) { 38 | this.sceneViewer.viewerState.sceneTitleText = null; 39 | } 40 | } 41 | } 42 | 43 | export { TextAnimationProcess }; 44 | -------------------------------------------------------------------------------- /src/process/sequencer/ViewerSequencer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | 8 | import { CameraMovementAnimationProcess } from "../anim/CameraMoveAnimationProcess"; 9 | import { DateTimeAnimationProcess } from "../anim/DateTimeAnimationProcess"; 10 | import { TextAnimationProcess } from "../anim/TextAnimationProcess"; 11 | 12 | import { SceneViewer } from "../../SceneViewer"; 13 | // import ScenePosition from "../ScenePosition"; 14 | /* eslint-disable no-console, */ 15 | 16 | type Step = (string | number)[]; 17 | type Sequence = Step[]; 18 | 19 | // TODO: this should be a ViewerProcess 20 | class ViewerSequencer { 21 | 22 | sceneViewer: SceneViewer; 23 | seq: Sequence | null; 24 | time: number; 25 | index: number; 26 | playing: boolean; 27 | 28 | waitTime: number; 29 | 30 | constructor( sceneViewer: SceneViewer ) { 31 | 32 | this.sceneViewer = sceneViewer; 33 | 34 | this.seq = null; 35 | this.playing = false; 36 | this.time = 0.0; 37 | this.index = 0; 38 | this.waitTime = 0.0; 39 | } 40 | 41 | update( deltaTime: number ): void { 42 | 43 | if ( !( this.playing )) { return; } 44 | 45 | this.time += deltaTime; 46 | 47 | if ( this.waitTime > 0.0 ) { 48 | this.waitTime -= deltaTime; 49 | return; 50 | } 51 | 52 | // Run all possible steps 53 | while ( this.index < this.seq!.length && this.waitTime <= 0.0 ) { 54 | const step = this.seq![this.index]; 55 | this.index++; 56 | this.runStep( step ); 57 | } 58 | 59 | } 60 | 61 | runStep( step: Step ): void { 62 | 63 | console.debug( "Running step: ", step ); 64 | 65 | const command: string | number = step[0]; 66 | //if ( ! ((command instanceof String )) throw new Error( "No command specified." ); 67 | 68 | if ( command === "m" ) { 69 | const posString: string | null = this.sceneViewer.positionString(8); 70 | 71 | if ( posString ) { 72 | const move_start = this.sceneViewer.parsePositionString( posString ); 73 | const move_end = this.sceneViewer.parsePositionString( step[1] ); 74 | const animTime = step[2]; 75 | const moveAnimationProcess = new CameraMovementAnimationProcess( this.sceneViewer, move_start, move_end, animTime ); 76 | console.debug(move_start, move_end, animTime); 77 | this.sceneViewer.processes.add( moveAnimationProcess ); 78 | } 79 | 80 | } else if ( command === "dt" ) { 81 | const dtStart = this.sceneViewer.viewerState.positionDate; 82 | console.debug( dtStart ); 83 | const dtEnd = new Date( dtStart ); 84 | console.debug( dtEnd ); 85 | dtEnd.setHours( parseInt( ( step[1]).split( ":" )[0])); 86 | dtEnd.setMinutes( parseInt( ( step[1]).split( ":" )[1])); 87 | console.debug( dtEnd ); 88 | const animTime = step[2]; 89 | const process = new DateTimeAnimationProcess( this.sceneViewer, dtStart, dtEnd, animTime ); 90 | this.sceneViewer.processes.add( process ); 91 | 92 | } else if ( command === "t" ) { 93 | const text = step[1]; 94 | const animTime = step[2]; 95 | const process = new TextAnimationProcess( this.sceneViewer, text, animTime ); 96 | this.sceneViewer.processes.add( process ); 97 | 98 | } else if ( command === "s" ) { 99 | this.waitTime = step[1]; 100 | 101 | 102 | /*} else if ( command === "u" ) { 103 | const url = step[1]; 104 | // Do not change URL if in settings 105 | if ( this.sceneViewer.app.$route.name !== "sceneTools" ) { 106 | this.sceneViewer.app.$router.push( url ); 107 | }*/ 108 | 109 | 110 | 111 | } else if ( command === "goto" ) { 112 | this.index = step[1]; 113 | 114 | } else if ( (command).startsWith( "#" )) { 115 | // Command is a comment. Ignore. 116 | } else { 117 | // Unknown step type 118 | console.debug( "Invalid sequence step: ", step ); 119 | } 120 | } 121 | 122 | play( seq: Sequence ): void { 123 | console.debug( "Playing sequence: ", seq ); 124 | 125 | this.sceneViewer.camera!.detachControl(); 126 | this.sceneViewer.viewerState.sceneViewModeShow = false; 127 | this.seq = seq; 128 | this.playing = true; 129 | this.time = 0.0; 130 | this.index = 0; 131 | } 132 | } 133 | 134 | export { ViewerSequencer }; 135 | -------------------------------------------------------------------------------- /src/render/environment/DefaultEnvironment.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | 10 | /** 11 | * Manages environment rendering, using time, date or other information 12 | * to set up the skybox and lighting. 13 | */ 14 | class DefaultEnvironment { 15 | 16 | protected dddViewer: SceneViewer; 17 | 18 | constructor(dddViewer: SceneViewer) { 19 | 20 | // Reference to DDDViewer 21 | this.dddViewer = dddViewer; 22 | 23 | // Babylon camera which we are controlling 24 | //this.camera = dddViewer.camera; 25 | } 26 | 27 | update(deltaTime: number): void { 28 | 29 | } 30 | 31 | initialize(): void { 32 | 33 | } 34 | 35 | dispose(): void { 36 | 37 | } 38 | 39 | } 40 | 41 | export { DefaultEnvironment }; -------------------------------------------------------------------------------- /src/render/materials/SkyboxMaterial.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable indent */ 2 | 3 | import { Effect, Material, Scene, ShaderMaterial, Texture } from "@babylonjs/core"; 4 | 5 | /* eslint-disable no-unused-vars, no-var, no-undef, no-debugger, no-console, */ 6 | 7 | class SkyMaterialWrapper { 8 | 9 | material: Material; 10 | 11 | constructor(scene: Scene) { //, splatmapTexture, atlasTexture, atlasNormalTexture, options) { 12 | this.material = this.initMaterial(scene); 13 | //this.testSplatMaterial(scene); 14 | } 15 | 16 | initMaterial(scene: Scene): Material { 17 | //, options: any) { 18 | 19 | Effect.ShadersStore["customVertexShader"] = ` 20 | precision highp float; 21 | 22 | // Attributes 23 | attribute vec3 position; 24 | attribute vec3 normal; 25 | attribute vec2 uv; 26 | 27 | // Uniforms 28 | uniform mat4 worldViewProjection; 29 | 30 | // Varying 31 | varying vec4 vPosition; 32 | varying vec3 vNormal; 33 | varying vec2 vUV; 34 | void main() { 35 | 36 | vec4 p = vec4( position, 1. ); 37 | 38 | vPosition = p; 39 | vNormal = normal; 40 | 41 | vUV = uv; 42 | // vUV.y =1.-vUV.y; // flip uv screen ; 43 | gl_Position = worldViewProjection * p; 44 | 45 | }`; 46 | 47 | Effect.ShadersStore["customFragmentShader"] = ` 48 | precision highp float; 49 | 50 | uniform mat4 worldView; 51 | 52 | varying vec4 vPosition; 53 | 54 | precision mediump float; 55 | 56 | // Day and night sky cycle. By László Matuska (@BitOfGold) 57 | // Creates a sky texture for a skydome 58 | // https://www.shadertoy.com/view/ltlSWB 59 | 60 | 61 | // based on other shaders, Greetings goes to: 62 | 63 | // Weather. By David Hoskins, May 2014. 64 | // https://www.shadertoy.com/view/4dsXWn 65 | 66 | // Edge of atmosphere 67 | // created by dmytro rubalskyi (ruba) 68 | // https://www.shadertoy.com/view/XlXGzB 69 | 70 | // Starfield01 by xaot88 71 | // https://www.shadertoy.com/view/Md2SR3 72 | // ====================================================================== 73 | 74 | //#define shadertoy 1 75 | 76 | //#define cloud2 1 //second layer of clouds, altocumulus or stratocumulus. (in 4K, too slow on my GTX970. HD is OK.) 77 | //plan was to make cirrus too... 78 | 79 | #ifdef GL_ES 80 | precision highp float; 81 | #endif 82 | 83 | const float M_PI = 3.1415926535; 84 | const float DEGRAD = M_PI / 180.0; 85 | 86 | #ifdef shadertoy 87 | float height = 500.0; //viewer height 88 | float cloudy = 0.6; //0.6 //0.0 clear sky 89 | #else 90 | varying vec3 vNormal; 91 | varying vec2 vUV; 92 | uniform sampler2D iChannel0; 93 | uniform float sunx; 94 | uniform float suny; 95 | //uniform float moonx; 96 | //uniform float moony; 97 | //uniform float cloudy; 98 | // uniform float height; 99 | uniform float time; 100 | #endif 101 | //float moonx = 1.0; 102 | //float moony = 9.6; 103 | //float sunx = 1.0; 104 | //float suny = 1.6; 105 | float cloudy = 0.1; 106 | float height = 500.0; 107 | 108 | //rendering quality 109 | const int steps = 8; //16 is fast, 128 or 256 is extreme high 110 | const int stepss = 8; //16 is fast, 16 or 32 is high 111 | 112 | //float t = 12.0; //fix time. 12.0 91.0, 97.0, 188.0, 72.0, 74.0 113 | 114 | float camroty = 0. * DEGRAD; //20. 115 | float haze = 0.1; //0.2 116 | float cloudyhigh = 0.05; //if cloud2 defined 117 | 118 | float cloudnear = 1.0; //9e3 12e3 //do not render too close clouds on the zenith 119 | float cloudfar = 1e3; //15e3 17e3 120 | 121 | float startreshold = 0.99; //0.99 0.98 star density treshold. 122 | 123 | const float I = 10.; //sun light power, 10.0 is normal 124 | const float g = 0.45; //light concentration .76 //.45 //.6 .45 is normaL 125 | const float g2 = g * g; 126 | 127 | //Reyleigh scattering (sky color, atmospheric up to 8km) 128 | vec3 bR = vec3(5.8e-6, 13.5e-6, 33.1e-6); //normal earth 129 | //vec3 bR = vec3(5.8e-6, 33.1e-6, 13.5e-6); //purple 130 | //vec3 bR = vec3( 63.5e-6, 13.1e-6, 50.8e-6 ); //green 131 | //vec3 bR = vec3( 13.5e-6, 23.1e-6, 115.8e-6 ); //yellow 132 | //vec3 bR = vec3( 5.5e-6, 15.1e-6, 355.8e-6 ); //yeellow 133 | //vec3 bR = vec3(3.5e-6, 333.1e-6, 235.8e-6 ); //red-purple 134 | 135 | //Mie scattering (water particles up to 1km) 136 | vec3 bM = vec3(21e-6); //normal mie 137 | //vec3 bM = vec3(50e-6); //high mie 138 | 139 | //----- 140 | //positions 141 | 142 | const float Hr = 8000.0; //Reyleight scattering top 143 | const float Hm = 1000.0; //Mie scattering top 144 | 145 | const float R0 = 6360e3; //planet radius 146 | const float Ra = 6380e3; //atmosphere radius 147 | vec3 C = vec3(0., -R0, 0.); //planet center 148 | vec3 Ds = normalize(vec3(0., .09, -1.)); //sun direction? 149 | 150 | //-------------------------------------------------------------------------- 151 | //Starfield 152 | // License Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License. 153 | 154 | // Return random noise in the range [0.0, 1.0], as a function of x. 155 | float Noise2d( in vec2 x ) 156 | { 157 | float xhash = cos( x.x * 37.0 ); 158 | float yhash = cos( x.y * 57.0 ); 159 | return fract( 415.92653 * ( xhash + yhash ) ); 160 | } 161 | 162 | // Convert Noise2d() into a "star field" by stomping everthing below fThreshhold to zero. 163 | float NoisyStarField( in vec2 vSamplePos, float fThreshhold ) 164 | { 165 | float StarVal = Noise2d( vSamplePos ); 166 | if ( StarVal >= fThreshhold ) 167 | StarVal = pow( (StarVal - fThreshhold)/(1.0 - fThreshhold), 6.0 ); 168 | else 169 | StarVal = 0.0; 170 | return StarVal; 171 | } 172 | 173 | // Stabilize NoisyStarField() by only sampling at integer values. 174 | float StableStarField( in vec2 vSamplePos, float fThreshhold ) 175 | { 176 | // Linear interpolation between four samples. 177 | // Note: This approach has some visual artifacts. 178 | // There must be a better way to "anti alias" the star field. 179 | float fractX = fract( vSamplePos.x ); 180 | float fractY = fract( vSamplePos.y ); 181 | vec2 floorSample = floor( vSamplePos ); 182 | float v1 = NoisyStarField( floorSample, fThreshhold ); 183 | float v2 = NoisyStarField( floorSample + vec2( 0.0, 1.0 ), fThreshhold ); 184 | float v3 = NoisyStarField( floorSample + vec2( 1.0, 0.0 ), fThreshhold ); 185 | float v4 = NoisyStarField( floorSample + vec2( 1.0, 1.0 ), fThreshhold ); 186 | 187 | float StarVal = v1 * ( 1.0 - fractX ) * ( 1.0 - fractY ) 188 | + v2 * ( 1.0 - fractX ) * fractY 189 | + v3 * fractX * ( 1.0 - fractY ) 190 | + v4 * fractX * fractY; 191 | return StarVal; 192 | } 193 | 194 | 195 | //-------------------------------------------------------------------------- 196 | //Cloud noise 197 | 198 | float Noise( in vec3 x ) 199 | { 200 | vec3 p = floor(x); 201 | vec3 f = fract(x); 202 | f = f*f*(3.0-2.0*f); 203 | 204 | vec2 uv = (p.xy+vec2(37.0,17.0)*p.z) + f.xy; 205 | 206 | vec2 rg = texture( iChannel0, (uv+ 0.5)/256.0, -100.0).yx; 207 | return mix( rg.x, rg.y, f.z ); 208 | } 209 | 210 | float fnoise( vec3 p, in float t ) 211 | { 212 | p *= .25; 213 | float f; 214 | 215 | f = 0.5000 * Noise(p); p = p * 3.02; p.y -= t*.2; 216 | f += 0.2500 * Noise(p); p = p * 3.03; p.y += t*.06; 217 | f += 0.1250 * Noise(p); p = p * 3.01; 218 | f += 0.0625 * Noise(p); p = p * 3.03; 219 | f += 0.03125 * Noise(p); p = p * 3.02; 220 | f += 0.015625 * Noise(p); 221 | return f; 222 | } 223 | 224 | //-------------------------------------------------------------------------- 225 | //clouds, scattering 226 | 227 | float cloud(vec3 p, in float t) { 228 | float cld = fnoise(p*2e-4,t) + cloudy*0.1 ; 229 | cld = smoothstep(.4+.04, .6+.04, cld); 230 | cld *= 70.; 231 | return cld+haze; 232 | } 233 | 234 | 235 | void densities(in vec3 pos, out float rayleigh, out float mie, in float t) { 236 | float h = length(pos - C) - R0; 237 | rayleigh = exp(-h/Hr); 238 | vec3 d = pos; 239 | d.y = 0.0; 240 | float dist = length(d); 241 | 242 | float cld = 0.; 243 | if (5e3 < h && h < 8e3) { 244 | cld = cloud(pos+vec3(23175.7, 0.,-t*3e3), t); 245 | cld *= sin(3.1415*(h-5e3)/5e3) * cloudy; 246 | } 247 | #ifdef cloud2 248 | float cld2 = 0.; 249 | if (12e3 < h && h < 15.5e3) { 250 | cld2 = fnoise(pos*3e-4,t)*cloud(pos*32.0+vec3(27612.3, 0.,-t*15e3), t); 251 | cld2 *= sin(3.1413*(h-12e3)/12e3) * cloudyhigh; 252 | cld2 = clamp(cld2,0.0,1.0); 253 | } 254 | 255 | #endif 256 | 257 | 258 | if ( dist < cloudfar) { 259 | float factor = clamp(1.0-((cloudfar - dist)/(cloudfar-cloudnear)),0.0,1.0); 260 | cld *= factor; 261 | } 262 | 263 | mie = exp(-h/Hm) + cld + haze; 264 | #ifdef cloud2 265 | mie += cld2; 266 | #endif 267 | 268 | } 269 | 270 | float escape(in vec3 p, in vec3 d, in float R) { 271 | 272 | vec3 v = p - C; 273 | float b = dot(v, d); 274 | float c = dot(v, v) - R*R; 275 | float det2 = b * b - c; 276 | if (det2 < 0.) return -1.; 277 | float det = sqrt(det2); 278 | float t1 = -b - det, t2 = -b + det; 279 | return (t1 >= 0.) ? t1 : t2; 280 | } 281 | 282 | // this can be explained: http://www.scratchapixel.com/lessons/3d-advanced-lessons/simulating-the-colors-of-the-sky/atmospheric-scattering/ 283 | void scatter(vec3 o, vec3 d, out vec3 col, out float scat, in float t) { 284 | float L = escape(o, d, Ra); 285 | float mu = dot(d, Ds); 286 | float opmu2 = 1. + mu*mu; 287 | float phaseR = .0596831 * opmu2; 288 | float phaseM = .1193662 * (1. - g2) * opmu2 / ((2. + g2) * pow(1. + g2 - 2.*g*mu, 1.5)); 289 | 290 | float depthR = 0., depthM = 0.; 291 | vec3 R = vec3(0.), M = vec3(0.); 292 | 293 | float dl = L / float(steps); 294 | for (int i = 0; i < steps; ++i) { 295 | float l = float(i) * dl; 296 | vec3 p = o + d * l; 297 | 298 | float dR, dM; 299 | densities(p, dR, dM, t); 300 | dR *= dl; dM *= dl; 301 | depthR += dR; 302 | depthM += dM; 303 | 304 | float Ls = escape(p, Ds, Ra); 305 | if (Ls > 0.) { 306 | float dls = Ls / float(stepss); 307 | float depthRs = 0., depthMs = 0.; 308 | for (int j = 0; j < stepss; ++j) { 309 | float ls = float(j) * dls; 310 | vec3 ps = p + Ds * ls; 311 | float dRs, dMs; 312 | densities(ps, dRs, dMs, t); 313 | depthRs += dRs * dls; 314 | depthMs += dMs * dls; 315 | } 316 | 317 | vec3 A = exp(-(bR * (depthRs + depthR) + bM * (depthMs + depthM))); 318 | R += A * dR; 319 | M += A * dM; 320 | } 321 | } 322 | 323 | 324 | col = I * (R * bR * phaseR + M * bM * phaseM); 325 | scat = 1.0 - clamp(depthM*1e-5,0.,1.); 326 | } 327 | 328 | //-------------------------------------------------------------------------- 329 | // ray casting 330 | 331 | 332 | vec3 rotate_y(vec3 v, float angle) 333 | { 334 | float ca = cos(angle); float sa = sin(angle); 335 | return v*mat3( 336 | +ca, +.0, -sa, 337 | +.0,+1.0, +.0, 338 | +sa, +.0, +ca); 339 | } 340 | 341 | vec3 rotate_x(vec3 v, float angle) 342 | { 343 | float ca = cos(angle); float sa = sin(angle); 344 | return v*mat3( 345 | +1.0, +.0, +.0, 346 | +.0, +ca, -sa, 347 | +.0, +sa, +ca); 348 | } 349 | 350 | float map(float value, float min1, float max1, float min2, float max2) { 351 | return min2 + (value - min1) * (max2 - min2) / (max1 - min1); 352 | } 353 | 354 | vec4 generate(in vec2 uv, in vec2 fragCoord, in vec2 sunpos, in float t) { 355 | 356 | //if (fragCoord.y < -0.25) discard; 357 | 358 | float att = 1.0; 359 | float staratt = 0.0; 360 | if (sunpos.y < 0.10) { 361 | staratt = map(sunpos.y, 0.20, -1.0, 0.0, 1.0); 362 | } 363 | if (sunpos.y < -0.10) { 364 | att = map(sunpos.y, -0.10, -1.0, 1.0, 0.25); 365 | //sunpos.y = -sunpos.y; 366 | } 367 | 368 | vec3 O = vec3(0., height, 0.); 369 | 370 | vec3 D = normalize(rotate_y(rotate_x(vec3(0.0, 0.0, 1.0),-uv.y*M_PI/2.0),-uv.x*M_PI+camroty)); 371 | 372 | if (D.y <= -0.15) { 373 | D.y = -0.3 -D.y; 374 | } 375 | 376 | Ds= normalize(rotate_y(rotate_x(vec3(0.0, 0.0, 1.0),-sunpos.y*M_PI/2.0),-sunpos.x*M_PI)); 377 | float scat = 0.; 378 | vec3 color = vec3(0.); 379 | scatter(O, D, color, scat, t); 380 | color *= att; 381 | 382 | float starcolor = StableStarField(fragCoord * 1024.0, startreshold); 383 | color += vec3(scat * starcolor * staratt); 384 | 385 | // Water mix to bottom half, black at night 386 | if (fragCoord.y < 0.0) { 387 | float waterFactor = smoothstep(-0.05, 0.0, sunpos.y); 388 | vec3 waterColor = vec3(70.0 / 255.0, 135.0 / 255.0, 240.0 / 255.0) * waterFactor; 389 | float waterMix = smoothstep(0.0, -0.1, fragCoord.y); 390 | waterColor = (color + waterColor) / 2.0; 391 | color = color + (waterColor - color) * waterMix; 392 | } 393 | 394 | float env = 1.0; 395 | return(vec4(env * pow(color, vec3(.7)),1.0)); 396 | } 397 | 398 | void main() { 399 | vec2 uv = vec2(2.0 * vUV.x - 1.0, -2.0 * vUV.y + 1.0); 400 | vec2 sunpos = vec2(sunx,suny); 401 | float t = time; 402 | gl_FragColor = generate(uv, uv, sunpos,t); 403 | } 404 | 405 | `; 406 | 407 | 408 | // Compile 409 | var shaderMaterial = new ShaderMaterial( "skyShader", scene, { 410 | vertex: "custom", 411 | fragment: "custom", 412 | }, 413 | { 414 | attributes: [ "position", "normal", "uv" ], 415 | uniforms: [ "world", "worldView", "worldViewProjection", "view", "projection" ] 416 | }); 417 | 418 | const mainTexture = new Texture("/textures/skynoise.png", scene, true, false, 12); // NEAREST 419 | 420 | //https://www.shadertoy.com/view/ltlSWB 421 | shaderMaterial.setTexture("iChannel0", mainTexture); 422 | shaderMaterial.setFloat("time", 0); 423 | shaderMaterial.setFloat("offset", 10); 424 | shaderMaterial.setFloat("sunx", 2.0); 425 | shaderMaterial.setFloat("suny", 0.9); 426 | shaderMaterial.backFaceCulling = false; 427 | 428 | 429 | /* 430 | var time = 0; 431 | scene.registerBeforeRender(function () { 432 | var shaderMaterial = scene.getMaterialByName("skyShader"); 433 | shaderMaterial.setFloat("time", time); 434 | 435 | //Animate Move Sun 436 | shaderMaterial.setFloat("suny", Math.sin(time/3)); 437 | shaderMaterial.setFloat("sunx", Math.sin(time/3)); 438 | time += 0.008; 439 | }); 440 | */ 441 | 442 | this.material = shaderMaterial; 443 | return shaderMaterial; 444 | 445 | } 446 | 447 | } 448 | 449 | export { SkyMaterialWrapper }; -------------------------------------------------------------------------------- /src/render/materials/TerrainMaterial.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable object-curly-spacing */ 2 | /* eslint-disable array-bracket-spacing */ 3 | /* eslint-disable indent */ 4 | 5 | //import * as BABYLON from "babylonjs"; 6 | //import * as BABYLONMAT from "babylonjs-materials"; 7 | 8 | //import "babylonjs-loaders"; 9 | import { PBRCustomMaterial } from "@babylonjs/materials"; 10 | import { 11 | AbstractMesh, 12 | Color3, 13 | Effect, 14 | Mesh, 15 | Scene, 16 | Texture, 17 | Vector2, 18 | } from "@babylonjs/core"; 19 | import { SceneViewer } from "SceneViewer"; 20 | 21 | /* eslint-disable no-unused-vars, no-var, no-undef, no-debugger, no-console, */ 22 | 23 | /** 24 | * From: https://forum.babylonjs.com/t/pbr-texture-splatting-up-to-64-textures/1994/28 25 | * and: https://www.babylonjs-playground.com/#LIVRIY#58 26 | */ 27 | class TerrainMaterialWrapper { 28 | sceneViewer: SceneViewer; 29 | material: PBRCustomMaterial; 30 | dedupDouble: boolean; 31 | 32 | options: any; 33 | 34 | tileIndexes = []; 35 | shaderinjectpoint1 = ""; 36 | shaderinjectpoint2 = ""; 37 | shaderinjectpoint3 = ""; 38 | shaderinjectpoint4 = ""; 39 | 40 | numTilesHorizontal: number = 0; 41 | numTilesVertical: number = 0; 42 | totalTiles: number = 0; 43 | tileScale: Vector2 = new Vector2(1, 1); 44 | 45 | numSplatTilesHorizontal: number = 0; 46 | numSplatTilesVertical: number = 0; 47 | totalSplatTiles: number = 0; 48 | splatScale: Vector2 = new Vector2(1, 1); 49 | 50 | splatMap: Texture | null = null; 51 | atlasBumpTexture: Texture | null = null; 52 | 53 | static terrainMaterialShared: PBRCustomMaterial | null = null; 54 | static terrainEffectShared: Effect | null = null; 55 | static matIdx = 1; 56 | initialized = false; 57 | 58 | constructor( 59 | sceneViewer: SceneViewer, 60 | splatmapTexture: Texture, 61 | atlasTexture: Texture, 62 | atlasNormalTexture: Texture, 63 | options: any = null, 64 | ) { 65 | // TODO: Options should be merged with defaults! 66 | 67 | this.sceneViewer = sceneViewer; 68 | this.dedupDouble = false; 69 | 70 | const compileShader = true; 71 | // Compile shader only first time 72 | if (compileShader || TerrainMaterialWrapper.terrainMaterialShared == null) { 73 | this.material = this.initSplatMaterial( 74 | this.sceneViewer.scene, 75 | splatmapTexture, 76 | atlasTexture, 77 | atlasNormalTexture, 78 | options, 79 | ); 80 | TerrainMaterialWrapper.terrainMaterialShared = this.material; 81 | } else { 82 | //this.material = TerrainMaterialWrapper.terrainMaterialShared; 83 | 84 | this.splatMap = splatmapTexture; 85 | if (atlasTexture !== null) { 86 | this.atlasBumpTexture = atlasNormalTexture; 87 | } 88 | this.material = new PBRCustomMaterial( 89 | "splatMaterial" + (TerrainMaterialWrapper.matIdx++).toString(), 90 | this.sceneViewer.scene, 91 | ); // + (TerrainMaterialWrapper.matIdx++) 92 | this.material.metallic = 0.0; // 0.0; 93 | this.material.roughness = 0.0; // 0.43 (asphalt); // 0.95; 94 | //this.material.environmentIntensity = 1.0; // This one is needed to avoid saturation due to env 95 | this.material.albedoTexture = atlasTexture; 96 | this.material.AddUniform("splatmap", "sampler2D", {}); 97 | this.material.AddUniform("atlasNormalsSampler", "sampler2D", {}); 98 | // Reuse shader 99 | this.material["_prepareEffect"] = () => { 100 | if (TerrainMaterialWrapper.terrainMaterialShared!["_activeEffect"]) { 101 | TerrainMaterialWrapper.terrainEffectShared = 102 | TerrainMaterialWrapper.terrainMaterialShared!["_activeEffect"]; 103 | } 104 | 105 | if (TerrainMaterialWrapper.terrainEffectShared) { 106 | return TerrainMaterialWrapper.terrainEffectShared; 107 | } 108 | return null; 109 | }; 110 | } 111 | 112 | // TODO: Check how much this impacts performance, it has a subtle impact on terrain and roads but it's nice to have it. 113 | // TODO: At least make it optional / linked to quality settings. 114 | this.material.reflectionTexture = this.sceneViewer.scene.environmentTexture; 115 | 116 | // This one is needed to control saturation due to env, should be aligned with PBRMaterials, StandarMaterials and light intensity 117 | this.material.environmentIntensity = 118 | this.sceneViewer.baseEnvironmentIntensity; 119 | 120 | this.material.onBindObservable.add((mesh) => { 121 | this.bind(mesh); 122 | //this.material.getEffect().setTexture("splatmap", this.splatMap); 123 | 124 | const ubo = this.material["_uniformBuffer"]; 125 | ubo.update(); 126 | }); 127 | } 128 | 129 | initSplatMaterial( 130 | scene: Scene, 131 | splatMap: Texture, 132 | atlas: Texture, 133 | atlasnormals: Texture, 134 | options: any = null, 135 | ): PBRCustomMaterial { 136 | // TODO: Options should be merged with defaults! 137 | 138 | var defScale = 100.0; 139 | if (!options) { 140 | options = { 141 | numTilesHorizontal: 4, 142 | numTilesVertical: 4, 143 | numSplatTilesHorizontal: 2, 144 | numSplatTilesVertical: 2, 145 | //tileScale:[[20.0,20.0],[20.0,20.0],[20.0,20.0]], 146 | splatInfos: { 147 | // Positions is X / Y, Y grows upwards 148 | layers: [ 149 | { 150 | name: "Ground", 151 | position: [0, 3], 152 | scale: [defScale, defScale], 153 | displScales: 1.0, 154 | }, 155 | // ... 156 | ], 157 | positions: [ 158 | [0.0, 3.0], 159 | [1.0, 3.0], 160 | [2.0, 3.0], 161 | [3.0, 3.0], // encoded by first splat (top left), first row (index 3 from bottom) 162 | [0.0, 2.0], 163 | [1.0, 2.0], 164 | [2.0, 2.0], 165 | [3.0, 2.0], // encoded by splat (top right), second row (index 2 from bottom) 166 | [0.0, 1.0], 167 | [1.0, 1.0], 168 | [2.0, 1.0], 169 | [3.0, 1.0], // encoded by third splat (down left), third row (index 1 starting from bottom) 170 | [0.0, 0.0], 171 | [1.0, 0.0], 172 | [2.0, 0.0], 173 | [3.0, 0.0], // encoded by splat (down right), last row (index 0 from bottom) 174 | ], 175 | scales: [ 176 | [defScale * 0.75, defScale * 0.75], 177 | [defScale, defScale], 178 | [defScale, defScale], 179 | [defScale * 0.5, defScale * 0.5], 180 | [defScale * 0.5, defScale * 0.5], 181 | [defScale * 0.5, defScale * 0.5], 182 | [defScale, defScale], 183 | [defScale, defScale], 184 | [defScale * 1.0, defScale * 1.0], 185 | [defScale * 1.6, defScale * 1.6], 186 | [defScale, defScale], 187 | [defScale, defScale], // Grass 188 | [defScale, defScale], 189 | [defScale * 0.5, defScale * 0.5], 190 | [defScale * 0.25, defScale * 0.25], 191 | [defScale, defScale], 192 | ], // Sand, rock, rock orange 193 | displScales: [ 194 | 0.0, 0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0.0, 0.0, 0.0, 195 | ], 196 | dedupScales: [ 197 | 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.1, 1.1, 1.1, 1.2, 1.2, 198 | 1.2, 0.5, 1.1, 199 | ], 200 | roughness: [ 201 | 1.2, 1.3, 2.5, 0.8, 0.8, 0.9, 1.0, 1.0, 1.85, 1.85, 1.15, 1.25, 202 | 1.45, 0.8, 1.0, 1.0, 203 | ], 204 | metallic: [ 205 | 0.2, 0.1, 0.1, 0.3, 0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 206 | 0.4, 0.0, 0.0, 207 | ], 208 | }, 209 | }; 210 | } 211 | 212 | this.options = options; 213 | this.tileIndexes = []; 214 | this.shaderinjectpoint1 = ""; 215 | this.shaderinjectpoint2 = ""; 216 | this.shaderinjectpoint3 = ""; 217 | this.shaderinjectpoint4 = ""; 218 | 219 | // 4x4 = 16 220 | this.numTilesHorizontal = options.numTilesHorizontal; 221 | this.numTilesVertical = options.numTilesVertical; 222 | this.totalTiles = this.numTilesVertical * this.numTilesHorizontal; 223 | this.tileScale = new Vector2( 224 | 1.0 / this.numTilesHorizontal, 225 | 1.0 / this.numTilesVertical, 226 | ); 227 | 228 | // 2x2 = 4 229 | this.numSplatTilesHorizontal = options.numSplatTilesHorizontal; 230 | this.numSplatTilesVertical = options.numSplatTilesVertical; 231 | this.totalSplatTiles = 232 | this.numSplatTilesVertical * this.numSplatTilesHorizontal; 233 | this.splatScale = new Vector2( 234 | 1.0 / this.numSplatTilesHorizontal, 235 | 1.0 / this.numSplatTilesVertical, 236 | ); 237 | 238 | this.shaderinjectpoint1 += 239 | "vec2 splatScale = vec2(" + 240 | this.splatScale.x + 241 | "," + 242 | this.splatScale.y + 243 | ");\r\n"; 244 | this.shaderinjectpoint1 += 245 | "vec2 scale = vec2(" + 246 | this.tileScale.x + 247 | "," + 248 | this.tileScale.y + 249 | ");\r\n"; 250 | 251 | //this.shaderinjectpoint3 += 'normalW = vec3(0.0, 1.0, 0.0);\r\n'; 252 | 253 | this.shaderinjectpoint3 += "vec4 finalColor1 = baseColor1;\r\n"; 254 | this.shaderinjectpoint3 += "vec3 finalNormal1 = baseNormal1;\r\n"; 255 | //this.shaderinjectpoint3 += 'finalColor1.a = 0.05;\r\n'; 256 | 257 | this.shaderinjectpoint4 += "vec4 finalColor1 = baseColor1;\r\n"; 258 | this.shaderinjectpoint4 += "float finalRough1 = baseRough1;\r\n"; 259 | this.shaderinjectpoint4 += "float finalMetallic1 = baseMetallic1;\r\n"; 260 | //this.shaderinjectpoint4 += 'finalColor1.a = 0.05;\r\n'; 261 | 262 | var v = 0.0, 263 | h = 0.0; 264 | for (let i = 0; i < this.totalTiles; i++) { 265 | var tpos = Math.floor(i / 4); 266 | h = tpos % this.numSplatTilesHorizontal; 267 | v = 268 | this.numSplatTilesHorizontal - 269 | 1 - 270 | Math.floor(tpos / this.numSplatTilesHorizontal); 271 | 272 | if (i < this.totalTiles - 1) { 273 | this.shaderinjectpoint3 += 274 | ` 275 | ` + 276 | "//vec4 finalColor" + 277 | (i + 2) + 278 | " = finalColor" + 279 | (i + 1) + 280 | ".a >= baseColor" + 281 | (i + 2) + 282 | ".a ? finalColor" + 283 | (i + 1) + 284 | " : baseColor" + 285 | (i + 2) + 286 | `; 287 | ` + 288 | "//vec4 finalColor" + 289 | (i + 2) + 290 | " = finalColor" + 291 | (i + 1) + 292 | " * (1.0 - baseColor" + 293 | (i + 2) + 294 | ".a) + baseColor" + 295 | (i + 2) + 296 | " * baseColor" + 297 | (i + 2) + 298 | `.a; 299 | ` + 300 | "vec4 finalColor" + 301 | (i + 2) + 302 | " = blend(finalColor" + 303 | (i + 1) + 304 | ", " + 305 | this.options.splatInfos.displScales[i].toFixed(5) + 306 | ", baseColor" + 307 | (i + 2) + 308 | ", " + 309 | this.options.splatInfos.displScales[i + 1].toFixed(5) + 310 | "); " + 311 | ` 312 | //finalColor` + 313 | (i + 2) + 314 | `.a *= 0.95; 315 | 316 | //vec3 finalNormal` + 317 | (i + 2) + 318 | " = finalColor" + 319 | (i + 1) + 320 | ".a >= baseColor" + 321 | (i + 2) + 322 | ".a ? finalNormal" + 323 | (i + 1) + 324 | " : baseNormal" + 325 | (i + 2) + 326 | `; 327 | vec3 finalNormal` + 328 | (i + 2) + 329 | " = blendNormal(finalColor" + 330 | (i + 1) + 331 | ", " + 332 | this.options.splatInfos.displScales[i].toFixed(5) + 333 | ", baseColor" + 334 | (i + 2) + 335 | ", " + 336 | this.options.splatInfos.displScales[i + 1].toFixed(5) + 337 | ", finalNormal" + 338 | (i + 1) + 339 | ", baseNormal" + 340 | (i + 2) + 341 | "); " + 342 | ` 343 | `; 344 | 345 | this.shaderinjectpoint4 += 346 | ` 347 | ` + 348 | "vec4 finalColor" + 349 | (i + 2) + 350 | " = blend(finalColor" + 351 | (i + 1) + 352 | ", " + 353 | this.options.splatInfos.displScales[i].toFixed(5) + 354 | ", baseColor" + 355 | (i + 2) + 356 | ", " + 357 | this.options.splatInfos.displScales[i + 1].toFixed(5) + 358 | "); " + 359 | ` 360 | float finalRough` + 361 | (i + 2) + 362 | " = finalColor" + 363 | (i + 1) + 364 | ".a >= baseColor" + 365 | (i + 2) + 366 | ".a ? finalRough" + 367 | (i + 1) + 368 | " : baseRough" + 369 | (i + 2) + 370 | `; 371 | float finalMetallic` + 372 | (i + 2) + 373 | " = finalColor" + 374 | (i + 1) + 375 | ".a >= baseColor" + 376 | (i + 2) + 377 | ".a ? finalMetallic" + 378 | (i + 1) + 379 | " : baseMetallic" + 380 | (i + 2) + 381 | `; 382 | `; 383 | } 384 | 385 | // Get basecolors from tiles 386 | this.shaderinjectpoint2 += 387 | "vec2 uv" + 388 | (i + 1) + 389 | " = vec2((vAlbedoUV.x + " + 390 | h + 391 | ".0) * splatScale.x, (vAlbedoUV.y + " + 392 | v + 393 | ".0) * splatScale.y);\r\n"; 394 | this.shaderinjectpoint2 += 395 | "mat4 chanInfo" + 396 | (i + 1) + 397 | " = colInfo(vAlbedoUV, uv" + 398 | (i + 1) + 399 | ", " + 400 | this.options.splatInfos.dedupScales[i].toFixed(5) + 401 | ", vec2(" + 402 | this.options.splatInfos.scales[i][0] + 403 | "," + 404 | this.options.splatInfos.scales[i][1] + 405 | "), vec2(" + 406 | this.options.splatInfos.positions[i][0] + 407 | "," + 408 | this.options.splatInfos.positions[i][1] + 409 | "), " + 410 | (i % 4) + 411 | ", scale, splatmap, albedoSampler, atlasNormalsSampler);\r\n"; 412 | //this.shaderinjectpoint2 += 'vec4 baseColor' + (i + 1) +' = col(vAlbedoUV, uv' + (i + 1) + ', vec2('+this.options.splatInfos.scales[i][0]+','+this.options.splatInfos.scales[i][1]+'), vec2('+this.options.splatInfos.positions[i][0] + ','+this.options.splatInfos.positions[i][1]+'), ' + (i % 4) + ', scale, splatmap, albedoSampler, bumpSampler);\r\n'; 413 | this.shaderinjectpoint2 += 414 | "vec4 baseColor" + (i + 1) + " = chanInfo" + (i + 1) + "[0];\r\n"; 415 | this.shaderinjectpoint2 += 416 | "vec3 baseNormal" + 417 | (i + 1) + 418 | " = vec3(chanInfo" + 419 | (i + 1) + 420 | "[1].x, chanInfo" + 421 | (i + 1) + 422 | "[1].y, chanInfo" + 423 | (i + 1) + 424 | "[1].z);\r\n"; 425 | 426 | this.shaderinjectpoint2 += 427 | "float baseRough" + 428 | (i + 1) + 429 | " = chanInfo" + 430 | (i + 1) + 431 | "[1].a * " + 432 | this.options.splatInfos.roughness[i].toFixed(5) + 433 | ";\r\n"; 434 | //this.shaderinjectpoint2 += "float baseRough" + (i + 1) +" = /*chanInfo" + (i + 1) + "[1].a * */ " + this.options.splatInfos.roughness[i].toFixed(5) + ";\r\n"; 435 | //this.shaderinjectpoint2 += "float baseRough" + (i + 1) +" = chanInfo" + (i + 1) + "[1].a; /* " + this.options.splatInfos.roughness[i].toFixed(5) + "; */\r\n"; 436 | 437 | this.shaderinjectpoint2 += 438 | "float baseMetallic" + 439 | (i + 1) + 440 | " = " + 441 | this.options.splatInfos.metallic[i].toFixed(5) + 442 | ";\r\n"; 443 | } 444 | 445 | //this.shaderinjectpoint3 += 'finalColor16 = col(vAlbedoUV, uv16, vec2(20.0, 20.0), vec2(1.0, 2.0), 0, scale, splatmap, albedoSampler);'; 446 | 447 | this.shaderinjectpoint3 += ` 448 | mat3 TBN = cotangent_frame(normalW, vPositionW, vAlbedoUV, vec2(-1.0, 1.0)); 449 | `; 450 | 451 | this.shaderinjectpoint3 += 452 | "normalW = TBN * finalNormal" + this.totalTiles + ";"; // TODO: adding these vectors is incorrect 453 | //this.shaderinjectpoint3 += "normalW = normalize(normalW * 0.75 + 0.25 * finalNormal" + (this.totalTiles) + ");"; // TODO: adding these vectors is incorrect 454 | //this.shaderinjectpoint3 += 'normalW = perturbNormal(cotangentFrame, finalNormal' + (this.totalTiles) + ', 1.0);'; 455 | //this.shaderinjectpoint3 += 'normalW = normalW;'; 456 | //this.shaderinjectpoint3 += 'normalW.y *= -1.0;'; 457 | //this.shaderinjectpoint3 += 'result = finalNormal' + (this.totalTiles) + ';'; 458 | this.shaderinjectpoint3 += 459 | "result = finalColor" + this.totalTiles + ".rgb;"; 460 | 461 | //this.shaderinjectpoint4 += 'normalW = normalW + finalNormal' + (this.totalTiles) + ';'; // TODO: adding these vectors is incorrect 462 | 463 | // MetallicRoughness.r is the computed metallicness 464 | // MetallicRoughness.g is the computed roughness 465 | this.shaderinjectpoint4 += 466 | "metallicRoughness.g = finalRough" + this.totalTiles + ";"; 467 | this.shaderinjectpoint4 += 468 | "metallicRoughness.r = finalMetallic" + this.totalTiles + ";"; 469 | //this.shaderinjectpoint4 += "metallicRoughness.r = 0.;"; 470 | //this.shaderinjectpoint4 += "metallicRoughness.r = 0.;"; 471 | //this.shaderinjectpoint4 += "metallicRoughness.g = 0.43;"; 472 | //this.shaderinjectpoint4 += "reflectivityOut.microSurface = 0.0;"; 473 | 474 | this.splatMap = splatMap; 475 | //this.needsUpdating = true; 476 | 477 | this.material = new PBRCustomMaterial("splatMaterial", scene); 478 | this.material.metallic = 0.0; // 0.0; 479 | this.material.roughness = 0.0; // 0.43 (asphalt); // 0.95; 480 | //this.material.indexOfRefraction = 1.4; 481 | 482 | //this.material.twoSidedLighting = true; 483 | //this.material.disableLighting = false; 484 | //this.material.ambientColor = new Color3(1.0, 1.0, 1.0); // Color3.Black(); 485 | //this.material.disableBumpMap = true; 486 | //this.material.metallicspecularColor = new Color3(0.15, 0.15, 0.15); // Color3.Black(); 487 | //this.material.specularIntensity = 1.0; 488 | //this.material.emissiveColor = new Color3(0.0, 0.0, 0.0); // Color3.Black(); 489 | //this.material.emissiveIntensity = 0.0; 490 | //this.material.usePhysicalLightFalloff= false; 491 | //this.material.environmentIntensity = 1.0; // This one is needed to avoid saturation due to env 492 | 493 | this.material.albedoTexture = atlas; 494 | if (atlasnormals !== null) { 495 | //this.material.bumpTexture = atlasnormals; 496 | this.atlasBumpTexture = atlasnormals; 497 | } 498 | this.material.AddUniform("splatmap", "sampler2D", {}); 499 | this.material.AddUniform("atlasNormalsSampler", "sampler2D", {}); 500 | 501 | this.material.Vertex_Before_PositionUpdated(` 502 | uvUpdated = vec2(positionUpdated.x, -positionUpdated.z); 503 | `); 504 | 505 | this.material.Vertex_MainEnd(` 506 | //uvUpdated = uvUpdated + 0.5; 507 | `); 508 | 509 | this.material.Fragment_Definitions(` 510 | //#define REFLECTION 511 | #define TANGENT 512 | //#define METALLICWORKFLOW 513 | `); 514 | 515 | this.material.Fragment_Begin( 516 | "precision highp float;\r\n" + 517 | "precision highp int;\r\n" + 518 | this.shaderinjectpoint1 + 519 | ` 520 | 521 | // From: https://github.com/BabylonJS/Babylon.js/blob/master/src/Shaders/ShadersInclude/bumpFragmentMainFunctions.fx 522 | 523 | vec3 perturbNormalBase(mat3 cotangentFrame, vec3 normal, float scale) 524 | { 525 | #ifdef NORMALXYSCALE 526 | normal = normalize(normal * vec3(scale, scale, 1.0)); 527 | #endif 528 | 529 | return normalize(cotangentFrame * normal); 530 | } 531 | 532 | vec3 perturbNormal(mat3 cotangentFrame, vec3 textureSample, float scale) 533 | { 534 | return perturbNormalBase(cotangentFrame, textureSample * 2.0 - 1.0, scale); 535 | } 536 | 537 | mat3 cotangent_frame(vec3 normal, vec3 p, vec2 uv, vec2 tangentSpaceParams) 538 | { 539 | // get edge vectors of the pixel triangle 540 | vec3 dp1 = dFdx(p); 541 | vec3 dp2 = dFdy(p); 542 | vec2 duv1 = dFdx(uv); 543 | vec2 duv2 = dFdy(uv); 544 | 545 | // solve the linear system 546 | vec3 dp2perp = cross(dp2, normal); 547 | vec3 dp1perp = cross(normal, dp1); 548 | vec3 tangent = dp2perp * duv1.x + dp1perp * duv2.x; 549 | vec3 bitangent = dp2perp * duv1.y + dp1perp * duv2.y; 550 | 551 | // invert the tangent/bitangent if requested 552 | tangent *= tangentSpaceParams.x; 553 | bitangent *= tangentSpaceParams.y; 554 | 555 | // construct a scale-invariant frame 556 | float invmax = inversesqrt(max(dot(tangent, tangent), dot(bitangent, bitangent))); 557 | return mat3(tangent * invmax, bitangent * invmax, normal); 558 | } 559 | 560 | // Functions 561 | 562 | float heightval(vec4 col) { 563 | //return ((col.r + col.g + col.b) / 3.0); 564 | return col.a; 565 | } 566 | 567 | /* 568 | vec4 blend(vec4 texture1, float displScale1, vec4 texture2, float displScale2) { 569 | return ((texture1.a * displScale1) > (texture2.a * displScale2) ? texture1 : texture2); 570 | } 571 | */ 572 | 573 | vec4 blend(vec4 texture1, float displScale1, vec4 texture2, float displScale2) { 574 | if (texture2.a == 0.) return texture1; 575 | if (texture1.a == 0.) return texture2; 576 | float a1 = texture1.a + displScale1; 577 | float a2 = texture2.a + displScale2; 578 | float depth = 0.2; 579 | float ma = max(texture1.a + a1, texture2.a + a2) - depth; 580 | 581 | float b1 = max(texture1.a + a1 - ma, 0.0); 582 | float b2 = max(texture2.a + a2 - ma, 0.0); 583 | 584 | vec4 result = (texture1 * b1 + texture2 * b2) / (b1 + b2); 585 | result.a = (texture1.a > 0. && texture2.a > 0.) ? (a1 + a2) / (2.0 * (b1 + b2)) : result.a; 586 | return result; 587 | } 588 | 589 | vec3 blendNormal(vec4 texture1, float displScale1, vec4 texture2, float displScale2, vec3 normal1, vec3 normal2) { 590 | float a1 = texture1.a + displScale1; 591 | float a2 = texture2.a + displScale2; 592 | float depth = 0.2; 593 | float ma = max(texture1.a + a1, texture2.a + a2) - depth; 594 | 595 | float b1 = max(texture1.a + a1 - ma, 0.0); 596 | float b2 = max(texture2.a + a2 - ma, 0.0); 597 | 598 | vec3 result = (normal1 * b1 + normal2 * b2) / (b1 + b2); 599 | result = normalize(result); 600 | 601 | return result; 602 | } 603 | 604 | 605 | vec2 hash22(vec2 p) 606 | { 607 | p = p * mat2(127.1, 311.7, 269.5, 183.3); 608 | p = -1.0 + 2.0 * fract(sin(p) * 43758.5453123); 609 | return sin(p * 6.283); // + timeScale 610 | } 611 | 612 | float interpolationNoise(vec2 p) 613 | { 614 | vec2 pi = floor(p); 615 | vec2 pf = p-pi; 616 | 617 | vec2 w = pf * pf * (3.-2. * pf); 618 | 619 | float f00 = dot(hash22(pi + vec2(.0,.0)), pf-vec2(.0,.0)); 620 | float f01 = dot(hash22(pi + vec2(.0,1.)), pf-vec2(.0,1.)); 621 | float f10 = dot(hash22(pi + vec2(1.0,0.)), pf-vec2(1.0,0.)); 622 | float f11 = dot(hash22(pi + vec2(1.0,1.)), pf-vec2(1.0,1.)); 623 | 624 | float xm1 = mix(f00,f10,w.x); 625 | float xm2 = mix(f01,f11,w.x); 626 | 627 | float ym = mix(xm1,xm2,w.y); 628 | return ym; 629 | 630 | } 631 | 632 | float perlinNoise2D(float x,float y) 633 | { 634 | int OCTAVES = 3; 635 | float persistence = 0.5; 636 | float sum = 0.0; 637 | float frequency = 0.0; 638 | float amplitude = 0.0; 639 | for(int i = 0; i < OCTAVES; i++) 640 | { 641 | frequency = pow(2.0, float(i)); 642 | amplitude = pow(persistence, float(i)); 643 | sum = sum + interpolationNoise(vec2(x * frequency, y * frequency)) * amplitude; 644 | } 645 | 646 | return sum; 647 | } 648 | ` + 649 | //+"vec4 col(vec2 vAlbedoUV, vec2 uvT, vec2 tile1Scale, vec2 tile1Position, int chanIdx, vec2 scale, sampler2D splatmap, sampler2D atlas, sampler2D atlasNormals) {" 650 | "mat4 colInfo(vec2 vAlbedoUV, vec2 uvT, float dedupScale, vec2 tile1Scale, vec2 tile1Position, int chanIdx, vec2 scale, sampler2D splatmap, sampler2D atlas, sampler2D atlasNormals) {" + 651 | ` 652 | float offsetInputScale = 2.0; 653 | float offsetBaseScale = 0.25; 654 | vec2 offset = vec2(perlinNoise2D(offsetInputScale * (uvT.x + 0.01 * tile1Position.x), offsetInputScale * (uvT.y + 0.1 * tile1Position.y)), 655 | perlinNoise2D(offsetInputScale * (uvT.x + 0.07 * tile1Position.y), offsetInputScale * (-uvT.y + 0.13 * tile1Position.x))); 656 | offset = offset * offsetBaseScale * dedupScale; 657 | ` + 658 | // if this.dedupDouble... 659 | /* 660 | + ` 661 | vec2 offset2 = vec2(perlinNoise2D(uvT.y - 0.08 * tile1Position.x, uvT.x + 0.04 * tile1Position.y), 662 | perlinNoise2D(uvT.y + 0.05 * tile1Position.y, -uvT.x + 0.19 * tile1Position.x)); 663 | offset2 = offset * offsetBaseScale * dedupScale; 664 | ` 665 | */ 666 | 667 | "vec2 scaledUv1 = fract((vAlbedoUV + offset) * tile1Scale);" + // Curvy antitiling factor 668 | "scaledUv1 = scaledUv1 * (254.0/256.0) + vec2(1.0 / 256.0, 1.0 / 256.0);" + 669 | "vec2 uv1 = vec2((scaledUv1.x + tile1Position.x) * scale.x, (scaledUv1.y + tile1Position.y) * scale.y);" + 670 | /* 671 | +'vec2 scaledUv2 = fract((vAlbedoUV + offset2) * tile1Scale).yx;' // Curvy antitiling factor 672 | +'scaledUv2 = scaledUv2 * (254.0/256.0) + vec2(1.0 / 256.0, 1.0 / 256.0);' 673 | +'vec2 uv2 = vec2((scaledUv2.x + tile1Position.x) * scale.x, (scaledUv2.y + tile1Position.y) * scale.y);' 674 | */ 675 | 676 | ` 677 | vec4 splatColor1 = texture2D(splatmap, uvT); 678 | ` + 679 | ` 680 | float dedupMix = perlinNoise2D(100.0 * uvT.x, 100.0 * uvT.y); 681 | dedupMix = dedupScale > 0.0 ? smoothstep(-0.02, 0.02, dedupMix) : 0.0; 682 | 683 | //vec4 diffuse1Color = texture2DLodEXT(atlas, uv1, -1.0); 684 | vec4 diffuseColorA = texture2D(atlas, uv1); 685 | 686 | //vec4 diffuseColorB = texture2D(atlas, uv2); 687 | 688 | //vec4 diffuse1Color = vec4(dedupMix, 0.0, 0.0, 1.0); // Debug dedup mix factor 689 | //vec4 diffuse1Color = diffuseColorA * dedupMix + diffuseColorB * (1.0 - dedupMix); 690 | vec4 diffuse1Color = diffuseColorA; 691 | 692 | vec4 diffuseNormalA = texture2D(atlasNormals, uv1); 693 | diffuseNormalA.rgb = (diffuseNormalA.rgb * 2.0 - 1.0); 694 | //vec4 diffuseNormalB = texture2D(atlasNormals, uv2); 695 | //diffuseNormalB.rgb = (diffuseNormalB.rgb * 2.0 - 1.0); 696 | 697 | /* 698 | vec3 diffuse1NormalVec = normalize(diffuseNormalA.xyz * dedupMix + diffuseNormalB.xyz * (1.0 - dedupMix)); 699 | float diffuse1NormalAlpha = diffuseNormalA.a * dedupMix + diffuseNormalB.a * (1.0 - dedupMix); 700 | vec4 diffuse1Normal = vec4(diffuse1NormalVec.x, diffuse1NormalVec.y, diffuse1NormalVec.z, diffuse1NormalAlpha); 701 | */ 702 | 703 | vec4 diffuse1Normal = diffuseNormalA; 704 | ` + 705 | ` 706 | float blend = (chanIdx == 0 ? splatColor1.r : (chanIdx == 1 ? splatColor1.g : (chanIdx == 2 ? splatColor1.b : splatColor1.a))); 707 | //blend = 1.0; 708 | 709 | //diffuse1Color.rgb = splatColor1.rgb; 710 | //diffuse1Color.a = blend; 711 | 712 | //diffuse1Color.a = ((blend > 0.0) ? (heightval(diffuse1Color) * blend) : 0.0); 713 | diffuse1Color.a = ((blend > 0.0) ? (diffuse1Color.a * blend) : 0.0); 714 | 715 | mat4 chanInfo = mat4(diffuse1Color, vec4(diffuse1Normal.x, diffuse1Normal.y, diffuse1Normal.z, diffuse1Normal.a), vec4(0.0), vec4(0.0)); 716 | 717 | ` + 718 | //+"return diffuse1Color;" 719 | "return chanInfo;" + 720 | "} ", 721 | ); 722 | 723 | this.material.Fragment_MainBegin(this.shaderinjectpoint2); 724 | 725 | this.material.Fragment_Custom_Albedo(this.shaderinjectpoint3); 726 | 727 | this.material.Fragment_Custom_MetallicRoughness(this.shaderinjectpoint4); 728 | 729 | return this.material; 730 | } 731 | 732 | bind(mesh: AbstractMesh): void { 733 | //console.debug("Binding mesh for TerrainMaterial."); 734 | if (!this.material.getEffect()) return; 735 | this.material.getEffect().setTexture("splatmap", this.splatMap); 736 | this.material 737 | .getEffect() 738 | .setTexture("atlasNormalsSampler", this.atlasBumpTexture); 739 | 740 | /* 741 | 742 | //this.material.freeze(); 743 | 744 | //this.material.reflectionTexture = this.envReflectionProbe.cubeTexture; 745 | //this.sceneViewer.scene.environmentTexture = this.sceneViewer.envReflectionProbe.cubeTexture; 746 | //this.sceneViewer.scene.environmentTexture = this.sceneViewer.envReflectionProbe!.cubeTexture; 747 | 748 | //this.material.detailMap.texture = this.sceneViewer.textureDetailSurfaceImp; 749 | this.material.useHorizonOcclusion = true; 750 | if (this.sceneViewer.envReflectionProbe) { 751 | const reflectionTexture = this.sceneViewer.envReflectionProbe.cubeTexture; 752 | //this.material.reflectionTexture = reflectionTexture; 753 | //this.material.getEffect().setTexture( "reflectionSampler", reflectionTexture); 754 | this.material.getEffect().setTexture("reflectionSampler", reflectionTexture._lodTextureMid || reflectionTexture); 755 | this.material.getEffect().setTexture("reflectionSamplerLow", reflectionTexture._lodTextureLow || reflectionTexture); 756 | this.material.getEffect().setTexture("reflectionSamplerHigh", reflectionTexture._lodTextureHigh || reflectionTexture); 757 | //this.material.getEffect().setTexture("environmentBrdfSampler", TerrainMaterialWrapper.terrainMaterialShared!._environmentBRDFTexture); 758 | } 759 | */ 760 | 761 | /* 762 | const ubo = this.material['_uniformBuffer']; 763 | ubo.update(); 764 | console.debug("Done"); 765 | */ 766 | } 767 | } 768 | 769 | export { TerrainMaterialWrapper }; 770 | -------------------------------------------------------------------------------- /src/render/materials/TextMaterial.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { PBRCustomMaterial } from "@babylonjs/materials"; 8 | import { BaseTexture, Color3, Scene, Texture, Vector2 } from "@babylonjs/core"; 9 | import { SceneViewer } from "SceneViewer"; 10 | 11 | /** 12 | */ 13 | class TextMaterialWrapper { 14 | 15 | sceneViewer: SceneViewer; 16 | material: PBRCustomMaterial; 17 | 18 | options: any; 19 | 20 | constructor(sceneViewer: SceneViewer, atlasTexture: BaseTexture, atlasNormalTexture: BaseTexture | null, options: any = null) { 21 | this.sceneViewer = sceneViewer; 22 | this.options = options; 23 | this.material = this.initMaterial( this.sceneViewer.scene, atlasTexture, atlasNormalTexture); 24 | } 25 | 26 | initMaterial(scene: Scene, atlas: BaseTexture, atlasnormals: BaseTexture | null): PBRCustomMaterial { 27 | 28 | this.material = new PBRCustomMaterial("TextMaterial", scene); 29 | this.material.metallic = 0.05; // 0.0; 30 | this.material.roughness = 0.975; // 0.43 (asphalt); // 0.95; 31 | //this.material.indexOfRefraction = 1.4; 32 | 33 | //this.material.twoSidedLighting = true; 34 | //this.material.disableLighting = false; 35 | //this.material.ambientColor = new Color3(1.0, 1.0, 1.0); // Color3.Black(); 36 | //this.material.disableBumpMap = true; 37 | //this.material.metallicspecularColor = new Color3(0.15, 0.15, 0.15); // Color3.Black(); 38 | //this.material.specularIntensity = 1.0; 39 | //this.material.emissiveColor = new Color3(0.0, 0.0, 0.0); // Color3.Black(); 40 | //this.material.emissiveIntensity = 0.0; 41 | //this.material.usePhysicalLightFalloff= false; 42 | //this.material.environmentIntensity = sceneViewer.baseEnvironmentIntensity; // This one is needed to avoid saturation due to env 43 | 44 | this.material.albedoTexture = atlas; 45 | if (atlasnormals !== null) { 46 | //this.material.bumpTexture = atlasnormals; 47 | this.material.bumpTexture = atlasnormals; 48 | } 49 | 50 | this.material.Fragment_Custom_Albedo(` 51 | vec3 txtColor = result; 52 | if (txtColor.r < 0.02) { 53 | discard; 54 | } 55 | //vec3 col = vec3(1.0, 0.8, 0.8); 56 | //vec3 col = vec3(1.0, 1.0, 1.0) * tmp; 57 | //col = vec3(col.r * result.r, col.g * result.g, col.b * result.b); 58 | vec3 col = vec3(0.0, 0.0, 0.0); 59 | surfaceAlbedo = col; 60 | `); 61 | 62 | /* 63 | this.material.onBindObservable.add(() => { 64 | this.update(); 65 | }); 66 | */ 67 | 68 | return this.material; 69 | } 70 | 71 | update(): void { 72 | //this.material.getEffect().setTexture( "splatmap", this.splatMap ); 73 | //this.material.getEffect().setTexture( "atlasNormalsSampler", this.atlasBumpTexture ); 74 | 75 | //this.material.reflectionTexture = this.envReflectionProbe.cubeTexture; 76 | //this.material.reflectionTexture = this.scene.environmentTexture; 77 | //this.sceneViewer.scene.environmentTexture = this.sceneViewer.envReflectionProbe.cubeTexture; 78 | //this.scene.environmentTexture = this.envReflectionProbe.cubeTexture; 79 | } 80 | 81 | } 82 | 83 | export { TextMaterialWrapper }; 84 | -------------------------------------------------------------------------------- /src/render/pipeline/DefaultRenderPipeline.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { 8 | Camera, 9 | Color3, 10 | ColorCurves, 11 | DefaultRenderingPipeline, 12 | ImageProcessingPostProcess, 13 | LensRenderingPipeline, 14 | Scene, 15 | } from "@babylonjs/core"; 16 | import { SceneViewer } from "SceneViewer"; 17 | import { ViewerState } from "ViewerState"; 18 | 19 | /** 20 | * Rendering and pipeline configuration 21 | * (effects, shadows...) 22 | */ 23 | class DefaultRenderPipeline { 24 | protected dddViewer: SceneViewer; 25 | 26 | protected scene: Scene; 27 | 28 | protected viewerState: ViewerState; 29 | 30 | constructor(dddViewer: SceneViewer) { 31 | // Reference to DDDViewer 32 | this.dddViewer = dddViewer; 33 | this.viewerState = dddViewer.viewerState; 34 | this.scene = dddViewer.scene; 35 | 36 | // Babylon camera which we are controlling 37 | //this.camera = dddViewer.camera; 38 | } 39 | 40 | update(deltaTime: number): void {} 41 | 42 | initialize(): void {} 43 | 44 | dispose(): void {} 45 | 46 | scenePostprocessingSetEnabled(value: boolean): void { 47 | this.viewerState.scenePostprocessingEnabled = value; 48 | //localStorage.setItem('dddScenePostprocessingSetEnabled', value); 49 | //alert('Reload the viewer for changes to take effect.'); 50 | this.updateRenderPipeline(); 51 | } 52 | 53 | updateRenderPipeline(): void { 54 | this.scene.postProcessesEnabled = 55 | this.viewerState.scenePostprocessingEnabled; 56 | 57 | if (!this.viewerState.scenePostprocessingEnabled) { 58 | return; 59 | } 60 | } 61 | 62 | initRenderPipeline(): void { 63 | // Postprocess 64 | // The default pipeline applies other settings, we'd better off using Bloom independently if possible 65 | // Also note this is tied to the camera, and thus if used, this should be updated when the camera changes 66 | const defaultPipeline = new DefaultRenderingPipeline( 67 | "default", 68 | true, 69 | this.scene, 70 | [ this.dddViewer.camera ], 71 | ); 72 | defaultPipeline.fxaaEnabled = true; 73 | defaultPipeline.bloomEnabled = true; 74 | defaultPipeline.bloomWeight = 1.0; // 1.5 is exagerated but maybe usable for pics 75 | //defaultPipeline.cameraFov = this.camera.fov; 76 | defaultPipeline.imageProcessing.toneMappingEnabled = true; 77 | 78 | //var postProcessHighlights = new HighlightsPostProcess("highlights", 0.1, camera); 79 | //var postProcessTonemap = new TonemapPostProcess("tonemap", TonemappingOperator.Hable, 1.2, camera); 80 | 81 | // See: https://doc.babylonjs.com/divingDeeper/postProcesses/postProcessRenderPipeline 82 | /* 83 | var standardPipeline = new PostProcessRenderPipeline(this.engine, "standardPipeline"); 84 | var effectBloom = new BloomEffect(this.scene, 4, 5.0, 2.0); 85 | //var effectDepthOfField = new DepthOfFieldEffect(this.scene); 86 | var postProcessChain = new PostProcessRenderEffect(this.engine, "postProcessChain", function() { return [effectBloom, effectDepthOfField] }); 87 | standardPipeline.addEffect(effectBloom); 88 | this.scene.postProcessRenderPipelineManager.addPipeline(standardPipeline); 89 | */ 90 | 91 | // Screen space reflections 92 | /* 93 | const ssr = new ScreenSpaceReflectionPostProcess( 94 | "ssr", // The name of the post-process 95 | this.scene, // The scene where to add the post-process 96 | 1.0, // The ratio of the post-process 97 | this.camera // To camera to attach the post-process 98 | ); 99 | ssr.reflectionSamples = 32; // Low quality. 100 | ssr.strength = 2; // Set default strength of reflections. 101 | ssr.reflectionSpecularFalloffExponent = 3; // Attenuate the reflections a little bit. (typically in interval [1, 3]) 102 | */ 103 | 104 | const lensRenderingPipeline = new LensRenderingPipeline( 105 | "lens", 106 | { 107 | edge_blur: 0.25, // 1.0 is too distorted in the borders for walk/view mode (maybe for pictures) 108 | chromatic_aberration: 1.0, 109 | distortion: 0.7, // (dilate effect) 0.5 -> subtle 110 | dof_focus_distance: 60, 111 | dof_aperture: 1.0, // 1.2 is already too blurry for OSM, 6.0 is very high 112 | grain_amount: 0.0, // 0.5, 113 | dof_pentagon: false, // true, 114 | dof_gain: 1.0, 115 | dof_threshold: 1.0, 116 | dof_darken: 0.25, 117 | }, 118 | this.scene, 119 | 1.0, 120 | [ this.dddViewer.camera ], 121 | ); 122 | //this.scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline('lensEffects', camera); 123 | 124 | /* 125 | const ssao = new SSAO2RenderingPipeline('ssao', this.scene, { 126 | ssaoRatio: .5, 127 | blurRatio: 1 128 | }, [ this.camera ], true) 129 | */ 130 | 131 | const curve = new ColorCurves(); 132 | curve.globalHue = 0; 133 | curve.globalDensity = 80; 134 | curve.globalSaturation = 5; 135 | curve.highlightsHue = 0; 136 | curve.highlightsDensity = 80; 137 | curve.highlightsSaturation = 40; 138 | curve.shadowsHue = 0; 139 | curve.shadowsDensity = 80; 140 | curve.shadowsSaturation = 40; 141 | this.scene.imageProcessingConfiguration.colorCurvesEnabled = true; 142 | this.scene.imageProcessingConfiguration.colorCurves = curve; 143 | const postProcess = new ImageProcessingPostProcess( 144 | "processing", 145 | 1.0, 146 | this.dddViewer.camera, 147 | ); 148 | 149 | // Fog 150 | //this.scene.fogMode = Scene.FOGMODE_EXP; 151 | //this.scene.fogDensity = 0.005; // default is 0.1 152 | this.scene.fogMode = Scene.FOGMODE_LINEAR; 153 | this.scene.fogStart = 350.0; 154 | this.scene.fogEnd = 700.0; 155 | this.scene.fogColor = new Color3(0.75, 0.75, 0.85); 156 | 157 | /* 158 | pixels = rp.cubeTexture.readPixels(0,0) 159 | // i take the first pixel of the reflection probe texture for fog color. 160 | // since pixels are stored as buffer array, first pixel are first 4 values of array [r,g,b,a....] 161 | scene.fogColor = new Color3(pixels[0]/255, pixels[1]/255, pixels[2]/255) 162 | */ 163 | } 164 | } 165 | 166 | export { DefaultRenderPipeline }; 167 | -------------------------------------------------------------------------------- /src/render/skybox/CubemapSkybox.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { Skybox } from "./Skybox"; 10 | 11 | /** 12 | * A skybox based on a cubemap. 13 | */ 14 | class CubemapSkybox extends Skybox { 15 | 16 | update(deltaTime: number): void { 17 | throw new Error("Method not implemented."); 18 | } 19 | 20 | } 21 | 22 | export { CubemapSkybox }; -------------------------------------------------------------------------------- /src/render/skybox/DynamicSkybox.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | import { Skybox } from "./Skybox"; 10 | 11 | /** 12 | * A skybox based on a shader. 13 | */ 14 | class DynamicSkybox extends Skybox { 15 | 16 | update(deltaTime: number): void { 17 | throw new Error("Method not implemented."); 18 | } 19 | 20 | } 21 | 22 | export { DynamicSkybox }; -------------------------------------------------------------------------------- /src/render/skybox/Skybox.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * DDDViewer - DDD(3Ds) Viewer library for DDD-generated GIS 3D scenes 3 | * Copyright 2021 Jose Juan Montes and Contributors 4 | * MIT License (see LICENSE file) 5 | */ 6 | 7 | import { Camera } from "@babylonjs/core"; 8 | import { SceneViewer } from "SceneViewer"; 9 | 10 | /** 11 | * DDD Viewer base skybox class. 12 | */ 13 | abstract class Skybox { 14 | 15 | protected dddViewer: SceneViewer; 16 | 17 | constructor(dddViewer: SceneViewer) { 18 | 19 | // Reference to DDDViewer 20 | this.dddViewer = dddViewer; 21 | 22 | // Babylon camera which we are controlling 23 | //this.camera = dddViewer.camera; 24 | } 25 | 26 | abstract update(deltaTime: number): void; 27 | 28 | } 29 | 30 | export { Skybox }; -------------------------------------------------------------------------------- /test/blah.test.ts.disabled: -------------------------------------------------------------------------------- 1 | // import { sum } from "../src"; 2 | 3 | // describe( "blah", () => { 4 | // it( "works", () => { 5 | // expect( sum( 1, 1 )).toEqual( 2 ); 6 | // }); 7 | // }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": [ 4 | "src" 5 | //"types" 6 | ], 7 | "compilerOptions": { 8 | "baseUrl": "src", 9 | /*"paths": { 10 | "./main/*": ["main/*"], 11 | "./layers/*": ["layers/*"], 12 | "./loading/*": ["loading/*"], 13 | "./process/*": ["process/*"], 14 | "./render/*": ["render/*"], 15 | },*/ 16 | "target": "es5", 17 | "module": "esnext", 18 | "types": [ 19 | "ol", 20 | "proj4", 21 | "suncalc" 22 | ], 23 | "lib": [ 24 | "DOM", 25 | "ESNext" 26 | ], 27 | "importHelpers": true, 28 | // output .d.ts declaration files for consumers 29 | "declaration": true, 30 | // output .js.map sourcemap files for consumers 31 | "sourceMap": true, 32 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 33 | "rootDir": "./src", 34 | // stricter type-checking for stronger correctness. Recommended by TS 35 | "strict": true, 36 | // linter checks for common issues 37 | "noImplicitReturns": true, 38 | "noFallthroughCasesInSwitch": true, 39 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 40 | "noUnusedLocals": false, 41 | "noUnusedParameters": false, 42 | // use Node's module resolution algorithm, instead of the legacy TS one 43 | "moduleResolution": "node", 44 | // transpile JSX to React.createElement 45 | //"jsx": "react", 46 | // interop between ESM and CJS modules. Recommended by TS 47 | "esModuleInterop": true, 48 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 49 | "skipLibCheck": true, 50 | // error out if import and file system have a casing mismatch. Recommended by TS 51 | "forceConsistentCasingInFileNames": true, 52 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 53 | "noEmit": true 54 | } 55 | } 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | // Not transpiled with TypeScript or Babel, so use plain Es6/Node.js! 2 | const replace = require("@rollup/plugin-replace"); 3 | 4 | module.exports = { 5 | // This function will run for each entry/format/env combination 6 | rollup(config, opts) { 7 | 8 | // TODO: See https://github.com/formium/tsdx to see if options currently package.json 9 | 10 | // Added to workaround deprecation warning in @rollup/plugin-replace about 'preventAssignment' default value change 11 | config.plugins = config.plugins.map(p => 12 | p.name === "replace" 13 | ? replace({ "process.env.NODE_ENV": JSON.stringify(opts.env), preventAssignment: true, }) 14 | : p 15 | ); 16 | 17 | // In order to bundle external deps in UMD format: 18 | // From: https://github.com/formium/tsdx/issues/179 19 | // and: https://github.com/formium/tsdx/issues/898 20 | if (config.output.format === "umd") { 21 | const origExternal = config.external; 22 | config.external = (id) => { 23 | //console.log(id); 24 | //if (id.startsWith("@babylonjs")) return true; 25 | //return origExternal(id); 26 | return false; // Embed all libraries by default 27 | }; 28 | //config.output.globals["babylon"] = "BABYLON"; 29 | } 30 | 31 | // Return final customized config for the plugin 32 | return config; 33 | }, 34 | }; 35 | 36 | --------------------------------------------------------------------------------