├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── README.md ├── RELEASE.md ├── babel.config.js ├── demos ├── boxes │ ├── app.ts │ ├── index.html │ ├── package.json │ └── rollup.config.js ├── hello-world │ ├── app.ts │ ├── index.html │ ├── package.json │ └── rollup.config.js └── playground │ ├── app.ts │ ├── index.html │ ├── package.json │ └── rollup.config.js ├── jest.config.js ├── package.json ├── renovate.json ├── rollup.config.js ├── src ├── -private │ ├── babylon-manager.ts │ ├── ecsy-types.ts │ ├── systems │ │ ├── factory-array.ts │ │ ├── factory.ts │ │ └── with-core.ts │ └── utils │ │ ├── assign.ts │ │ ├── debug.ts │ │ └── guid.ts ├── components │ ├── _instance-array.ts │ ├── _instance.ts │ ├── action.ts │ ├── babylon-core.ts │ ├── camera.ts │ ├── camera │ │ ├── arc-rotate.ts │ │ └── target.ts │ ├── index.ts │ ├── light.ts │ ├── light │ │ ├── _light.ts │ │ ├── _shadow.ts │ │ ├── directional.ts │ │ ├── hemispheric.ts │ │ ├── point.ts │ │ └── spot.ts │ ├── material.ts │ ├── material │ │ ├── background.ts │ │ ├── pbr.ts │ │ ├── shadow-only.ts │ │ └── standard.ts │ ├── mesh.ts │ ├── parent.ts │ ├── pivot-point.ts │ ├── position.ts │ ├── post-process-render-pipeline.ts │ ├── post-process-render-pipeline │ │ ├── default.ts │ │ └── ssao.ts │ ├── post-process.ts │ ├── post-process │ │ ├── black-and-white.ts │ │ ├── blur.ts │ │ └── motion-blur.ts │ ├── primitive │ │ ├── box.ts │ │ ├── lines.ts │ │ ├── plane.ts │ │ └── sphere.ts │ ├── rotation.ts │ ├── scale.ts │ ├── shadow-generator.ts │ ├── transform-node.ts │ ├── transitions.ts │ └── xr │ │ └── default.ts ├── index.ts ├── systems │ ├── action.ts │ ├── babylon.ts │ ├── camera.ts │ ├── camera │ │ ├── arc-rotate.ts │ │ └── target.ts │ ├── index.ts │ ├── light.ts │ ├── light │ │ ├── directional.ts │ │ ├── hemispheric.ts │ │ ├── point.ts │ │ └── spot.ts │ ├── material.ts │ ├── material │ │ ├── background.ts │ │ ├── pbr.ts │ │ ├── shadow-only.ts │ │ └── standard.ts │ ├── mesh.ts │ ├── post-process-render-pipeline.ts │ ├── post-process-render-pipeline │ │ ├── default.ts │ │ └── ssao.ts │ ├── post-process.ts │ ├── post-process │ │ ├── black-and-white.ts │ │ ├── blur.ts │ │ └── motion-blur.ts │ ├── primitive │ │ ├── box.ts │ │ ├── lines.ts │ │ ├── plane.ts │ │ └── sphere.ts │ ├── shadow.ts │ ├── transform.ts │ ├── transition.ts │ └── xr │ │ └── default.ts ├── types.ts └── world.ts ├── test ├── babylon.test.ts ├── camera.test.ts ├── helpers │ ├── setup-world.ts │ └── wait.ts ├── index.test.ts ├── light.test.ts ├── material.test.ts ├── mesh.test.ts ├── post-process-render-pipeline.test.ts ├── post-processing.test.ts ├── primitive.test.ts ├── shadow.test.ts ├── transform.test.ts └── transition.test.ts ├── tsconfig.build.json ├── tsconfig.json ├── tsconfig.rollup.json ├── types └── global.d.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | #root = true 2 | 3 | [*] 4 | indent_style = space 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 100 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | jest.config.js 3 | rollup.config.js 4 | babel.config.js 5 | 6 | # compiled output 7 | /dist/ 8 | /docs/ 9 | 10 | # dependencies 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | build 17 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | 4 | parser: "@typescript-eslint/parser" 5 | 6 | parserOptions: 7 | ecmaVersion: 2018 8 | sourceType: module 9 | 10 | project: 11 | - "./tsconfig.json" 12 | 13 | plugins: 14 | - "@typescript-eslint" 15 | - prettier 16 | - jest 17 | 18 | extends: 19 | - eslint:recommended 20 | - plugin:@typescript-eslint/eslint-recommended 21 | - plugin:@typescript-eslint/recommended 22 | - plugin:@typescript-eslint/recommended-requiring-type-checking 23 | - standard 24 | 25 | - plugin:jest/recommended 26 | - plugin:jest/style 27 | 28 | # This one should come last 29 | - plugin:prettier/recommended 30 | 31 | rules: 32 | prettier/prettier: error 33 | # this prevents method overloading, and tsc will fail for duplicate class members anyway 34 | "no-dupe-class-members": off 35 | # explicit any will silently disable a lot of type checking, try to abstain as much as possible! 36 | "@typescript-eslint/no-explicit-any": error 37 | "@typescript-eslint/no-unused-vars": [error, {argsIgnorePattern: "^_"}] 38 | "@typescript-eslint/no-unused-expressions": "error" 39 | "@typescript-eslint/no-non-null-assertion": off 40 | no-use-before-define: off 41 | no-restricted-imports: 42 | - error 43 | - name: "@babylonjs/core" 44 | message: "Don't import from @babylonjs/core, use a direct import instead! See https://doc.babylonjs.com/features/es6_support#tree-shaking" 45 | - name: "@babylonjs/materials" 46 | message: "Don't import from @babylonjs/materials, use a direct import instead! See https://doc.babylonjs.com/features/es6_support#tree-shaking" 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - renovate/* 8 | tags: 9 | - '*' 10 | pull_request: 11 | schedule: 12 | - cron: '0 8 * * 1' # Mondays at 8am 13 | 14 | jobs: 15 | test: 16 | name: Tests 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | - name: Setup node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 12 26 | - name: Install dependencies 27 | uses: bahmutov/npm-install@v1 28 | - name: Lint 29 | run: yarn lint 30 | - name: Test 31 | run: yarn test 32 | build: 33 | name: Build 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v3 39 | - name: Setup node.js 40 | uses: actions/setup-node@v3 41 | with: 42 | node-version: 12 43 | - name: Install dependencies 44 | uses: bahmutov/npm-install@v1 45 | - name: Build 46 | run: yarn build 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | compiled 10 | .awcache 11 | .rpt2_cache 12 | docs 13 | build 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /demos 2 | /src 3 | /types 4 | /test 5 | 6 | # misc 7 | /.dependabot 8 | /.github 9 | /.editorconfig 10 | /.env* 11 | /.eslintignore 12 | /.eslintrc.yml 13 | /.git/ 14 | /.gitignore 15 | /babel.config.js 16 | /yarn.lock 17 | /jest.config.js 18 | /rollup.config.js 19 | /tsconfig.* 20 | .gitkeep 21 | .idea 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecsy-babylon 2 | 3 | [![CI](https://github.com/kaliber5/ecsy-babylon/actions/workflows/ci.yml/badge.svg)](https://github.com/kaliber5/ecsy-babylon/actions/workflows/ci.yml) 4 | 5 | ecsy-babylon is an experimental implementation of [ECSY](https://ecsy.io/) in [babylon.js](https://www.babylonjs.com/). 6 | 7 | ## Example 8 | 9 | In the spirit of learning-by-doing lets walk through how a simple babylon app would be converted to ecsy-babylon. 10 | 11 | ### Vanilla babylon.js 12 | 13 | Consider the following code: 14 | 15 | ```ts 16 | // initialize the core elements 17 | const canvas = document.getElementsByTagName("canvas")[0]; 18 | const engine = new BABYLON.Engine(canvas, true); 19 | const scene = new BABYLON.Scene(engine); 20 | 21 | // create objects to inhabit the scene 22 | const camera = new BABYLON.ArcRotateCamera("camera", 23 | -Math.PI / 2, Math.PI / 2.5, 3, new BABYLON.Vector3(0, 0, 0), scene); 24 | camera.attachControl(canvas, true); 25 | const light = new BABYLON.HemisphericLight("light", 26 | new BABYLON.Vector3(0, 1, 0), scene); 27 | const box = BABYLON.MeshBuilder.CreateBox("box", {}, scene); 28 | 29 | 30 | // set variables 31 | const freq = Math.PI; //0.5 Hz 32 | const amp = 0.1; 33 | 34 | // apply behaviour on each frame 35 | engine.runRenderLoop(() => { 36 | box.position.y = Math.sin(Date.now() * 0.001 * freq) * amp; 37 | scene.render(); 38 | }); 39 | 40 | ``` 41 | 42 | We create a scene and then a box to bob up and down within it. At the moment our app is fairly simple but as it expands we will need to consider how to manage the increasing complexity. 43 | 44 | ### ecsy-babylon 45 | 46 | ECS design will help us to write more organized apps by introducing some strict rules as to how they shall be structured: 47 | 1. Conceptual elements of the app are organized into **entities** 48 | 2. All game state and data are fields of **components** 49 | 3. All behaviour exists in **systems** 50 | 51 | For more information please check out the [ECSY architecture docs](https://ecsy.io/docs/#/manual/Architecture). 52 | 53 | In keeping with rule 1, lets use ecsy-babylon to convert our existing objects to components on entities. 54 | 55 | ```ts 56 | import { 57 | ArcRotateCamera, 58 | BabylonCore, 59 | Box, 60 | components, 61 | HemisphericLight, 62 | Parent, 63 | Position, 64 | systems, 65 | World, 66 | } from 'ecsy-babylon'; 67 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 68 | import { Component, Types, System } from 'ecsy'; 69 | 70 | // ... 71 | 72 | const world = new World(); 73 | components.forEach((component) => world.registerComponent(component)); 74 | systems.forEach((system) => world.registerSystem(system)); 75 | 76 | world.registerComponent(BoxMoveComponent).registerSystem(BoxMoveSystem); 77 | 78 | world.createEntity('singleton').addComponent(BabylonCore, { 79 | world, 80 | canvas: document.getElementsByTagName('canvas')[0], 81 | }); 82 | 83 | world 84 | .createEntity('camera') 85 | .addComponent(Parent) 86 | .addComponent(ArcRotateCamera, { 87 | alpha: -Math.PI / 2, 88 | beta: Math.PI / 2.5, 89 | radius: 3, 90 | target: new Vector3(0, 0, 0), 91 | }); 92 | 93 | world 94 | .createEntity('light') 95 | .addComponent(Parent) 96 | .addComponent(HemisphericLight, { 97 | direction: new Vector3(0, 1, 0), 98 | }); 99 | 100 | world 101 | .createEntity('box') 102 | .addComponent(Parent) 103 | .addComponent(Position) 104 | .addComponent(Box) 105 | .addComponent(BoxMoveComponent, { 106 | freq: Math.PI, // 0.5 Hz 107 | amp: 0.1, 108 | }); 109 | 110 | world.execute(0, 0); 111 | ``` 112 | 113 | Now we need to implement our BoxMoveComponent 114 | 115 | ```ts 116 | class BoxMoveComponent extends Component{ 117 | freq!: number 118 | amp!: number 119 | static schema = { 120 | freq: { 121 | type: Types.Number, 122 | default: Math.PI * 2 123 | }, 124 | amp: { 125 | type: Types.Number, 126 | default: 1 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | Finaly our BoxMoveSystem will manage state 133 | 134 | ```ts 135 | class BoxMoveSystem extends System { 136 | 137 | execute(): void { 138 | this.queries.movableBoxes.results.forEach(entity => { 139 | const boxMove = entity.getComponent(BoxMoveComponent)!; 140 | const position = entity.getMutableComponent(ecsyBabylon.Position)! 141 | position.value.y = Math.sin(Date.now() * 0.001 * boxMove.freq) * boxMove.amp; 142 | }) 143 | } 144 | static queries = { 145 | movableBoxes: { 146 | components: [BoxMoveComponent, ecsyBabylon.Position] 147 | } 148 | } 149 | } 150 | ``` 151 | 152 | A couple of notes regarding the above example: 153 | - Every ecsy-babylon app requires a singleton entity with a `BabylonCore` component 154 | - All objects placed in the scene require a `Parent` component 155 | 156 | Feel free to check out the demo's complete [source code](demos/hello-world). 157 | 158 | ## Further Reading 159 | - [ecsy documentation](https://ecsy.io/docs/#/) 160 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | 8 | ## Preparation 9 | 10 | Since the majority of the actual release process is automated, the primary 11 | remaining task prior to releasing is confirming that all pull requests that 12 | have been merged since the last release have been labeled with the appropriate 13 | `lerna-changelog` labels and the titles have been updated to ensure they 14 | represent something that would make sense to our users. Some great information 15 | on why this is important can be found at 16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 17 | guiding principle here is that changelogs are for humans, not machines. 18 | 19 | When reviewing merged PR's the labels to be used are: 20 | 21 | * breaking - Used when the PR is considered a breaking change. 22 | * enhancement - Used when the PR adds a new feature or enhancement. 23 | * bug - Used when the PR fixes a bug included in a previous release. 24 | * documentation - Used when the PR adds or updates documentation. 25 | * internal - Used for internal changes that still require a mention in the 26 | changelog/release notes. 27 | 28 | 29 | ## Release 30 | 31 | Once the prep work is completed, the actual release is straight forward: 32 | 33 | * First, ensure that you have installed your projects dependencies: 34 | 35 | ``` 36 | yarn install 37 | ``` 38 | 39 | * Second, ensure that you have obtained a 40 | [GitHub personal access token][generate-token] with the `repo` scope (no 41 | other permissions are needed). Make sure the token is available as the 42 | `GITHUB_AUTH` environment variable. 43 | 44 | For instance: 45 | 46 | ```bash 47 | export GITHUB_AUTH=abc123def456 48 | ``` 49 | 50 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 51 | 52 | * And last (but not least 😁) do your release. 53 | 54 | ``` 55 | npx release-it 56 | ``` 57 | 58 | [release-it](https://github.com/release-it/release-it/) manages the actual 59 | release process. It will prompt you to to choose the version number after which 60 | you will have the chance to hand tweak the changelog to be used (for the 61 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 62 | pushing the tag and commits, etc. 63 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /demos/boxes/app.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { 3 | ArcRotateCamera, 4 | BabylonCore, 5 | Box, 6 | DirectionalLight, 7 | Material, 8 | Parent, 9 | Position, 10 | Rotation, 11 | } from '../../src/components'; 12 | import { components, systems, World } from '../../src'; 13 | import { NormalMaterial } from '@babylonjs/materials/normal/normalMaterial'; 14 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 15 | import { Color4 } from '@babylonjs/core/Maths/math.color'; 16 | 17 | const canvas = document.querySelector('canvas'); 18 | const select = document.querySelector('select'); 19 | const fpsEl = document.querySelector('#fps'); 20 | if (canvas === null || select === null || fpsEl === null) { 21 | throw new Error('Required DOM elements not found'); 22 | } 23 | 24 | const boxOptions = { 25 | size: 2, 26 | }; 27 | const boxes: Entity[] = []; 28 | const frames = Array(30).fill(0); 29 | 30 | function random(): number { 31 | return Math.random() * 360; 32 | } 33 | 34 | function beforeRender(_delta: number, _time: number): void { 35 | boxes.forEach((box) => { 36 | const rotation = box.getMutableComponent(Rotation)!; 37 | rotation.value.addInPlaceFromFloats(0.5, 0.5, 0.5); 38 | }); 39 | } 40 | 41 | function afterRender(delta: number, _time: number): void { 42 | frames.shift(); 43 | frames[frames.length] = 1000 / delta; 44 | 45 | const fps = frames.reduce((total, frame) => total + frame, 0) / frames.length; 46 | fpsEl!.innerHTML = `${Math.round(fps)}`; 47 | } 48 | 49 | const world = new World(); 50 | components.forEach((component) => world.registerComponent(component)); 51 | systems.forEach((system) => world.registerSystem(system)); 52 | 53 | const entity = world.createEntity(); 54 | 55 | entity.addComponent(BabylonCore, { 56 | world, 57 | canvas, 58 | beforeRender, 59 | afterRender, 60 | }); 61 | 62 | world.createEntity().addComponent(Parent).addComponent(ArcRotateCamera); 63 | 64 | world 65 | .createEntity() 66 | .addComponent(Parent) 67 | .addComponent(DirectionalLight, { direction: new Vector3(-5, 0, -10) }); 68 | 69 | world.execute(0, 0); 70 | 71 | const scene = entity.getComponent(BabylonCore)!.scene; 72 | // @todo can we pass this directly to the component? 73 | scene.clearColor = new Color4(1, 1, 1); 74 | 75 | const material = new NormalMaterial('normal', scene); 76 | 77 | function syncBoxes(): void { 78 | const count = parseInt(select!.value, 10); 79 | const diff = boxes.length - count; 80 | const boxesHaveMore = diff > 0; 81 | const boxesHaveLess = diff < 0; 82 | const same = diff === 0; 83 | 84 | if (same) return; 85 | 86 | if (boxesHaveLess) { 87 | for (let i = 0; i < 0 - diff; i++) { 88 | const entity = world.createEntity(); 89 | entity 90 | .addComponent(Parent) 91 | .addComponent(Box, boxOptions) 92 | .addComponent(Material, { value: material }) 93 | .addComponent(Position, { value: new Vector3(0, 0, 0) }) // @todo why is this required? 94 | .addComponent(Rotation, { value: new Vector3(random(), random(), random()) }); 95 | boxes.push(entity); 96 | } 97 | return; 98 | } 99 | 100 | if (boxesHaveMore) { 101 | for (let i = 0; i < diff; i++) { 102 | const box = boxes.pop(); 103 | box!.remove(); 104 | } 105 | } 106 | } 107 | 108 | syncBoxes(); 109 | 110 | select.addEventListener('change', syncBoxes); 111 | -------------------------------------------------------------------------------- /demos/boxes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Boxes Demo 6 | 7 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 |
41 | 53 |
54 | 55 | 60 |
61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /demos/boxes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boxes", 3 | "version": "0.0.0", 4 | "description": "Boxes demo", 5 | "main": "app.js", 6 | "scripts": { 7 | "build": "rollup -c rollup.config.js", 8 | "start": "rollup -c rollup.config.js -w" 9 | }, 10 | "author": "", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /demos/boxes/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import replace from '@rollup/plugin-replace'; 5 | 6 | export default [ 7 | { 8 | input: 'app.ts', 9 | output: { file: 'build/app.js', name: 'playground', format: 'iife', sourcemap: false }, 10 | plugins: [ 11 | typescript({ 12 | tsconfig: '../../tsconfig.json', 13 | include: ['*.ts', '**/*.ts', '../../src/**/*.ts'], 14 | }), 15 | replace({ 16 | 'process.env.NODE_ENV': JSON.stringify('development'), 17 | delimiters: ['', ''], 18 | }), 19 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 20 | commonjs(), 21 | resolve(), 22 | ], 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /demos/hello-world/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArcRotateCamera, 3 | BabylonCore, 4 | Box, 5 | components, 6 | HemisphericLight, 7 | Parent, 8 | Position, 9 | systems, 10 | World, 11 | } from '../../src'; 12 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 13 | import { Component, Types, System } from 'ecsy'; 14 | 15 | class BoxMoveComponent extends Component { 16 | freq!: number; 17 | amp!: number; 18 | 19 | static schema = { 20 | freq: { 21 | type: Types.Number, 22 | default: Math.PI * 2, 23 | }, 24 | amp: { 25 | type: Types.Number, 26 | default: 1, 27 | }, 28 | }; 29 | } 30 | 31 | class BoxMoveSystem extends System { 32 | execute(): void { 33 | this.queries.movableBoxes.results.forEach((entity) => { 34 | const boxMove = entity.getComponent(BoxMoveComponent)!; 35 | const position = entity.getMutableComponent(Position)!; 36 | position.value.y = Math.sin(Date.now() * 0.001 * boxMove.freq) * boxMove.amp; 37 | }); 38 | } 39 | 40 | static queries = { 41 | movableBoxes: { 42 | components: [BoxMoveComponent, Position], 43 | }, 44 | }; 45 | } 46 | 47 | const world = new World(); 48 | components.forEach((component) => world.registerComponent(component)); 49 | systems.forEach((system) => world.registerSystem(system)); 50 | 51 | world.registerComponent(BoxMoveComponent).registerSystem(BoxMoveSystem); 52 | 53 | world.createEntity('singleton').addComponent(BabylonCore, { 54 | world, 55 | canvas: document.getElementsByTagName('canvas')[0], 56 | }); 57 | 58 | world 59 | .createEntity('camera') 60 | .addComponent(Parent) 61 | .addComponent(ArcRotateCamera, { 62 | alpha: -Math.PI / 2, 63 | beta: Math.PI / 2.5, 64 | radius: 3, 65 | target: new Vector3(0, 0, 0), 66 | }); 67 | 68 | world 69 | .createEntity('light') 70 | .addComponent(Parent) 71 | .addComponent(HemisphericLight, { 72 | direction: new Vector3(0, 1, 0), 73 | }); 74 | 75 | world.createEntity('box').addComponent(Parent).addComponent(Position).addComponent(Box).addComponent(BoxMoveComponent, { 76 | freq: Math.PI, // 0.5 Hz 77 | amp: 0.1, 78 | }); 79 | 80 | world.execute(0, 0); 81 | -------------------------------------------------------------------------------- /demos/hello-world/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ecsy babylon hello world 7 | 8 | 23 | 24 | 25 | 26 |
27 | 28 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /demos/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-world", 3 | "version": "0.0.0", 4 | "description": "Boxes demo", 5 | "main": "app.js", 6 | "scripts": { 7 | "build": "rollup -c rollup.config.js", 8 | "start": "rollup -c rollup.config.js -w" 9 | }, 10 | "author": "", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /demos/hello-world/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import replace from '@rollup/plugin-replace'; 5 | 6 | export default [ 7 | { 8 | input: 'app.ts', 9 | output: { file: 'build/app.js', name: 'playground', format: 'iife', sourcemap: false }, 10 | plugins: [ 11 | typescript({ 12 | tsconfig: '../../tsconfig.json', 13 | include: ['*.ts', '**/*.ts', '../../src/**/*.ts'], 14 | }), 15 | replace({ 16 | 'process.env.NODE_ENV': JSON.stringify('development'), 17 | delimiters: ['', ''], 18 | }), 19 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 20 | commonjs(), 21 | resolve(), 22 | ], 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /demos/playground/app.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArcRotateCamera, 3 | BabylonCore, 4 | Box, 5 | DefaultRenderingPipeline, 6 | DirectionalLight, 7 | Lines, 8 | Parent, 9 | PbrMaterial, 10 | Position, 11 | Rotation, 12 | Sphere, 13 | Transitions, 14 | WebXrDefaultExperience, 15 | } from '../../src/components'; 16 | import { components, systems, World } from '../../src'; 17 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 18 | import { Color3, Color4 } from '@babylonjs/core/Maths/math.color'; 19 | import { Engine } from '@babylonjs/core/Engines/engine'; 20 | import { BounceEase, EasingFunction } from '@babylonjs/core/Animations/easing'; 21 | 22 | const canvas = document.querySelector('canvas'); 23 | const fpsEl = document.querySelector('#fps'); 24 | if (canvas === null || fpsEl === null) { 25 | throw new Error('Required DOM elements not found'); 26 | } 27 | 28 | const frames = Array(30).fill(0); 29 | 30 | function afterRender(delta: number, _time: number): void { 31 | frames.shift(); 32 | frames[frames.length] = 1000 / delta; 33 | 34 | const fps = frames.reduce((total, frame) => total + frame, 0) / frames.length; 35 | fpsEl!.innerHTML = `${Math.round(fps)}`; 36 | } 37 | 38 | const world = new World(); 39 | const engine = new Engine(canvas, true, {}, false); 40 | components.forEach((component) => world.registerComponent(component)); 41 | systems.forEach((system) => world.registerSystem(system)); 42 | 43 | const entity = world.createEntity(); 44 | 45 | entity.addComponent(BabylonCore, { 46 | world, 47 | canvas, 48 | engine, 49 | afterRender, 50 | }); 51 | 52 | world.createEntity().addComponent(WebXrDefaultExperience); 53 | 54 | world 55 | .createEntity() 56 | .addComponent(Parent) 57 | .addComponent(ArcRotateCamera, { alpha: Math.PI * 1.5, beta: 1.3 }) 58 | .addComponent(DefaultRenderingPipeline, { 59 | depthOfFieldEnabled: true, 60 | fxaaEnabled: true, 61 | depthOfField: { 62 | focusDistance: 5000, 63 | focalLength: 150, 64 | }, 65 | }); 66 | 67 | world 68 | .createEntity() 69 | .addComponent(Parent) 70 | .addComponent(DirectionalLight, { direction: new Vector3(5, -7, 10) }); 71 | 72 | const parentEntity = world.createEntity().addComponent(Parent); 73 | 74 | world 75 | .createEntity() 76 | .addComponent(Parent, { value: parentEntity }) 77 | .addComponent(Box) 78 | .addComponent(PbrMaterial, { 79 | albedoColor: new Color3(1, 0, 0), 80 | ambientColor: new Color3(1, 0, 0), 81 | metallic: 0, 82 | roughness: 0, 83 | }) 84 | .addComponent(Position, { value: new Vector3(-2, 0, 0) }) 85 | .addComponent(Rotation, { value: new Vector3(0, 45, 0) }); 86 | 87 | const easingFunction = new BounceEase(); 88 | easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEOUT); 89 | 90 | const sphere = world 91 | .createEntity() 92 | .addComponent(Parent) 93 | .addComponent(Sphere, { diameter: 1.5 }) 94 | .addComponent(PbrMaterial, { 95 | albedoColor: new Color3(0, 1, 0), 96 | ambientColor: new Color3(0, 1, 0), 97 | metallic: 0, 98 | roughness: 0.3, 99 | }) 100 | .addComponent(Position, { value: new Vector3(-2, 0, 0) }) 101 | .addComponent(Transitions, { 102 | value: [ 103 | { 104 | property: 'transform.position', 105 | frameRate: 30, 106 | duration: 5000, 107 | easingFunction, 108 | }, 109 | ], 110 | }); 111 | 112 | world 113 | .createEntity() 114 | .addComponent(Parent) 115 | .addComponent(Lines, { 116 | points: [new Vector3(-2, 0, 0), new Vector3(2, 0, 0)], 117 | color: Color3.Black(), 118 | }); 119 | 120 | world.execute(0, 0); 121 | 122 | const scene = entity.getComponent(BabylonCore)!.scene; 123 | // @todo can we pass this directly to the component? 124 | scene.clearColor = new Color4(1, 1, 1); 125 | scene.ambientColor = new Color3(0.1, 0.1, 0.1); 126 | 127 | const pos = sphere.getMutableComponent(Position)!; 128 | pos.value = new Vector3(2, 0, 0); 129 | -------------------------------------------------------------------------------- /demos/playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Playground 6 | 7 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demos/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.0.0", 4 | "description": "Boxes demo", 5 | "main": "app.js", 6 | "scripts": { 7 | "build": "rollup -c rollup.config.js", 8 | "start": "rollup -c rollup.config.js -w" 9 | }, 10 | "author": "", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /demos/playground/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from 'rollup-plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import replace from '@rollup/plugin-replace'; 5 | 6 | export default [ 7 | { 8 | input: 'app.ts', 9 | output: { file: 'build/app.js', name: 'playground', format: 'iife', sourcemap: false }, 10 | plugins: [ 11 | typescript({ 12 | tsconfig: '../../tsconfig.json', 13 | include: ['*.ts', '**/*.ts', '../../src/**/*.ts'], 14 | }), 15 | replace({ 16 | 'process.env.NODE_ENV': JSON.stringify('development'), 17 | delimiters: ['', ''], 18 | }), 19 | // Allow bundling cjs modules (unlike webpack, rollup doesn't understand cjs) 20 | commonjs(), 21 | resolve(), 22 | ], 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/q3/yzggf_l965zbzps_bk8xr37c0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | preset: 'ts-jest/presets/js-with-babel', 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | setupFilesAfterEnv: ['jest-extended'], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: 'jsdom', 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | // testPathIgnorePatterns: [ 151 | // "/node_modules/" 152 | // ], 153 | 154 | // The regexp pattern or array of patterns that Jest uses to detect test files 155 | // testRegex: [], 156 | 157 | // This option allows the use of a custom results processor 158 | // testResultsProcessor: null, 159 | 160 | // This option allows use of a custom test runner 161 | // testRunner: "jasmine2", 162 | 163 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 164 | // testURL: "http://localhost", 165 | 166 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 167 | // timers: "real", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: null, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | transformIgnorePatterns: ['/node_modules/(?!(@babylonjs)/)'], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: null, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | }; 187 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ecsy-babylon", 3 | "version": "0.8.0", 4 | "description": "", 5 | "keywords": [], 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/kaliber5/ecsy-babylon" 9 | }, 10 | "license": "MIT", 11 | "author": "Simon Ihmig ", 12 | "sideEffects": false, 13 | "main": "index.cjs.js", 14 | "module": "index.js", 15 | "typings": "index.d.ts", 16 | "scripts": { 17 | "clean": "rimraf world.js world.d.ts index.js index.cjs.js index.d.ts *.map components systems -private", 18 | "prebuild": "yarn clean", 19 | "build": "rollup -c rollup.config.js && tsc -b tsconfig.build.json", 20 | "docs": "typedoc --out docs --target es6 --theme minimal --mode file src", 21 | "lint": "yarn lint:ts && yarn lint:eslint", 22 | "lint:eslint": "eslint . --ext .js,.ts", 23 | "lint:ts": "tsc --noEmit", 24 | "precommit": "lint-staged", 25 | "prepublishOnly": "yarn build", 26 | "postpublish": "yarn clean", 27 | "report-coverage": "cat ./coverage/lcov.info | coveralls", 28 | "start": "rollup -c rollup.config.js -w", 29 | "test": "jest", 30 | "test:prod": "yarn run lint && yarn run test -- --no-cache", 31 | "test:watch": "jest --coverage --watch" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "lint-staged" 36 | } 37 | }, 38 | "lint-staged": { 39 | "{src,test}/**/*.ts": [ 40 | "yarn lint:eslint --fix" 41 | ] 42 | }, 43 | "prettier": { 44 | "arrowParens": "always", 45 | "printWidth": 120, 46 | "semi": true, 47 | "singleQuote": true, 48 | "trailingComma": "es5" 49 | }, 50 | "dependencies": { 51 | "@babylonjs/core": "^5.6.1", 52 | "@babylonjs/materials": "^5.6.1", 53 | "ecsy": "^0.4.2" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "7.18.6", 57 | "@babel/preset-env": "7.18.6", 58 | "@rollup/plugin-node-resolve": "13.3.0", 59 | "@rollup/plugin-replace": "3.1.0", 60 | "@rollup/plugin-typescript": "8.3.3", 61 | "@types/jest": "27.5.2", 62 | "@types/lodash-es": "4.17.6", 63 | "@types/node": "15.12.5", 64 | "@types/rollup-plugin-sourcemaps": "0.5.0", 65 | "@typescript-eslint/eslint-plugin": "5.30.5", 66 | "@typescript-eslint/parser": "5.30.5", 67 | "babel-jest": "27.5.1", 68 | "coveralls": "3.1.1", 69 | "cross-env": "7.0.3", 70 | "eslint": "8.19.0", 71 | "eslint-config-prettier": "8.5.0", 72 | "eslint-config-standard": "16.0.3", 73 | "eslint-import-resolver-typescript": "3.2.5", 74 | "eslint-plugin-import": "2.26.0", 75 | "eslint-plugin-jest": "26.5.3", 76 | "eslint-plugin-node": "11.1.0", 77 | "eslint-plugin-prettier": "4.2.1", 78 | "eslint-plugin-promise": "6.0.0", 79 | "eslint-plugin-standard": "5.0.0", 80 | "husky": "4.3.8", 81 | "jest": "27.5.1", 82 | "jest-config": "27.5.1", 83 | "jest-extended": "0.11.5", 84 | "lint-staged": "11.2.6", 85 | "prettier": "2.7.1", 86 | "release-it": "14.14.3", 87 | "release-it-lerna-changelog": "4.0.1", 88 | "rimraf": "3.0.2", 89 | "rollup": "2.76.0", 90 | "rollup-plugin-commonjs": "10.1.0", 91 | "rollup-plugin-sourcemaps": "0.6.3", 92 | "ts-jest": "27.1.5", 93 | "typedoc": "0.22.18", 94 | "typescript": "4.6.4" 95 | }, 96 | "engines": { 97 | "node": "12.* || >= 14" 98 | }, 99 | "publishConfig": { 100 | "registry": "https://registry.npmjs.org" 101 | }, 102 | "release-it": { 103 | "plugins": { 104 | "release-it-lerna-changelog": { 105 | "infile": "CHANGELOG.md", 106 | "launchEditor": true 107 | } 108 | }, 109 | "git": { 110 | "tagName": "v${version}" 111 | }, 112 | "github": { 113 | "release": true, 114 | "tokenRef": "GITHUB_AUTH" 115 | } 116 | }, 117 | "volta": { 118 | "node": "12.22.12", 119 | "yarn": "1.22.19" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>kaliber5/renovate-config:js-lib" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import sourceMaps from 'rollup-plugin-sourcemaps'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const pkg = require('./package.json'); 5 | 6 | export default [ 7 | { 8 | input: `src/index.ts`, 9 | output: [{ file: pkg.main, format: 'cjs', sourcemap: true }], 10 | // Indicate here external modules you don't wanna include in your bundle (i.e.: 'lodash') 11 | external: ['@babylonjs/core', '@babylonjs/inspector', '@babylonjs/loaders', '@babylonjs/materials', 'ecsy'], 12 | watch: { 13 | include: 'src/**', 14 | }, 15 | plugins: [typescript({ tsconfig: 'tsconfig.rollup.json' }), sourceMaps()], 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /src/-private/babylon-manager.ts: -------------------------------------------------------------------------------- 1 | import { assign, assignProperty } from './utils/assign'; 2 | import { TransitionConfig } from '../components/transitions'; 3 | import { Entity } from 'ecsy'; 4 | import { assert } from './utils/debug'; 5 | import { Animation } from '@babylonjs/core/Animations/animation'; 6 | import { IAnimatable } from '@babylonjs/core/Animations/animatable.interface'; 7 | import { Scene } from '@babylonjs/core/scene'; 8 | import { Matrix, Quaternion, Vector2, Vector3 } from '@babylonjs/core/Maths/math.vector'; 9 | import { Color3, Color4 } from '@babylonjs/core/Maths/math.color'; 10 | 11 | const DEFAULT_FRAMERATE = 60; 12 | 13 | export default class BabylonManager { 14 | private transitionRegistry = new WeakMap>(); 15 | 16 | private Animation?: typeof Animation; 17 | 18 | injectAnimationDependencies(AnimationClass: typeof Animation): void { 19 | this.Animation = AnimationClass; 20 | } 21 | 22 | setProperties(entity: Entity, target: object, props: Record): void { 23 | assign(target, props); 24 | } 25 | 26 | setProperty(entity: Entity, target: object, property: string, value: unknown): void { 27 | assignProperty(target as never, property, value as never); 28 | } 29 | 30 | updateProperties(entity: Entity, target: object, transitionTarget: string, props: Record): void { 31 | Object.entries(props).forEach(([property, value]) => 32 | this.updateProperty(entity, target, transitionTarget, property, value) 33 | ); 34 | } 35 | 36 | updateProperty(entity: Entity, target: object, transitionTarget: string, property: string, value: unknown): void { 37 | const transitionKey = transitionTarget + '.' + property; 38 | const transition = this.getTransition(entity, transitionKey); 39 | 40 | if (this.hasAnimationSupport && transition && transition.duration > 0) { 41 | this.transitionProperty(target as never, transition, property, value); 42 | } else { 43 | assignProperty(target as never, property, value as never); 44 | } 45 | } 46 | 47 | registerTransition(entity: Entity, config: TransitionConfig): void { 48 | if (!this.transitionRegistry.has(entity)) { 49 | this.transitionRegistry.set(entity, new Map()); 50 | } 51 | 52 | const transitionSet = this.transitionRegistry.get(entity)!; 53 | transitionSet.set(config.property, config); 54 | } 55 | 56 | unregisterTransition(entity: Entity, config?: TransitionConfig): void { 57 | if (config) { 58 | const transitions = this.transitionRegistry.get(entity); 59 | if (transitions) { 60 | transitions.delete(config.property); 61 | } 62 | } else { 63 | this.transitionRegistry.delete(entity); 64 | } 65 | } 66 | 67 | private transitionProperty( 68 | target: IAnimatable & { getScene: () => Scene }, 69 | transitionConfig: TransitionConfig, 70 | property: string, 71 | value: unknown 72 | ): void { 73 | const scene = target.getScene(); 74 | const { frameRate = DEFAULT_FRAMERATE, duration, easingFunction } = transitionConfig; 75 | const { Animation } = this; 76 | 77 | assert('Cannot transition property without Animation support', Animation); 78 | 79 | // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call 80 | const initial: unknown = (target as any)[property].clone 81 | ? // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call 82 | (target as any)[property].clone() 83 | : // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call 84 | (target as any)[property]; 85 | const transition = new Animation( 86 | `${property}Transition`, 87 | property, 88 | frameRate, 89 | this.getAnimationType(value), 90 | Animation.ANIMATIONLOOPMODE_CONSTANT 91 | ); 92 | if (easingFunction) { 93 | transition.setEasingFunction(easingFunction); 94 | } 95 | 96 | // code mostly taken from Animation.TransitionTo, which we cannot use as it stops existing animations 97 | const endFrame: number = frameRate * (duration / 1000); 98 | 99 | transition.setKeys([ 100 | { 101 | frame: 0, 102 | value: initial, 103 | }, 104 | { 105 | frame: endFrame, 106 | value, 107 | }, 108 | ]); 109 | 110 | scene.beginDirectAnimation(target, [transition], 0, endFrame, false); 111 | } 112 | 113 | private getAnimationType(value: unknown): number { 114 | const { Animation } = this; 115 | 116 | if (value instanceof Vector2) { 117 | return Animation!.ANIMATIONTYPE_VECTOR2; 118 | } 119 | if (value instanceof Vector3) { 120 | return Animation!.ANIMATIONTYPE_VECTOR3; 121 | } 122 | if (value instanceof Color3) { 123 | return Animation!.ANIMATIONTYPE_COLOR3; 124 | } 125 | if (value instanceof Color4) { 126 | return Animation!.ANIMATIONTYPE_COLOR4; 127 | } 128 | if (value instanceof Matrix) { 129 | return Animation!.ANIMATIONTYPE_MATRIX; 130 | } 131 | if (value instanceof Quaternion) { 132 | return Animation!.ANIMATIONTYPE_QUATERNION; 133 | } 134 | if (typeof value === 'number') { 135 | return Animation!.ANIMATIONTYPE_FLOAT; 136 | } 137 | 138 | throw new Error(`Could not determine animation type for value ${String(value)}`); 139 | } 140 | 141 | private getTransition(entity: Entity, property: string): TransitionConfig | undefined { 142 | const propertyMap = this.transitionRegistry.get(entity); 143 | if (!propertyMap) { 144 | return undefined; 145 | } 146 | 147 | return propertyMap.get(property); 148 | } 149 | 150 | private get hasAnimationSupport(): boolean { 151 | return !!this.Animation; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/-private/ecsy-types.ts: -------------------------------------------------------------------------------- 1 | import { Vector2, Vector3 } from '@babylonjs/core/Maths/math.vector'; 2 | import { Color3, Color4 } from '@babylonjs/core/Maths/math.color'; 3 | import { createType } from 'ecsy'; 4 | 5 | interface Copyable { 6 | copyFrom: (src: this) => this; 7 | } 8 | 9 | interface Cloneable { 10 | clone: () => this; 11 | } 12 | 13 | function copyBabylon(src?: A, dest?: A): A | undefined { 14 | if (!src) { 15 | return src; 16 | } 17 | 18 | if (!dest) { 19 | return src.clone(); 20 | } 21 | 22 | return dest.copyFrom(src); 23 | } 24 | 25 | function cloneBabylon(src?: A): A | undefined { 26 | return src && src.clone(); 27 | } 28 | 29 | export const BabylonTypes = { 30 | Vector2: createType({ 31 | name: 'Vector2', 32 | default: new Vector2(), 33 | copy: copyBabylon, 34 | clone: cloneBabylon, 35 | }), 36 | Vector3: createType({ 37 | name: 'Vector3', 38 | default: new Vector3(), 39 | copy: copyBabylon, 40 | clone: cloneBabylon, 41 | }), 42 | Color3: createType({ 43 | name: 'Color3', 44 | default: new Color3(), 45 | copy: copyBabylon, 46 | clone: cloneBabylon, 47 | }), 48 | Color4: createType({ 49 | name: 'Color4', 50 | default: new Color4(), 51 | copy: copyBabylon, 52 | clone: cloneBabylon, 53 | }), 54 | }; 55 | 56 | export { Types } from 'ecsy'; 57 | -------------------------------------------------------------------------------- /src/-private/systems/factory-array.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentConstructor, Entity } from 'ecsy'; 2 | import SystemWithCore from '../../-private/systems/with-core'; 3 | import { assert } from '../utils/debug'; 4 | import InstanceArrayComponent from '../../components/_instance-array'; 5 | import World from '../../world'; 6 | import { Attributes } from 'ecsy/src/System'; 7 | import { IDisposable } from '@babylonjs/core/scene'; 8 | import { Constructor } from '../../types'; 9 | 10 | export default abstract class FactoryArraySystem< 11 | C extends Component, 12 | D extends InstanceArrayComponent, 13 | I extends IDisposable 14 | > extends SystemWithCore { 15 | protected abstract create(component: C): I; 16 | protected abstract instanceComponentConstructor: ComponentConstructor; 17 | protected factoryComponentConstructor: ComponentConstructor; 18 | protected abstract instanceConstructor: Constructor; 19 | 20 | constructor(world: World, attributes?: Attributes) { 21 | super(world, attributes); 22 | 23 | assert( 24 | 'System derived from FactoryArraySystem must define "factory" query', 25 | typeof (this.constructor as typeof FactoryArraySystem).queries.factory !== 'undefined' 26 | ); 27 | 28 | this.factoryComponentConstructor = (this.constructor as typeof FactoryArraySystem).queries.factory 29 | .components[0] as ComponentConstructor; 30 | } 31 | 32 | execute(): void { 33 | super.execute(); 34 | 35 | this.queries.factory.added?.forEach((e: Entity) => this.setup(e)); 36 | this.queries.factory.changed?.forEach((e: Entity) => this.update(e)); 37 | this.queries.factory.removed?.forEach((e: Entity) => this.remove(e)); 38 | 39 | super.afterExecute(); 40 | } 41 | 42 | setup(entity: Entity): void { 43 | const c = entity.getComponent(this.factoryComponentConstructor)!; 44 | 45 | const instance = this.create(c); 46 | this.addInstance(entity, instance); 47 | } 48 | 49 | update(entity: Entity): void { 50 | const c = entity.getComponent(this.factoryComponentConstructor)!; 51 | const instanceComponent = entity.getComponent(this.instanceComponentConstructor); 52 | assert('No instance component found', instanceComponent); 53 | const ic = instanceComponent as InstanceArrayComponent; 54 | assert('Existing instance array component has invalid value', ic.value); 55 | const instance = ic.value.find((i) => i instanceof this.instanceConstructor); 56 | assert('No instance found', instance); 57 | this.updateInstance(entity, instance, c); 58 | } 59 | 60 | remove(entity: Entity): void { 61 | this.removeInstance(entity); 62 | } 63 | 64 | protected updateInstance(entity: Entity, instance: I, c: C): void { 65 | this.world.babylonManager.setProperties(entity, instance, c as never); 66 | } 67 | 68 | private addInstance(entity: Entity, instance: I): void { 69 | const instanceComponent = entity.getMutableComponent(this.instanceComponentConstructor); 70 | if (instanceComponent) { 71 | const ic = instanceComponent as InstanceArrayComponent; 72 | assert('Existing instance array component has invalid value', ic.value); 73 | ic.value = [...ic.value, instance]; 74 | } else { 75 | entity.addComponent(this.instanceComponentConstructor as ComponentConstructor>, { 76 | value: [instance], 77 | }); 78 | } 79 | } 80 | 81 | private removeInstance(entity: Entity): void { 82 | const instanceComponent = entity.getComponent(this.instanceComponentConstructor, true); 83 | assert('No instance component found', instanceComponent?.value); 84 | 85 | const ic = instanceComponent as InstanceArrayComponent; 86 | assert('Existing instance array component has invalid value', ic.value); 87 | const removedInstance = ic.value.find((i) => i instanceof this.instanceConstructor); 88 | assert('No instance found to remove', removedInstance); 89 | const instances = ic.value.filter((i) => i !== removedInstance); 90 | 91 | if (instances.length > 0) { 92 | const _instanceComponent = entity.getMutableComponent( 93 | this.instanceComponentConstructor 94 | ) as InstanceArrayComponent; 95 | _instanceComponent.value = instances; 96 | } else { 97 | entity.removeComponent(this.instanceComponentConstructor); 98 | } 99 | removedInstance.dispose(); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/-private/systems/factory.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentConstructor, Entity } from 'ecsy'; 2 | import SystemWithCore from '../../-private/systems/with-core'; 3 | import { assert } from '../utils/debug'; 4 | import InstanceComponent from '../../components/_instance'; 5 | import World from '../../world'; 6 | import { Attributes } from 'ecsy/src/System'; 7 | import { IDisposable } from '@babylonjs/core/scene'; 8 | 9 | export default abstract class FactorySystem< 10 | C extends Component, 11 | D extends InstanceComponent, 12 | I extends IDisposable 13 | > extends SystemWithCore { 14 | protected abstract create(component: C): I; 15 | protected abstract instanceComponentConstructor: ComponentConstructor; 16 | protected factoryComponentConstructor: ComponentConstructor; 17 | protected recreateInstanceOnUpdate = false; 18 | protected transitionTarget?: string; 19 | 20 | constructor(world: World, attributes?: Attributes) { 21 | super(world, attributes); 22 | 23 | assert( 24 | 'System derived from FactorySystem must define "factory" query', 25 | typeof (this.constructor as typeof FactorySystem).queries.factory !== 'undefined' 26 | ); 27 | 28 | this.factoryComponentConstructor = (this.constructor as typeof FactorySystem).queries.factory 29 | .components[0] as ComponentConstructor; 30 | } 31 | 32 | execute(): void { 33 | super.execute(); 34 | 35 | this.queries.factory.added?.forEach((e: Entity) => this.setup(e)); 36 | this.queries.factory.changed?.forEach((e: Entity) => this.update(e)); 37 | this.queries.factory.removed?.forEach((e: Entity) => this.remove(e)); 38 | 39 | super.afterExecute(); 40 | } 41 | 42 | setup(entity: Entity): void { 43 | const c = entity.getComponent(this.factoryComponentConstructor)!; 44 | 45 | const instance = this.create(c); 46 | this.addInstance(entity, instance); 47 | } 48 | 49 | update(entity: Entity): void { 50 | const c = entity.getComponent(this.factoryComponentConstructor)!; 51 | 52 | if (this.recreateInstanceOnUpdate) { 53 | const instanceComponent = entity.getMutableComponent(this.instanceComponentConstructor); 54 | assert('No instance component found', instanceComponent); 55 | 56 | const instance = this.create(c); 57 | instanceComponent.value = instance; 58 | } else { 59 | const instanceComponent = entity.getComponent(this.instanceComponentConstructor); 60 | assert('No instance found', instanceComponent?.value); 61 | if (this.transitionTarget) { 62 | this.world.babylonManager.updateProperties(entity, instanceComponent.value, this.transitionTarget, c); 63 | } else { 64 | this.world.babylonManager.setProperties(entity, instanceComponent.value, c); 65 | } 66 | } 67 | } 68 | 69 | remove(entity: Entity): void { 70 | this.removeInstance(entity); 71 | } 72 | 73 | private addInstance(entity: Entity, instance: I): void { 74 | const instanceComponent = entity.getMutableComponent(this.instanceComponentConstructor); 75 | if (instanceComponent) { 76 | instanceComponent.value = instance; 77 | } else { 78 | entity.addComponent(this.instanceComponentConstructor as ComponentConstructor>, { 79 | value: instance, 80 | }); 81 | } 82 | } 83 | 84 | private removeInstance(entity: Entity): void { 85 | const instanceComponent = entity.getComponent(this.instanceComponentConstructor, true); 86 | assert('No instance component found', instanceComponent?.value); 87 | 88 | entity.removeComponent(this.instanceComponentConstructor); 89 | instanceComponent.value.dispose(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/-private/systems/with-core.ts: -------------------------------------------------------------------------------- 1 | import { System } from 'ecsy'; 2 | import { BabylonCore } from '../../components'; 3 | import World from '../../world'; 4 | 5 | export default class SystemWithCore extends System { 6 | world!: World; 7 | core?: BabylonCore; 8 | 9 | execute(): void { 10 | if (this.queries.core.added?.length) { 11 | if (this.queries.core.added.length > 1) { 12 | throw new Error('More than 1 core has been added.'); 13 | } 14 | 15 | this.core = this.queries.core.added[0].getComponent(BabylonCore); 16 | } 17 | } 18 | 19 | // this needs to run after the other queries have run in the systems that extend from this 20 | afterExecute(): void { 21 | if (this.queries.core.removed?.length) { 22 | this.core = undefined; 23 | } 24 | } 25 | } 26 | 27 | export const queries = { 28 | core: { 29 | components: [BabylonCore], 30 | listen: { 31 | added: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | 37 | SystemWithCore.queries = queries; 38 | -------------------------------------------------------------------------------- /src/-private/utils/assign.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assign properties of a component to target. Similar to Object.assign, but ignores undefined values 3 | * @param target 4 | * @param source 5 | */ 6 | import { Component } from 'ecsy'; 7 | import { Vector2, Vector3, Vector4, Matrix, Quaternion } from '@babylonjs/core/Maths/math.vector'; 8 | import { Color3, Color4 } from '@babylonjs/core/Maths/math.color'; 9 | import { assert } from './debug'; 10 | 11 | export function assignProperty( 12 | target: T, 13 | property: string & keyof T, 14 | value: T[typeof property] 15 | ): void { 16 | if (value === undefined) { 17 | return; 18 | } 19 | 20 | const originalValue = target[property]; 21 | 22 | const setter = `set${property[0].toUpperCase() + property.slice(1)}`; 23 | if (typeof (target as never)[setter] === 'function') { 24 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access 25 | (target as any)[setter](value); 26 | } else if (originalValue instanceof Vector4) { 27 | assert(`Expected Vector4, got: ${String(value)}`, value instanceof Vector4); 28 | originalValue.copyFrom(value); 29 | } else if (originalValue instanceof Vector3) { 30 | assert(`Expected Vector3, got: ${String(value)}`, value instanceof Vector3); 31 | originalValue.copyFrom(value); 32 | } else if (originalValue instanceof Vector2) { 33 | assert(`Expected Vector2, got: ${String(value)}`, value instanceof Vector2); 34 | originalValue.copyFrom(value); 35 | } else if (originalValue instanceof Matrix) { 36 | assert(`Expected Matrix, got: ${String(value)}`, value instanceof Matrix); 37 | originalValue.copyFrom(value); 38 | } else if (originalValue instanceof Quaternion) { 39 | assert(`Expected Quaternion, got: ${String(value)}`, value instanceof Quaternion); 40 | originalValue.copyFrom(value); 41 | } else if (originalValue instanceof Color3) { 42 | assert(`Expected Color3, got: ${String(value)}`, value instanceof Color3); 43 | originalValue.copyFrom(value); 44 | } else if (originalValue instanceof Color4) { 45 | assert(`Expected Color4, got: ${String(value)}`, value instanceof Color4); 46 | originalValue.copyFrom(value); 47 | } else { 48 | target[property] = value; 49 | } 50 | } 51 | 52 | export function assign( 53 | target: T, 54 | source: Partial | Component> | undefined | null 55 | ): void { 56 | if (!source) { 57 | return; 58 | } 59 | for (const [key, value] of Object.entries(source)) { 60 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument 61 | assignProperty(target, key as string & keyof T, value); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/-private/utils/debug.ts: -------------------------------------------------------------------------------- 1 | export function assert(description: string, test: unknown): asserts test { 2 | if (!test) { 3 | throw new Error('Assertion failed: ' + description); 4 | } 5 | } 6 | 7 | export function warn(message: string): void { 8 | console.warn(message); 9 | } 10 | -------------------------------------------------------------------------------- /src/-private/utils/guid.ts: -------------------------------------------------------------------------------- 1 | // @todo do we really need this? 2 | // eslint-disable-next-line @typescript-eslint/ban-types 3 | export default function guidFor(object: {}): string { 4 | return object.toString(); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/_instance-array.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema, Types } from 'ecsy'; 2 | import InstanceComponent from './_instance'; 3 | 4 | export default abstract class InstanceArrayComponent extends InstanceComponent { 5 | static schema: ComponentSchema = { 6 | value: { type: Types.Array }, 7 | overrides: { type: Types.JSON, default: {} }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/_instance.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | 3 | export default abstract class InstanceComponent extends Component { 4 | overrides!: Record; 5 | private _value?: I; 6 | private _previousValue?: I; 7 | 8 | get value(): I | undefined { 9 | return this._value; 10 | } 11 | 12 | set value(value: I | undefined) { 13 | this._previousValue = this._value; 14 | this._value = value; 15 | } 16 | 17 | get previousValue(): I | undefined { 18 | return this._previousValue; 19 | } 20 | 21 | static schema: ComponentSchema = { 22 | value: { type: Types.Ref }, 23 | overrides: { type: Types.JSON, default: {} }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/action.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { ActionEvent } from '@babylonjs/core/Actions/actionEvent'; 3 | import { IAction } from '@babylonjs/core/Actions/action'; 4 | 5 | type ActionCallback = (evt: ActionEvent) => void; 6 | 7 | export default class Action extends Component { 8 | pick?: ActionCallback; 9 | doublePick?: ActionCallback; 10 | centerPick?: ActionCallback; 11 | everyFrame?: ActionCallback; 12 | intersectionEnter?: ActionCallback; 13 | intersectionExit?: ActionCallback; 14 | keyDown?: ActionCallback; 15 | keyUp?: ActionCallback; 16 | leftPick?: ActionCallback; 17 | longPress?: ActionCallback; 18 | pickDown?: ActionCallback; 19 | pickOut?: ActionCallback; 20 | pickUp?: ActionCallback; 21 | pointerOut?: ActionCallback; 22 | pointerOver?: ActionCallback; 23 | rightPick?: ActionCallback; 24 | 25 | _actions: { 26 | [key: string]: IAction; 27 | } = {}; 28 | 29 | static schema: ComponentSchema = { 30 | pick: { 31 | type: Types.Ref, 32 | }, 33 | doublePick: { 34 | type: Types.Ref, 35 | }, 36 | centerPick: { 37 | type: Types.Ref, 38 | }, 39 | everyFrame: { 40 | type: Types.Ref, 41 | }, 42 | intersectionEnter: { 43 | type: Types.Ref, 44 | }, 45 | intersectionExit: { 46 | type: Types.Ref, 47 | }, 48 | keyDown: { 49 | type: Types.Ref, 50 | }, 51 | keyUp: { 52 | type: Types.Ref, 53 | }, 54 | leftPick: { 55 | type: Types.Ref, 56 | }, 57 | longPress: { 58 | type: Types.Ref, 59 | }, 60 | pickDown: { 61 | type: Types.Ref, 62 | }, 63 | pickOut: { 64 | type: Types.Ref, 65 | }, 66 | pickUp: { 67 | type: Types.Ref, 68 | }, 69 | pointerOut: { 70 | type: Types.Ref, 71 | }, 72 | pointerOver: { 73 | type: Types.Ref, 74 | }, 75 | rightPick: { 76 | type: Types.Ref, 77 | }, 78 | _actions: { 79 | type: Types.Ref, 80 | }, 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /src/components/babylon-core.ts: -------------------------------------------------------------------------------- 1 | import { Scene } from '@babylonjs/core/scene'; 2 | import { Engine } from '@babylonjs/core/Engines/engine'; 3 | import { Component, ComponentSchema, World, Types } from 'ecsy'; 4 | import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; 5 | 6 | export default class BabylonCore extends Component { 7 | world!: World; 8 | canvas!: HTMLCanvasElement; 9 | engine!: Engine; 10 | scene!: Scene; 11 | shadowGenerators: Set = new Set(); 12 | beforeRender?: (delta: number, time: number) => void; 13 | afterRender?: (delta: number, time: number) => void; 14 | 15 | static schema: ComponentSchema = { 16 | world: { 17 | type: Types.Ref, 18 | }, 19 | canvas: { 20 | type: Types.Ref, 21 | }, 22 | engine: { 23 | type: Types.Ref, 24 | }, 25 | scene: { 26 | type: Types.Ref, 27 | }, 28 | shadowGenerators: { 29 | type: Types.Ref, 30 | }, 31 | beforeRender: { 32 | type: Types.Ref, 33 | }, 34 | afterRender: { 35 | type: Types.Ref, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/components/camera.ts: -------------------------------------------------------------------------------- 1 | import InstanceComponent from './_instance'; 2 | import { Camera } from '@babylonjs/core/Cameras/camera'; 3 | 4 | export default class CameraComponent extends InstanceComponent {} 5 | -------------------------------------------------------------------------------- /src/components/camera/arc-rotate.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema, Types } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | import { AbstractTargetCamera, schema as baseSchema } from './target'; 5 | 6 | export default class ArcRotateCamera extends AbstractTargetCamera { 7 | alpha!: number; 8 | beta!: number; 9 | radius!: number; 10 | target!: Vector3; 11 | 12 | lowerAlphaLimit!: number | null; 13 | lowerBetaLimit?: number; 14 | lowerRadiusLimit!: number | null; 15 | 16 | upperAlphaLimit!: number | null; 17 | upperBetaLimit?: number; 18 | upperRadiusLimit!: number | null; 19 | 20 | static schema: ComponentSchema = { 21 | ...baseSchema, 22 | alpha: { type: Types.Number }, 23 | beta: { type: Types.Number }, 24 | radius: { type: Types.Number, default: 10 }, 25 | target: { type: BabylonTypes.Vector3 }, 26 | lowerAlphaLimit: { type: Types.Number, default: null }, 27 | lowerBetaLimit: { type: Types.Number, default: undefined }, 28 | lowerRadiusLimit: { type: Types.Number, default: null }, 29 | upperAlphaLimit: { type: Types.Number, default: null }, 30 | upperBetaLimit: { type: Types.Number, default: undefined }, 31 | upperRadiusLimit: { type: Types.Number, default: null }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/camera/target.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | 5 | export const schema: ComponentSchema = { 6 | position: { type: BabylonTypes.Vector3, default: undefined }, 7 | rotation: { type: BabylonTypes.Vector3, default: undefined }, 8 | target: { type: BabylonTypes.Vector3, default: undefined }, 9 | minZ: { type: Types.Number, default: undefined }, 10 | maxZ: { type: Types.Number, default: undefined }, 11 | inertia: { type: Types.Number, default: undefined }, 12 | fov: { type: Types.Number, default: undefined }, 13 | fovMode: { type: Types.Number, default: undefined }, 14 | }; 15 | 16 | export abstract class AbstractTargetCamera extends Component { 17 | position?: Vector3; 18 | rotation?: Vector3; 19 | target?: Vector3; 20 | minZ?: number; 21 | maxZ?: number; 22 | inertia?: number; 23 | fov?: number; 24 | fovMode?: number; 25 | 26 | static schema: ComponentSchema = schema; 27 | } 28 | 29 | export default class TargetCamera extends AbstractTargetCamera {} 30 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import BabylonCore from './babylon-core'; 2 | import TargetCamera from './camera/target'; 3 | import ArcRotateCamera from './camera/arc-rotate'; 4 | import Mesh from './mesh'; 5 | import Plane from './primitive/plane'; 6 | import Box from './primitive/box'; 7 | import Camera from './camera'; 8 | import Sphere from './primitive/sphere'; 9 | import Lines from './primitive/lines'; 10 | import Material from './material'; 11 | import StandardMaterial from './material/standard'; 12 | import PbrMaterial from './material/pbr'; 13 | import BackgroundMaterial from './material/background'; 14 | import ShadowOnlyMaterial from './material/shadow-only'; 15 | import Light from './light'; 16 | import HemisphericLight from './light/hemispheric'; 17 | import SpotLight from './light/spot'; 18 | import DirectionalLight from './light/directional'; 19 | import PointLight from './light/point'; 20 | import ShadowGenerator from './shadow-generator'; 21 | import TransformNode from './transform-node'; 22 | import Position from './position'; 23 | import Rotation from './rotation'; 24 | import Scale from './scale'; 25 | import PivotPoint from './pivot-point'; 26 | import Action from './action'; 27 | import Parent from './parent'; 28 | import Transitions from './transitions'; 29 | import PostProcess from './post-process'; 30 | import BlackAndWhitePostProcess from './post-process/black-and-white'; 31 | import BlurPostProcess from './post-process/blur'; 32 | import MotionBlurPostProcess from './post-process/motion-blur'; 33 | import PostProcessRenderPipeline from './post-process-render-pipeline'; 34 | import SsaoRenderingPipeline from './post-process-render-pipeline/ssao'; 35 | import DefaultRenderingPipeline from './post-process-render-pipeline/default'; 36 | import WebXrDefaultExperience from './xr/default'; 37 | 38 | export { 39 | BabylonCore, 40 | TargetCamera, 41 | ArcRotateCamera, 42 | Mesh, 43 | Plane, 44 | Box, 45 | Camera, 46 | Sphere, 47 | Lines, 48 | Material, 49 | StandardMaterial, 50 | PbrMaterial, 51 | BackgroundMaterial, 52 | ShadowOnlyMaterial, 53 | Light, 54 | HemisphericLight, 55 | DirectionalLight, 56 | SpotLight, 57 | PointLight, 58 | ShadowGenerator, 59 | TransformNode, 60 | Position, 61 | Rotation, 62 | Scale, 63 | PivotPoint, 64 | Action, 65 | Parent, 66 | PostProcess, 67 | BlackAndWhitePostProcess, 68 | BlurPostProcess, 69 | MotionBlurPostProcess, 70 | PostProcessRenderPipeline, 71 | SsaoRenderingPipeline, 72 | DefaultRenderingPipeline, 73 | Transitions, 74 | WebXrDefaultExperience, 75 | }; 76 | -------------------------------------------------------------------------------- /src/components/light.ts: -------------------------------------------------------------------------------- 1 | import InstanceComponent from './_instance'; 2 | import { Light as BabylonLight } from '@babylonjs/core/Lights/light'; 3 | 4 | export default class Light extends InstanceComponent {} 5 | -------------------------------------------------------------------------------- /src/components/light/_light.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | 5 | export default abstract class Light extends Component { 6 | intensity!: number; 7 | diffuse!: Color3; 8 | specular!: Color3; 9 | } 10 | 11 | export const schema: ComponentSchema = { 12 | intensity: { type: Types.Number, default: 1 }, 13 | diffuse: { type: BabylonTypes.Color3, default: Color3.White() }, 14 | specular: { type: BabylonTypes.Color3, default: Color3.White() }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/components/light/_shadow.ts: -------------------------------------------------------------------------------- 1 | import { ComponentSchema, Types } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | import Light, { schema as baseSchema } from './_light'; 5 | 6 | export default abstract class ShadowLight extends Light { 7 | position!: Vector3; 8 | shadowMinZ?: number; 9 | shadowMaxZ?: number; 10 | } 11 | 12 | export const schema: ComponentSchema = { 13 | ...baseSchema, 14 | shadowMinZ: { type: Types.Number, default: undefined }, 15 | shadowMaxZ: { type: Types.Number, default: undefined }, 16 | position: { type: BabylonTypes.Vector3, default: undefined }, 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/light/directional.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 2 | import ShadowLight, { schema as baseSchema } from './_shadow'; 3 | import { ComponentSchema } from 'ecsy'; 4 | import { BabylonTypes } from '../../-private/ecsy-types'; 5 | 6 | export default class DirectionalLight extends ShadowLight { 7 | direction!: Vector3; 8 | 9 | static schema: ComponentSchema = { 10 | ...baseSchema, 11 | direction: { type: BabylonTypes.Vector3, default: new Vector3(0, -1, 0) }, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/light/hemispheric.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 2 | import { schema as baseSchema } from './_shadow'; 3 | import { ComponentSchema } from 'ecsy'; 4 | import { BabylonTypes } from '../../-private/ecsy-types'; 5 | import Light from './_light'; 6 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 7 | 8 | export default class HemisphericLight extends Light { 9 | direction!: Vector3; 10 | groundColor!: Color3; 11 | 12 | static schema: ComponentSchema = { 13 | ...baseSchema, 14 | direction: { type: BabylonTypes.Vector3, default: new Vector3(0, -1, 0) }, 15 | groundColor: { type: BabylonTypes.Color3, default: Color3.Black() }, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/light/point.ts: -------------------------------------------------------------------------------- 1 | import ShadowLight, { schema as baseSchema } from './_shadow'; 2 | import { ComponentSchema } from 'ecsy'; 3 | 4 | export default class PointLight extends ShadowLight { 5 | static schema: ComponentSchema = { 6 | ...baseSchema, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /src/components/light/spot.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 2 | import ShadowLight, { schema as baseSchema } from './_shadow'; 3 | import { ComponentSchema, Types } from 'ecsy'; 4 | import { BabylonTypes } from '../../-private/ecsy-types'; 5 | 6 | export default class SpotLight extends ShadowLight { 7 | direction!: Vector3; 8 | angle!: number; 9 | exponent!: number; 10 | 11 | static schema: ComponentSchema = { 12 | ...baseSchema, 13 | direction: { type: BabylonTypes.Vector3, default: new Vector3(0, -1, 0) }, 14 | angle: { type: Types.Number, default: Math.PI / 3 }, 15 | exponent: { type: Types.Number, default: 2 }, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/material.ts: -------------------------------------------------------------------------------- 1 | import { Material as BabylonMaterial } from '@babylonjs/core/Materials/material'; 2 | import InstanceComponent from './_instance'; 3 | 4 | export default class Material extends InstanceComponent {} 5 | -------------------------------------------------------------------------------- /src/components/material/background.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Texture } from '@babylonjs/core/Materials/Textures/texture'; 3 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 4 | import { Constants } from '@babylonjs/core/Engines/constants'; 5 | import { BabylonTypes } from '../../-private/ecsy-types'; 6 | 7 | export default class BackgroundMaterial extends Component { 8 | diffuseTexture!: Texture | null; 9 | reflectionTexture!: Texture | null; 10 | alpha!: number; 11 | alphaMode!: number; 12 | shadowLevel!: number; 13 | primaryColor!: Color3; 14 | useRGBColor!: boolean; 15 | enableNoise!: boolean; 16 | 17 | static schema: ComponentSchema = { 18 | diffuseTexture: { type: Types.Ref, default: null }, 19 | reflectionTexture: { type: Types.Ref, default: null }, 20 | alpha: { type: Types.Number, default: 1 }, 21 | alphaMode: { type: Types.Number, default: Constants.ALPHA_PREMULTIPLIED_PORTERDUFF }, 22 | shadowLevel: { type: Types.Number, default: 0 }, 23 | primaryColor: { type: BabylonTypes.Color3, default: Color3.White() }, 24 | useRGBColor: { type: Types.Boolean }, 25 | enableNoise: { type: Types.Boolean }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/material/pbr.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { PBRMaterial as BabylonPBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial'; 3 | import { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture'; 4 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 5 | import { BabylonTypes } from '../../-private/ecsy-types'; 6 | 7 | export default class PbrMaterial extends Component { 8 | alpha!: number; 9 | directIntensity!: number; 10 | emissiveIntensity!: number; 11 | environmentIntensity!: number; 12 | specularIntensity!: number; 13 | albedoTexture!: BaseTexture | null; 14 | ambientTexture!: BaseTexture | null; 15 | ambientTextureStrength!: number; 16 | ambientTextureImpactOnAnalyticalLights!: number; 17 | opacityTexture!: BaseTexture | null; 18 | reflectionTexture!: BaseTexture | null; 19 | emissiveTexture!: BaseTexture | null; 20 | reflectivityTexture!: BaseTexture | null; 21 | metallicTexture!: BaseTexture | null; 22 | roughness!: number | null; 23 | metallic!: number | null; 24 | metallicF0Factor!: number; 25 | useMetallicF0FactorFromMetallicTexture = false; 26 | microSurfaceTexture!: BaseTexture | null; 27 | bumpTexture!: BaseTexture | null; 28 | lightmapTexture!: BaseTexture | null; 29 | ambientColor!: Color3; 30 | albedoColor!: Color3; 31 | reflectivityColor!: Color3; 32 | reflectionColor!: Color3; 33 | emissiveColor!: Color3; 34 | microSurface!: number; 35 | useLightmapAsShadowmap = false; 36 | useAlphaFromAlbedoTexture = false; 37 | forceAlphaTest = false; 38 | alphaCutOff!: number; 39 | useSpecularOverAlpha = true; 40 | useMicroSurfaceFromReflectivityMapAlpha = false; 41 | useRoughnessFromMetallicTextureAlpha = true; 42 | useRoughnessFromMetallicTextureGreen = false; 43 | useMetallnessFromMetallicTextureBlue = false; 44 | useAmbientOcclusionFromMetallicTextureRed = false; 45 | useAmbientInGrayScale = false; 46 | useAutoMicroSurfaceFromReflectivityMap = false; 47 | useRadianceOverAlpha = true; 48 | useObjectSpaceNormalMap = false; 49 | useParallax = false; 50 | useParallaxOcclusion = false; 51 | parallaxScaleBias!: number; 52 | disableLighting = false; 53 | forceIrradianceInFragment = false; 54 | maxSimultaneousLights!: number; 55 | invertNormalMapX = false; 56 | invertNormalMapY = false; 57 | twoSidedLighting = false; 58 | useAlphaFresnel = false; 59 | useLinearAlphaFresnel = false; 60 | environmentBRDFTexture!: BaseTexture | null; 61 | forceNormalForward = false; 62 | enableSpecularAntiAliasing = false; 63 | useHorizonOcclusion = true; 64 | useRadianceOcclusion = true; 65 | unlit = false; 66 | indexOfRefraction!: number; 67 | 68 | static schema: ComponentSchema = { 69 | alpha: { type: Types.Number, default: 1 }, 70 | directIntensity: { type: Types.Number, default: 1 }, 71 | emissiveIntensity: { type: Types.Number, default: 1 }, 72 | environmentIntensity: { type: Types.Number, default: 1 }, 73 | specularIntensity: { type: Types.Number, default: 1 }, 74 | albedoTexture: { type: Types.Ref, default: null }, 75 | ambientTexture: { type: Types.Ref, default: null }, 76 | ambientTextureStrength: { type: Types.Number, default: 1 }, 77 | ambientTextureImpactOnAnalyticalLights: { 78 | type: Types.Number, 79 | default: BabylonPBRMaterial.DEFAULT_AO_ON_ANALYTICAL_LIGHTS, 80 | }, 81 | opacityTexture: { type: Types.Ref, default: null }, 82 | reflectionTexture: { type: Types.Ref, default: null }, 83 | emissiveTexture: { type: Types.Ref, default: null }, 84 | reflectivityTexture: { type: Types.Ref, default: null }, 85 | metallicTexture: { type: Types.Ref, default: null }, 86 | roughness: { type: Types.Number, default: null }, 87 | metallic: { type: Types.Number, default: null }, 88 | metallicF0Factor: { type: Types.Number, default: 0.5 }, 89 | useMetallicF0FactorFromMetallicTexture: { type: Types.Boolean }, 90 | microSurfaceTexture: { type: Types.Ref, default: null }, 91 | bumpTexture: { type: Types.Ref, default: null }, 92 | lightmapTexture: { type: Types.Ref, default: null }, 93 | ambientColor: { type: BabylonTypes.Color3, default: Color3.Black() }, 94 | albedoColor: { type: BabylonTypes.Color3, default: Color3.White() }, 95 | reflectivityColor: { type: BabylonTypes.Color3, default: Color3.White() }, 96 | reflectionColor: { type: BabylonTypes.Color3, default: Color3.White() }, 97 | emissiveColor: { type: BabylonTypes.Color3, default: Color3.Black() }, 98 | microSurface: { type: Types.Number, default: 1 }, 99 | useLightmapAsShadowmap: { type: Types.Boolean }, 100 | useAlphaFromAlbedoTexture: { type: Types.Boolean }, 101 | forceAlphaTest: { type: Types.Boolean }, 102 | alphaCutOff: { type: Types.Number, default: 0.4 }, 103 | useSpecularOverAlpha: { type: Types.Boolean, default: true }, 104 | useMicroSurfaceFromReflectivityMapAlpha: { type: Types.Boolean }, 105 | useRoughnessFromMetallicTextureAlpha: { type: Types.Boolean, default: true }, 106 | useRoughnessFromMetallicTextureGreen: { type: Types.Boolean }, 107 | useMetallnessFromMetallicTextureBlue: { type: Types.Boolean }, 108 | useAmbientOcclusionFromMetallicTextureRed: { type: Types.Boolean }, 109 | useAmbientInGrayScale: { type: Types.Boolean }, 110 | useAutoMicroSurfaceFromReflectivityMap: { type: Types.Boolean }, 111 | useRadianceOverAlpha: { type: Types.Boolean, default: true }, 112 | useObjectSpaceNormalMap: { type: Types.Boolean }, 113 | useParallax: { type: Types.Boolean }, 114 | useParallaxOcclusion: { type: Types.Boolean }, 115 | parallaxScaleBias: { type: Types.Number, default: 0.05 }, 116 | disableLighting: { type: Types.Boolean }, 117 | forceIrradianceInFragment: { type: Types.Boolean }, 118 | maxSimultaneousLights: { type: Types.Number, default: 4 }, 119 | invertNormalMapX: { type: Types.Boolean }, 120 | invertNormalMapY: { type: Types.Boolean }, 121 | twoSidedLighting: { type: Types.Boolean }, 122 | useAlphaFresnel: { type: Types.Boolean }, 123 | useLinearAlphaFresnel: { type: Types.Boolean }, 124 | environmentBRDFTexture: { type: Types.Ref, default: null }, 125 | forceNormalForward: { type: Types.Boolean }, 126 | enableSpecularAntiAliasing: { type: Types.Boolean }, 127 | useHorizonOcclusion: { type: Types.Boolean, default: true }, 128 | useRadianceOcclusion: { type: Types.Boolean, default: true }, 129 | unlit: { type: Types.Boolean }, 130 | indexOfRefraction: { type: Types.Number, default: 1.5 }, 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /src/components/material/shadow-only.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Light } from '@babylonjs/core/Lights/light'; 3 | 4 | export default class ShadowOnlyMaterial extends Component { 5 | activeLight!: Light | null; 6 | 7 | static schema: ComponentSchema = { 8 | activeLight: { type: Types.Ref, default: null }, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/material/standard.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | import { BaseTexture } from '@babylonjs/core/Materials/Textures/baseTexture'; 5 | 6 | export default class StandardMaterial extends Component { 7 | ambientTexture!: BaseTexture | null; 8 | bumpTexture!: BaseTexture | null; 9 | diffuseTexture!: BaseTexture | null; 10 | emissiveTexture!: BaseTexture | null; 11 | lightmapTexture!: BaseTexture | null; 12 | opacityTexture!: BaseTexture | null; 13 | reflectionTexture!: BaseTexture | null; 14 | refractionTexture!: BaseTexture | null; 15 | specularTexture!: BaseTexture | null; 16 | ambientColor!: Color3; 17 | diffuseColor!: Color3; 18 | emissiveColor!: Color3; 19 | specularColor!: Color3; 20 | disableLighting!: boolean; 21 | alpha!: number; 22 | 23 | static schema: ComponentSchema = { 24 | ambientTexture: { type: Types.Ref, default: null }, 25 | bumpTexture: { type: Types.Ref, default: null }, 26 | diffuseTexture: { type: Types.Ref, default: null }, 27 | emissiveTexture: { type: Types.Ref, default: null }, 28 | lightmapTexture: { type: Types.Ref, default: null }, 29 | opacityTexture: { type: Types.Ref, default: null }, 30 | reflectionTexture: { type: Types.Ref, default: null }, 31 | refractionTexture: { type: Types.Ref, default: null }, 32 | specularTexture: { type: Types.Ref, default: null }, 33 | ambientColor: { type: BabylonTypes.Color3, default: Color3.Black() }, 34 | diffuseColor: { type: BabylonTypes.Color3, default: Color3.White() }, 35 | emissiveColor: { type: BabylonTypes.Color3, default: Color3.Black() }, 36 | specularColor: { type: BabylonTypes.Color3, default: Color3.White() }, 37 | disableLighting: { type: Types.Boolean }, 38 | alpha: { type: Types.Number, default: 1 }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/mesh.ts: -------------------------------------------------------------------------------- 1 | import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; 2 | import InstanceComponent from './_instance'; 3 | 4 | export default class MeshComponent extends InstanceComponent {} 5 | -------------------------------------------------------------------------------- /src/components/parent.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Entity, Types } from 'ecsy'; 2 | 3 | export default class Parent extends Component { 4 | value?: Entity; 5 | 6 | static schema: ComponentSchema = { 7 | value: { type: Types.Ref }, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/pivot-point.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../-private/ecsy-types'; 4 | 5 | export default class PivotPoint extends Component { 6 | value!: Vector3; 7 | 8 | static schema: ComponentSchema = { 9 | value: { type: BabylonTypes.Vector3 }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/position.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../-private/ecsy-types'; 4 | 5 | export default class Position extends Component { 6 | value!: Vector3; 7 | 8 | static schema: ComponentSchema = { 9 | value: { type: BabylonTypes.Vector3 }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/post-process-render-pipeline.ts: -------------------------------------------------------------------------------- 1 | import { PostProcessRenderPipeline as BabylonPostProcessRenderPipeline } from '@babylonjs/core/PostProcesses/RenderPipeline/index'; 2 | import InstanceArrayComponent from './_instance-array'; 3 | 4 | export default class PostProcessRenderPipeline< 5 | I extends BabylonPostProcessRenderPipeline = BabylonPostProcessRenderPipeline 6 | > extends InstanceArrayComponent {} 7 | -------------------------------------------------------------------------------- /src/components/post-process-render-pipeline/default.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { ImageProcessingPostProcess } from '@babylonjs/core/PostProcesses/imageProcessingPostProcess'; 3 | import { ChromaticAberrationPostProcess } from '@babylonjs/core/PostProcesses/chromaticAberrationPostProcess'; 4 | 5 | interface ImageProcessingOptions { 6 | colorCurvesEnabled: ImageProcessingPostProcess['colorCurvesEnabled']; 7 | colorCurves: ImageProcessingPostProcess['colorCurves']; 8 | colorGradingEnabled: ImageProcessingPostProcess['colorGradingEnabled']; 9 | colorGradingTexture: ImageProcessingPostProcess['colorGradingTexture']; 10 | exposure: ImageProcessingPostProcess['exposure']; 11 | toneMappingEnabled: ImageProcessingPostProcess['toneMappingEnabled']; 12 | toneMappingType: ImageProcessingPostProcess['toneMappingType']; 13 | contrast: ImageProcessingPostProcess['contrast']; 14 | vignetteStretch: ImageProcessingPostProcess['vignetteStretch']; 15 | vignetteCentreX: ImageProcessingPostProcess['vignetteCentreX']; 16 | vignetteCentreY: ImageProcessingPostProcess['vignetteCentreY']; 17 | vignetteWeight: ImageProcessingPostProcess['vignetteWeight']; 18 | vignetteColor: ImageProcessingPostProcess['vignetteColor']; 19 | vignetteCameraFov: ImageProcessingPostProcess['vignetteCameraFov']; 20 | vignetteBlendMode: ImageProcessingPostProcess['vignetteBlendMode']; 21 | vignetteEnabled: ImageProcessingPostProcess['vignetteEnabled']; 22 | fromLinearSpace: ImageProcessingPostProcess['fromLinearSpace']; 23 | } 24 | 25 | interface ChromaticAberrationOptions { 26 | aberrationAmount: ChromaticAberrationPostProcess['aberrationAmount']; 27 | adaptScaleToCurrentViewport: ChromaticAberrationPostProcess['adaptScaleToCurrentViewport']; 28 | alphaMode: ChromaticAberrationPostProcess['alphaMode']; 29 | alwaysForcePOT: ChromaticAberrationPostProcess['alwaysForcePOT']; 30 | enablePixelPerfectMode: ChromaticAberrationPostProcess['enablePixelPerfectMode']; 31 | forceFullscreenViewport: ChromaticAberrationPostProcess['forceFullscreenViewport']; 32 | } 33 | 34 | interface DepthOfFieldOptions { 35 | fStop: number; 36 | focalLength: number; 37 | focusDistance: number; 38 | lensSize: number; 39 | } 40 | 41 | interface FxaaOptions { 42 | samples: number; 43 | adaptScaleToCurrentViewport: boolean; 44 | } 45 | 46 | interface GlowLayerOptions { 47 | blurKernelSize: number; 48 | intensity: number; 49 | } 50 | 51 | interface GrainOptions { 52 | animated: boolean; 53 | intensity: number; 54 | adaptScaleToCurrentViewport: boolean; 55 | } 56 | 57 | interface SharpenOptions { 58 | edgeAmount: number; 59 | colorAmount: number; 60 | adaptScaleToCurrentViewport: boolean; 61 | } 62 | 63 | export default class DefaultRenderingPipeline extends Component { 64 | name!: string; 65 | imageProcessingEnabled?: boolean; 66 | imageProcessing: Partial | null | undefined; 67 | 68 | bloomEnabled?: boolean; 69 | bloomKernel?: number; 70 | bloomScale?: number; 71 | bloomThreshold?: number; 72 | bloomWeight?: number; 73 | 74 | chromaticAberrationEnabled?: boolean; 75 | chromaticAberration: Partial | null | undefined; 76 | 77 | depthOfFieldEnabled?: boolean; 78 | depthOfFieldBlurLevel?: number; 79 | depthOfField: Partial | null | undefined; 80 | 81 | samples?: number; 82 | 83 | fxaaEnabled?: boolean; 84 | fxaa: Partial | null | undefined; 85 | 86 | glowLayerEnabled?: boolean; 87 | glowLayer: Partial | null | undefined; 88 | 89 | grainEnabled?: boolean; 90 | grain: Partial | null | undefined; 91 | 92 | sharpenEnabled?: boolean; 93 | sharpen: Partial | null | undefined; 94 | 95 | static schema: ComponentSchema = { 96 | name: { type: Types.String, default: 'defaultPipeline' }, 97 | imageProcessingEnabled: { type: Types.Boolean, default: undefined }, 98 | imageProcessing: { type: Types.JSON }, 99 | bloomEnabled: { type: Types.Boolean, default: undefined }, 100 | bloomKernel: { type: Types.Number, default: undefined }, 101 | bloomScale: { type: Types.Number, default: undefined }, 102 | bloomThreshold: { type: Types.Number, default: undefined }, 103 | bloomWeight: { type: Types.Number, default: undefined }, 104 | chromaticAberrationEnabled: { type: Types.Boolean, default: undefined }, 105 | chromaticAberration: { type: Types.JSON }, 106 | 107 | depthOfFieldEnabled: { type: Types.Boolean, default: undefined }, 108 | depthOfFieldBlurLevel: { type: Types.Number, default: undefined }, 109 | depthOfField: { type: Types.JSON }, 110 | fxaaEnabled: { type: Types.Boolean, default: undefined }, 111 | 112 | samples: { type: Types.Number, default: undefined }, 113 | 114 | fxaa: { type: Types.JSON }, 115 | glowLayerEnabled: { type: Types.Boolean, default: undefined }, 116 | glowLayer: { type: Types.JSON }, 117 | grainEnabled: { type: Types.Boolean, default: undefined }, 118 | grain: { type: Types.JSON }, 119 | sharpenEnabled: { type: Types.Boolean, default: undefined }, 120 | sharpen: { type: Types.JSON }, 121 | }; 122 | } 123 | -------------------------------------------------------------------------------- /src/components/post-process-render-pipeline/ssao.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | 3 | export default class DefaultRenderingPipeline extends Component { 4 | name!: string; 5 | ssaoRatio!: number; 6 | combineRatio!: number; 7 | fallOff?: number; 8 | area?: number; 9 | radius?: number; 10 | totalStrength?: number; 11 | base?: number; 12 | 13 | static schema: ComponentSchema = { 14 | name: { type: Types.String, default: 'ssao' }, 15 | ssaoRatio: { type: Types.Number, default: 0.5 }, 16 | combineRatio: { type: Types.Number, default: 1 }, 17 | options: { type: Types.Number, default: 1 }, 18 | fallOff: { type: Types.Number, default: undefined }, 19 | area: { type: Types.Number, default: undefined }, 20 | radius: { type: Types.Number, default: undefined }, 21 | totalStrength: { type: Types.Number, default: undefined }, 22 | base: { type: Types.Number, default: undefined }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/components/post-process.ts: -------------------------------------------------------------------------------- 1 | import { PostProcess as BabylonPostProcess } from '@babylonjs/core/PostProcesses/postProcess'; 2 | import InstanceArrayComponent from './_instance-array'; 3 | 4 | export default class PostProcess extends InstanceArrayComponent< 5 | PostProcess, 6 | I 7 | > {} 8 | -------------------------------------------------------------------------------- /src/components/post-process/black-and-white.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { PostProcessOptions } from '@babylonjs/core/PostProcesses/postProcess'; 3 | import { Texture } from '@babylonjs/core/Materials/Textures/texture'; 4 | 5 | export default class BlackAndWhitePostProcess extends Component { 6 | name!: string; 7 | options!: number | PostProcessOptions; 8 | samplingMode!: number; 9 | 10 | static schema: ComponentSchema = { 11 | name: { type: Types.String, default: 'black-and-white' }, 12 | options: { type: Types.Number, default: 1 }, 13 | samplingMode: { type: Types.Number, default: Texture.BILINEAR_SAMPLINGMODE }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/components/post-process/blur.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Vector2 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../../-private/ecsy-types'; 4 | import { PostProcessOptions } from '@babylonjs/core/PostProcesses/postProcess'; 5 | import { Texture } from '@babylonjs/core/Materials/Textures/texture'; 6 | 7 | export default class BlurPostProcess extends Component { 8 | name!: string; 9 | direction!: Vector2; 10 | kernel!: number; 11 | options!: number | PostProcessOptions; 12 | samplingMode!: number; 13 | 14 | static schema: ComponentSchema = { 15 | name: { type: Types.String, default: 'blur' }, 16 | direction: { type: BabylonTypes.Vector2 }, 17 | kernel: { type: Types.Number }, 18 | options: { type: Types.Number, default: 1 }, 19 | samplingMode: { type: Types.Number, default: Texture.BILINEAR_SAMPLINGMODE }, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/post-process/motion-blur.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { PostProcessOptions } from '@babylonjs/core/PostProcesses/postProcess'; 3 | import { Texture } from '@babylonjs/core/Materials/Textures/texture'; 4 | 5 | export default class MotionBlurPostProcess extends Component { 6 | name!: string; 7 | options!: number | PostProcessOptions; 8 | samplingMode!: number; 9 | motionStrength!: number; 10 | motionBlurSamples!: number; 11 | 12 | static schema: ComponentSchema = { 13 | name: { type: Types.String, default: 'motion-blur' }, 14 | options: { type: Types.Number, default: 1 }, 15 | samplingMode: { type: Types.Number, default: Texture.BILINEAR_SAMPLINGMODE }, 16 | motionStrength: { type: Types.Number, default: 1 }, 17 | motionBlurSamples: { type: Types.Number, default: 32 }, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/primitive/box.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Mesh } from '@babylonjs/core/Meshes/mesh'; 3 | 4 | export default class Box extends Component { 5 | size!: number; 6 | width?: number; 7 | height?: number; 8 | depth?: number; 9 | updatable!: boolean; 10 | sideOrientation!: number; 11 | 12 | static schema: ComponentSchema = { 13 | size: { type: Types.Number, default: 1 }, 14 | width: { type: Types.Number, default: undefined }, 15 | height: { type: Types.Number, default: undefined }, 16 | depth: { type: Types.Number, default: undefined }, 17 | updatable: { type: Types.Boolean }, 18 | sideOrientation: { type: Types.Number, default: Mesh.DEFAULTSIDE }, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/primitive/lines.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { Color3, Color4 } from '@babylonjs/core/Maths/math.color'; 4 | import { BabylonTypes } from '../../-private/ecsy-types'; 5 | 6 | export default class LinesComponent extends Component { 7 | points!: Vector3[]; 8 | colors?: Color4[]; 9 | color!: Color3 | null; 10 | alpha!: number; 11 | useVertexAlpha!: boolean; 12 | updatable!: boolean; 13 | 14 | static schema: ComponentSchema = { 15 | points: { type: Types.Ref, default: [] }, 16 | colors: { type: Types.Ref, default: undefined }, 17 | color: { type: BabylonTypes.Color3, default: null }, 18 | alpha: { type: Types.Number, default: 1 }, 19 | useVertexAlpha: { type: Types.Boolean, default: true }, 20 | updatable: { type: Types.Boolean }, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /src/components/primitive/plane.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Mesh } from '@babylonjs/core/Meshes/mesh'; 3 | 4 | export default class PlaneComponent extends Component { 5 | size!: number; 6 | width?: number; 7 | height?: number; 8 | updatable!: boolean; 9 | sideOrientation!: number; 10 | 11 | static schema: ComponentSchema = { 12 | size: { type: Types.Number, default: 1 }, 13 | width: { type: Types.Number, default: undefined }, 14 | height: { type: Types.Number, default: undefined }, 15 | updatable: { type: Types.Boolean }, 16 | sideOrientation: { type: Types.Number, default: Mesh.DEFAULTSIDE }, 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/primitive/sphere.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { Mesh } from '@babylonjs/core/Meshes/mesh'; 3 | 4 | export default class SphereComponent extends Component { 5 | segments!: number; 6 | diameter!: number; 7 | diameterX?: number; 8 | diameterY?: number; 9 | diameterZ?: number; 10 | arc!: number; 11 | slice!: number; 12 | updatable!: boolean; 13 | sideOrientation!: number; 14 | 15 | static schema: ComponentSchema = { 16 | segments: { type: Types.Number, default: 32 }, 17 | diameter: { type: Types.Number, default: 1 }, 18 | diameterX: { type: Types.Number, default: undefined }, 19 | diameterY: { type: Types.Number, default: undefined }, 20 | diameterZ: { type: Types.Number, default: undefined }, 21 | arc: { type: Types.Number, default: 1 }, 22 | slice: { type: Types.Number, default: 1 }, 23 | updatable: { type: Types.Boolean }, 24 | sideOrientation: { type: Types.Number, default: Mesh.DEFAULTSIDE }, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/rotation.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../-private/ecsy-types'; 4 | 5 | export default class Rotation extends Component { 6 | value!: Vector3; 7 | 8 | static schema: ComponentSchema = { 9 | value: { type: BabylonTypes.Vector3 }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/scale.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema } from 'ecsy'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import { BabylonTypes } from '../-private/ecsy-types'; 4 | 5 | export default class Scale extends Component { 6 | value!: Vector3; 7 | 8 | static schema: ComponentSchema = { 9 | value: { type: BabylonTypes.Vector3 }, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/shadow-generator.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { ShadowGenerator as BabylonShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; 3 | 4 | export default class ShadowGenerator extends Component { 5 | value?: BabylonShadowGenerator; 6 | size = 512; 7 | enableSoftTransparentShadow?: boolean; 8 | forceBackFacesOnly?: boolean; 9 | frustumEdgeFalloff?: number; 10 | useBlurCloseExponentialShadowMap?: boolean; 11 | useBlurExponentialShadowMap?: boolean; 12 | useCloseExponentialShadowMap?: boolean; 13 | useContactHardeningShadow?: boolean; 14 | useExponentialShadowMap?: boolean; 15 | useKernelBlur?: boolean; 16 | usePercentageCloserFiltering?: boolean; 17 | usePoissonSampling?: boolean; 18 | blurKernel?: number; 19 | blurScale?: number; 20 | depthScale?: number; 21 | filter?: number; 22 | filteringQuality?: number; 23 | bias?: number; 24 | normalBias?: number; 25 | frustrumEdgeFallof?: number; 26 | darkness?: number; 27 | transparencyShadow?: boolean; 28 | 29 | static schema: ComponentSchema = { 30 | value: { type: Types.Ref }, 31 | size: { type: Types.Number, default: 512 }, 32 | enableSoftTransparentShadow: { type: Types.Boolean, default: undefined }, 33 | forceBackFacesOnly: { type: Types.Boolean, default: undefined }, 34 | frustumEdgeFalloff: { type: Types.Number, default: undefined }, 35 | useBlurCloseExponentialShadowMap: { type: Types.Boolean, default: undefined }, 36 | useBlurExponentialShadowMap: { type: Types.Boolean, default: undefined }, 37 | useCloseExponentialShadowMap: { type: Types.Boolean, default: undefined }, 38 | useContactHardeningShadow: { type: Types.Boolean, default: undefined }, 39 | useExponentialShadowMap: { type: Types.Boolean, default: undefined }, 40 | useKernelBlur: { type: Types.Boolean, default: undefined }, 41 | usePercentageCloserFiltering: { type: Types.Boolean, default: undefined }, 42 | usePoissonSampling: { type: Types.Boolean, default: undefined }, 43 | blurKernel: { type: Types.Number, default: undefined }, 44 | blurScale: { type: Types.Number, default: undefined }, 45 | depthScale: { type: Types.Number, default: undefined }, 46 | filter: { type: Types.Number, default: undefined }, 47 | filteringQuality: { type: Types.Number, default: undefined }, 48 | bias: { type: Types.Number, default: undefined }, 49 | normalBias: { type: Types.Number, default: undefined }, 50 | frustrumEdgeFallof: { type: Types.Number, default: undefined }, 51 | darkness: { type: Types.Number, default: undefined }, 52 | transparencyShadow: { type: Types.Boolean, default: undefined }, 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /src/components/transform-node.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { TransformNode as BabylonTransformNode } from '@babylonjs/core/Meshes/transformNode'; 3 | 4 | export default class TransformNode extends Component { 5 | value!: BabylonTransformNode | null; 6 | cloneNode!: boolean; 7 | 8 | static schema: ComponentSchema = { 9 | value: { type: Types.Ref, default: null }, 10 | cloneNode: { type: Types.Boolean }, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/transitions.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { EasingFunction } from '@babylonjs/core/Animations/easing'; 3 | 4 | export interface TransitionConfig { 5 | property: string; 6 | frameRate?: number; 7 | duration: number; 8 | easingFunction?: EasingFunction; 9 | } 10 | 11 | export default class Transitions extends Component { 12 | private _value!: TransitionConfig[]; 13 | private _previousValue?: TransitionConfig[]; 14 | 15 | get value(): TransitionConfig[] { 16 | return this._value; 17 | } 18 | 19 | set value(value: TransitionConfig[]) { 20 | this._previousValue = this._value; 21 | this._value = value; 22 | } 23 | 24 | get previousValue(): TransitionConfig[] | undefined { 25 | return this._previousValue; 26 | } 27 | 28 | static schema: ComponentSchema = { 29 | value: { type: Types.Array }, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/xr/default.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentSchema, Types } from 'ecsy'; 2 | import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; 3 | import { IWebXRInputOptions } from '@babylonjs/core/XR/webXRInput'; 4 | import { WebXRManagedOutputCanvasOptions } from '@babylonjs/core/XR/webXRManagedOutputCanvas'; 5 | import { WebXREnterExitUIOptions } from '@babylonjs/core/XR/webXREnterExitUI'; 6 | 7 | export default class WebXrDefaultExperience extends Component { 8 | /** 9 | * Enable or disable default UI to enter XR 10 | */ 11 | disableDefaultUI?: boolean; 12 | /** 13 | * Should teleportation not initialize. defaults to false. 14 | */ 15 | disableTeleportation?: boolean; 16 | /** 17 | * Floor meshes that will be used for teleport 18 | */ 19 | floorMeshes?: Array; 20 | /** 21 | * If set to true, the first frame will not be used to reset position 22 | * The first frame is mainly used when copying transformation from the old camera 23 | * Mainly used in AR 24 | */ 25 | ignoreNativeCameraTransformation?: boolean; 26 | /** 27 | * Disable the controller mesh-loading. Can be used if you want to load your own meshes 28 | */ 29 | inputOptions?: IWebXRInputOptions; 30 | /** 31 | * optional configuration for the output canvas 32 | */ 33 | outputCanvasOptions?: WebXRManagedOutputCanvasOptions; 34 | /** 35 | * optional UI options. This can be used among other to change session mode and reference space type 36 | */ 37 | uiOptions?: WebXREnterExitUIOptions; 38 | /** 39 | * When loading teleportation and pointer select, use stable versions instead of latest. 40 | */ 41 | useStablePlugins?: boolean; 42 | /** 43 | * An optional rendering group id that will be set globally for teleportation, pointer selection and default controller meshes 44 | */ 45 | renderingGroupId?: number; 46 | /** 47 | * A list of optional features to init the session with 48 | * If set to true, all features we support will be added 49 | */ 50 | optionalFeatures?: boolean | string[]; 51 | 52 | static schema: ComponentSchema = { 53 | disableDefaultUI: { type: Types.Boolean }, 54 | disableTeleportation: { type: Types.Boolean }, 55 | floorMeshes: { type: Types.Array }, 56 | ignoreNativeCameraTransformation: { type: Types.Boolean }, 57 | inputOptions: { type: Types.JSON }, 58 | outputCanvasOptions: { type: Types.JSON }, 59 | uiOptions: { type: Types.JSON }, 60 | useStablePlugins: { type: Types.Boolean }, 61 | renderingGroupId: { type: Types.Number }, 62 | optionalFeatures: { type: Types.JSON }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Component, ComponentConstructor, System, SystemConstructor } from 'ecsy'; 2 | import * as _components from './components'; 3 | import { 4 | ActionSystem, 5 | BabylonSystem, 6 | CameraSystem, 7 | MeshSystem, 8 | BoxPrimitiveSystem, 9 | LinesPrimitiveSystem, 10 | PlanePrimitiveSystem, 11 | SpherePrimitiveSystem, 12 | TransformSystem, 13 | PbrMaterialSystem, 14 | StandardMaterialSystem, 15 | BackgroundMaterialSystem, 16 | ShadowOnlyMaterialSystem, 17 | MaterialSystem, 18 | LightSystem, 19 | ShadowSystem, 20 | PostProcessSystem, 21 | BlurPostProcessSystem, 22 | MotionBlurPostProcessSystem, 23 | BlackAndWhitePostProcessSystem, 24 | TargetCameraSystem, 25 | ArcRotateCameraSystem, 26 | DirectionalLightSystem, 27 | SpotLightSystem, 28 | HemisphericLightSystem, 29 | PointLightSystem, 30 | PostProcessRenderPipelineSystem, 31 | SsaoRenderPipelineSystem, 32 | DefaultRenderPipelineSystem, 33 | TransitionSystem, 34 | WebXrDefaultExperienceSystem, 35 | } from './systems'; 36 | 37 | const components = Object.values(_components) as ComponentConstructor>[]; 38 | // export array of systems explicitly, as the order is relevant for proper system execution! 39 | const systems = [ 40 | BabylonSystem, 41 | TransitionSystem, 42 | TransformSystem, 43 | TargetCameraSystem, 44 | ArcRotateCameraSystem, 45 | CameraSystem, 46 | BoxPrimitiveSystem, 47 | LinesPrimitiveSystem, 48 | PlanePrimitiveSystem, 49 | SpherePrimitiveSystem, 50 | MeshSystem, 51 | PbrMaterialSystem, 52 | StandardMaterialSystem, 53 | BackgroundMaterialSystem, 54 | ShadowOnlyMaterialSystem, 55 | MaterialSystem, 56 | DirectionalLightSystem, 57 | SpotLightSystem, 58 | HemisphericLightSystem, 59 | PointLightSystem, 60 | LightSystem, 61 | ShadowSystem, 62 | ActionSystem, 63 | BlurPostProcessSystem, 64 | MotionBlurPostProcessSystem, 65 | BlackAndWhitePostProcessSystem, 66 | PostProcessSystem, 67 | DefaultRenderPipelineSystem, 68 | SsaoRenderPipelineSystem, 69 | PostProcessRenderPipelineSystem, 70 | WebXrDefaultExperienceSystem, 71 | ] as SystemConstructor[]; 72 | 73 | export { components, systems }; 74 | export * from './systems'; 75 | export * from './components'; 76 | export { default as World } from './world'; 77 | -------------------------------------------------------------------------------- /src/systems/action.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */ 2 | 3 | import { Entity } from 'ecsy'; 4 | import { Action, Mesh } from '../components'; 5 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 6 | import { assert } from '../-private/utils/debug'; 7 | import { ActionManager } from '@babylonjs/core/Actions/actionManager'; 8 | import { ExecuteCodeAction } from '@babylonjs/core/Actions/directActions'; 9 | import { AbstractActionManager } from '@babylonjs/core/Actions/abstractActionManager'; 10 | import '@babylonjs/core/Culling/ray'; // this has a side effect of making the scene support picks/rays, see https://doc.babylonjs.com/features/es6_support#side-effects 11 | 12 | const TRIGGER = { 13 | pick: ActionManager.OnPickTrigger, 14 | doublePick: ActionManager.OnDoublePickTrigger, 15 | centerPick: ActionManager.OnCenterPickTrigger, 16 | everyFrame: ActionManager.OnEveryFrameTrigger, 17 | intersectionEnter: ActionManager.OnIntersectionEnterTrigger, 18 | intersectionExit: ActionManager.OnIntersectionExitTrigger, 19 | keyDown: ActionManager.OnKeyDownTrigger, 20 | keyUp: ActionManager.OnKeyUpTrigger, 21 | leftPick: ActionManager.OnLeftPickTrigger, 22 | longPress: ActionManager.OnLongPressTrigger, 23 | pickDown: ActionManager.OnPickDownTrigger, 24 | pickOut: ActionManager.OnPickOutTrigger, 25 | pickUp: ActionManager.OnPickUpTrigger, 26 | pointerOut: ActionManager.OnPointerOutTrigger, 27 | pointerOver: ActionManager.OnPointerOverTrigger, 28 | rightPick: ActionManager.OnRightPickTrigger, 29 | }; 30 | 31 | export default class ActionSystem extends SystemWithCore { 32 | execute(): void { 33 | super.execute(); 34 | 35 | this.queries.action.added?.forEach((e: Entity) => this.setup(e)); 36 | this.queries.action.changed?.forEach((e: Entity) => this.setup(e)); 37 | this.queries.action.removed?.forEach((e: Entity) => this.remove(e)); 38 | 39 | super.afterExecute(); 40 | } 41 | 42 | setup(entity: Entity): void { 43 | assert('ActionSystem needs BabylonCoreComponent', this.core); 44 | const actionComponent = entity.getMutableComponent(Action)!; 45 | 46 | let actionManager = this.getActionManager(entity); 47 | if (actionManager === null) { 48 | const meshComponent = entity.getComponent(Mesh); 49 | assert('Action component can only be applied to Entities with a mesh', meshComponent?.value); 50 | 51 | actionManager = new ActionManager(this.core.scene); 52 | const mesh = meshComponent.value; 53 | mesh.actionManager = actionManager; 54 | } 55 | 56 | Object.keys(TRIGGER).forEach((triggerName) => { 57 | if (actionComponent._actions[triggerName]) { 58 | actionManager!.unregisterAction(actionComponent._actions[triggerName]); 59 | delete actionComponent._actions[triggerName]; 60 | } 61 | 62 | const fn = actionComponent[triggerName as keyof typeof TRIGGER]; 63 | if (!fn) { 64 | return; 65 | } 66 | 67 | const trigger = TRIGGER[triggerName as keyof typeof TRIGGER]; 68 | const action = actionManager!.registerAction(new ExecuteCodeAction(trigger, fn)); 69 | if (action) { 70 | actionComponent._actions[triggerName] = action; 71 | } 72 | }); 73 | } 74 | 75 | remove(entity: Entity): void { 76 | const meshComponent = entity.getComponent(Mesh); 77 | if (meshComponent?.value?.actionManager) { 78 | meshComponent.value.actionManager.dispose(); 79 | meshComponent.value.actionManager = null; 80 | } 81 | } 82 | 83 | private getActionManager(entity: Entity): AbstractActionManager | null { 84 | const meshComponent = entity.getComponent(Mesh); 85 | return (meshComponent && meshComponent.value && meshComponent.value.actionManager) || null; 86 | } 87 | 88 | static queries = { 89 | ...queries, 90 | action: { 91 | components: [Action, Mesh], 92 | listen: { 93 | added: true, 94 | changed: true, 95 | removed: true, 96 | }, 97 | }, 98 | }; 99 | } 100 | -------------------------------------------------------------------------------- /src/systems/babylon.ts: -------------------------------------------------------------------------------- 1 | import { Entity, System } from 'ecsy'; 2 | import { BabylonCore } from '../components'; 3 | import { Scene } from '@babylonjs/core/scene'; 4 | import { Engine } from '@babylonjs/core/Engines/engine'; 5 | 6 | export default class BabylonSystem extends System { 7 | listener?: EventListener; 8 | 9 | execute(): void { 10 | this.queries.core.added?.forEach((e: Entity) => this.setup(e)); 11 | this.queries.core.removed?.forEach((e: Entity) => this.remove(e)); 12 | } 13 | 14 | setup(entity: Entity): void { 15 | const core = entity.getMutableComponent(BabylonCore)!; 16 | 17 | core.engine = core.engine || new Engine(core.canvas, true, {}, false); 18 | core.scene = new Scene(core.engine); 19 | 20 | const startTime = window.performance.now(); 21 | core.engine.runRenderLoop((): void => { 22 | if (!core.engine || !core.scene) { 23 | throw new Error('Engine and/or Scene not found'); 24 | } 25 | 26 | const delta = core.engine.getDeltaTime(); 27 | const time = window.performance.now() - startTime; 28 | 29 | if (core.beforeRender) { 30 | core.beforeRender(delta, time); 31 | } 32 | core.world.execute(delta, time); 33 | 34 | // only render if there is an active camera 35 | if (core.scene.activeCamera) { 36 | core.scene.render(); 37 | } 38 | 39 | if (core.afterRender) { 40 | core.afterRender(delta, time); 41 | } 42 | }); 43 | } 44 | 45 | remove(entity: Entity): void { 46 | if (this.listener) { 47 | window.removeEventListener('resize', this.listener); 48 | } 49 | 50 | const core = entity.getRemovedComponent(BabylonCore)!; 51 | 52 | core.engine.stopRenderLoop(); 53 | core.scene.dispose(); 54 | core.engine.dispose(); 55 | } 56 | 57 | static queries = { 58 | core: { 59 | components: [BabylonCore], 60 | listen: { 61 | added: true, 62 | removed: true, 63 | }, 64 | }, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /src/systems/camera.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Camera, TransformNode } from '../components'; 3 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 4 | import { assert } from '../-private/utils/debug'; 5 | 6 | export default class CameraSystem extends SystemWithCore { 7 | execute(): void { 8 | super.execute(); 9 | 10 | this.queries.camera.added?.forEach((e: Entity) => this.setup(e)); 11 | this.queries.camera.changed?.forEach((e: Entity) => this.update(e)); 12 | this.queries.camera.removed?.forEach((e: Entity) => this.remove(e)); 13 | 14 | super.afterExecute(); 15 | } 16 | 17 | setup(entity: Entity): void { 18 | assert('CameraSystem needs BabylonCoreComponent', this.core); 19 | 20 | const { scene, canvas } = this.core; 21 | const { value: camera } = entity.getComponent(Camera)!; 22 | 23 | assert('Failed to add Camera, no camera instance found.', camera); 24 | 25 | scene.addCamera(camera); 26 | scene.activeCamera = camera; 27 | camera.attachControl(canvas, false); 28 | 29 | const transformNodeComponent = entity.getComponent(TransformNode); 30 | assert('TransformNode needed for cameras, add Parent component to fix', transformNodeComponent); 31 | camera.parent = transformNodeComponent.value; 32 | } 33 | 34 | update(entity: Entity): void { 35 | const { previousValue: prevInstance } = entity.getComponent(Camera)!; 36 | 37 | this.setup(entity); 38 | 39 | if (prevInstance) { 40 | prevInstance.dispose(); 41 | } 42 | } 43 | 44 | remove(entity: Entity): void { 45 | assert('CameraSystem needs BabylonCoreComponent', this.core); 46 | 47 | const cameraComponent = entity.getRemovedComponent(Camera)!; 48 | cameraComponent.value?.dispose(); 49 | } 50 | 51 | static queries = { 52 | ...queries, 53 | camera: { 54 | components: [Camera], 55 | listen: { 56 | added: true, 57 | changed: true, 58 | removed: true, 59 | }, 60 | }, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /src/systems/camera/arc-rotate.ts: -------------------------------------------------------------------------------- 1 | import { ArcRotateCamera, Camera } from '../../components'; 2 | import { ArcRotateCamera as BabylonArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | 8 | export default class ArcRotateCameraSystem extends FactorySystem< 9 | ArcRotateCamera, 10 | Camera, 11 | BabylonArcRotateCamera 12 | > { 13 | protected instanceComponentConstructor = Camera; 14 | protected transitionTarget = 'camera'; 15 | 16 | protected create(c: ArcRotateCamera): BabylonArcRotateCamera { 17 | assert('CameraSystem needs BabylonCoreComponent', this.core); 18 | 19 | const { alpha, beta, radius, target, ...rest } = c; 20 | const { scene } = this.core; 21 | const instance = new BabylonArcRotateCamera(ArcRotateCamera.name, alpha, beta, radius, target, scene, false); 22 | assign(instance, rest); 23 | 24 | return instance; 25 | } 26 | 27 | static queries = { 28 | ...queries, 29 | factory: { 30 | components: [ArcRotateCamera], 31 | listen: { 32 | added: true, 33 | changed: true, 34 | removed: true, 35 | }, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/systems/camera/target.ts: -------------------------------------------------------------------------------- 1 | import { Camera, TargetCamera } from '../../components'; 2 | import { TargetCamera as BabylonTargetCamera } from '@babylonjs/core/Cameras/targetCamera'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 8 | 9 | export default class TargetCameraSystem extends FactorySystem< 10 | TargetCamera, 11 | Camera, 12 | BabylonTargetCamera 13 | > { 14 | protected instanceComponentConstructor = Camera; 15 | protected transitionTarget = 'camera'; 16 | 17 | protected create(c: TargetCamera): BabylonTargetCamera { 18 | assert('CameraSystem needs BabylonCoreComponent', this.core); 19 | 20 | const { position, ...rest } = c; 21 | const { scene } = this.core; 22 | const camera = new BabylonTargetCamera(TargetCamera.name, position ?? Vector3.Zero(), scene, false); 23 | assign(camera, rest); 24 | 25 | return camera; 26 | } 27 | 28 | static queries = { 29 | ...queries, 30 | factory: { 31 | components: [TargetCamera], 32 | listen: { 33 | added: true, 34 | changed: true, 35 | removed: true, 36 | }, 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/systems/index.ts: -------------------------------------------------------------------------------- 1 | import BabylonSystem from './babylon'; 2 | import CameraSystem from './camera'; 3 | import TransformSystem from './transform'; 4 | import MeshSystem from './mesh'; 5 | import MaterialSystem from './material'; 6 | import LightSystem from './light'; 7 | import ShadowSystem from './shadow'; 8 | import ActionSystem from './action'; 9 | import PostProcessSystem from './post-process'; 10 | import ArcRotateCameraSystem from './camera/arc-rotate'; 11 | import DirectionalLightSystem from './light/directional'; 12 | import SpotLightSystem from './light/spot'; 13 | import BlurPostProcessSystem from './post-process/blur'; 14 | import MotionBlurPostProcessSystem from './post-process/motion-blur'; 15 | import BlackAndWhitePostProcessSystem from './post-process/black-and-white'; 16 | import HemisphericLightSystem from './light/hemispheric'; 17 | import PointLightSystem from './light/point'; 18 | import PbrMaterialSystem from './material/pbr'; 19 | import StandardMaterialSystem from './material/standard'; 20 | import BackgroundMaterialSystem from './material/background'; 21 | import ShadowOnlyMaterialSystem from './material/shadow-only'; 22 | import BoxPrimitiveSystem from './primitive/box'; 23 | import LinesPrimitiveSystem from './primitive/lines'; 24 | import PlanePrimitiveSystem from './primitive/plane'; 25 | import SpherePrimitiveSystem from './primitive/sphere'; 26 | import PostProcessRenderPipelineSystem from './post-process-render-pipeline'; 27 | import SsaoRenderPipelineSystem from './post-process-render-pipeline/ssao'; 28 | import DefaultRenderPipelineSystem from './post-process-render-pipeline/default'; 29 | import TargetCameraSystem from './camera/target'; 30 | import TransitionSystem from './transition'; 31 | import WebXrDefaultExperienceSystem from './xr/default'; 32 | 33 | export { 34 | BabylonSystem, 35 | TransformSystem, 36 | TargetCameraSystem, 37 | ArcRotateCameraSystem, 38 | CameraSystem, 39 | BoxPrimitiveSystem, 40 | LinesPrimitiveSystem, 41 | PlanePrimitiveSystem, 42 | SpherePrimitiveSystem, 43 | MeshSystem, 44 | PbrMaterialSystem, 45 | StandardMaterialSystem, 46 | BackgroundMaterialSystem, 47 | ShadowOnlyMaterialSystem, 48 | MaterialSystem, 49 | DirectionalLightSystem, 50 | SpotLightSystem, 51 | HemisphericLightSystem, 52 | PointLightSystem, 53 | LightSystem, 54 | ShadowSystem, 55 | ActionSystem, 56 | PostProcessSystem, 57 | BlurPostProcessSystem, 58 | MotionBlurPostProcessSystem, 59 | BlackAndWhitePostProcessSystem, 60 | PostProcessRenderPipelineSystem, 61 | SsaoRenderPipelineSystem, 62 | DefaultRenderPipelineSystem, 63 | TransitionSystem, 64 | WebXrDefaultExperienceSystem, 65 | }; 66 | -------------------------------------------------------------------------------- /src/systems/light.ts: -------------------------------------------------------------------------------- 1 | import { Entity, System } from 'ecsy'; 2 | import { Light, TransformNode } from '../components'; 3 | import { assert } from '../-private/utils/debug'; 4 | 5 | export default class LightSystem extends System { 6 | execute(): void { 7 | this.queries.light.added?.forEach((e: Entity) => this.setup(e)); 8 | this.queries.light.changed?.forEach((e: Entity) => this.update(e)); 9 | this.queries.light.removed?.forEach((e: Entity) => this.remove(e)); 10 | } 11 | 12 | setup(entity: Entity): void { 13 | const component = entity.getComponent(Light)!; 14 | assert('No light instance found', component.value); 15 | 16 | const transformNodeComponent = entity.getComponent(TransformNode); 17 | assert('TransformNode needed for lights, add Parent component to fix', transformNodeComponent); 18 | 19 | component.value.parent = transformNodeComponent.value; 20 | } 21 | 22 | update(entity: Entity): void { 23 | const { previousValue: prevInstance } = entity.getComponent(Light)!; 24 | 25 | this.setup(entity); 26 | 27 | if (prevInstance) { 28 | prevInstance.dispose(); 29 | } 30 | } 31 | 32 | remove(entity: Entity): void { 33 | const component = entity.getComponent(Light, true)!; 34 | component.value?.dispose(); 35 | } 36 | 37 | static queries = { 38 | light: { 39 | components: [Light], 40 | listen: { 41 | added: true, 42 | changed: true, 43 | removed: true, 44 | }, 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/systems/light/directional.ts: -------------------------------------------------------------------------------- 1 | import { DirectionalLight } from '../../components'; 2 | import { DirectionalLight as BabylonDirectionalLight } from '@babylonjs/core/Lights/directionalLight'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | import Light from '../../components/light'; 8 | 9 | export default class DirectionalLightSystem extends FactorySystem< 10 | DirectionalLight, 11 | Light, 12 | BabylonDirectionalLight 13 | > { 14 | protected instanceComponentConstructor = Light; 15 | protected transitionTarget = 'light'; 16 | 17 | protected create(c: DirectionalLight): BabylonDirectionalLight { 18 | assert('DirectionalLightSystem needs BabylonCoreComponent', this.core); 19 | 20 | const { direction, ...options } = c; 21 | const instance = new BabylonDirectionalLight(DirectionalLight.name, direction, this.core.scene); 22 | assign(instance, options); 23 | 24 | return instance; 25 | } 26 | 27 | static queries = { 28 | ...queries, 29 | factory: { 30 | components: [DirectionalLight], 31 | listen: { 32 | added: true, 33 | changed: true, 34 | removed: true, 35 | }, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/systems/light/hemispheric.ts: -------------------------------------------------------------------------------- 1 | import { HemisphericLight } from '../../components'; 2 | import { HemisphericLight as BabylonHemisphericLight } from '@babylonjs/core/Lights/hemisphericLight'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | import Light from '../../components/light'; 8 | 9 | export default class HemisphericLightSystem extends FactorySystem< 10 | HemisphericLight, 11 | Light, 12 | BabylonHemisphericLight 13 | > { 14 | protected instanceComponentConstructor = Light; 15 | protected transitionTarget = 'light'; 16 | 17 | protected create(c: HemisphericLight): BabylonHemisphericLight { 18 | assert('HemisphericLightSystem needs BabylonCoreComponent', this.core); 19 | 20 | const { direction, ...options } = c; 21 | const instance = new BabylonHemisphericLight(HemisphericLight.name, direction, this.core.scene); 22 | assign(instance, options); 23 | 24 | return instance; 25 | } 26 | 27 | static queries = { 28 | ...queries, 29 | factory: { 30 | components: [HemisphericLight], 31 | listen: { 32 | added: true, 33 | changed: true, 34 | removed: true, 35 | }, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/systems/light/point.ts: -------------------------------------------------------------------------------- 1 | import { PointLight } from '../../components'; 2 | import { PointLight as BabylonPointLight } from '@babylonjs/core/Lights/pointLight'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | import Light from '../../components/light'; 8 | 9 | export default class PointLightSystem extends FactorySystem, BabylonPointLight> { 10 | protected instanceComponentConstructor = Light; 11 | protected transitionTarget = 'light'; 12 | 13 | protected create(c: PointLight): BabylonPointLight { 14 | assert('PointLightSystem needs BabylonCoreComponent', this.core); 15 | 16 | const { position, ...options } = c; 17 | const instance = new BabylonPointLight(PointLight.name, position, this.core.scene); 18 | assign(instance, options); 19 | 20 | return instance; 21 | } 22 | 23 | static queries = { 24 | ...queries, 25 | factory: { 26 | components: [PointLight], 27 | listen: { 28 | added: true, 29 | changed: true, 30 | removed: true, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/systems/light/spot.ts: -------------------------------------------------------------------------------- 1 | import { SpotLight } from '../../components'; 2 | import { SpotLight as BabylonSpotLight } from '@babylonjs/core/Lights/spotLight'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | import Light from '../../components/light'; 8 | 9 | export default class SpotLightSystem extends FactorySystem, BabylonSpotLight> { 10 | protected instanceComponentConstructor = Light; 11 | protected transitionTarget = 'light'; 12 | 13 | protected create(c: SpotLight): BabylonSpotLight { 14 | assert('SpotLightSystem needs BabylonCoreComponent', this.core); 15 | 16 | const { direction, position, angle, exponent, ...options } = c; 17 | const instance = new BabylonSpotLight(SpotLight.name, position, direction, angle, exponent, this.core.scene); 18 | assign(instance, options); 19 | 20 | return instance; 21 | } 22 | 23 | static queries = { 24 | ...queries, 25 | factory: { 26 | components: [SpotLight], 27 | listen: { 28 | added: true, 29 | changed: true, 30 | removed: true, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/systems/material.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Material, Mesh } from '../components'; 3 | import { assert } from '../-private/utils/debug'; 4 | import { assign } from '../-private/utils/assign'; 5 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 6 | import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; 7 | 8 | export default class MaterialSystem extends SystemWithCore { 9 | execute(): void { 10 | super.execute(); 11 | 12 | this.queries.Material.removed?.forEach((e: Entity) => this.remove(e)); 13 | this.queries.Material.added?.forEach((e: Entity) => this.setup(e)); 14 | this.queries.Material.changed?.forEach((e: Entity) => this.setup(e)); 15 | 16 | super.afterExecute(); 17 | } 18 | 19 | hasMesh(entity: Entity, removed = false): boolean { 20 | const component = entity.getComponent(Mesh, removed); 21 | 22 | return !!component?.value; 23 | } 24 | 25 | getMesh(entity: Entity, removed = false): AbstractMesh { 26 | // Optionally allow getting the TransformNode as a removed component. 27 | // Useful in the case where the entire Entity is being removed. 28 | const meshComponent = removed 29 | ? entity.getComponent(Mesh) || entity.getRemovedComponent(Mesh) 30 | : entity.getComponent(Mesh); 31 | 32 | assert('No valid ECSY Mesh component found on this Entity.', meshComponent && meshComponent.value); 33 | 34 | return meshComponent.value; 35 | } 36 | 37 | setup(entity: Entity): void { 38 | const mesh = this.getMesh(entity); 39 | const materialComponent = entity.getComponent(Material)!; 40 | 41 | if (materialComponent.value) { 42 | const { value, overrides } = materialComponent; 43 | 44 | assign(value, overrides); 45 | mesh.material = value; 46 | } else { 47 | console.warn(`No material was applied to mesh "${mesh.name}".`); 48 | } 49 | } 50 | 51 | remove(entity: Entity): void { 52 | // remove mesh from material if there still is one 53 | if (this.hasMesh(entity, true)) { 54 | const mesh = this.getMesh(entity, true); 55 | 56 | if (mesh.material) { 57 | mesh.material = null; 58 | } 59 | } 60 | } 61 | 62 | static queries = { 63 | ...queries, 64 | Material: { 65 | components: [Mesh, Material], 66 | listen: { 67 | added: true, 68 | changed: [Material], 69 | removed: true, 70 | }, 71 | }, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/systems/material/background.ts: -------------------------------------------------------------------------------- 1 | import { BackgroundMaterial, Material } from '../../components'; 2 | import { BackgroundMaterial as BabylonBackgroundMaterial } from '@babylonjs/core/Materials/Background/backgroundMaterial'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | 8 | export default class BackgroundMaterialSystem extends FactorySystem< 9 | BackgroundMaterial, 10 | Material, 11 | BabylonBackgroundMaterial 12 | > { 13 | protected instanceComponentConstructor = Material; 14 | protected transitionTarget = 'material'; 15 | 16 | protected create(c: BackgroundMaterial): BabylonBackgroundMaterial { 17 | assert('PbrMaterialSystem needs BabylonCoreComponent', this.core); 18 | 19 | const instance = new BabylonBackgroundMaterial(BackgroundMaterial.name, this.core.scene); 20 | assign(instance, c); 21 | 22 | return instance; 23 | } 24 | 25 | static queries = { 26 | ...queries, 27 | factory: { 28 | components: [BackgroundMaterial], 29 | listen: { 30 | added: true, 31 | changed: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/systems/material/pbr.ts: -------------------------------------------------------------------------------- 1 | import { Material, PbrMaterial } from '../../components'; 2 | import { PBRMaterial as BabylonPBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | 8 | export default class PbrMaterialSystem extends FactorySystem< 9 | PbrMaterial, 10 | Material, 11 | BabylonPBRMaterial 12 | > { 13 | protected instanceComponentConstructor = Material; 14 | protected transitionTarget = 'material'; 15 | 16 | protected create(c: PbrMaterial): BabylonPBRMaterial { 17 | assert('PbrMaterialSystem needs BabylonCoreComponent', this.core); 18 | 19 | const instance = new BabylonPBRMaterial(PbrMaterial.name, this.core.scene); 20 | assign(instance, c); 21 | 22 | return instance; 23 | } 24 | 25 | static queries = { 26 | ...queries, 27 | factory: { 28 | components: [PbrMaterial], 29 | listen: { 30 | added: true, 31 | changed: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/systems/material/shadow-only.ts: -------------------------------------------------------------------------------- 1 | import { Material, ShadowOnlyMaterial } from '../../components'; 2 | import { ShadowOnlyMaterial as BabylonShadowOnlyMaterial } from '@babylonjs/materials/shadowOnly/shadowOnlyMaterial'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | 8 | export default class ShadowOnlyMaterialSystem extends FactorySystem< 9 | ShadowOnlyMaterial, 10 | Material, 11 | BabylonShadowOnlyMaterial 12 | > { 13 | protected instanceComponentConstructor = Material; 14 | protected transitionTarget = 'material'; 15 | 16 | protected create(c: ShadowOnlyMaterial): BabylonShadowOnlyMaterial { 17 | assert('PbrMaterialSystem needs BabylonCoreComponent', this.core); 18 | 19 | const instance = new BabylonShadowOnlyMaterial(ShadowOnlyMaterial.name, this.core.scene); 20 | assign(instance, c); 21 | 22 | return instance; 23 | } 24 | 25 | static queries = { 26 | ...queries, 27 | factory: { 28 | components: [ShadowOnlyMaterial], 29 | listen: { 30 | added: true, 31 | changed: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/systems/material/standard.ts: -------------------------------------------------------------------------------- 1 | import { Material, StandardMaterial } from '../../components'; 2 | import { StandardMaterial as BabylonStandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import { assert } from '../../-private/utils/debug'; 7 | 8 | export default class StandardMaterialSystem extends FactorySystem< 9 | StandardMaterial, 10 | Material, 11 | BabylonStandardMaterial 12 | > { 13 | protected instanceComponentConstructor = Material; 14 | protected transitionTarget = 'material'; 15 | 16 | protected create(c: StandardMaterial): BabylonStandardMaterial { 17 | assert('StandardMaterialSystem needs BabylonCoreComponent', this.core); 18 | 19 | const instance = new BabylonStandardMaterial(StandardMaterial.name, this.core.scene); 20 | assign(instance, c); 21 | 22 | return instance; 23 | } 24 | 25 | static queries = { 26 | ...queries, 27 | factory: { 28 | components: [StandardMaterial], 29 | listen: { 30 | added: true, 31 | changed: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/systems/mesh.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Material, Mesh, TransformNode } from '../components'; 3 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 4 | import { assert } from '../-private/utils/debug'; 5 | import { assign } from '../-private/utils/assign'; 6 | import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh'; 7 | 8 | function detachFromScene(mesh: AbstractMesh): void { 9 | const scene = mesh.getScene(); 10 | if (scene) { 11 | scene.removeMesh(mesh); 12 | } 13 | } 14 | 15 | export default class MeshSystem extends SystemWithCore { 16 | execute(): void { 17 | super.execute(); 18 | 19 | this.queries.meshes.added?.forEach((e: Entity) => this.setup(e)); 20 | this.queries.meshes.changed?.forEach((e: Entity) => this.update(e)); 21 | this.queries.meshes.removed?.forEach((e: Entity) => this.remove(e)); 22 | 23 | super.afterExecute(); 24 | } 25 | 26 | setup(entity: Entity): void { 27 | const meshComponent = entity.getComponent(Mesh); 28 | 29 | assert('MeshSystem needs BabylonCoreComponent', this.core); 30 | assert('Failed to add Mesh Component. No valid Mesh found.', !!meshComponent?.value); 31 | 32 | const mesh = meshComponent.value; 33 | detachFromScene(mesh); 34 | 35 | const transformNodeComponent = entity.getComponent(TransformNode); 36 | assert('TransformNode needed for meshes, add Parent component to fix', transformNodeComponent); 37 | 38 | mesh.parent = transformNodeComponent.value; 39 | mesh.computeWorldMatrix(true); // @todo still needed? 40 | 41 | const { value, overrides } = meshComponent; 42 | assign(value, overrides); 43 | 44 | this.core.scene.addMesh(mesh, true); 45 | } 46 | 47 | update(entity: Entity): void { 48 | const meshComponent = entity.getComponent(Mesh)!; 49 | const mesh = meshComponent.value; 50 | const previousMesh = meshComponent.previousValue; 51 | 52 | if (previousMesh && mesh !== previousMesh) { 53 | this.removeMesh(previousMesh); 54 | } 55 | 56 | this.setup(entity); 57 | if (previousMesh && mesh) { 58 | mesh.material = previousMesh.material; 59 | } 60 | } 61 | 62 | remove(entity: Entity): void { 63 | const meshComponent = entity.getRemovedComponent(Mesh); 64 | 65 | assert('MeshSystem needs BabylonCoreComponent', this.core); 66 | assert( 67 | 'No removed Mesh Component found. Make sure this system is registered at the correct time.', 68 | !!meshComponent?.value 69 | ); 70 | const mesh = meshComponent.value; 71 | if (entity.hasComponent(Material) || entity.hasRemovedComponent(Material)) { 72 | // unset the material so it is not also disposed of here 73 | meshComponent.value.material = null; 74 | } 75 | 76 | const isUsed = this.queries.meshes.results.some((e) => e !== entity && e.getComponent(Mesh)!.value === mesh); 77 | 78 | if (!isUsed) { 79 | this.removeMesh(meshComponent.value); 80 | } 81 | } 82 | 83 | private removeMesh(mesh: AbstractMesh): void { 84 | assert('MeshSystem needs BabylonCoreComponent', this.core); 85 | 86 | this.core.scene.removeMesh(mesh, false); 87 | 88 | mesh.dispose(true, false); 89 | } 90 | 91 | static queries = { 92 | ...queries, 93 | meshes: { 94 | components: [Mesh], 95 | listen: { 96 | added: true, 97 | changed: true, 98 | removed: true, 99 | }, 100 | }, 101 | }; 102 | } 103 | -------------------------------------------------------------------------------- /src/systems/post-process-render-pipeline.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Camera, PostProcessRenderPipeline } from '../components'; 3 | import { Camera as BabylonCamera } from '@babylonjs/core/Cameras/camera'; 4 | import { PostProcessRenderPipeline as BabylonPostProcessRenderPipeline } from '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipeline'; 5 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 6 | import { assert } from '../-private/utils/debug'; 7 | import '@babylonjs/core/PostProcesses/RenderPipeline/postProcessRenderPipelineManagerSceneComponent'; 8 | 9 | export default class PostProcessRenderPipelineSystem extends SystemWithCore { 10 | execute(): void { 11 | super.execute(); 12 | 13 | this.queries.postprocess.added?.forEach((e: Entity) => this.setup(e)); 14 | this.queries.postprocess.changed?.forEach((e: Entity) => this.update(e)); 15 | this.queries.postprocess.removed?.forEach((e: Entity) => this.remove(e)); 16 | 17 | super.afterExecute(); 18 | } 19 | 20 | setup(entity: Entity): void { 21 | const ppComponent = entity.getComponent(PostProcessRenderPipeline); 22 | 23 | assert('Failed to add PostProcess Component. No valid PostProcess found.', !!ppComponent?.value); 24 | 25 | const pps = ppComponent.value; 26 | const camera = this.getCamera(entity); 27 | pps.forEach((pp) => this.addPostProcess(camera, pp)); 28 | } 29 | 30 | private getCamera(entity: Entity): BabylonCamera { 31 | const cameraComponent = entity.getComponent(Camera, true); 32 | assert('No Camera found for post processing', cameraComponent?.value); 33 | return cameraComponent.value; 34 | } 35 | 36 | private addPostProcess(camera: BabylonCamera, pp: BabylonPostProcessRenderPipeline): void { 37 | const { postProcessRenderPipelineManager } = camera.getScene(); 38 | 39 | if (!postProcessRenderPipelineManager.supportedPipelines.includes(pp)) { 40 | postProcessRenderPipelineManager.addPipeline(pp); 41 | postProcessRenderPipelineManager.attachCamerasToRenderPipeline(pp.name, camera); 42 | } 43 | } 44 | 45 | private removePostProcess(camera: BabylonCamera, pp: BabylonPostProcessRenderPipeline): void { 46 | const { postProcessRenderPipelineManager } = camera.getScene(); 47 | 48 | // @todo no addPipeline() equivalent needed? 49 | postProcessRenderPipelineManager.detachCamerasFromRenderPipeline(pp.name, camera); 50 | } 51 | 52 | update(entity: Entity): void { 53 | const ppComponent = entity.getMutableComponent(PostProcessRenderPipeline); 54 | assert( 55 | 'Failed to add PostProcessRenderPipeline Component. No valid PostProcessRenderPipeline found.', 56 | !!ppComponent?.value 57 | ); 58 | 59 | const pps = ppComponent.value; 60 | const prevPps = ppComponent.previousValue; 61 | 62 | let added: BabylonPostProcessRenderPipeline[]; 63 | let removed: BabylonPostProcessRenderPipeline[] | undefined; 64 | 65 | if (prevPps) { 66 | added = pps.filter((pp) => !prevPps.includes(pp)); 67 | removed = prevPps.filter((pp) => !pps.includes(pp)); 68 | } else { 69 | added = pps; 70 | } 71 | 72 | const camera = this.getCamera(entity); 73 | if (removed) { 74 | removed.forEach((pp) => this.removePostProcess(camera, pp)); 75 | } 76 | added.forEach((pp) => this.addPostProcess(camera, pp)); 77 | } 78 | 79 | remove(entity: Entity): void { 80 | const ppComponent = entity.getRemovedComponent(PostProcessRenderPipeline); 81 | assert('Failed to remove PostProcess Component. No valid PostProcess found.', !!ppComponent?.value); 82 | 83 | const camera = this.getCamera(entity); 84 | ppComponent.value.forEach((pp) => this.removePostProcess(camera, pp)); 85 | } 86 | 87 | static queries = { 88 | ...queries, 89 | postprocess: { 90 | components: [PostProcessRenderPipeline], 91 | listen: { 92 | added: true, 93 | changed: true, 94 | removed: true, 95 | }, 96 | }, 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /src/systems/post-process-render-pipeline/default.ts: -------------------------------------------------------------------------------- 1 | import { DefaultRenderingPipeline, PostProcessRenderPipeline } from '../../components'; 2 | import { queries } from '../../-private/systems/with-core'; 3 | import { assert } from '../../-private/utils/debug'; 4 | import { DefaultRenderingPipeline as BabylonDefaultRenderingPipeline } from '@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/defaultRenderingPipeline'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import FactoryArraySystem from '../../-private/systems/factory-array'; 7 | import '@babylonjs/core/Rendering/depthRendererSceneComponent'; 8 | import { Entity } from 'ecsy'; 9 | 10 | export default class DefaultRenderPipelineSystem extends FactoryArraySystem< 11 | DefaultRenderingPipeline, 12 | PostProcessRenderPipeline, 13 | BabylonDefaultRenderingPipeline 14 | > { 15 | protected instanceComponentConstructor = PostProcessRenderPipeline; 16 | protected instanceConstructor = BabylonDefaultRenderingPipeline; 17 | 18 | protected create(c: DefaultRenderingPipeline): BabylonDefaultRenderingPipeline { 19 | assert('SsaoRenderPipelineSystem needs BabylonCoreComponent', this.core); 20 | 21 | const instance = new BabylonDefaultRenderingPipeline(c.name, true, this.core.scene); 22 | this._updateInstance(instance, c); 23 | 24 | return instance; 25 | } 26 | 27 | protected updateInstance( 28 | entity: Entity, 29 | instance: BabylonDefaultRenderingPipeline, 30 | c: DefaultRenderingPipeline 31 | ): void { 32 | this._updateInstance(instance, c); 33 | } 34 | 35 | private _updateInstance(instance: BabylonDefaultRenderingPipeline, c: DefaultRenderingPipeline): void { 36 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 37 | const { name, imageProcessing, chromaticAberration, depthOfField, fxaa, glowLayer, grain, sharpen, ...args } = c; 38 | assign(instance, args); 39 | assign(instance.imageProcessing, imageProcessing); 40 | assign(instance.chromaticAberration, chromaticAberration); 41 | assign(instance.depthOfField, depthOfField); 42 | assign(instance.fxaa, fxaa); 43 | if (instance.glowLayer) assign(instance.glowLayer, glowLayer); 44 | assign(instance.grain, grain); 45 | assign(instance.sharpen, sharpen); 46 | } 47 | 48 | static queries = { 49 | ...queries, 50 | factory: { 51 | components: [DefaultRenderingPipeline], 52 | listen: { 53 | added: true, 54 | changed: true, 55 | removed: true, 56 | }, 57 | }, 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /src/systems/post-process-render-pipeline/ssao.ts: -------------------------------------------------------------------------------- 1 | import { PostProcessRenderPipeline, SsaoRenderingPipeline } from '../../components'; 2 | import { queries } from '../../-private/systems/with-core'; 3 | import { assert } from '../../-private/utils/debug'; 4 | import { SSAORenderingPipeline as BabylonSSAORenderingPipeline } from '@babylonjs/core/PostProcesses/RenderPipeline/Pipelines/ssaoRenderingPipeline'; 5 | import { assign } from '../../-private/utils/assign'; 6 | import FactoryArraySystem from '../../-private/systems/factory-array'; 7 | import '@babylonjs/core/Rendering/depthRendererSceneComponent'; 8 | 9 | export default class SsaoRenderPipelineSystem extends FactoryArraySystem< 10 | SsaoRenderingPipeline, 11 | PostProcessRenderPipeline, 12 | BabylonSSAORenderingPipeline 13 | > { 14 | protected instanceComponentConstructor = PostProcessRenderPipeline; 15 | protected instanceConstructor = BabylonSSAORenderingPipeline; 16 | 17 | protected create(c: SsaoRenderingPipeline): BabylonSSAORenderingPipeline { 18 | assert('SsaoRenderPipelineSystem needs BabylonCoreComponent', this.core); 19 | 20 | const { name, combineRatio, ssaoRatio, ...args } = c; 21 | const instance = new BabylonSSAORenderingPipeline(name, this.core.scene, { combineRatio, ssaoRatio }); 22 | assign(instance, args); 23 | 24 | return instance; 25 | } 26 | 27 | static queries = { 28 | ...queries, 29 | factory: { 30 | components: [SsaoRenderingPipeline], 31 | listen: { 32 | added: true, 33 | changed: true, 34 | removed: true, 35 | }, 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/systems/post-process.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Camera, PostProcess } from '../components'; 3 | import { Camera as BabylonCamera } from '@babylonjs/core/Cameras/camera'; 4 | import { PostProcess as BabylonPostProcess } from '@babylonjs/core/PostProcesses/postProcess'; 5 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 6 | import { assert } from '../-private/utils/debug'; 7 | 8 | export default class PostProcessSystem extends SystemWithCore { 9 | execute(): void { 10 | super.execute(); 11 | 12 | this.queries.postprocess.added?.forEach((e: Entity) => this.setup(e)); 13 | this.queries.postprocess.changed?.forEach((e: Entity) => this.update(e)); 14 | this.queries.postprocess.removed?.forEach((e: Entity) => this.remove(e)); 15 | 16 | super.afterExecute(); 17 | } 18 | 19 | setup(entity: Entity): void { 20 | const ppComponent = entity.getComponent(PostProcess); 21 | 22 | assert('Failed to add PostProcess Component. No valid PostProcess found.', !!ppComponent?.value); 23 | 24 | const pps = ppComponent.value; 25 | const camera = this.getCamera(entity); 26 | pps.forEach((pp) => this.addPostProcess(camera, pp)); 27 | } 28 | 29 | private getCamera(entity: Entity): BabylonCamera { 30 | const cameraComponent = entity.getComponent(Camera, true); 31 | assert('No Camera found for post processing', cameraComponent?.value); 32 | return cameraComponent.value; 33 | } 34 | 35 | private addPostProcess(camera: BabylonCamera, pp: BabylonPostProcess): void { 36 | const scene = camera.getScene(); 37 | 38 | camera.attachPostProcess(pp); 39 | if (!scene.postProcesses.includes(pp)) { 40 | scene.postProcesses.push(pp); 41 | } 42 | } 43 | 44 | private removePostProcess(camera: BabylonCamera, pp: BabylonPostProcess): void { 45 | const scene = camera.getScene(); 46 | const index = scene.postProcesses.indexOf(pp); 47 | 48 | camera.detachPostProcess(pp); 49 | if (index !== -1) { 50 | scene.postProcesses.splice(index, 1); 51 | } 52 | } 53 | 54 | update(entity: Entity): void { 55 | const ppComponent = entity.getMutableComponent(PostProcess); 56 | assert('Failed to add PostProcess Component. No valid PostProcess found.', !!ppComponent?.value); 57 | 58 | const pps = ppComponent.value; 59 | const prevPps = ppComponent.previousValue; 60 | 61 | let added: BabylonPostProcess[]; 62 | let removed: BabylonPostProcess[] | undefined; 63 | 64 | if (prevPps) { 65 | added = pps.filter((pp) => !prevPps.includes(pp)); 66 | removed = prevPps.filter((pp) => !pps.includes(pp)); 67 | } else { 68 | added = pps; 69 | } 70 | 71 | const camera = this.getCamera(entity); 72 | if (removed) { 73 | removed.forEach((pp) => this.removePostProcess(camera, pp)); 74 | } 75 | added.forEach((pp) => this.addPostProcess(camera, pp)); 76 | } 77 | 78 | remove(entity: Entity): void { 79 | const ppComponent = entity.getRemovedComponent(PostProcess); 80 | assert('Failed to remove PostProcess Component. No valid PostProcess found.', !!ppComponent?.value); 81 | 82 | const camera = this.getCamera(entity); 83 | ppComponent.value.forEach((pp) => this.removePostProcess(camera, pp)); 84 | } 85 | 86 | static queries = { 87 | ...queries, 88 | postprocess: { 89 | components: [PostProcess], 90 | listen: { 91 | added: true, 92 | changed: true, 93 | removed: true, 94 | }, 95 | }, 96 | }; 97 | } 98 | -------------------------------------------------------------------------------- /src/systems/post-process/black-and-white.ts: -------------------------------------------------------------------------------- 1 | import { BlackAndWhitePostProcess, PostProcess } from '../../components'; 2 | import { BlackAndWhitePostProcess as BabylonBlackAndWhitePostProcess } from '@babylonjs/core/PostProcesses/blackAndWhitePostProcess'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import { Camera } from '@babylonjs/core/Cameras/camera'; 5 | import FactoryArraySystem from '../../-private/systems/factory-array'; 6 | 7 | export default class BlackAndWhitePostProcessSystem extends FactoryArraySystem< 8 | BlackAndWhitePostProcess, 9 | PostProcess, 10 | BabylonBlackAndWhitePostProcess 11 | > { 12 | protected instanceComponentConstructor = PostProcess; 13 | protected instanceConstructor = BabylonBlackAndWhitePostProcess; 14 | 15 | protected create(c: BlackAndWhitePostProcess): BabylonBlackAndWhitePostProcess { 16 | return new BabylonBlackAndWhitePostProcess( 17 | c.name, 18 | c.options, 19 | null as unknown as Camera, // class constructor is wrongly typed in Babylon 20 | c.samplingMode, 21 | this.core?.engine 22 | ); 23 | } 24 | 25 | static queries = { 26 | ...queries, 27 | factory: { 28 | components: [BlackAndWhitePostProcess], 29 | listen: { 30 | added: true, 31 | changed: true, 32 | removed: true, 33 | }, 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/systems/post-process/blur.ts: -------------------------------------------------------------------------------- 1 | import { BlurPostProcess, PostProcess } from '../../components'; 2 | import { BlurPostProcess as BabylonBlurPostProcess } from '@babylonjs/core/PostProcesses/blurPostProcess'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactoryArraySystem from '../../-private/systems/factory-array'; 5 | 6 | export default class BlurPostProcessSystem extends FactoryArraySystem< 7 | BlurPostProcess, 8 | PostProcess, 9 | BabylonBlurPostProcess 10 | > { 11 | protected instanceComponentConstructor = PostProcess; 12 | protected instanceConstructor = BabylonBlurPostProcess; 13 | 14 | protected create(c: BlurPostProcess): BabylonBlurPostProcess { 15 | return new BabylonBlurPostProcess( 16 | c.name, 17 | c.direction, 18 | c.kernel, 19 | c.options, 20 | null, 21 | c.samplingMode, 22 | this.core?.engine 23 | ); 24 | } 25 | 26 | static queries = { 27 | ...queries, 28 | factory: { 29 | components: [BlurPostProcess], 30 | listen: { 31 | added: true, 32 | changed: true, 33 | removed: true, 34 | }, 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/systems/post-process/motion-blur.ts: -------------------------------------------------------------------------------- 1 | import { MotionBlurPostProcess, PostProcess } from '../../components'; 2 | import { MotionBlurPostProcess as BabylonMotionBlurPostProcess } from '@babylonjs/core/PostProcesses/motionBlurPostProcess'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactoryArraySystem from '../../-private/systems/factory-array'; 5 | import { assign } from '../../-private/utils/assign'; 6 | 7 | export default class MotionMotionBlurPostProcessSystem extends FactoryArraySystem< 8 | MotionBlurPostProcess, 9 | PostProcess, 10 | BabylonMotionBlurPostProcess 11 | > { 12 | protected instanceComponentConstructor = PostProcess; 13 | protected instanceConstructor = BabylonMotionBlurPostProcess; 14 | 15 | protected create(c: MotionBlurPostProcess): BabylonMotionBlurPostProcess { 16 | const { name, options, samplingMode, ...rest } = c; 17 | const pp = new BabylonMotionBlurPostProcess(name, this.core!.scene, options, null, samplingMode, this.core!.engine); 18 | assign(pp, rest); 19 | 20 | return pp; 21 | } 22 | 23 | static queries = { 24 | ...queries, 25 | factory: { 26 | components: [MotionBlurPostProcess], 27 | listen: { 28 | added: true, 29 | changed: true, 30 | removed: true, 31 | }, 32 | }, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/systems/primitive/box.ts: -------------------------------------------------------------------------------- 1 | import { Box, Mesh } from '../../components'; 2 | import { Mesh as BabylonMesh } from '@babylonjs/core/Meshes/mesh'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assert } from '../../-private/utils/debug'; 6 | import { BoxBuilder } from '@babylonjs/core/Meshes/Builders/boxBuilder'; 7 | 8 | export default class BoxPrimitiveSystem extends FactorySystem, BabylonMesh> { 9 | protected instanceComponentConstructor = Mesh; 10 | protected recreateInstanceOnUpdate = true; 11 | 12 | protected create(c: Box): BabylonMesh { 13 | assert('BoxPrimitiveSystem needs BabylonCoreComponent', this.core); 14 | 15 | // Babylon's Builder unfortunately mutate the passed options, so we need to spread to clone them 16 | return BoxBuilder.CreateBox(Box.name, { ...c }, this.core.scene); 17 | } 18 | 19 | static queries = { 20 | ...queries, 21 | factory: { 22 | components: [Box], 23 | listen: { 24 | added: true, 25 | changed: true, 26 | removed: true, 27 | }, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/systems/primitive/lines.ts: -------------------------------------------------------------------------------- 1 | import { Lines, Mesh } from '../../components'; 2 | import { Mesh as BabylonMesh } from '@babylonjs/core/Meshes/mesh'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assert } from '../../-private/utils/debug'; 6 | import { LinesBuilder } from '@babylonjs/core/Meshes/Builders/linesBuilder'; 7 | 8 | export default class LinesPrimitiveSystem extends FactorySystem, BabylonMesh> { 9 | protected instanceComponentConstructor = Mesh; 10 | protected recreateInstanceOnUpdate = true; 11 | 12 | protected create(c: Lines): BabylonMesh { 13 | assert('LinesPrimitiveSystem needs BabylonCoreComponent', this.core); 14 | 15 | const { color, alpha, ...rest } = c; 16 | 17 | const linesMesh = LinesBuilder.CreateLines(Lines.name, rest, this.core.scene); 18 | if (color) { 19 | linesMesh.color = color; 20 | } 21 | linesMesh.alpha = alpha; 22 | 23 | return linesMesh; 24 | } 25 | 26 | static queries = { 27 | ...queries, 28 | factory: { 29 | components: [Lines], 30 | listen: { 31 | added: true, 32 | changed: true, 33 | removed: true, 34 | }, 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/systems/primitive/plane.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, Plane } from '../../components'; 2 | import { Mesh as BabylonMesh } from '@babylonjs/core/Meshes/mesh'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assert } from '../../-private/utils/debug'; 6 | import { PlaneBuilder } from '@babylonjs/core/Meshes/Builders/planeBuilder'; 7 | 8 | export default class PlanePrimitiveSystem extends FactorySystem, BabylonMesh> { 9 | protected instanceComponentConstructor = Mesh; 10 | protected recreateInstanceOnUpdate = true; 11 | 12 | protected create(c: Plane): BabylonMesh { 13 | assert('PlanePrimitiveSystem needs BabylonCoreComponent', this.core); 14 | 15 | // Babylon's Builder unfortunately mutate the passed options, so we need to spread to clone them 16 | return PlaneBuilder.CreatePlane(Plane.name, { ...c }, this.core.scene); 17 | } 18 | 19 | static queries = { 20 | ...queries, 21 | factory: { 22 | components: [Plane], 23 | listen: { 24 | added: true, 25 | changed: true, 26 | removed: true, 27 | }, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/systems/primitive/sphere.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, Sphere } from '../../components'; 2 | import { Mesh as BabylonMesh } from '@babylonjs/core/Meshes/mesh'; 3 | import { queries } from '../../-private/systems/with-core'; 4 | import FactorySystem from '../../-private/systems/factory'; 5 | import { assert } from '../../-private/utils/debug'; 6 | import { SphereBuilder } from '@babylonjs/core/Meshes/Builders/sphereBuilder'; 7 | 8 | export default class SpherePrimitiveSystem extends FactorySystem, BabylonMesh> { 9 | protected instanceComponentConstructor = Mesh; 10 | protected recreateInstanceOnUpdate = true; 11 | 12 | protected create(c: Sphere): BabylonMesh { 13 | assert('SpherePrimitiveSystem needs BabylonCoreComponent', this.core); 14 | 15 | // Babylon's Builder unfortunately mutate the passed options, so we need to spread to clone them 16 | return SphereBuilder.CreateSphere(Sphere.name, { ...c }, this.core.scene); 17 | } 18 | 19 | static queries = { 20 | ...queries, 21 | factory: { 22 | components: [Sphere], 23 | listen: { 24 | added: true, 25 | changed: true, 26 | removed: true, 27 | }, 28 | }, 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/systems/shadow.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Light, Mesh, ShadowGenerator } from '../components'; 3 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 4 | import { assert } from '../-private/utils/debug'; 5 | import { InstancedMesh } from '@babylonjs/core/Meshes/instancedMesh'; 6 | import { ShadowGenerator as _ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator'; 7 | import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent'; 8 | import { assign } from '../-private/utils/assign'; 9 | import { ShadowLight } from '@babylonjs/core/Lights/shadowLight'; 10 | 11 | export default class ShadowSystem extends SystemWithCore { 12 | execute(): void { 13 | super.execute(); 14 | 15 | this.queries.shadowGenerator.added?.forEach((e: Entity) => this.setup(e)); 16 | this.queries.shadowGenerator.changed?.forEach((e: Entity) => this.update(e)); 17 | 18 | this.queries.mesh.added?.forEach((e: Entity) => this.addMesh(e)); 19 | // this.queries.mesh.removed?.forEach((e: Entity) => this.removeMesh(e)); 20 | 21 | this.queries.shadowGenerator.removed?.forEach((e: Entity) => this.remove(e)); 22 | 23 | super.afterExecute(); 24 | } 25 | 26 | setup(entity: Entity): void { 27 | assert('ShadowSystem needs BabylonCoreComponent', this.core); 28 | 29 | const lightComponent = entity.getComponent(Light); 30 | assert('No light instance was found on this light component.', lightComponent?.value); 31 | 32 | const light = lightComponent.value; 33 | 34 | const component = entity.getMutableComponent(ShadowGenerator)!; 35 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 36 | const { value, ...options } = component; 37 | 38 | assert('ShadowLight instance required for shadows', light instanceof ShadowLight); 39 | const shadowGenerator = new _ShadowGenerator(options.size, light); 40 | assign(shadowGenerator, options); 41 | 42 | // disable continuous shadow calculation 43 | // light.autoUpdateExtends = false; 44 | // shadowGenerator.getShadowMap().refreshRate = RenderTargetTexture.REFRESHRATE_RENDER_ONCE; 45 | 46 | this.core.scene.meshes.forEach((m) => { 47 | // TODO: remove, pass this option to the mesh or primitive directly 48 | const mesh = m instanceof InstancedMesh ? m.sourceMesh : m; 49 | mesh.receiveShadows = true; 50 | 51 | shadowGenerator.addShadowCaster(m, false); 52 | }); 53 | 54 | component.value = shadowGenerator; 55 | this.core.shadowGenerators.add(shadowGenerator); 56 | } 57 | 58 | update(entity: Entity): void { 59 | assert('ShadowSystem needs BabylonCoreComponent', this.core); 60 | 61 | const shadowComponent = entity.getComponent(ShadowGenerator); 62 | assert('No shadow generator instance was found.', shadowComponent?.value); 63 | 64 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 65 | const { value, ...options } = shadowComponent; 66 | 67 | assign(shadowComponent.value, options); 68 | } 69 | 70 | addMesh(entity: Entity): void { 71 | assert('ShadowSystem needs BabylonCoreComponent', this.core); 72 | 73 | const meshComponent = entity.getComponent(Mesh); 74 | 75 | if (meshComponent?.value) { 76 | const mesh = meshComponent.value; 77 | mesh.receiveShadows = true; 78 | this.core.shadowGenerators.forEach((sg) => sg.addShadowCaster(mesh, false)); 79 | } 80 | } 81 | 82 | // TODO: currently this method does nothing, it runs when a Mesh is removed 83 | // but a removed Mesh is always disposed right now. 84 | // removeMesh(entity: Entity): void { 85 | // const meshComponent = entity.getRemovedComponent(Mesh); 86 | // 87 | // // we only need to remove the shadowCaster if the Mesh still exists 88 | // if (meshComponent?.value) { 89 | // const component = entity.getMutableComponent(ShadowGenerator); 90 | // // eslint-disable-next-line no-unused-expressions 91 | // component.value?.removeShadowCaster(meshComponent.value, false); 92 | // } 93 | // } 94 | 95 | remove(entity: Entity): void { 96 | assert('ShadowSystem needs BabylonCoreComponent', this.core); 97 | 98 | const component = entity.getRemovedComponent(ShadowGenerator)!; 99 | 100 | if (component.value) { 101 | this.core.shadowGenerators.delete(component.value); 102 | component.value.dispose(); 103 | // component.value = undefined; 104 | } 105 | } 106 | 107 | static queries = { 108 | ...queries, 109 | shadowGenerator: { 110 | components: [ShadowGenerator], 111 | listen: { 112 | added: true, 113 | changed: true, 114 | removed: true, 115 | }, 116 | }, 117 | mesh: { 118 | components: [Mesh], 119 | listen: { 120 | added: true, 121 | removed: true, 122 | }, 123 | }, 124 | }; 125 | } 126 | -------------------------------------------------------------------------------- /src/systems/transform.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Parent, PivotPoint, Position, Rotation, Scale, TransformNode } from '../components'; 3 | import guidFor from '../-private/utils/guid'; 4 | import { assert } from '../-private/utils/debug'; 5 | import { TransformNode as BabylonTransformNode } from '@babylonjs/core/Meshes/transformNode'; 6 | import { World } from '../index'; 7 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 8 | import SystemWithCore, { queries } from '../-private/systems/with-core'; 9 | 10 | export default class TransformSystem extends SystemWithCore { 11 | world!: World; 12 | 13 | execute(): void { 14 | super.execute(); 15 | 16 | this.queries.parent.added?.forEach((e: Entity) => this.setup(e)); 17 | this.queries.transformNode.added?.forEach((e: Entity) => this.setupTransformNode(e)); 18 | 19 | this.queries.position.added?.forEach((e: Entity) => this.setPosition(e)); 20 | this.queries.position.changed?.forEach((e: Entity) => this.updatePosition(e)); 21 | this.queries.position.removed?.forEach((e: Entity) => this.removePosition(e)); 22 | this.queries.rotation.added?.forEach((e: Entity) => this.setRotation(e)); 23 | this.queries.rotation.changed?.forEach((e: Entity) => this.updateRotation(e)); 24 | this.queries.rotation.removed?.forEach((e: Entity) => this.removeRotation(e)); 25 | this.queries.scale.added?.forEach((e: Entity) => this.setScale(e)); 26 | this.queries.scale.changed?.forEach((e: Entity) => this.updateScale(e)); 27 | this.queries.scale.removed?.forEach((e: Entity) => this.removeScale(e)); 28 | this.queries.pivot.added?.forEach((e: Entity) => this.setPivot(e)); 29 | this.queries.pivot.changed?.forEach((e: Entity) => this.updatePivot(e)); 30 | this.queries.pivot.removed?.forEach((e: Entity) => this.removePivot(e)); 31 | 32 | // entity might remove TransformNode, so it needs to run before 33 | this.queries.parent.removed?.forEach((e: Entity) => this.remove(e)); 34 | this.queries.transformNode.removed?.forEach((e: Entity) => this.removeTransformNode(e)); 35 | 36 | super.afterExecute(); 37 | } 38 | 39 | setup(entity: Entity): void { 40 | if (entity.hasComponent(TransformNode)) { 41 | return; 42 | } 43 | 44 | assert('TransformSystem needs BabylonCoreComponent', this.core); 45 | 46 | const { scene } = this.core; 47 | const transformNode = new BabylonTransformNode(`${guidFor(entity)}__TransformNode`, scene); 48 | 49 | entity.addComponent(TransformNode, { value: transformNode }); 50 | } 51 | 52 | remove(entity: Entity): void { 53 | entity.removeComponent(TransformNode); 54 | } 55 | 56 | getTransformNode(entity: Entity, removed = false): BabylonTransformNode { 57 | // Optionally allow getting the TransformNode as a removed component. 58 | // Useful in the case where the entire Entity is being removed. 59 | const transformNodeComponent = removed 60 | ? entity.getComponent(TransformNode) || entity.getRemovedComponent(TransformNode) 61 | : entity.getComponent(TransformNode); 62 | 63 | assert( 64 | 'No valid ECSY TransformNode component found on this Entity.', 65 | transformNodeComponent && transformNodeComponent.value 66 | ); 67 | 68 | return transformNodeComponent.value; 69 | } 70 | 71 | setupTransformNode(entity: Entity): void { 72 | const parentComponent = entity.getComponent(Parent); 73 | assert('No Parent component found', parentComponent); 74 | 75 | const transformNodeComponent = entity.getMutableComponent(TransformNode)!; 76 | const parentEntity = parentComponent.value; 77 | 78 | const node = transformNodeComponent.value; 79 | if (node) { 80 | if (transformNodeComponent.cloneNode) { 81 | transformNodeComponent.value = node.clone(`${guidFor(entity)}__TransformNode__cloned`, null, true); 82 | } 83 | 84 | if (parentEntity) { 85 | const parentTransformNodeComponent = parentEntity.getComponent(TransformNode); 86 | 87 | assert('The parent Entity does not have a valid TransformNode component', parentTransformNodeComponent?.value); 88 | 89 | node.parent = parentTransformNodeComponent.value; 90 | node.computeWorldMatrix(true); 91 | } 92 | } 93 | } 94 | 95 | removeTransformNode(entity: Entity): void { 96 | const transformNodeComponent = entity.getRemovedComponent(TransformNode)!; 97 | assert('TransformNode Component does not have a valid TransformNode instance.', transformNodeComponent.value); 98 | 99 | // we do not recursively dispose of all children of this transform node, they will clean up themselves 100 | transformNodeComponent.value.getChildren().forEach((c) => (c.parent = null)); 101 | transformNodeComponent.value.dispose(); 102 | } 103 | 104 | setPosition(entity: Entity): void { 105 | this.world.babylonManager.setProperty( 106 | entity, 107 | this.getTransformNode(entity), 108 | 'position', 109 | entity.getComponent(Position)!.value 110 | ); 111 | } 112 | 113 | updatePosition(entity: Entity): void { 114 | this.world.babylonManager.updateProperty( 115 | entity, 116 | this.getTransformNode(entity), 117 | 'transform', 118 | 'position', 119 | entity.getComponent(Position)!.value 120 | ); 121 | } 122 | 123 | removePosition(entity: Entity): void { 124 | const tn = this.getTransformNode(entity, true); 125 | tn.position.setAll(0); 126 | } 127 | 128 | setRotation(entity: Entity): void { 129 | this.world.babylonManager.setProperty( 130 | entity, 131 | this.getTransformNode(entity), 132 | 'rotation', 133 | entity.getComponent(Rotation)!.value 134 | ); 135 | } 136 | 137 | updateRotation(entity: Entity): void { 138 | this.world.babylonManager.updateProperty( 139 | entity, 140 | this.getTransformNode(entity), 141 | 'transform', 142 | 'rotation', 143 | entity.getComponent(Rotation)!.value 144 | ); 145 | } 146 | 147 | removeRotation(entity: Entity): void { 148 | const tn = this.getTransformNode(entity, true); 149 | tn.rotation.setAll(0); 150 | } 151 | 152 | setScale(entity: Entity): void { 153 | this.world.babylonManager.setProperty( 154 | entity, 155 | this.getTransformNode(entity), 156 | 'scaling', 157 | entity.getComponent(Scale)!.value 158 | ); 159 | } 160 | 161 | updateScale(entity: Entity): void { 162 | this.world.babylonManager.updateProperty( 163 | entity, 164 | this.getTransformNode(entity), 165 | 'transform', 166 | 'scaling', 167 | entity.getComponent(Scale)!.value 168 | ); 169 | } 170 | 171 | removeScale(entity: Entity): void { 172 | const tn = this.getTransformNode(entity, true); 173 | tn.scaling.setAll(1); 174 | } 175 | 176 | setPivot(entity: Entity): void { 177 | this.world.babylonManager.setProperty( 178 | entity, 179 | this.getTransformNode(entity), 180 | 'pivotPoint', 181 | entity.getComponent(PivotPoint)!.value 182 | ); 183 | } 184 | 185 | updatePivot(entity: Entity): void { 186 | this.world.babylonManager.updateProperty( 187 | entity, 188 | this.getTransformNode(entity), 189 | 'transform', 190 | 'pivotPoint', 191 | entity.getComponent(PivotPoint)!.value 192 | ); 193 | } 194 | 195 | removePivot(entity: Entity): void { 196 | const tn = this.getTransformNode(entity, true); 197 | tn.setPivotPoint(Vector3.Zero()); 198 | } 199 | 200 | static queries = { 201 | ...queries, 202 | parent: { 203 | components: [Parent], 204 | listen: { 205 | added: true, 206 | removed: true, 207 | }, 208 | }, 209 | transformNode: { 210 | components: [TransformNode], 211 | listen: { 212 | added: true, 213 | removed: true, 214 | }, 215 | }, 216 | position: { 217 | components: [Position], 218 | listen: { 219 | added: true, 220 | changed: true, 221 | removed: true, 222 | }, 223 | }, 224 | rotation: { 225 | components: [Rotation], 226 | listen: { 227 | added: true, 228 | changed: true, 229 | removed: true, 230 | }, 231 | }, 232 | scale: { 233 | components: [Scale], 234 | listen: { 235 | added: true, 236 | changed: true, 237 | removed: true, 238 | }, 239 | }, 240 | pivot: { 241 | components: [PivotPoint], 242 | listen: { 243 | added: true, 244 | changed: true, 245 | removed: true, 246 | }, 247 | }, 248 | }; 249 | } 250 | -------------------------------------------------------------------------------- /src/systems/transition.ts: -------------------------------------------------------------------------------- 1 | import { Attributes, Entity, System } from 'ecsy'; 2 | import { Transitions } from '../components'; 3 | import { World } from '../index'; 4 | import BabylonWorld from '../world'; 5 | import { Animation } from '@babylonjs/core/Animations/animation'; 6 | import '@babylonjs/core/Animations/animatable'; // needed to enable animation support on Scene in a tree-shaken build 7 | 8 | export default class TransitionSystem extends System { 9 | world!: World; 10 | 11 | constructor(world: BabylonWorld, attributes: Attributes) { 12 | super(world, attributes); 13 | 14 | this.world.babylonManager.injectAnimationDependencies(Animation); 15 | } 16 | 17 | execute(): void { 18 | this.queries.transitions.added?.forEach((e: Entity) => this.setup(e)); 19 | this.queries.transitions.changed?.forEach((e: Entity) => this.update(e)); 20 | this.queries.transitions.removed?.forEach((e: Entity) => this.remove(e)); 21 | } 22 | 23 | setup(entity: Entity): void { 24 | const c = entity.getComponent(Transitions)!; 25 | 26 | c.value.forEach((transitionConfig) => { 27 | this.world.babylonManager.registerTransition(entity, transitionConfig); 28 | }); 29 | } 30 | 31 | update(entity: Entity): void { 32 | const { value, previousValue } = entity.getComponent(Transitions)!; 33 | 34 | value.forEach((transition) => this.world.babylonManager.registerTransition(entity, transition)); 35 | if (previousValue) { 36 | previousValue 37 | .filter((transition) => !value.includes(transition)) 38 | .forEach((transition) => this.world.babylonManager.unregisterTransition(entity, transition)); 39 | } 40 | } 41 | 42 | remove(entity: Entity): void { 43 | this.world.babylonManager.unregisterTransition(entity); 44 | } 45 | 46 | static queries = { 47 | transitions: { 48 | components: [Transitions], 49 | listen: { 50 | added: true, 51 | changed: true, 52 | removed: true, 53 | }, 54 | }, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/systems/xr/default.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from 'ecsy'; 2 | import { Scene } from '@babylonjs/core/scene'; 3 | import { assert } from '../../-private/utils/debug'; 4 | import SystemWithCore, { queries } from '../../-private/systems/with-core'; 5 | import WebXrDefaultExperience from '../../components/xr/default'; 6 | import { WebXRDefaultExperience } from '@babylonjs/core/XR/webXRDefaultExperience'; 7 | 8 | const experiences = new WeakMap(); 9 | 10 | export default class WebXrDefaultExperienceSystem extends SystemWithCore { 11 | execute(): void { 12 | super.execute(); 13 | 14 | this.queries.xp.added?.forEach((e: Entity) => this.setup(e) as unknown as void); 15 | this.queries.xp.removed?.forEach(() => this.remove()); 16 | 17 | super.afterExecute(); 18 | } 19 | 20 | async setup(entity: Entity): Promise { 21 | assert('WebXrDefaultExperienceSystem needs BabylonCoreComponent', this.core); 22 | assert('Scene already has a WebXRDefaultExperience', !experiences.has(this.core.scene)); 23 | 24 | const options = entity.getComponent(WebXrDefaultExperience); 25 | 26 | const defaultXRExperience = await WebXRDefaultExperience.CreateAsync(this.core.scene, options); 27 | experiences.set(this.core.scene, defaultXRExperience); 28 | } 29 | 30 | remove(): void { 31 | experiences.get(this.core!.scene)?.dispose(); 32 | } 33 | 34 | static queries = { 35 | ...queries, 36 | xp: { 37 | components: [WebXrDefaultExperience], 38 | listen: { 39 | added: true, 40 | removed: true, 41 | }, 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Constructor { 2 | new (...args: never[]): C; 3 | } 4 | -------------------------------------------------------------------------------- /src/world.ts: -------------------------------------------------------------------------------- 1 | import { World, WorldOptions } from 'ecsy'; 2 | import BabylonManager from './-private/babylon-manager'; 3 | 4 | export default class BabylonWorld extends World { 5 | babylonManager: BabylonManager; 6 | 7 | constructor(options?: WorldOptions) { 8 | super(options); 9 | 10 | this.babylonManager = new BabylonManager(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/babylon.test.ts: -------------------------------------------------------------------------------- 1 | import { BabylonCore } from '../src/components'; 2 | import { waitForRAF } from './helpers/wait'; 3 | import setupWorld from './helpers/setup-world'; 4 | 5 | describe('babylon system', function () { 6 | it('sets up babylon scene', function () { 7 | const { rootEntity } = setupWorld(); 8 | 9 | const babylonComponent = rootEntity.getComponent(BabylonCore)!; 10 | 11 | expect(babylonComponent.engine).toBeDefined(); 12 | expect(babylonComponent.scene).toBeDefined(); 13 | }); 14 | 15 | it('calls render beforeRender and afterRender', async function () { 16 | const beforeRender = jest.fn(); 17 | const afterRender = jest.fn(); 18 | 19 | setupWorld({ 20 | rootEntityValues: { 21 | beforeRender, 22 | afterRender, 23 | }, 24 | }); 25 | 26 | await waitForRAF(); 27 | 28 | expect(beforeRender).toHaveBeenCalled(); 29 | expect(beforeRender.mock.calls[0][0]).toBe(0); 30 | expect(beforeRender.mock.calls[0][1]).toBeGreaterThan(1); // an animation frame 31 | 32 | expect(afterRender).toHaveBeenCalled(); 33 | expect(afterRender.mock.calls[0][0]).toBe(0); 34 | expect(afterRender.mock.calls[0][1]).toBeGreaterThan(1); // an animation frame 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /test/camera.test.ts: -------------------------------------------------------------------------------- 1 | import { ArcRotateCamera, Parent, TargetCamera } from '../src/components'; 2 | import { ArcRotateCamera as BabylonArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera'; 3 | import { TargetCamera as BabylonTargetCamera } from '@babylonjs/core/Cameras/targetCamera'; 4 | import setupWorld from './helpers/setup-world'; 5 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 6 | 7 | describe('camera system', function () { 8 | describe('arc-rotate camera', function () { 9 | it('can add arc-rotate camera', function () { 10 | const { world, scene } = setupWorld(); 11 | 12 | const cameraEntity = world.createEntity(); 13 | cameraEntity.addComponent(Parent).addComponent(ArcRotateCamera); 14 | 15 | world.execute(0, 0); 16 | 17 | expect(scene.activeCamera).toBeInstanceOf(BabylonArcRotateCamera); 18 | expect(scene.cameras).toHaveLength(2); 19 | 20 | const camera = scene.activeCamera as BabylonArcRotateCamera; 21 | expect(camera.alpha).toBe(0); 22 | expect(camera.beta).toBe(0); 23 | expect(camera.radius).toBe(10); 24 | expect(camera.lowerAlphaLimit).toBeNull(); 25 | expect(camera.lowerBetaLimit).toBeGreaterThan(0); // has a default value! 26 | expect(camera.lowerRadiusLimit).toBeNull(); 27 | expect(camera.upperAlphaLimit).toBeNull(); 28 | expect(camera.upperBetaLimit).toBeGreaterThan(0); // has a default value! 29 | expect(camera.upperRadiusLimit).toBeNull(); 30 | }); 31 | 32 | it('can add arc-rotate camera with custom arguments', function () { 33 | const { world, scene } = setupWorld(); 34 | 35 | const cameraEntity = world.createEntity(); 36 | cameraEntity.addComponent(Parent).addComponent(ArcRotateCamera, { 37 | alpha: 0.5, 38 | beta: 0, 39 | radius: 12, 40 | lowerAlphaLimit: -1, 41 | lowerBetaLimit: -0.1, 42 | lowerRadiusLimit: 5, 43 | upperAlphaLimit: 1, 44 | upperBetaLimit: 0.1, 45 | upperRadiusLimit: 15, 46 | }); 47 | 48 | world.execute(0, 0); 49 | 50 | expect(scene.activeCamera).toBeInstanceOf(BabylonArcRotateCamera); 51 | 52 | const camera = scene.activeCamera as BabylonArcRotateCamera; 53 | expect(camera.alpha).toBe(0.5); 54 | expect(camera.beta).toBe(0); 55 | expect(camera.radius).toBe(12); 56 | expect(camera.lowerAlphaLimit).toEqual(-1); 57 | expect(camera.lowerBetaLimit).toEqual(-0.1); 58 | expect(camera.lowerRadiusLimit).toBe(5); 59 | expect(camera.upperAlphaLimit).toBe(1); 60 | expect(camera.upperBetaLimit).toBe(0.1); 61 | expect(camera.upperRadiusLimit).toBe(15); 62 | }); 63 | 64 | it('can update arc-rotate camera', function () { 65 | const { world, scene } = setupWorld(); 66 | 67 | const cameraEntity = world.createEntity(); 68 | cameraEntity.addComponent(Parent).addComponent(ArcRotateCamera); 69 | 70 | world.execute(0, 0); 71 | 72 | expect(scene.activeCamera).toBeInstanceOf(BabylonArcRotateCamera); 73 | 74 | const camera = scene.activeCamera as BabylonArcRotateCamera; 75 | 76 | const component = cameraEntity.getMutableComponent(ArcRotateCamera); 77 | Object.assign(component, { 78 | alpha: 0.5, 79 | beta: 0, 80 | radius: 12, 81 | lowerAlphaLimit: -1, 82 | lowerBetaLimit: -0.1, 83 | lowerRadiusLimit: 5, 84 | upperAlphaLimit: 1, 85 | upperBetaLimit: 0.1, 86 | upperRadiusLimit: 15, 87 | }); 88 | 89 | world.execute(0, 0); 90 | 91 | expect(camera.alpha).toBe(0.5); 92 | expect(camera.beta).toBe(0); 93 | expect(camera.radius).toBe(12); 94 | expect(camera.lowerAlphaLimit).toEqual(-1); 95 | expect(camera.lowerBetaLimit).toEqual(-0.1); 96 | expect(camera.lowerRadiusLimit).toBe(5); 97 | expect(camera.upperAlphaLimit).toBe(1); 98 | expect(camera.upperBetaLimit).toBe(0.1); 99 | expect(camera.upperRadiusLimit).toBe(15); 100 | }); 101 | 102 | it('can remove arc-rotate camera', function () { 103 | const { world, scene } = setupWorld(); 104 | 105 | const cameraEntity = world.createEntity(); 106 | cameraEntity.addComponent(Parent).addComponent(ArcRotateCamera); 107 | 108 | world.execute(0, 0); 109 | 110 | const camera = scene.activeCamera; 111 | 112 | expect(camera).toBeInstanceOf(BabylonArcRotateCamera); 113 | 114 | cameraEntity.remove(); 115 | world.execute(0, 0); 116 | 117 | expect(scene.activeCamera).toBeNull(); 118 | expect(scene.cameras).toHaveLength(0); 119 | }); 120 | 121 | it('throws without parent component', function () { 122 | const { world } = setupWorld(); 123 | 124 | const cameraEntity = world.createEntity(); 125 | cameraEntity.addComponent(ArcRotateCamera); 126 | 127 | expect(() => world.execute(0, 0)).toThrow(); 128 | }); 129 | }); 130 | 131 | describe('target camera', function () { 132 | it('can add target camera', function () { 133 | const { world, scene } = setupWorld(); 134 | 135 | const cameraEntity = world.createEntity(); 136 | cameraEntity.addComponent(Parent).addComponent(TargetCamera); 137 | 138 | world.execute(0, 0); 139 | 140 | expect(scene.activeCamera).toBeInstanceOf(BabylonTargetCamera); 141 | expect(scene.cameras).toHaveLength(2); 142 | 143 | const camera = scene.activeCamera as BabylonTargetCamera; 144 | expect(camera.position.equalsToFloats(0, 0, 0)).toBeTrue(); 145 | expect(camera.getTarget().equalsToFloats(0, 0, 0)).toBeTrue(); 146 | expect(camera.fov).toBe(0.8); 147 | }); 148 | 149 | it('can add target camera with custom arguments', function () { 150 | const { world, scene } = setupWorld(); 151 | 152 | const cameraEntity = world.createEntity(); 153 | cameraEntity.addComponent(Parent).addComponent(TargetCamera, { 154 | position: new Vector3(0, 0, -10), 155 | target: new Vector3(0, 1, 0), 156 | fov: 0.5, 157 | }); 158 | 159 | world.execute(0, 0); 160 | 161 | expect(scene.activeCamera).toBeInstanceOf(BabylonTargetCamera); 162 | 163 | const camera = scene.activeCamera as BabylonTargetCamera; 164 | expect(camera.position.equalsToFloats(0, 0, -10)).toBeTrue(); 165 | // camera.getTarget() does not return target for TargetCamera, weird... 166 | // expect(camera.getTarget().equalsToFloats(0, 1, 0)).toBeTrue(); 167 | expect(camera.fov).toBe(0.5); 168 | }); 169 | 170 | it('can update target camera', function () { 171 | const { world, scene } = setupWorld(); 172 | 173 | const cameraEntity = world.createEntity(); 174 | cameraEntity.addComponent(Parent).addComponent(TargetCamera); 175 | 176 | world.execute(0, 0); 177 | 178 | expect(scene.activeCamera).toBeInstanceOf(BabylonTargetCamera); 179 | 180 | const camera = scene.activeCamera as BabylonTargetCamera; 181 | 182 | const component = cameraEntity.getMutableComponent(TargetCamera); 183 | Object.assign(component, { 184 | position: new Vector3(0, 0, -10), 185 | target: new Vector3(0, 1, 0), 186 | fov: 0.5, 187 | }); 188 | 189 | world.execute(0, 0); 190 | 191 | expect(camera.position.equalsToFloats(0, 0, -10)).toBeTrue(); 192 | // camera.getTarget() does not return target for TargetCamera, weird... 193 | // expect(camera.getTarget().equalsToFloats(0, 1, 0)).toBeTrue(); 194 | expect(camera.fov).toBe(0.5); 195 | }); 196 | 197 | it('can remove target camera', function () { 198 | const { world, scene } = setupWorld(); 199 | 200 | const cameraEntity = world.createEntity(); 201 | cameraEntity.addComponent(Parent).addComponent(TargetCamera); 202 | 203 | world.execute(0, 0); 204 | 205 | const camera = scene.activeCamera; 206 | 207 | expect(camera).toBeInstanceOf(BabylonTargetCamera); 208 | 209 | cameraEntity.remove(); 210 | world.execute(0, 0); 211 | 212 | expect(scene.activeCamera).toBeNull(); 213 | expect(scene.cameras).toHaveLength(0); 214 | }); 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /test/helpers/setup-world.ts: -------------------------------------------------------------------------------- 1 | import { ComponentConstructor, Entity, System, SystemConstructor } from 'ecsy'; 2 | import { BabylonCore } from '../../src/components'; 3 | import { components, systems, World } from '../../src'; 4 | import { NullEngine } from '@babylonjs/core/Engines/nullEngine'; 5 | import { Component } from 'ecsy/src/Component'; 6 | import { Engine } from '@babylonjs/core/Engines/engine'; 7 | import { Scene } from '@babylonjs/core/scene'; 8 | 9 | export interface SetupWorld { 10 | world: World; 11 | rootEntity: Entity; 12 | engine: Engine; 13 | scene: Scene; 14 | } 15 | 16 | export interface SetupWorldOptions { 17 | systems?: SystemConstructor[]; 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | components?: ComponentConstructor>[]; 20 | rootEntityValues?: Partial; 21 | } 22 | 23 | export default function setupWorld(options: SetupWorldOptions = {}): SetupWorld { 24 | const canvas = document.createElement('canvas'); 25 | const world = new World(); 26 | 27 | for (const Component of options.components ?? components) { 28 | world.registerComponent(Component); 29 | } 30 | 31 | for (const system of options.systems ?? systems) { 32 | world.registerSystem(system); 33 | } 34 | 35 | const rootEntity = world.createEntity(); 36 | const engine = new NullEngine(); 37 | 38 | rootEntity.addComponent(BabylonCore, { 39 | world, 40 | canvas, 41 | engine, 42 | ...(options.rootEntityValues ?? {}), 43 | }); 44 | 45 | world.execute(0, 0); 46 | const { scene } = rootEntity.getComponent(BabylonCore)!; 47 | 48 | // This has the side effect that Engine.LastCreatedScene is null, which forces us to always explicitly pass a Scene 49 | // when created new Babylon instances that require it. Omitting to do that works in general, as Babylon will then use 50 | // this Engine.LastCreatedScene, but this can break when dealing with multiple Scenes in a single browser session (e.g. 51 | // adding and tearing down a new in a SPA). 52 | const dummyScene = new Scene(engine); 53 | dummyScene.dispose(); 54 | 55 | return { 56 | world, 57 | rootEntity, 58 | engine, 59 | scene, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /test/helpers/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(delay: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, delay)); 3 | } 4 | 5 | export function waitForRAF(): Promise { 6 | return new Promise((resolve) => window.requestAnimationFrame(() => resolve())); 7 | } 8 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { components, systems } from '../src'; 2 | 3 | describe('Index', function () { 4 | it('exports all systems', function () { 5 | expect(systems).toBeArray(); 6 | }); 7 | 8 | it('exports all components', function () { 9 | expect(components).toBeArray(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/material.test.ts: -------------------------------------------------------------------------------- 1 | import { Box, Parent, PbrMaterial, StandardMaterial } from '../src/components'; 2 | import { Color3 } from '@babylonjs/core/Maths/math.color'; 3 | import { PBRMaterial as BabylonPBRMaterial } from '@babylonjs/core/Materials/PBR/pbrMaterial'; 4 | import setupWorld from './helpers/setup-world'; 5 | import { StandardMaterial as BabylonStandardMaterial } from '@babylonjs/core/Materials/standardMaterial'; 6 | import Material from '../src/components/material'; 7 | 8 | describe('material system', function () { 9 | describe('pbr material', function () { 10 | it('can add material', function () { 11 | const { world, scene } = setupWorld(); 12 | 13 | const entity = world.createEntity(); 14 | entity.addComponent(Parent).addComponent(Box).addComponent(PbrMaterial); 15 | 16 | world.execute(0, 0); 17 | 18 | const material = scene.getMaterialByName('PbrMaterial') as BabylonPBRMaterial; 19 | 20 | expect(material).toBeInstanceOf(BabylonPBRMaterial); 21 | expect(scene.meshes[0].material).toEqual(material); 22 | expect(material.albedoColor.equalsFloats(1, 1, 1)).toBeTrue(); 23 | }); 24 | 25 | it('can add material with custom values', function () { 26 | const { world, scene } = setupWorld(); 27 | 28 | const entity = world.createEntity(); 29 | entity 30 | .addComponent(Parent) 31 | .addComponent(Box) 32 | .addComponent(PbrMaterial, { 33 | albedoColor: new Color3(1, 0, 0), 34 | ambientColor: new Color3(0, 1, 0), 35 | emissiveColor: new Color3(0, 0, 1), 36 | roughness: 0.5, 37 | metallic: 0.1, 38 | }); 39 | 40 | world.execute(0, 0); 41 | 42 | const material = scene.getMaterialByName('PbrMaterial') as BabylonPBRMaterial; 43 | 44 | expect(material).toBeInstanceOf(BabylonPBRMaterial); 45 | expect(scene.meshes[0].material).toEqual(material); 46 | expect(material.albedoColor.equalsFloats(1, 0, 0)).toBeTrue(); 47 | expect(material.ambientColor.equalsFloats(0, 1, 0)).toBeTrue(); 48 | expect(material.emissiveColor.equalsFloats(0, 0, 1)).toBeTrue(); 49 | expect(material.roughness).toBe(0.5); 50 | expect(material.metallic).toBe(0.1); 51 | }); 52 | 53 | it('can update material', function () { 54 | const { world, scene } = setupWorld(); 55 | 56 | const entity = world.createEntity(); 57 | entity.addComponent(Parent).addComponent(Box).addComponent(PbrMaterial); 58 | 59 | world.execute(0, 0); 60 | 61 | const component = entity.getMutableComponent(PbrMaterial); 62 | Object.assign(component, { 63 | albedoColor: new Color3(1, 0, 0), 64 | ambientColor: new Color3(0, 1, 0), 65 | emissiveColor: new Color3(0, 0, 1), 66 | roughness: 0.5, 67 | metallic: 0.1, 68 | }); 69 | 70 | world.execute(0, 0); 71 | 72 | const material = scene.getMaterialByName('PbrMaterial') as BabylonPBRMaterial; 73 | 74 | expect(material).toBeInstanceOf(BabylonPBRMaterial); 75 | expect(scene.meshes[0].material).toEqual(material); 76 | expect(material.albedoColor.equalsFloats(1, 0, 0)).toBeTrue(); 77 | expect(material.ambientColor.equalsFloats(0, 1, 0)).toBeTrue(); 78 | expect(material.emissiveColor.equalsFloats(0, 0, 1)).toBeTrue(); 79 | expect(material.roughness).toBe(0.5); 80 | expect(material.metallic).toBe(0.1); 81 | }); 82 | 83 | it('can apply material to updated mesh', function () { 84 | const { world, scene } = setupWorld(); 85 | 86 | const entity = world.createEntity(); 87 | entity 88 | .addComponent(Parent) 89 | .addComponent(Box) 90 | .addComponent(PbrMaterial, { albedoColor: new Color3(1, 0, 0) }); 91 | 92 | world.execute(0, 0); 93 | 94 | const component = entity.getMutableComponent(Box)!; 95 | component.size = 2; 96 | 97 | world.execute(0, 0); 98 | 99 | const material = scene.getMaterialByName('PbrMaterial') as BabylonPBRMaterial; 100 | 101 | expect(material).toBeInstanceOf(BabylonPBRMaterial); 102 | expect(scene.meshes[0].material).toEqual(material); 103 | expect(material.albedoColor.equalsFloats(1, 0, 0)).toBeTrue(); 104 | }); 105 | 106 | it('can remove material', function () { 107 | const { world, scene } = setupWorld(); 108 | 109 | const entity = world.createEntity(); 110 | entity.addComponent(Parent).addComponent(Box).addComponent(PbrMaterial); 111 | 112 | world.execute(0, 0); 113 | 114 | entity.removeComponent(PbrMaterial); 115 | world.execute(0, 0); 116 | 117 | const material = scene.getMaterialByName('PbrMaterial'); 118 | 119 | expect(material).toBeNull(); 120 | expect(scene.meshes[0].material).toBeNull(); 121 | }); 122 | 123 | it('can remove whole entity', function () { 124 | const { world, scene } = setupWorld(); 125 | 126 | const entity = world.createEntity(); 127 | entity.addComponent(Parent).addComponent(Box).addComponent(PbrMaterial); 128 | 129 | world.execute(0, 0); 130 | 131 | entity.remove(); 132 | world.execute(0, 0); 133 | 134 | const material = scene.getMaterialByName('PbrMaterial'); 135 | 136 | expect(material).toBeNull(); 137 | }); 138 | }); 139 | 140 | describe('standard material', function () { 141 | it('can add material', function () { 142 | const { world, scene } = setupWorld(); 143 | 144 | const entity = world.createEntity(); 145 | entity.addComponent(Parent).addComponent(Box).addComponent(StandardMaterial); 146 | 147 | world.execute(0, 0); 148 | 149 | const material = scene.getMaterialByName('StandardMaterial') as BabylonStandardMaterial; 150 | 151 | expect(material).toBeInstanceOf(BabylonStandardMaterial); 152 | expect(scene.meshes[0].material).toEqual(material); 153 | expect(material.diffuseColor.equalsFloats(1, 1, 1)).toBeTrue(); 154 | }); 155 | 156 | it('can add material with custom values', function () { 157 | const { world, scene } = setupWorld(); 158 | 159 | const entity = world.createEntity(); 160 | entity 161 | .addComponent(Parent) 162 | .addComponent(Box) 163 | .addComponent(StandardMaterial, { 164 | diffuseColor: new Color3(1, 0, 0), 165 | ambientColor: new Color3(0, 1, 0), 166 | emissiveColor: new Color3(0, 0, 1), 167 | }); 168 | 169 | world.execute(0, 0); 170 | 171 | const material = scene.getMaterialByName('StandardMaterial') as BabylonStandardMaterial; 172 | 173 | expect(material).toBeInstanceOf(BabylonStandardMaterial); 174 | expect(scene.meshes[0].material).toEqual(material); 175 | expect(material.diffuseColor.equalsFloats(1, 0, 0)).toBeTrue(); 176 | expect(material.ambientColor.equalsFloats(0, 1, 0)).toBeTrue(); 177 | expect(material.emissiveColor.equalsFloats(0, 0, 1)).toBeTrue(); 178 | }); 179 | 180 | it('can update material', function () { 181 | const { world, scene } = setupWorld(); 182 | 183 | const entity = world.createEntity(); 184 | entity.addComponent(Parent).addComponent(Box).addComponent(StandardMaterial); 185 | 186 | world.execute(0, 0); 187 | 188 | const component = entity.getMutableComponent(StandardMaterial); 189 | Object.assign(component, { 190 | diffuseColor: new Color3(1, 0, 0), 191 | ambientColor: new Color3(0, 1, 0), 192 | emissiveColor: new Color3(0, 0, 1), 193 | }); 194 | 195 | world.execute(0, 0); 196 | 197 | const material = scene.getMaterialByName('StandardMaterial') as BabylonStandardMaterial; 198 | 199 | expect(material).toBeInstanceOf(BabylonStandardMaterial); 200 | expect(scene.meshes[0].material).toEqual(material); 201 | expect(material.diffuseColor.equalsFloats(1, 0, 0)).toBeTrue(); 202 | expect(material.ambientColor.equalsFloats(0, 1, 0)).toBeTrue(); 203 | expect(material.emissiveColor.equalsFloats(0, 0, 1)).toBeTrue(); 204 | }); 205 | 206 | it('can remove material', function () { 207 | const { world, scene } = setupWorld(); 208 | 209 | const entity = world.createEntity(); 210 | entity.addComponent(Parent).addComponent(Box).addComponent(StandardMaterial); 211 | 212 | world.execute(0, 0); 213 | 214 | entity.removeComponent(StandardMaterial); 215 | world.execute(0, 0); 216 | 217 | const material = scene.getMaterialByName('StandardMaterial'); 218 | 219 | expect(material).toBeNull(); 220 | expect(scene.meshes[0].material).toBeNull(); 221 | }); 222 | }); 223 | 224 | describe('material', function () { 225 | it('can add material instance', function () { 226 | const { world, scene } = setupWorld(); 227 | 228 | world.execute(0, 0); 229 | 230 | const entity = world.createEntity(); 231 | 232 | const material = new BabylonPBRMaterial('PbrMaterial', scene); 233 | entity.addComponent(Parent).addComponent(Box).addComponent(Material, { value: material }); 234 | 235 | world.execute(0, 0); 236 | 237 | expect(scene.meshes[0].material).toEqual(material); 238 | }); 239 | 240 | it('can update material instance', function () { 241 | const { world, scene } = setupWorld(); 242 | 243 | world.execute(0, 0); 244 | 245 | const entity = world.createEntity(); 246 | 247 | const material = new BabylonPBRMaterial('PbrMaterial', scene); 248 | entity.addComponent(Parent).addComponent(Box).addComponent(Material, { value: material }); 249 | 250 | world.execute(0, 0); 251 | 252 | const component = entity.getMutableComponent(Material)!; 253 | const material2 = new BabylonStandardMaterial('Standard', scene); 254 | component.value = material2; 255 | 256 | world.execute(0, 0); 257 | 258 | expect(scene.meshes[0].material).toEqual(material2); 259 | }); 260 | 261 | it('can remove material instance', function () { 262 | const { world, scene } = setupWorld(); 263 | 264 | world.execute(0, 0); 265 | 266 | const entity = world.createEntity(); 267 | 268 | const material = new BabylonPBRMaterial('PbrMaterial', scene); 269 | entity.addComponent(Parent).addComponent(Box).addComponent(Material, { value: material }); 270 | 271 | world.execute(0, 0); 272 | 273 | entity.removeComponent(Material); 274 | 275 | world.execute(0, 0); 276 | 277 | expect(scene.meshes[0].material).toBeNull(); 278 | }); 279 | }); 280 | }); 281 | -------------------------------------------------------------------------------- /test/mesh.test.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, Parent } from '../src/components'; 2 | import setupWorld from './helpers/setup-world'; 3 | import { BoxBuilder } from '@babylonjs/core/Meshes/Builders/boxBuilder'; 4 | import { Mesh as BabylonMesh } from '@babylonjs/core/Meshes/mesh'; 5 | 6 | describe('mesh system', function () { 7 | it('can add mesh', function () { 8 | const { world, scene } = setupWorld(); 9 | 10 | const entity = world.createEntity(); 11 | entity.addComponent(Parent).addComponent(Mesh, { value: BoxBuilder.CreateBox('test', { size: 1 }, scene) }); 12 | 13 | world.execute(0, 0); 14 | 15 | expect(scene.meshes).toHaveLength(1); 16 | expect(scene.geometries).toHaveLength(1); 17 | 18 | const mesh = scene.meshes[0]; 19 | expect(mesh).toBeInstanceOf(BabylonMesh); 20 | expect(mesh.name).toMatch(/test/); 21 | }); 22 | 23 | it('integrates with transformNodes properly', function () { 24 | const { world, scene } = setupWorld(); 25 | 26 | const entity = world.createEntity(); 27 | entity.addComponent(Parent).addComponent(Mesh, { value: BoxBuilder.CreateBox('test', { size: 1 }, scene) }); 28 | 29 | world.execute(0, 0); 30 | 31 | expect(scene.meshes).toHaveLength(1); 32 | expect(scene.transformNodes).toHaveLength(1); 33 | expect(scene.rootNodes).toHaveLength(1); 34 | 35 | const mesh = scene.meshes[0]; 36 | const tn = scene.transformNodes[0]; 37 | 38 | expect(scene.rootNodes).toIncludeAllMembers([tn]); 39 | expect(tn.getChildren()).toHaveLength(1); 40 | expect(tn.getChildren()[0]).toEqual(mesh); 41 | expect(mesh.parent).toEqual(tn); 42 | }); 43 | 44 | it('can update mesh', function () { 45 | const { world, scene } = setupWorld(); 46 | 47 | const entity = world.createEntity(); 48 | entity.addComponent(Parent).addComponent(Mesh, { value: BoxBuilder.CreateBox('box1', { size: 1 }, scene) }); 49 | 50 | world.execute(0, 0); 51 | 52 | expect(scene.meshes).toHaveLength(1); 53 | expect(scene.geometries).toHaveLength(1); 54 | expect(scene.meshes[0]).toBeInstanceOf(BabylonMesh); 55 | expect(scene.meshes[0].name).toMatch(/box1/); 56 | 57 | const meshComponent = entity.getMutableComponent(Mesh)!; 58 | expect(meshComponent).toBeDefined(); 59 | 60 | meshComponent.value = BoxBuilder.CreateBox('box2', { size: 1 }, scene); 61 | 62 | world.execute(0, 0); 63 | 64 | expect(scene.meshes).toHaveLength(1); 65 | expect(scene.geometries).toHaveLength(1); 66 | expect(scene.meshes[0]).toBeInstanceOf(BabylonMesh); 67 | expect(scene.meshes[0].name).toMatch(/box2/); 68 | }); 69 | 70 | it('can remove mesh', function () { 71 | const { world, scene } = setupWorld(); 72 | 73 | const entity = world.createEntity(); 74 | entity.addComponent(Parent).addComponent(Mesh, { value: BoxBuilder.CreateBox('test', { size: 1 }, scene) }); 75 | 76 | world.execute(0, 0); 77 | 78 | expect(scene.meshes).toHaveLength(1); 79 | expect(scene.geometries).toHaveLength(1); 80 | 81 | entity.remove(); 82 | world.execute(0, 0); 83 | 84 | expect(scene.meshes).toHaveLength(0); 85 | expect(scene.geometries).toHaveLength(0); 86 | }); 87 | 88 | it('does not remove mesh used elsewhere', function () { 89 | const { world, scene } = setupWorld(); 90 | 91 | const mesh = BoxBuilder.CreateBox('test', { size: 1 }, scene); 92 | const entity1 = world.createEntity(); 93 | const entity2 = world.createEntity(); 94 | entity1.addComponent(Parent).addComponent(Mesh, { value: mesh }); 95 | entity2.addComponent(Parent); 96 | 97 | world.execute(0, 0); 98 | 99 | expect(scene.meshes).toHaveLength(1); 100 | expect(scene.geometries).toHaveLength(1); 101 | 102 | entity1.removeComponent(Mesh); 103 | entity2.addComponent(Mesh, { value: mesh }); 104 | 105 | world.execute(0, 0); 106 | 107 | expect(scene.meshes).toHaveLength(1); 108 | expect(scene.geometries).toHaveLength(1); 109 | 110 | entity1.remove(); 111 | entity2.remove(); 112 | 113 | world.execute(0, 0); 114 | 115 | expect(scene.meshes).toHaveLength(0); 116 | expect(scene.geometries).toHaveLength(0); 117 | }); 118 | 119 | it('can override mesh properties', function () { 120 | const { world, scene } = setupWorld(); 121 | 122 | const entity = world.createEntity(); 123 | entity 124 | .addComponent(Parent) 125 | .addComponent(Mesh, { value: BoxBuilder.CreateBox('test', { size: 1 }, scene), overrides: { isVisible: false } }); 126 | 127 | world.execute(0, 0); 128 | 129 | expect(scene.meshes).toHaveLength(1); 130 | expect(scene.geometries).toHaveLength(1); 131 | 132 | const mesh = scene.meshes[0]; 133 | expect(mesh).toBeInstanceOf(BabylonMesh); 134 | expect(mesh.name).toMatch(/test/); 135 | expect(mesh.isVisible).toBeFalse(); 136 | }); 137 | 138 | it('can update overridden mesh properties', function () { 139 | const { world, scene } = setupWorld(); 140 | 141 | const entity = world.createEntity(); 142 | entity 143 | .addComponent(Parent) 144 | .addComponent(Mesh, { value: BoxBuilder.CreateBox('test', { size: 1 }, scene), overrides: { isVisible: false } }); 145 | 146 | world.execute(0, 0); 147 | 148 | expect(scene.meshes).toHaveLength(1); 149 | expect(scene.geometries).toHaveLength(1); 150 | 151 | const meshComponent = entity.getMutableComponent(Mesh)!; 152 | expect(meshComponent).toBeDefined(); 153 | meshComponent.overrides.isVisible = true; 154 | 155 | world.execute(0, 0); 156 | 157 | const mesh = scene.meshes[0]; 158 | expect(mesh).toBeInstanceOf(BabylonMesh); 159 | expect(mesh.name).toMatch(/test/); 160 | expect(mesh.isVisible).toBeTrue(); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /test/shadow.test.ts: -------------------------------------------------------------------------------- 1 | import { Box, DirectionalLight, Parent, ShadowGenerator } from '../src/components'; 2 | import { DirectionalLight as BabylonDirectionalLight } from '@babylonjs/core/Lights/directionalLight'; 3 | import setupWorld from './helpers/setup-world'; 4 | 5 | describe('shadow system', function () { 6 | it('can add shadow generator', function () { 7 | const { world, scene } = setupWorld(); 8 | 9 | const lightEntity = world.createEntity(); 10 | lightEntity.addComponent(Parent).addComponent(DirectionalLight).addComponent(ShadowGenerator); 11 | 12 | const meshEntity = world.createEntity(); 13 | meshEntity.addComponent(Parent).addComponent(Box); 14 | 15 | world.execute(0, 0); 16 | 17 | expect(scene.lights).toHaveLength(1); 18 | 19 | const light = scene.lights[0] as BabylonDirectionalLight; 20 | expect(light).toBeInstanceOf(BabylonDirectionalLight); 21 | 22 | const shadowGenerator = light.getShadowGenerator() as unknown as ShadowGenerator; 23 | expect(shadowGenerator).toBeDefined(); 24 | expect(shadowGenerator.size).toBe(512); 25 | expect(shadowGenerator.forceBackFacesOnly).toBeFalse(); 26 | expect(shadowGenerator.darkness).toBe(0); // make sure default values are preserved 27 | }); 28 | 29 | it('can add shadow generator with custom arguments', function () { 30 | const { world, scene } = setupWorld(); 31 | 32 | const lightEntity = world.createEntity(); 33 | lightEntity.addComponent(Parent).addComponent(DirectionalLight).addComponent(ShadowGenerator, { 34 | size: 1024, 35 | forceBackFacesOnly: true, 36 | }); 37 | 38 | const meshEntity = world.createEntity(); 39 | meshEntity.addComponent(Parent).addComponent(Box); 40 | 41 | world.execute(0, 0); 42 | 43 | expect(scene.lights).toHaveLength(1); 44 | 45 | const light = scene.lights[0] as BabylonDirectionalLight; 46 | expect(light).toBeInstanceOf(BabylonDirectionalLight); 47 | 48 | const shadowGenerator = light.getShadowGenerator() as unknown as ShadowGenerator; 49 | expect(shadowGenerator.size).toBe(1024); 50 | expect(shadowGenerator.forceBackFacesOnly).toBeTrue(); 51 | expect(shadowGenerator.darkness).toBe(0); // make sure default values are preserved 52 | }); 53 | 54 | it('can update shadow generator', function () { 55 | const { world, scene } = setupWorld(); 56 | 57 | const lightEntity = world.createEntity(); 58 | lightEntity.addComponent(Parent).addComponent(DirectionalLight).addComponent(ShadowGenerator); 59 | 60 | const meshEntity = world.createEntity(); 61 | meshEntity.addComponent(Parent).addComponent(Box); 62 | 63 | world.execute(0, 0); 64 | 65 | const component = lightEntity.getMutableComponent(ShadowGenerator)!; 66 | component.size = 1024; 67 | component.forceBackFacesOnly = true; 68 | 69 | world.execute(0, 0); 70 | 71 | expect(scene.lights).toHaveLength(1); 72 | 73 | const light = scene.lights[0] as BabylonDirectionalLight; 74 | const shadowGenerator = light.getShadowGenerator() as unknown as ShadowGenerator; 75 | expect(shadowGenerator.size).toBe(1024); 76 | expect(shadowGenerator.forceBackFacesOnly).toBeTrue(); 77 | expect(shadowGenerator.darkness).toBe(0); // make sure default values are preserved 78 | }); 79 | 80 | it('can remove shadow generator', function () { 81 | const { world, scene } = setupWorld(); 82 | 83 | const lightEntity = world.createEntity(); 84 | lightEntity.addComponent(Parent).addComponent(DirectionalLight).addComponent(ShadowGenerator); 85 | 86 | const meshEntity = world.createEntity(); 87 | meshEntity.addComponent(Parent).addComponent(Box); 88 | 89 | world.execute(0, 0); 90 | 91 | lightEntity.removeComponent(ShadowGenerator); 92 | world.execute(0, 0); 93 | 94 | const light = scene.lights[0] as BabylonDirectionalLight; 95 | expect(light.getShadowGenerator()).toBeNull(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/transform.test.ts: -------------------------------------------------------------------------------- 1 | import { Box, Parent, Position, Rotation, Scale } from '../src/components'; 2 | import { Vector3 } from '@babylonjs/core/Maths/math.vector'; 3 | import setupWorld from './helpers/setup-world'; 4 | import { PivotPoint } from '../src'; 5 | import { TransformNode } from '@babylonjs/core/Meshes/transformNode'; 6 | 7 | describe('transform system', function () { 8 | describe('position', function () { 9 | it('can add position', function () { 10 | const { world, scene } = setupWorld(); 11 | 12 | const entity = world.createEntity(); 13 | entity 14 | .addComponent(Parent) 15 | .addComponent(Box) 16 | .addComponent(Position, { value: new Vector3(1, 2, 3) }); 17 | 18 | world.execute(0, 0); 19 | 20 | expect(scene.meshes).toHaveLength(1); 21 | scene.meshes[0].computeWorldMatrix(true); 22 | expect(scene.meshes[0].getWorldMatrix().getTranslation().equalsToFloats(1, 2, 3)).toBeTrue(); 23 | }); 24 | 25 | it('can update position', function () { 26 | const { world, scene } = setupWorld(); 27 | 28 | const entity = world.createEntity(); 29 | entity.addComponent(Parent).addComponent(Box).addComponent(Position, { value: Vector3.Zero() }); 30 | 31 | world.execute(0, 0); 32 | 33 | const component = entity.getMutableComponent(Position)!; 34 | component.value = new Vector3(1, 2, 3); 35 | 36 | world.execute(0, 0); 37 | 38 | expect(scene.meshes).toHaveLength(1); 39 | scene.meshes[0].computeWorldMatrix(true); 40 | expect(scene.meshes[0].getWorldMatrix().getTranslation().equalsToFloats(1, 2, 3)).toBeTrue(); 41 | }); 42 | 43 | it('can remove position', function () { 44 | const { world, scene } = setupWorld(); 45 | 46 | const entity = world.createEntity(); 47 | entity 48 | .addComponent(Parent) 49 | .addComponent(Box) 50 | .addComponent(Position, { value: new Vector3(1, 0, 1) }); 51 | 52 | world.execute(0, 0); 53 | 54 | entity.removeComponent(Position); 55 | world.execute(0, 0); 56 | 57 | expect(scene.meshes).toHaveLength(1); 58 | scene.meshes[0].computeWorldMatrix(true); 59 | expect(scene.meshes[0].getWorldMatrix().getTranslation().equalsToFloats(0, 0, 0)).toBeTrue(); 60 | }); 61 | }); 62 | describe('rotation', function () { 63 | it('can add rotation', function () { 64 | const { world, scene } = setupWorld(); 65 | 66 | const entity = world.createEntity(); 67 | entity 68 | .addComponent(Parent) 69 | .addComponent(Box) 70 | .addComponent(Rotation, { value: new Vector3(0, Math.PI, 0) }); 71 | 72 | world.execute(0, 0); 73 | 74 | expect(scene.meshes).toHaveLength(1); 75 | scene.meshes[0].computeWorldMatrix(true); 76 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[0]).toBeCloseTo(-1); 77 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[1]).toBeCloseTo(0); 78 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[2]).toBeCloseTo(0); 79 | }); 80 | 81 | it('can update rotation', function () { 82 | const { world, scene } = setupWorld(); 83 | 84 | const entity = world.createEntity(); 85 | entity.addComponent(Parent).addComponent(Box).addComponent(Rotation, { value: Vector3.Zero() }); 86 | 87 | world.execute(0, 0); 88 | 89 | const component = entity.getMutableComponent(Rotation)!; 90 | component.value = new Vector3(0, Math.PI, 0); 91 | 92 | world.execute(0, 0); 93 | 94 | expect(scene.meshes).toHaveLength(1); 95 | scene.meshes[0].computeWorldMatrix(true); 96 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[0]).toBeCloseTo(-1); 97 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[1]).toBeCloseTo(0); 98 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[2]).toBeCloseTo(0); 99 | }); 100 | 101 | it('can remove rotation', function () { 102 | const { world, scene } = setupWorld(); 103 | 104 | const entity = world.createEntity(); 105 | entity 106 | .addComponent(Parent) 107 | .addComponent(Box) 108 | .addComponent(Rotation, { value: new Vector3(1, 0, 1) }); 109 | 110 | world.execute(0, 0); 111 | 112 | entity.removeComponent(Rotation); 113 | world.execute(0, 0); 114 | 115 | expect(scene.meshes).toHaveLength(1); 116 | scene.meshes[0].computeWorldMatrix(true); 117 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[0]).toBeCloseTo(1); 118 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[1]).toBeCloseTo(0); 119 | expect(scene.meshes[0].getWorldMatrix().getRotationMatrix().asArray()[2]).toBeCloseTo(0); 120 | }); 121 | }); 122 | describe('scale', function () { 123 | it('can add scale', function () { 124 | const { world, scene } = setupWorld(); 125 | 126 | const entity = world.createEntity(); 127 | entity 128 | .addComponent(Parent) 129 | .addComponent(Box) 130 | .addComponent(Scale, { value: new Vector3(2, 1, 1) }); 131 | 132 | world.execute(0, 0); 133 | 134 | expect(scene.meshes).toHaveLength(1); 135 | scene.meshes[0].computeWorldMatrix(true); 136 | expect(scene.meshes[0].getWorldMatrix().asArray()[0]).toBeCloseTo(2); 137 | expect(scene.meshes[0].getWorldMatrix().asArray()[1]).toBeCloseTo(0); 138 | expect(scene.meshes[0].getWorldMatrix().asArray()[2]).toBeCloseTo(0); 139 | }); 140 | 141 | it('can update scale', function () { 142 | const { world, scene } = setupWorld(); 143 | 144 | const entity = world.createEntity(); 145 | entity.addComponent(Parent).addComponent(Box).addComponent(Scale, { value: Vector3.One() }); 146 | 147 | world.execute(0, 0); 148 | 149 | const component = entity.getMutableComponent(Scale)!; 150 | component.value = new Vector3(2, 1, 1); 151 | 152 | world.execute(0, 0); 153 | 154 | expect(scene.meshes).toHaveLength(1); 155 | scene.meshes[0].computeWorldMatrix(true); 156 | expect(scene.meshes[0].getWorldMatrix().asArray()[0]).toBeCloseTo(2); 157 | expect(scene.meshes[0].getWorldMatrix().asArray()[1]).toBeCloseTo(0); 158 | expect(scene.meshes[0].getWorldMatrix().asArray()[2]).toBeCloseTo(0); 159 | }); 160 | 161 | it('can remove scale', function () { 162 | const { world, scene } = setupWorld(); 163 | 164 | const entity = world.createEntity(); 165 | entity 166 | .addComponent(Parent) 167 | .addComponent(Box) 168 | .addComponent(Scale, { value: new Vector3(3, 1, 1) }); 169 | 170 | world.execute(0, 0); 171 | 172 | entity.removeComponent(Scale); 173 | world.execute(0, 0); 174 | 175 | expect(scene.meshes).toHaveLength(1); 176 | scene.meshes[0].computeWorldMatrix(true); 177 | 178 | expect(scene.meshes[0].getWorldMatrix().asArray()[0]).toBeCloseTo(1); 179 | expect(scene.meshes[0].getWorldMatrix().asArray()[1]).toBeCloseTo(0); 180 | expect(scene.meshes[0].getWorldMatrix().asArray()[2]).toBeCloseTo(0); 181 | }); 182 | }); 183 | describe('pivot point', function () { 184 | it('can add pivot point', function () { 185 | const { world, scene } = setupWorld(); 186 | 187 | const entity = world.createEntity(); 188 | entity 189 | .addComponent(Parent) 190 | .addComponent(Box) 191 | .addComponent(PivotPoint, { value: new Vector3(1, 0, 0) }); 192 | 193 | world.execute(0, 0); 194 | 195 | expect(scene.meshes).toHaveLength(1); 196 | expect((scene.meshes[0].parent as TransformNode).getAbsolutePivotPoint().equalsToFloats(1, 0, 0)).toBeTrue(); 197 | }); 198 | 199 | it('can update pivot point', function () { 200 | const { world, scene } = setupWorld(); 201 | 202 | const entity = world.createEntity(); 203 | entity.addComponent(Parent).addComponent(Box).addComponent(PivotPoint, { value: Vector3.Zero() }); 204 | 205 | world.execute(0, 0); 206 | 207 | expect(scene.meshes).toHaveLength(1); 208 | expect((scene.meshes[0].parent as TransformNode).getAbsolutePivotPoint().equalsToFloats(0, 0, 0)).toBeTrue(); 209 | 210 | const component = entity.getMutableComponent(PivotPoint)!; 211 | component.value = new Vector3(1, 0, 0); 212 | 213 | world.execute(0, 0); 214 | 215 | expect((scene.meshes[0].parent as TransformNode).getAbsolutePivotPoint().equalsToFloats(1, 0, 0)).toBeTrue(); 216 | }); 217 | 218 | it('can remove pivot point', function () { 219 | const { world, scene } = setupWorld(); 220 | 221 | const entity = world.createEntity(); 222 | entity 223 | .addComponent(Parent) 224 | .addComponent(Box) 225 | .addComponent(PivotPoint, { value: new Vector3(1, 0, 0) }); 226 | 227 | world.execute(0, 0); 228 | 229 | expect(scene.meshes).toHaveLength(1); 230 | expect((scene.meshes[0].parent as TransformNode).getAbsolutePivotPoint().equalsToFloats(1, 0, 0)).toBeTrue(); 231 | 232 | entity.removeComponent(PivotPoint); 233 | world.execute(0, 0); 234 | 235 | expect((scene.meshes[0].parent as TransformNode).getAbsolutePivotPoint().equalsToFloats(0, 0, 0)).toBeTrue(); 236 | }); 237 | }); 238 | }); 239 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationDir": ".", 6 | "outDir": "." 7 | }, 8 | "include": [ 9 | "src", 10 | "types/**/*" 11 | ], 12 | "exclude": [] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "moduleResolution": "node", 5 | "target": "es2017", 6 | "module":"es2015", 7 | "lib": [ 8 | "es2015", 9 | "es2016", 10 | "es2017", 11 | "dom" 12 | ], 13 | "strict": true, 14 | "sourceMap": true, 15 | "declaration": false, 16 | "allowSyntheticDefaultImports": true, 17 | "experimentalDecorators": true, 18 | "emitDecoratorMetadata": true, 19 | "skipLibCheck": true, 20 | "typeRoots": [ 21 | "node_modules/@types" 22 | ], 23 | "baseUrl": ".", 24 | "paths": { 25 | "*": [ 26 | "types/*" 27 | ] 28 | } 29 | }, 30 | "include": [ 31 | "src", 32 | "test", 33 | "types/**/*", 34 | "demos" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.rollup.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": [ 4 | "src", 5 | "types/**/*" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | import 'jest-extended'; 2 | --------------------------------------------------------------------------------