├── .github
├── codeql
│ └── codeql-config.yml
└── workflows
│ ├── codeql.yml
│ ├── docs.yaml
│ ├── eslint.yml
│ ├── node.js.yml
│ └── npm-publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── .gitignore
├── CONTRIBUTE.md
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── apple-touch-icon.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ └── low_poly_space_ship.glb
├── src
│ ├── assets
│ │ ├── samoyed-mascot-moved.png
│ │ ├── samoyed-mascot.png
│ │ └── site.webmanifest
│ ├── components
│ │ ├── Example
│ │ │ ├── CodeWrapper.astro
│ │ │ ├── Example.astro
│ │ │ ├── OpenInStackblitz.astro
│ │ │ ├── stackblitz-files
│ │ │ │ ├── index.html
│ │ │ │ ├── stackblitz-package.json
│ │ │ │ ├── stackblitz-tsconfig.json
│ │ │ │ └── stackblitz-vite.config.ts
│ │ │ ├── stackblitz-template.ts
│ │ │ └── types.ts
│ │ └── Intro
│ │ │ ├── Intro.astro
│ │ │ ├── floor.ts
│ │ │ ├── intro.ts
│ │ │ ├── random.ts
│ │ │ ├── smoke.ts
│ │ │ └── spaceship.ts
│ ├── content
│ │ ├── config.ts
│ │ └── docs
│ │ │ ├── advanced
│ │ │ ├── buffer-capacity.mdx
│ │ │ ├── lod.mdx
│ │ │ ├── multimaterial.mdx
│ │ │ ├── patch-shader.mdx
│ │ │ ├── shadow-lod.mdx
│ │ │ ├── skinning.mdx
│ │ │ └── texture-partial-update.mdx
│ │ │ ├── basics
│ │ │ ├── 00-add-remove.mdx
│ │ │ ├── 01-Instancedentity.mdx
│ │ │ ├── 02-animation.mdx
│ │ │ ├── 03-euler.mdx
│ │ │ ├── 04-tween.mdx
│ │ │ ├── 05-custom-data.mdx
│ │ │ ├── 06-frustum-culling.mdx
│ │ │ ├── 07-sorting.mdx
│ │ │ ├── 08-raycasting.mdx
│ │ │ ├── 09-bvh-build.mdx
│ │ │ └── 10-uniform-per-instance.mdx
│ │ │ ├── getting-started
│ │ │ ├── 00-introduction.mdx
│ │ │ ├── 01-installation.mdx
│ │ │ └── 02-first-instancedmesh2.mdx
│ │ │ ├── index.mdx
│ │ │ └── more
│ │ │ ├── faq.mdx
│ │ │ ├── know-issue.mdx
│ │ │ └── performance-tips.mdx
│ ├── env.d.ts
│ ├── examples
│ │ ├── add-remove
│ │ │ └── index.ts
│ │ ├── animation
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── euler
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── first
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── frustum-culling
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── instances-array
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── instances-custom-data
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ ├── raycasting
│ │ │ └── index.ts
│ │ ├── sorting
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ │ └── tween
│ │ │ ├── app.ts
│ │ │ └── index.ts
│ └── pages
│ │ └── examples
│ │ ├── Loader.astro
│ │ └── [...slug].astro
├── tsconfig.build.json
└── tsconfig.json
├── eslint.config.js
├── examples
├── LOD.ts
├── createEntities.ts
├── customMaterial.ts
├── customMaterialTextureArray.ts
├── dynamicBVH.ts
├── fastRaycasting.ts
├── instancedMeshParse.ts
├── morph.ts
├── multimaterial.ts
├── multimaterial_tree.ts
├── objects
│ ├── createSimplifiedGeometry.ts
│ ├── random.ts
│ ├── tileLambertMaterial.ts
│ └── tileMaterial.ts
├── overrideMaterial.ts
├── remove-instance.ts
├── remove-instances.ts
├── shadowLOD.ts
├── skeleton.ts
├── sorting.ts
├── template.ts
├── test.ts
├── trees.ts
├── tween.ts
└── uniforms.ts
├── index.html
├── package-lock.json
├── package.json
├── public
├── banner.png
├── grass.jpg
├── grass_normal.jpg
├── js.png
├── pattern.jpg
├── pine.glb
├── pine_low.glb
├── planks.jpg
├── texture.png
├── tree.glb
├── ts.png
└── wall.jpg
├── src
├── core
│ ├── InstancedEntity.ts
│ ├── InstancedMesh2.ts
│ ├── InstancedMeshBVH.ts
│ ├── feature
│ │ ├── Capacity.ts
│ │ ├── FrustumCulling.ts
│ │ ├── Instances.ts
│ │ ├── LOD.ts
│ │ ├── Morph.ts
│ │ ├── Raycasting.ts
│ │ ├── Skeleton.ts
│ │ └── Uniforms.ts
│ └── utils
│ │ ├── GLInstancedBufferAttribute.ts
│ │ ├── InstancedRenderList.ts
│ │ └── SquareDataTexture.ts
├── index.ts
├── shaders
│ ├── ShaderChunk.ts
│ └── chunks
│ │ ├── instanced_color_pars_vertex.glsl
│ │ ├── instanced_color_vertex.glsl
│ │ ├── instanced_pars_vertex.glsl
│ │ ├── instanced_skinning_pars_vertex.glsl
│ │ └── instanced_vertex.glsl
└── utils
│ ├── CreateFrom.ts
│ └── SortingUtils.ts
├── tsconfig.build.json
├── tsconfig.json
└── vite.config.js
/.github/codeql/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL config"
2 |
3 | paths-ignore:
4 | - "node_modules"
5 | - "examples"
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | branches: [ "master" ]
19 | schedule:
20 | - cron: '23 22 * * 3'
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze (${{ matrix.language }})
25 | # Runner size impacts CodeQL analysis time. To learn more, please see:
26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
27 | # - https://gh.io/supported-runners-and-hardware-resources
28 | # - https://gh.io/using-larger-runners (GitHub.com only)
29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31 | permissions:
32 | # required for all workflows
33 | security-events: write
34 |
35 | # required to fetch internal or private CodeQL packs
36 | packages: read
37 |
38 | # only required for workflows in private repositories
39 | actions: read
40 | contents: read
41 |
42 | strategy:
43 | fail-fast: false
44 | matrix:
45 | include:
46 | - language: javascript-typescript
47 | build-mode: none
48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
49 | # Use `c-cpp` to analyze code written in C, C++ or both
50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
56 | steps:
57 | - name: Checkout repository
58 | uses: actions/checkout@v4
59 | - run: npm ci
60 | - run: npm run build
61 |
62 |
63 | # Initializes the CodeQL tools for scanning.
64 | - name: Initialize CodeQL
65 | uses: github/codeql-action/init@v3
66 | with:
67 | languages: ${{ matrix.language }}
68 | build-mode: ${{ matrix.build-mode }}
69 |
70 | # If you wish to specify custom queries, you can do so here or in a config file.
71 | # By default, queries listed here will override any specified in a config file.
72 | # Prefix the list here with "+" to use these queries and those in the config file.
73 |
74 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
75 | # queries: security-extended,security-and-quality
76 |
77 | - name: Autobuild
78 | uses: github/codeql-action/autobuild@v3
79 |
80 | - name: Perform CodeQL Analysis
81 | uses: github/codeql-action/analyze@v3
82 | with:
83 | category: "/language:${{matrix.language}}"
84 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy docs to GitHub Pages
2 |
3 | on:
4 | # Trigger the workflow every time you push or open pull_request to the `master` branch
5 | # Using a different branch name? Replace `master` with your branch’s name
6 | push:
7 | branches: [ master ]
8 | pull_request:
9 | branches: [ master ]
10 | workflow_dispatch:
11 |
12 | # Allow this job to clone the repo and create a page deployment
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | jobs:
19 | build:
20 | runs-on: ubuntu-latest
21 | steps:
22 | - name: Checkout your repository using git
23 | uses: actions/checkout@v4
24 | - name: Install dependencies
25 | shell: bash
26 | working-directory: ./docs
27 | run: npm install
28 | - name: Build documentation
29 | shell: bash
30 | working-directory: ./docs
31 | run: npm run build
32 | - name: Upload Pages artifact
33 | if: github.event_name != 'pull_request'
34 | uses: actions/upload-pages-artifact@v3
35 | with:
36 | path: "./docs/dist/"
37 | deploy:
38 | needs: build
39 | if: github.event_name != 'pull_request'
40 | runs-on: ubuntu-latest
41 | environment:
42 | name: github-pages
43 | url: ${{ steps.deployment.outputs.page_url }}
44 | steps:
45 | - name: Deploy to GitHub Pages
46 | id: deployment
47 | uses: actions/deploy-pages@v4
--------------------------------------------------------------------------------
/.github/workflows/eslint.yml:
--------------------------------------------------------------------------------
1 | name: ESLint
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [ "*" ]
9 | schedule:
10 | - cron: '19 7 * * 2'
11 |
12 | jobs:
13 | eslint:
14 | name: Run eslint scanning
15 | runs-on: ubuntu-latest
16 | permissions:
17 | contents: read
18 | security-events: write
19 | actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
20 | steps:
21 | - name: Checkout code
22 | uses: actions/checkout@v4
23 |
24 | - name: Install ESLint
25 | run: |
26 | npm ci
27 | npm install @microsoft/eslint-formatter-sarif@2.1.7
28 |
29 | - name: Run ESLint
30 | run: npx eslint .
31 | --format @microsoft/eslint-formatter-sarif
32 | --output-file eslint-results.sarif
33 | continue-on-error: true
34 |
35 | - name: Upload analysis results to GitHub
36 | uses: github/codeql-action/upload-sarif@v3
37 | with:
38 | sarif_file: eslint-results.sarif
39 | wait-for-processing: true
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "*" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [20.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Use Node.js ${{ matrix.node-version }}
25 | uses: actions/setup-node@v4
26 | with:
27 | node-version: ${{ matrix.node-version }}
28 | cache: 'npm'
29 | - run: npm ci
30 | - run: npm run build --if-present
31 | - run: npm test
32 |
--------------------------------------------------------------------------------
/.github/workflows/npm-publish.yml:
--------------------------------------------------------------------------------
1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3 |
4 | name: Node.js Package
5 |
6 | on:
7 | release:
8 | types: [created]
9 |
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: 20
18 | - run: npm ci
19 | - run: npm test
20 |
21 | publish-npm:
22 | needs: build
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 | - uses: actions/setup-node@v4
27 | with:
28 | node-version: 20
29 | registry-url: https://registry.npmjs.org/
30 | - run: npm ci
31 | - run: npm run build
32 | - run: npm publish
33 | env:
34 | NODE_AUTH_TOKEN: ${{secrets.npm_token}}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Andrea Gargaro
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Three.ez - InstancedMesh2
4 |
5 | Simplify your three.js application development with three.ez!
6 | three.ez/batched-mesh-extensions - BatchedMesh
extension methods and enhancements for better performance and usability
7 |
8 |
9 |
10 |
11 | [](https://discord.gg/MVTwrdX3JM)
12 | [](https://www.npmjs.com/package/@three.ez/instanced-mesh)
13 | [](https://github.com/three-ez/instanced-mesh)
14 | [](https://bundlephobia.com/package/@three.ez/instanced-mesh)
15 | [](https://sonarcloud.io/summary/new_code?id=agargaro_instanced-mesh)
16 | [](https://deepscan.io/dashboard#view=project&tid=21196&pid=27990&bid=896898)
17 |
18 |
19 |
20 | `InstancedMesh2` is an alternative version of `InstancedMesh` with enhanced features for performance and usability.
21 |
22 | ```ts
23 | const myInstancedMesh = new InstancedMesh2(geometry, material);
24 |
25 | myInstancedMesh.addInstances(count, (obj, index) => {
26 | obj.position.x = index;
27 | });
28 | ```
29 |
30 | - [**Dynamic capacity**](#dynamic-capacity): *add or remove instances seamlessly.*
31 | - [**Object3D-like instances**](#object3d-like-instances): *use instances like `Object3D` with transforms and custom data.*
32 | - [**Per-instance frustum culling**](#per-instance-frustum-culling): *skip rendering for out-of-view instances.*
33 | - [**Spatial indexing (dynamic BVH)**](#spatial-indexing-dynamic-bvh): *speed up raycasting and frustum culling.*
34 | - [**Sorting**](#sorting): *reduce overdraw and manage transparent objects efficiently.*
35 | - [**Per-instance visibility**](#per-instance-visibility): *toggle visibility for each instance individually.*
36 | - [**Per-instance opacity**](#per-instance-opacity): *set opacity for each instance individually.*
37 | - [**Per-instance uniforms**](#per-instance-uniforms): *assign unique shader data to individual instances.*
38 | - [**Level of Detail (LOD)**](#level-of-detail-lod): *dynamically adjust instance detail based on distance.*
39 | - [**Shadow LOD**](#shadow-lod): *optimize shadow rendering with lower detail for distant instances.*
40 | - [**Skinning**](#skinning): *apply skeletal animations to instances for more complex and dynamic movements.*
41 |
42 | ## 🧑💻 Live Examples
43 |
44 | **Vanilla**
45 | -
[Dynamic adding with BVH](https://stackblitz.com/edit/stackblitz-starters-au96fmqz?file=index.html) (thanks to [Saumac](https://github.com/saumac))
46 |
47 | **Using three.ez/main**
48 | -
[1kk static trees](https://stackblitz.com/edit/three-ezinstancedmesh2-1kk-static-trees?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
49 | -
[Instances array dynamic](https://stackblitz.com/edit/three-ezinstancedmesh2-instances-array-dynamic?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
50 | -
[Sorting](https://stackblitz.com/edit/three-ezinstancedmesh2-sorting?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
51 | -
[Uniforms per instance](https://stackblitz.com/edit/three-ezinstancedmesh2-custom-material?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
52 | -
[Dynamic BVH (no vite)](https://stackblitz.com/edit/three-ezinstancedmesh2-dynamic-bvh?file=index.ts&embed=1&hideDevTools=1&view=preview)
53 | -
[Fast raycasting](https://stackblitz.com/edit/three-ezinstancedmesh2-fast-raycasting?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
54 | -
[LOD](https://stackblitz.com/edit/three-ezinstancedmesh2-instancedmeshlod?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
55 | -
[Shadow LOD](https://stackblitz.com/edit/three-ezinstancedmesh2-shadow-lod?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
56 | -
[Skinning 3k instances](https://stackblitz.com/edit/three-ezinstancedmesh2-skinning?file=src%2Fmain.ts&embed=1&hideDevTools=1&view=preview)
57 | -
[Dynamic adding with BVH](https://glitch.com/edit/#!/three-ez-instanced-mesh-dynamic-adding-with-bvh?path=main.js)
58 | -
[Skinning](https://glitch.com/edit/#!/instancedmesh2-skinning?path=main.js)
59 |
60 | **Using other libraries**
61 | - Threlte
62 | -
[Tres.js](https://stackblitz.com/edit/vitejs-vite-nhoadwww?file=src/components/TheExperience.vue) (thanks to [JaimeTorrealba](https://github.com/JaimeTorrealba))
63 | -
[React-three-fiber](https://stackblitz.com/edit/vitejs-vite-zahmbaan?file=src%2FApp.tsx) (thanks to [Saumac](https://github.com/saumac))
64 | -
[React-three-fiber](https://stackblitz.com/~/github.com/Lunakepio/ac-2-dna-ui) (thanks to [Lunakepio](https://github.com/Lunakepio))
65 |
66 | ## ❔ Need help?
67 |
68 | Join us on [Discord](https://discord.gg/MVTwrdX3JM) or open an issue on GitHub.
69 |
70 | ## ⭐ Like it?
71 |
72 | If you like this project, please leave a star. Thank you! ❤️
73 |
74 | ## 📚 Documentation
75 |
76 | The documentation is available [here](https://agargaro.github.io/instanced-mesh).
77 |
78 | ## ⬇️ Installation
79 |
80 | You can install it via npm using the following command:
81 |
82 | ```bash
83 | npm install @three.ez/instanced-mesh
84 | ```
85 |
86 | Or you can import it from CDN:
87 |
88 | ```html
89 |
99 | ```
100 |
101 | ## 🚀 Features
102 |
103 | ### Dynamic capacity
104 |
105 | Manage a dynamic number of instances, automatically expanding the data buffers as needed to accommodate additional instances.
106 |
107 | If not specified, `capacity` is `1000`.
108 |
109 | ```ts
110 | const myInstancedMesh = new InstancedMesh2(geometry, material, { capacity: count });
111 |
112 | myInstancedMesh.addInstances(count, (obj, index) => { ... }); // add instances and expand buffer if necessary
113 |
114 | myInstancedMesh.removeInstances(id0, id1, ...);
115 |
116 | myInstancedMesh.clearInstances(); // remove all instances
117 | ```
118 |
119 | ### Object3D-like instances
120 |
121 | It's possible to create an array of `InstancedEntity` **(Object3D-like)** in order to easily manipulate instances, using more memory.
122 |
123 | ```ts
124 | const myInstancedMesh = new InstancedMesh2(geometry, material, { createEntities: true });
125 |
126 | myInstancedMesh.instances[0].customData = {};
127 | myInstancedMesh.instances[0].position.random();
128 | myInstancedMesh.instances[0].rotateX(Math.PI);
129 | myInstancedMesh.instances[0].updateMatrix(); // necessary after transformations
130 | ```
131 |
132 | ### Per-instance frustum culling
133 |
134 | Avoiding rendering objects outside the camera frustum can drastically improve performance (especially for complex geometries).
135 | Frustum culling by default is performed by iterating all instances, [but it is possible to speed up this process by creating a spatial indexing data structure **(BVH)**](#spatial-indexing-dynamic-bvh).
136 |
137 | By default `perObjectFrustumCulled` is `true`.
138 |
139 | ### Spatial indexing (dynamic BVH)
140 |
141 | **To speed up raycasting and frustum culling**, a spatial indexing data structure can be created to contain the boundingBoxes of all instances.
142 | This works very well if the instances are **mostly static** (updating a BVH can be expensive) and scattered in world space.
143 |
144 | Setting a margin makes BVH updating faster, but may make raycasting and frustum culling slightly slower.
145 | ```ts
146 | myInstancedMesh.computeBVH({ margin: 0 }); // margin is optional
147 | ```
148 |
149 | ### Sorting
150 |
151 | Sorting can be used to decrease overdraw and render transparent objects.
152 |
153 | It's possible to improve sort performance adding a `customSort`, like built-in `createRadixSort`.
154 |
155 | By default `sortObjects` is `false`.
156 |
157 | ```ts
158 | import { createRadixSort } from '@three.ez/instanced-mesh';
159 |
160 | myInstancedMesh.sortObjects = true;
161 | myInstancedMesh.customSort = createRadixSort(myInstancedMesh);
162 | ```
163 |
164 | ### Per-instance visibility
165 |
166 | Set the visibility status of each instance.
167 |
168 | ```ts
169 | myInstancedMesh.setVisibilityAt(index, false);
170 | myInstancedMesh.instances[0].visible = false; // if instances array is created
171 | ```
172 |
173 | ### Per-instance opacity
174 |
175 | Set the opacity of each instance. It's recommended to enable [**instances sorting**](#sorting) and disable the `depthWriting` of the material.
176 |
177 | ```ts
178 | myInstancedMesh.setOpacityAt(index, 0.5);
179 | myInstancedMesh.instances[0].opacity = 0.5; // if instances array is created
180 | ```
181 |
182 | ### Per-instance uniforms
183 |
184 | Assign unique shader uniforms to each instance, working with every materials.
185 |
186 | ```ts
187 | myInstancedMesh.initUniformsPerInstance({ fragment: { metalness: 'float', roughness: 'float', emissive: 'vec3' } });
188 |
189 | myInstancedMesh.setUniformAt(index, 'metalness', 0.5);
190 | myInstancedMesh.instances[0].setUniform('emissive', new Color('white')); // if instances array is created
191 | ```
192 |
193 | ### Level of Detail (LOD)
194 |
195 | Improve rendering performance by dynamically adjusting the detail level of instances based on their distance from the camera.
196 | Use simplified geometries for distant objects to optimize resources.
197 |
198 | ```ts
199 | myInstancedMesh.addLOD(geometryMid, material, 50);
200 | myInstancedMesh.addLOD(geometryLow, material, 200);
201 | ```
202 |
203 | ### Shadow LOD
204 |
205 | Optimize shadow rendering by reducing the detail level of instances casting shadows based on their distance from the camera.
206 |
207 | ```ts
208 | myInstancedMesh.addShadowLOD(geometryMid);
209 | myInstancedMesh.addShadowLOD(geometryLow, 100);
210 | ```
211 |
212 | ### Skinning
213 |
214 | Apply skeletal animations to instances for more complex and dynamic movements.
215 |
216 | ```ts
217 | myInstancedMesh.initSkeleton(skeleton);
218 |
219 | mixer.update(time);
220 | myInstancedMesh.setBonesAt(index);
221 | ```
222 |
223 | ### Raycasting tips
224 |
225 | If you are not using a BVH, you can set the `raycastOnlyFrustum` property to **true** to avoid iterating over all instances.
226 |
227 | It's recommended to use [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh) to create a geometry BVH.
228 |
229 | ## 🤝 Special thanks to
230 |
231 | - [gkjohnson](https://github.com/gkjohnson)
232 | - [manthrax](https://github.com/manthrax)
233 | - [jungle_hacker](https://github.com/lambocorp)
234 |
235 | ## 📖 References
236 |
237 | - [three-mesh-bvh](https://github.com/gkjohnson/three-mesh-bvh)
238 | - [ErinCatto_DynamicBVH](https://box2d.org/files/ErinCatto_DynamicBVH_Full.pdf)
239 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # generated api
10 | src/content/docs/api/
11 | docs/public/examples/
12 | # logs
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 | # generate js examples
18 | public/examples/**
19 |
20 | # environment variables
21 | .env
22 | .env.production
23 |
24 | # macOS-specific files
25 | .DS_Store
26 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | # Contributing to Documentation
2 |
3 | This guide explains how to add and maintain documentation for this project, following the [Diátaxis](https://diataxis.fr/) framework principles.
4 |
5 | ## Documentation Types
6 |
7 | We organize documentation into:
8 |
9 | - **Tutorials**: Step-by-step lessons for beginners
10 | - **How-to Guides**: Practical guides in `src/content/docs/guides/`
11 | - **Reference**: Technical details in `src/content/docs/reference/`
12 | - **Explanation**: Concept discussions and background
13 |
14 | ## Adding Documentation Pages
15 |
16 | 1. Choose appropriate directory based on content type:
17 |
18 | ```
19 | src/content/docs/guides/ # For how-to guides
20 | src/content/docs/reference/ # For technical reference
21 | src/content/docs/tutorials/ # For tutorials
22 | src/content/docs/concepts/ # For explanations
23 | ```
24 |
25 | 2. Add required frontmatter:
26 | ```md
27 | ---
28 | title: Your Page Title
29 | description: Brief description
30 | ---
31 | ```
32 |
33 | ## Adding Code Examples
34 |
35 | 1. Create your example:
36 |
37 | ```bash
38 | code src/examples/myExample/index.ts
39 | ```
40 |
41 | > **Note**: Maximum 2 levels of nesting allowed. Deeper nesting is not supported.
42 |
43 | 2. Write your Three.js code in `index.ts`:
44 |
45 | ```typescript
46 | import { Scene, PerspectiveCamera } from 'three';
47 |
48 | // Your Three.js example code here
49 | ```
50 |
51 | > **Note**: you can also import from local files, like `import { MyComponent } from './MyComponent'`.
52 |
53 | 3. Reference in docs with:
54 |
55 | ```md
56 |
57 | ```
58 |
59 | | Prop | Type | Default | Description |
60 | | ---------------- | ------- | -------- | --------------------------------------- |
61 | | `path` | string | required | Directory path relative to src/examples |
62 | | `hideCode` | boolean | `false` | Hides the source code section |
63 | | `hidePreview` | boolean | `false` | Hides the example preview |
64 | | `hideStackblitz` | boolean | `false` | Hides "Open in Stackblitz" button |
65 |
66 | > **Note**: You can see the example in full screen in path `http://localhost:4321/instanced-mesh/examples/`
67 |
68 | ## Example Guidelines
69 |
70 | ### Keep It Simple
71 |
72 | - Focus on one concept per example
73 | - Add clear code comments
74 | - Avoid mixing multiple complex features
75 |
76 | ### Use Clear Names
77 |
78 | - Use descriptive directory names (e.g. `frustum-culling`)
79 | - Follow kebab-case for directory names
80 | - Avoid generic names
81 |
82 | ### Development
83 |
84 | 1. Available scripts:
85 |
86 | ```bash
87 | npm run dev # Dev mode with hot reload
88 | npm run start # Production preview
89 | npm run build # Production build
90 | ```
91 |
92 | > **Note**: all of those scripts build the examples, in the public folder.
93 |
94 | ### Dependencies
95 |
96 | - Examples use import maps in `[...slug].astro`
97 | ### Dependencies
98 |
99 | - Examples use import maps in `[...slug].astro`
100 | - Pre-configured libraries:
101 | - `three`
102 | - `@three.ez/main`
103 | - `@three.ez/instanced-mesh`
104 | - `three/examples/jsm/`
105 | - `bvh.js`
106 | - Add new dependencies to `importmap` if needed.
107 |
--------------------------------------------------------------------------------
/docs/astro.config.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { defineConfig } from 'astro/config';
3 | import starlight from '@astrojs/starlight';
4 | import starlightTypeDoc, { typeDocSidebarGroup } from 'starlight-typedoc';
5 | import AutoImport from 'astro-auto-import';
6 | import { resolve } from 'path';
7 | import mdx from '@astrojs/mdx';
8 | // https://astro.build/config
9 | export default defineConfig({
10 | site: 'https://agargaro.github.io/instanced-mesh',
11 | base: 'instanced-mesh',
12 | output: 'static',
13 | vite: {
14 | resolve: {
15 | alias: { $components: resolve('./src/components') },
16 | },
17 | },
18 | integrations: [
19 | AutoImport({
20 | imports: ['./src/components/Example/Example.astro'],
21 | }),
22 | starlight({
23 | plugins: [
24 | // Generate the documentation.
25 | starlightTypeDoc({
26 | entryPoints: ['../src/index.ts'],
27 | typeDoc: {
28 | exclude: ['./examples/**/*'],
29 | skipErrorChecking: true,
30 | excludeExternals: true,
31 | },
32 | tsconfig: '../tsconfig.json',
33 | }),
34 | ],
35 | title: 'InstancedMesh2',
36 | logo: {
37 | src: './src/assets/samoyed-mascot.png',
38 | alt: 'logo-samoyed-mascot',
39 | },
40 | favicon: './favicon.ico',
41 | social: {
42 | github: 'https://github.com/agargaro/instanced-mesh',
43 | discord: 'https://discord.gg/MVTwrdX3JM',
44 | },
45 | sidebar: [
46 | {
47 | label: 'Getting Started',
48 | autogenerate: { directory: 'getting-started' },
49 | },
50 | {
51 | label: 'Basics',
52 | autogenerate: { directory: 'basics' },
53 | },
54 | {
55 | label: 'Advanced',
56 | autogenerate: { directory: 'advanced' },
57 | },
58 | {
59 | label: 'More',
60 | autogenerate: { directory: 'more' },
61 | },
62 | // {
63 | // label: 'Reference',
64 | // autogenerate: { directory: 'reference' },
65 | // },
66 | // Add the generated sidebar group to the sidebar.
67 | typeDocSidebarGroup,
68 | ],
69 | }),
70 | // Make sure the MDX integration is included AFTER astro-auto-import
71 | mdx(),
72 | ],
73 | });
74 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "docs",
3 | "type": "module",
4 | "version": "0.0.1",
5 | "scripts": {
6 | "dev": "tsc --project tsconfig.build.json & astro dev --host",
7 | "start": "tsc --project tsconfig.build.json & astro dev",
8 | "build": "tsc --project tsconfig.build.json & astro check && astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/check": "^0.9.4",
14 | "@astrojs/mdx": "^4.1.1",
15 | "@astrojs/starlight": "^0.32.2",
16 | "@stackblitz/sdk": "^1.11.0",
17 | "@three.ez/instanced-mesh": "^0.3.1",
18 | "@three.ez/main": "latest",
19 | "astro": "^5.4.3",
20 | "astro-auto-import": "^0.4.4",
21 | "sharp": "^0.32.5",
22 | "starlight-typedoc": "^0.20.0",
23 | "three": "~0.172.0",
24 | "tweakpane": "^4.0.5",
25 | "typedoc-plugin-markdown": "^4.2.10",
26 | "typescript": "~5.7.2",
27 | "vite": "^6.2.0"
28 | },
29 | "devDependencies": {
30 | "@types/three": "~0.171.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/docs/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/docs/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/docs/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/docs/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/favicon-16x16.png
--------------------------------------------------------------------------------
/docs/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/favicon-32x32.png
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/public/low_poly_space_ship.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/public/low_poly_space_ship.glb
--------------------------------------------------------------------------------
/docs/src/assets/samoyed-mascot-moved.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/src/assets/samoyed-mascot-moved.png
--------------------------------------------------------------------------------
/docs/src/assets/samoyed-mascot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/docs/src/assets/samoyed-mascot.png
--------------------------------------------------------------------------------
/docs/src/assets/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/docs/src/components/Example/CodeWrapper.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import { Code } from '@astrojs/starlight/components';
3 |
4 | type SourceFile = {
5 | [key: string]: string;
6 | };
7 |
8 | interface Props {
9 | files: SourceFile;
10 | }
11 |
12 | const filesEntries = Object.entries(Astro.props.files);
13 |
14 | type TreeElement = {
15 | filePath: string;
16 | name: string;
17 | } & (
18 | | {
19 | isFile: true;
20 | content: string;
21 | children?: never;
22 | }
23 | | {
24 | isFile: false;
25 | children: TreeElement[];
26 | content?: never;
27 | }
28 | );
29 |
30 | function createRecursiveTree(entries: [string, string][], currentDir: string = ''): TreeElement[] {
31 | const tree: TreeElement[] = [];
32 | const filteredEntries = entries.filter(([path]) => path.startsWith(currentDir));
33 |
34 | for (const [path, content] of filteredEntries) {
35 | const relativePath = path.slice(currentDir.length).split('/').filter(Boolean);
36 | const [first, ...rest] = relativePath;
37 |
38 | if (rest.length === 0) {
39 | tree.push({
40 | filePath: path,
41 | name: relativePath[relativePath.length - 1],
42 | isFile: true,
43 | content,
44 | });
45 | } else {
46 | const existingDir = tree.find((item) => !item.isFile);
47 |
48 | if (existingDir) {
49 | (existingDir as any).children = createRecursiveTree(entries, `${currentDir}${first}/`);
50 | } else {
51 | tree.push({
52 | filePath: path,
53 | isFile: false,
54 | name: first,
55 | children: createRecursiveTree(entries, `${currentDir}${first}/`),
56 | });
57 | }
58 | }
59 | }
60 |
61 | return tree;
62 | }
63 |
64 | const tree = createRecursiveTree(filesEntries);
65 | ---
66 |
67 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
233 |
234 |
235 |
236 | {
237 | filesEntries.map(([filePath, content], index) => {
238 | if(index === 0) {
239 | return
242 | } else {
243 | return }
246 | })
247 | }
248 |
249 |
250 |
251 |
280 |
--------------------------------------------------------------------------------
/docs/src/components/Example/Example.astro:
--------------------------------------------------------------------------------
1 | ---
2 | import CodeWrapper from './CodeWrapper.astro';
3 | import OpenInStackblitz from './OpenInStackblitz.astro';
4 |
5 | export const c = (...args: (string | boolean)[]) => {
6 | return args.filter(Boolean).join(' ');
7 | };
8 |
9 | /**
10 | * Interface defining the properties (Props) that can be passed to the Example component.
11 | *
12 | * @interface Props
13 | * @example
14 | * ```astro
15 | *
16 | * ```
17 | */
18 | export interface Props {
19 | /**
20 | * The file path to be processed
21 | * @type {string}
22 | */
23 | path: string;
24 | /**
25 | * Flag to control the visibility of code section
26 | * @property {boolean} [hideCode] - When true, hides the code section of the component.
27 | * Default is false, showing the code section.
28 | */
29 | hideCode?: boolean;
30 | /**
31 | * Flag to control whether to hide the preview section.
32 | * When set to true, the preview section will not be displayed.
33 | * @type {boolean}
34 | * @optional
35 | */
36 | hidePreview?: boolean;
37 | /**
38 | * Whether to hide the Stackblitz editor button.
39 | * @property {boolean} [hideStackblitz=false] - When true, the Stackblitz editor button will not be displayed
40 | */
41 | hideStackblitz?: boolean;
42 | }
43 |
44 | const allModules = import.meta.glob('../../examples/**/*', {
45 | query: '?raw',
46 | import: 'default',
47 | eager: true,
48 | }) as Record;
49 |
50 | for (const path in allModules) {
51 | if (!path.replace('../../examples/', '').startsWith(Astro.props.path)) {
52 | delete allModules[path];
53 | }
54 | }
55 |
56 | const files = Astro.props.files ?? {};
57 |
58 | for (const modulePath in allModules) {
59 | let relativePath = modulePath.replace('../../examples/', '').replace(Astro.props.path, '').slice(1);
60 | if (relativePath.startsWith('/')) {
61 | relativePath = relativePath.slice(1);
62 | }
63 | files[relativePath] = allModules[modulePath];
64 | }
65 |
66 | const hideCodeResolved = Astro.props.hideCode ?? false;
67 | const hidePreview = Astro.props.hidePreview ?? false;
68 | ---
69 |
70 |
71 | {!hidePreview &&
}
72 | {
73 | !Astro.props.hideStackblitz && (
74 |
75 |
76 |
77 | )
78 | }
79 | {!hideCodeResolved &&
}
80 |
81 |
82 |
105 |
--------------------------------------------------------------------------------
/docs/src/components/Example/OpenInStackblitz.astro:
--------------------------------------------------------------------------------
1 | ---
2 | interface Props {
3 | files: Record;
4 | }
5 |
6 | const { files } = Astro.props;
7 |
8 | ---
9 |
10 |
39 |
40 |
71 |
72 |
83 |
--------------------------------------------------------------------------------
/docs/src/components/Example/stackblitz-files/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | three.ez example
7 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/src/components/Example/stackblitz-files/stackblitz-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module",
3 | "scripts": {
4 | "dev": "vite",
5 | "build": "tsc && vite build",
6 | "preview": "vite preview"
7 | },
8 | "dependencies": {
9 | "@three.ez/main": "*",
10 | "@three.ez/instanced-mesh": "*",
11 | "three": "*"
12 | },
13 | "devDependencies": {
14 | "@types/three": "*",
15 | "typescript": "*",
16 | "vite": "*"
17 | }
18 | }
--------------------------------------------------------------------------------
/docs/src/components/Example/stackblitz-files/stackblitz-tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "strict": true,
7 | "noImplicitOverride": true,
8 | "strictNullChecks": false,
9 | "stripInternal": true,
10 | "esModuleInterop": true,
11 | "noImplicitAny": false,
12 | "skipLibCheck": true
13 | }
14 | }
--------------------------------------------------------------------------------
/docs/src/components/Example/stackblitz-files/stackblitz-vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import path from 'path'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [],
7 | resolve: {
8 | alias: {
9 | $lib: path.resolve('./src/lib')
10 | }
11 | }
12 | })
13 |
--------------------------------------------------------------------------------
/docs/src/components/Example/stackblitz-template.ts:
--------------------------------------------------------------------------------
1 | import indexHtml from './stackblitz-files/index.html?raw'
2 | import packageJson from './stackblitz-files/stackblitz-package.json?raw'
3 | import tsconfigJson from './stackblitz-files/stackblitz-tsconfig.json?raw'
4 |
5 | export const files = {
6 | 'package.json': packageJson,
7 | 'index.html': indexHtml,
8 | 'tsconfig.json': tsconfigJson,
9 | }
10 |
--------------------------------------------------------------------------------
/docs/src/components/Example/types.ts:
--------------------------------------------------------------------------------
1 | export type File = {
2 | type: 'file'
3 | name: string
4 | path: string
5 | }
6 |
7 | export type Directory = {
8 | name: string
9 | type: 'directory'
10 | files: (File | Directory)[]
11 | }
12 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/Intro.astro:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | ---
4 |
5 |
16 |
17 |
18 | 🛈
19 |
20 | low poly space ship by chrisonciuconcepts (https://skfb.ly/H9qJ)
21 |
22 |
23 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/floor.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from "@three.ez/instanced-mesh";
2 | import { BoxGeometry, Color, MeshLambertMaterial } from "three";
3 | import { rand } from "./random";
4 |
5 | const capacity = 800;
6 |
7 | export class Floor extends InstancedMesh2<{ multiplier: number; startTime: number; }> {
8 | public floorSpeed = 5;
9 | public scaleSpeed = 0.8;
10 | private readonly maxHeight = 15;
11 |
12 | constructor() {
13 | super(new BoxGeometry(1, 1, 1), new MeshLambertMaterial(), { capacity, createEntities: true });
14 | this.matricesTexture.partialUpdate = false;
15 |
16 | const col = Math.sqrt(capacity / 2);
17 | const halfCol = col / 2;
18 |
19 | this.addInstances(capacity, (obj, index) => {
20 | obj.position.x = index % col - halfCol;
21 | obj.position.z = -Math.floor(index / col);
22 |
23 | const repeteadIndex = index % (col ** 2);
24 | obj.multiplier = this.maxHeight * this.easeInSine((Math.abs(obj.position.x) + 3) / (halfCol + 3));
25 | obj.startTime = (obj.position.x + 10) * repeteadIndex;
26 | obj.scale.y = Math.abs(Math.sin(obj.startTime)) * obj.multiplier;
27 |
28 | const seed = obj.position.x * repeteadIndex;
29 | const hue = (repeteadIndex / col ** 2) + rand(seed) * 0.15;
30 | const saturation = 0.5 + rand(seed) * 0.5;
31 | const lightness = 0.45 + rand(seed) * 0.1;
32 | obj.color = new Color().setHSL(hue, saturation, lightness);
33 | });
34 |
35 | this.on('animate', (e) => {
36 | const time = e.total * this.scaleSpeed;
37 | this.updateInstances((obj) => {
38 | obj.scale.y = Math.abs(Math.sin(obj.startTime + time)) * obj.multiplier;
39 | });
40 |
41 | this.position.z += e.delta * this.floorSpeed;
42 | if (this.position.z > col) {
43 | this.position.z %= col;
44 | }
45 | });
46 | }
47 |
48 | private easeInSine(x: number): number {
49 | return 1 - Math.cos((x * Math.PI) / 2);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/intro.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from "@three.ez/main";
2 | import { ACESFilmicToneMapping, Scene } from "three";
3 | import { Floor } from "./floor";
4 | import { Smoke } from "./smoke";
5 | import { SpaceShip } from "./spaceship";
6 |
7 | await Asset.preloadAllPending();
8 |
9 | const main = new Main({ showStats: false, rendererParameters: { canvas: document.getElementById("three-canvas") } });
10 | main.renderer.toneMapping = ACESFilmicToneMapping;
11 | main.renderer.toneMappingExposure = 0.5;
12 |
13 | const camera = new PerspectiveCameraAuto(50).translateY(8).rotateX(Math.PI / -4);
14 | const scene = new Scene();
15 | const spaceship = new SpaceShip();
16 | scene.add(spaceship, new Smoke(spaceship), new Floor());
17 |
18 | main.createView({ scene, camera, enabled: false });
19 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/random.ts:
--------------------------------------------------------------------------------
1 | export function rand(seed: number): number {
2 | seed = (seed + 0x6D2B79F5) | 0;
3 | let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
4 | t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
5 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
6 | }
7 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/smoke.ts:
--------------------------------------------------------------------------------
1 | import { createRadixSort, InstancedMesh2 } from "@three.ez/instanced-mesh";
2 | import { MeshLambertMaterial, OctahedronGeometry, Vector3 } from "three";
3 | import { type SpaceShip } from "./spaceship";
4 |
5 | export class Smoke extends InstancedMesh2<{ currentTime: number, dir: Vector3 }> {
6 | private readonly spawnPoints = [new Vector3(0.52, 0.75, -1), new Vector3(-0.52, 0.75, -1)];
7 | private readonly spawnTime = 0.005;
8 | private readonly lifeTime = 1;
9 | private readonly speed = 3;
10 | private readonly scaleMultiplier = 3;
11 | private readonly opacityMultiplier = 1;
12 | private readonly direction = new Vector3(0, 0.2, 1).normalize();
13 | private readonly dirDisplacement = 0.1;
14 | private time = 0;
15 |
16 | constructor(spaceship: SpaceShip) {
17 | const material = new MeshLambertMaterial({ emissive: 0x999999, transparent: true, depthWrite: false });
18 | super(new OctahedronGeometry(0.03, 1), material, { createEntities: true, capacity: 500 });
19 | this.frustumCulled = false;
20 |
21 | this.sortObjects = true;
22 | this.customSort = createRadixSort(this);
23 |
24 | this.on("animate", (e) => {
25 | this.updateParticles(e.delta);
26 | this.addParticles(spaceship, e.delta);
27 | });
28 | }
29 |
30 | private updateParticles(delta: number): void {
31 | this.updateInstances((obj) => {
32 | obj.currentTime += delta;
33 |
34 | if (obj.currentTime >= this.lifeTime) {
35 | obj.remove();
36 | return;
37 | }
38 |
39 | obj.position.addScaledVector(obj.dir, this.speed * delta);
40 | obj.scale.addScalar(this.scaleMultiplier * delta);
41 | obj.opacity -= delta * this.opacityMultiplier;
42 | });
43 | }
44 |
45 | private addParticles(spaceship: SpaceShip, delta: number): void {
46 | const dirDisplacement = this.dirDisplacement;
47 | const halfDirDisplacement = dirDisplacement / 2;
48 | this.time += delta;
49 |
50 | while (this.time >= this.spawnTime) {
51 | this.time -= this.spawnTime;
52 | if (this.time >= this.lifeTime) continue;
53 |
54 | this.addInstances(2, (obj, index) => {
55 | obj.currentTime = this.time;
56 | if (!obj.dir) {
57 | obj.dir = this.direction.clone();
58 | obj.dir.x += Math.random() * dirDisplacement - halfDirDisplacement;
59 | obj.dir.y += Math.random() * dirDisplacement - halfDirDisplacement;
60 | obj.dir.z += Math.random() * dirDisplacement - halfDirDisplacement;
61 | }
62 |
63 | obj.position.copy(this.spawnPoints[index % 2]);
64 | spaceship.localToWorld(obj.position);
65 |
66 | obj.position.addScaledVector(obj.dir, this.speed * obj.currentTime);
67 | obj.scale.addScalar(this.scaleMultiplier * obj.currentTime);
68 | obj.opacity = 1 - obj.currentTime * this.opacityMultiplier;
69 | });
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/docs/src/components/Intro/spaceship.ts:
--------------------------------------------------------------------------------
1 | import { Asset } from "@three.ez/main";
2 | import { BufferGeometry, Group, Mesh, MeshLambertMaterial, PointLight, Vector2, Vector3 } from "three";
3 | import { GLTFLoader, type GLTF } from "three/examples/jsm/Addons.js";
4 |
5 | const GLB_PATH = "/instanced-mesh/low_poly_space_ship.glb";
6 | Asset.preload(GLTFLoader, GLB_PATH);
7 |
8 | export class SpaceShip extends Group {
9 |
10 | constructor() {
11 | super();
12 | this.loadModel();
13 |
14 | const pointLight = new PointLight('white', 6, 12, 1).translateY(10);
15 | this.add(pointLight);
16 |
17 | this.position.set(0, 5, 20);
18 | this.rotation.y = Math.PI;
19 | this.scale.setScalar(0.15);
20 |
21 | this.bindInteraction();
22 | }
23 |
24 | private loadModel(): void {
25 | const gltf = Asset.get(GLB_PATH);
26 | const mesh = gltf.scene.querySelector("Mesh") as Mesh;
27 | mesh.material = new MeshLambertMaterial({ map: mesh.material.map });
28 | this.add(gltf.scene.children[0]);
29 | }
30 |
31 | private bindInteraction(): void {
32 | const pointer = new Vector2();
33 | const newPosition = new Vector3(0, 0, -2.5);
34 | const minPosition = new Vector3(-1, 0, -5);
35 | const maxPosition = new Vector3(1, 0, -1.5);
36 |
37 | window.addEventListener("pointermove", (e) => {
38 | pointer.x = e.clientX / window.innerWidth;
39 | pointer.y = e.clientY / window.innerHeight;
40 |
41 | newPosition.x = (pointer.x * (maxPosition.x - minPosition.x)) + minPosition.x;
42 | newPosition.z = (pointer.y * (maxPosition.z - minPosition.z)) + minPosition.z;
43 | });
44 |
45 |
46 | this.on("animate", (e) => {
47 | if (!newPosition) return;
48 |
49 | this.position.x += (newPosition.x - this.position.x) * e.delta * 5;
50 | this.position.z += (newPosition.z - this.position.z) * e.delta * 5;
51 |
52 | this.rotation.z = (newPosition.x - this.position.x) * e.delta * 20;
53 | this.rotation.x = (newPosition.z - this.position.z) * e.delta * 20;
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from 'astro:content';
2 | import { docsSchema } from '@astrojs/starlight/schema';
3 |
4 | export const collections = {
5 | docs: defineCollection({ schema: docsSchema() })
6 | };
7 |
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/buffer-capacity.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Buffer capacity
3 | ---
4 |
5 | WIP
6 |
7 | // reduce buffer capacity
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/lod.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: LOD
3 | ---
4 |
5 | WIP
6 |
7 | // specifica quante draw call
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/multimaterial.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Multi Material
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/patch-shader.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Patch Shader
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/shadow-lod.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Shadow LOD
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/skinning.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Skinning
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/advanced/texture-partial-update.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Texture Partial Update
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/00-add-remove.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Add and Remove Instances
3 | ---
4 |
5 | You can add instances without specifying a length: buffers will automatically expand as needed.
6 | To set a specific initial buffer capacity, set the [`capacity`](https://agargaro.github.io/instanced-mesh/api/interfaces/instancedmesh2params/#capacity)
7 | in the constructor parameters:
8 |
9 | ```ts
10 | const iMesh = new InstancedMesh2(geo, mat, { capacity: 10000 });
11 | ```
12 |
13 | ### Add Instances
14 |
15 | ```ts
16 | iMesh.addInstances(count, (obj, index) => {
17 | obj.position.x = index;
18 | obj.quaternion.random();
19 | });
20 | ```
21 |
22 | ### Remove Instances
23 |
24 | It's possible to remove instances by their IDs. When an instance is removed:
25 | - The instance is marked as inactive
26 | - Its ID is stored in a pool of available IDs
27 | - The slot is automatically reused when adding new instances
28 |
29 | ```ts
30 | iMesh.removeInstances(id0, id1, ...);
31 | ```
32 |
33 | ### Clear Instances
34 |
35 | ```ts
36 | iMesh.clearInstances();
37 | ```
38 |
39 | ### Example
40 |
41 | In this example, instances can be added and removed by clicking:
42 |
43 | :::note
44 | [`scene.on(eventName, callback)`](https://agargaro.github.io/three.ez/docs/api#-event-programming) is only available in the
45 | [`@three.ez/main`](https://github.com/agargaro/three.ez) package.
46 | :::
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/01-Instancedentity.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Create InstancedEntity
3 | ---
4 |
5 | Each instance can automatically create an associated [`InstancedEntity`](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity)
6 | (an object similar to [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D)).
7 | This provides a simple API to access and modify instance properties, as shown below:
8 |
9 | ```ts
10 | iMesh.instances[index].position.random();
11 | iMesh.instances[index].rotateX(Math.PI);
12 | iMesh.instances[index].updateMatrix(); // Required after transformations
13 | ```
14 |
15 | To enable entity creation, set the [`createEntities`](https://agargaro.github.io/instanced-mesh/api/interfaces/instancedmesh2params/#createentities)
16 | flag in the constructor parameters:
17 |
18 | ```ts
19 | const iMesh = new InstancedMesh2(geo, mat, { createEntities: true });
20 | ```
21 |
22 | :::note
23 | Check out the `InstancedEntity` API [here](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity).
24 | :::
25 |
26 | ### Example
27 |
28 |
29 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/02-animation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Animate Instances
3 | ---
4 |
5 | ### Animate one instance
6 |
7 | [If an array of instances has been created](https://agargaro.github.io/instanced-mesh/basics/00-Instancedentity/),
8 | it's possible to update an instance in this way:
9 |
10 |
11 | ```ts
12 | iMesh.instances[index].position.random();
13 | iMesh.instances[index].rotateX(Math.PI);
14 | iMesh.instances[index].updateMatrix(); // Required after transformations
15 | ```
16 |
17 | :::note
18 | After modifying `position`, `scale`, or `quaternion`, you must call the
19 | [`updateMatrix`](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity/#updatematrix) method to apply the changes.
20 | :::
21 |
22 | :::tip
23 | For better performance, if only `position` is updated, use the
24 | [`updateMatrixPosition`](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity/#updatematrixposition) method instead.
25 | :::
26 |
27 | ### Animate multiple instances with for loop
28 |
29 | [If an array of instances has been created](https://agargaro.github.io/instanced-mesh/basics/00-Instancedentity/),
30 | it's possible to update an instance in this way:
31 |
32 | ```ts
33 | for (const instance of iMesh.instances) {
34 | if (instance.active) { // if isn't removed
35 | instance.position.x += 0.1;
36 | instance.updateMatrixPosition();
37 | }
38 | }
39 | ```
40 |
41 | ### Animate all instances using `updateInstances`
42 |
43 | **This is the recommended method.**
44 | This method iterates over all active instances and automatically calls the `updateMatrix` method.
45 |
46 | ```ts
47 | this.updateInstances((obj) => {
48 | instance.scale.x += 0.1;
49 | });
50 | ```
51 |
52 | :::tip
53 | For better performance, if only `position` is updated, use the
54 | [`updateInstancesPosition`](https://agargaro.github.io/instanced-mesh/api/classes/instancedmesh2/#updateinstancesposition) method instead.
55 | :::
56 |
57 | :::caution
58 | [If an array of instances](https://agargaro.github.io/instanced-mesh/basics/00-Instancedentity/) has **not** been created,
59 | the `position`, `scale` and `quaternion` of the instances will not be maintained and thus reset in the callback.
60 | :::
61 |
62 | ### Example
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/03-euler.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Use Rotation Property
3 | ---
4 |
5 | By default, rotation in [`InstancedEntity`](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity) is handled using
6 | [`quaternion`](https://threejs.org/docs/#api/en/core/Object3D.quaternion), while the
7 | [`rotation`](https://threejs.org/docs/#api/en/core/Object3D.rotation) property
8 | (of type [`Euler`](https://threejs.org/docs/?q=euler#api/en/math/Euler)) is disabled.
9 |
10 | This design choice avoids the computational overhead of keeping `quaternion` and `rotation` synchronized, as both representations must be
11 | updated simultaneously for consistency.
12 |
13 | ### Enabling the `rotation` Property
14 |
15 | If you prefer to work with `Euler` angles instead of quaternions, you can enable the `rotation` property by setting the
16 | [`allowsEuler`](https://agargaro.github.io/instanced-mesh/api/interfaces/instancedmesh2params/#allowseuler) flag in the constructor:
17 |
18 | ```ts
19 | const iMesh = new InstancedMesh2(geo, mat, { createEntities: true, allowsEuler: true });
20 | ```
21 |
22 | Enabling this option allows you to modify rotation using `Euler` angles but comes with a minor performance trade-off due to the additional
23 | synchronization required.
24 |
25 | ### Example
26 |
27 |
28 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/04-tween.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Tween
3 | ---
4 |
5 | [If an array of instances has been created](https://agargaro.github.io/instanced-mesh/basics/00-Instancedentity/),
6 | you can apply tween animations to properties such as `position`, `scale`, `quaternion`, and `rotation`.
7 |
8 | :::caution
9 | The `color` property **cannot** be tweened because it is a getter that retrieves values from an internal array.
10 | :::
11 |
12 | ## Example
13 |
14 | :::note
15 | This example leverages the [`@three.ez/main`](https://github.com/agargaro/three.ez) package for tween animations,
16 | but you can use any tweening library of your choice!
17 | :::
18 |
19 |
20 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/05-custom-data.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Add Custom Data
3 | ---
4 |
5 | import { Tabs, TabItem } from '@astrojs/starlight/components';
6 |
7 | [If an array of instances has been created](https://agargaro.github.io/instanced-mesh/basics/00-Instancedentity/),
8 | you can assign custom data to each instance:
9 |
10 |
11 |
12 | ```ts
13 | type CustomData = { uuid: string };
14 |
15 | const iMesh = new InstancedMesh2(geo, mat, { createEntities: true });
16 |
17 | iMesh.addInstances(count, (obj, index) => {
18 | obj.uuid = MathUtils.generateUUID();
19 | });
20 | ```
21 |
22 |
23 |
24 | ```js
25 | const iMesh = new InstancedMesh2(geo, mat, { createEntities: true });
26 |
27 | iMesh.addInstances(count, (obj, index) => {
28 | obj.uuid = MathUtils.generateUUID();
29 | });
30 | ```
31 |
32 |
33 |
34 | ### Example
35 |
36 |
37 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/06-frustum-culling.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Frustum Culling
3 | ---
4 |
5 | With frustum culling, instances outside the camera's frustum are not rendered.
6 | This saves GPU resources by using the CPU to calculate the indices to render.
7 |
8 | ```ts
9 | iMesh.perObjectFrustumCulled = true; // default is true
10 | ```
11 |
12 | ### How It Works
13 |
14 | Frustum culling is performed every frame in two ways:
15 | - **Linear (default):** Iterates through all instances, checking if their boundingSphere is inside the camera's frustum.
16 | **Best for dynamic scenarios**.
17 | - **BVH:** [If a BVH is built](https://agargaro.github.io/instanced-mesh/basics/09-bvh-build/),
18 | its nodes are recursely iterated. If a node is outside the camera's frustum, the node and all its children are discarded.
19 | **Best for mostly static scenarios**.
20 |
21 | :::tip
22 | Linear frustum culling works better if the geometry is centered.
23 | :::
24 |
25 | ### When Not to Use It
26 |
27 | Sometimes, frustum culling can be more costly than beneficial, so it's better to skip it if:
28 |
29 | - Most instances are always within the camera's frustum.
30 | - The geometry is too simple (e.g., cubes, blades of grass, etc.).
31 |
32 | ### Disable Autoupdate
33 |
34 | It's possible to disable the automatic computing of frustum culling and sorting before each rendering in this way:
35 |
36 | ```ts
37 | iMesh.autoUpdate = false;
38 |
39 | // compute frustum culling and sorting manually
40 | iMesh.performFrustumCulling(camera);
41 | ```
42 |
43 | ### `OnFrustumEnter` Callback
44 |
45 | When frustum culling is performed, the [`onFrustumEnter`](https://agargaro.github.io/instanced-mesh/api/classes/instancedmesh2/#onfrustumenter)
46 | callback is called for each instance in the camera frustum.
47 | This callback is very useful for animating only the bones of visible instances.
48 |
49 | **If the callback returns true, the instance will be rendered**.
50 |
51 | ```ts
52 | iMesh.onFrustumEnter = (index, camera) => {
53 | // render only if not too far away
54 | return iMesh.getPositionAt(index).distanceTo(camera.position) <= maxDistance;
55 | };
56 | ```
57 |
58 | :::caution
59 | Do not update `position`, `scale`, or `quaternion` in this callback, otherwise frustum culling will not work properly.
60 | :::
61 |
62 | ### Example
63 |
64 |
65 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/07-sorting.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Sorting instances
3 | ---
4 |
5 | Sorting instances is useful to:
6 | - avoid wasting GPU power due to overdraw
7 | - handle transparent object rendering
8 |
9 | ```ts
10 | iMesh.sortObjects = true; // default is false
11 | ```
12 |
13 | ### Custom Sort
14 |
15 | It's possible to set a custom sort and use **radix sort** usually faster than default sort.
16 |
17 | ```ts
18 | iMesh.customSort = createRadixSort(iMesh);
19 | ```
20 |
21 | ### Disable Autoupdate
22 |
23 | It's possible to disable the automatic computing of frustum culling and sorting before each rendering in this way:
24 |
25 | ```ts
26 | iMesh.autoUpdate = false;
27 |
28 | // compute frustum culling and sorting manually
29 | iMesh.performFrustumCulling(camera);
30 | ```
31 |
32 | ### Example
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/08-raycasting.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Raycasting
3 | ---
4 |
5 | ### How It Works
6 |
7 | Raycasting is performed in two ways:
8 | - **Linear (default):** Iterates through all instances, calling [`raycast`](https://threejs.org/docs/#api/en/core/Object3D.raycast)
9 | for each instance.
10 | **Best for dynamic scenarios**.
11 | - **BVH:** [If a BVH is built](https://agargaro.github.io/instanced-mesh/basics/09-bvh-build/),
12 | its nodes are recursely iterated. If a node isn't intersected by the ray, the node and all its children are discarded.
13 | **Best for mostly static scenarios**.
14 |
15 | :::tip
16 | If complex geometry is used, it's recommended to use [`three-mesh-bvh`](https://github.com/gkjohnson/three-mesh-bvh)
17 | to acelerate the raycasting of a mesh, creating a BVH containing the triangles.
18 | :::
19 |
20 | ### `raycastOnlyFrustum` Property
21 |
22 | Using the linear approach, you can iterate only the instances rendered in the previous frame, in this way:
23 |
24 | ```ts
25 | iMesh.raycastOnlyFrustum = true;
26 | ```
27 |
28 | :::caution
29 | If a BVH has been built, this property will be ignored.
30 | :::
31 |
32 | ### Example
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/09-bvh-build.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Build a BVH
3 | ---
4 |
5 | WIP
6 |
7 | // dynamic and static scenes
--------------------------------------------------------------------------------
/docs/src/content/docs/basics/10-uniform-per-instance.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Uniforms per Instance
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/00-introduction.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Introduction
3 | ---
4 |
5 | `InstancedMesh2` is an enhanced alternative to [`InstancedMesh`](https://threejs.org/docs/#api/en/objects/InstancedMesh),
6 | offering improved performance and usability with features such as:
7 |
8 | - **Per-instance frustum culling** – *Skips rendering for out-of-view instances.*
9 | - **Sorting** – *Reduces overdraw and efficiently manages transparent objects.*
10 | - **Spatial indexing (dynamic BVH)** – *Speeds up raycasting and frustum culling.*
11 | - **Dynamic capacity** – *Seamlessly add or remove instances.*
12 | - **Per-instance visibility and opacity** – *Individually toggle visibility and set opacity.*
13 | - **Object3D-like instances** – *Instances behave like [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D), supporting transforms and custom data.*
14 | - **Per-instance uniforms** – *Assign unique shader data to individual instances.*
15 | - **Level of Detail (LOD)** – *Dynamically adjust instance detail based on distance.*
16 | - **Shadow LOD** – *Optimize shadow rendering by reducing detail for distant instances.*
17 | - **Skinning** – *Apply skeletal animations for complex and dynamic movements.*
18 |
19 | :::note
20 | Currently, `InstancedMesh2` only works with [`WebGLRenderer`](https://threejs.org/docs/#api/en/renderers/WebGLRenderer)
21 | and requires [`three.js r159+`](https://github.com/mrdoob/three.js).
22 | :::
23 |
24 | ### Differences from `InstancedMesh`
25 |
26 | ##### `InstancedMesh`
27 | - Uses an [`InstancedBufferAttribute`](https://threejs.org/docs/#api/en/core/InstancedBufferAttribute) to store instance matrices and data.
28 | - Renders instances sequentially, meaning all instances are processed in the order they were added, without skipping or reordering.
29 | This can be inefficient when dealing with a large number of instances, especially if many are off-screen or require different sorting orders for transparency.
30 |
31 | ##### `InstancedMesh2`
32 | - Uses [`SquareDataTexture`](https://agargaro.github.io/instanced-mesh/api/classes/squaredatatexture/)
33 | (an extended version of [`DataTexture`](https://threejs.org/docs/#api/en/textures/DataTexture) supporting partial updates)
34 | to store instance matrices and data.
35 | - Uses an `InstancedBufferAttribute` to manage the indexes of instances to be rendered, allowing for selective rendering, efficient culling,
36 | and sorting before sending data to the GPU.
37 |
38 | :::note
39 | Due to this additional indirection, rendering ***all*** instances is slightly slower.
40 | However, the benefits of efficient culling and sorting often outweigh the minor overhead.
41 | If none of the extra features are needed, using `InstancedMesh` is recommended.
42 | :::
43 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/01-installation.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | ---
4 |
5 | import { Tabs, TabItem } from '@astrojs/starlight/components';
6 |
7 | ### Install via Package Manager
8 |
9 | Choose your preferred package manager to install `@three.ez/instanced-mesh`:
10 |
11 |
12 |
13 | ```sh
14 | npm install @three.ez/instanced-mesh
15 | ```
16 |
17 |
18 |
19 | ```sh
20 | pnpm add @three.ez/instanced-mesh
21 | ```
22 |
23 |
24 |
25 | ```sh
26 | yarn add @three.ez/instanced-mesh
27 | ```
28 |
29 |
30 |
31 | ### Install via CDN
32 |
33 | Include the following `importmap` in your HTML file to load `@three.ez/instanced-mesh` directly from a CDN:
34 |
35 | ```html
36 |
46 | ```
47 |
--------------------------------------------------------------------------------
/docs/src/content/docs/getting-started/02-first-instancedmesh2.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Your First InstancedMesh2
3 | ---
4 |
5 | ### Initializing `InstancedMesh2`
6 |
7 | Start by initializing an `InstancedMesh2`:
8 |
9 | ```ts
10 | const iMesh = new InstancedMesh2(geometry, material);
11 | ```
12 |
13 | ### Adding and Positioning Instances
14 |
15 | Next, add instances and set their positions:
16 |
17 | ```ts
18 | iMesh.addInstances(count, (obj, index) => {
19 | obj.position.x = index;
20 | });
21 | ```
22 |
23 | :::note
24 | The `obj` used in the callback is an [`InstancedEntity`](https://agargaro.github.io/instanced-mesh/api/classes/instancedentity),
25 | an object similar to [`Object3D`](https://threejs.org/docs/#api/en/core/Object3D), allowing transformations and custom data.
26 | :::
27 |
28 | ### Example
29 |
30 | :::note
31 | All examples use the [`@three.ez/main`](https://github.com/agargaro/three.ez) package to simplify application development.
32 | However, `InstancedMesh2` is fully compatible with other libraries.
33 | :::
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Welcome to InstancedMesh2 Doc
3 | description: InstancedMesh2 is an enhanced InstancedMesh with frustum culling, fast raycasting (using a BVH), sorting, visibility, LOD, skinning and more.
4 | template: splash
5 | hero:
6 | tagline: Enhanced InstancedMesh with frustum culling, fast raycasting (using a BVH), sorting, visibility, LOD, skinning and more.
7 | actions:
8 | - text: Tutorial
9 | link: /instanced-mesh/getting-started/00-introduction/
10 | icon: right-arrow
11 | - text: API
12 | link: /instanced-mesh/api/readme
13 | icon: right-arrow
14 | - text: ""
15 | link: https://github.com/agargaro/instanced-mesh
16 | icon: github
17 | ---
18 |
19 | import Intro from '../../components/Intro/Intro.astro';
20 |
21 |
22 |
--------------------------------------------------------------------------------
/docs/src/content/docs/more/faq.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: FAQ
3 | ---
4 |
5 | WIP
6 |
7 | // When not to use?
8 | // why don't instances update?
9 | // why do they suddenly disappear?
10 | // don't use instancedBufferAttribute.
11 | // updateInstances doesn't update instances correctly. Are you using createEntities? // otherwise you have no persistent state.
12 | // when I start from 0 instances and add instances later, raycasting doesn't work. Use BVH or watch out for the boundingbox
13 |
--------------------------------------------------------------------------------
/docs/src/content/docs/more/know-issue.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Know Issue
3 | ---
4 |
5 | WIP
6 |
--------------------------------------------------------------------------------
/docs/src/content/docs/more/performance-tips.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Performance Tips
3 | ---
4 |
5 | WIP
6 |
7 | // use sort if you have overdraw or transparency
8 | // dont use bvh if no instances added or if instances are not valorized in callback
--------------------------------------------------------------------------------
/docs/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/docs/src/examples/add-remove/index.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { Main, PerspectiveCameraAuto, Utils } from '@three.ez/main';
3 | import { AmbientLight, BoxGeometry, DirectionalLight, MeshStandardMaterial, Scene } from 'three';
4 |
5 | const main = new Main();
6 | const scene = new Scene();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 |
9 | const boxes = new InstancedMesh2(new BoxGeometry(), new MeshStandardMaterial());
10 | boxes.computeBVH();
11 |
12 | scene.on('click', (e) => {
13 | if (e.target === boxes) {
14 | boxes.removeInstances(e.intersection.instanceId);
15 | } else {
16 | const position = Utils.getSceneIntersection(main.raycaster.ray, camera, camera.position.z)
17 |
18 | boxes.addInstances(1, (obj) => {
19 | obj.position.copy(position);
20 | obj.quaternion.random();
21 | obj.color = Math.random() * 0xffffff;
22 | });
23 | }
24 | });
25 |
26 | scene.on('keydown', (e) => {
27 | if (e.code === 'Enter') boxes.clearInstances();
28 | });
29 |
30 | scene.add(boxes);
31 |
32 | const ambientLight = new AmbientLight('white', 0.8);
33 | const dirLight = new DirectionalLight('white', 2);
34 | dirLight.position.set(0.5, 0.866, 0);
35 | camera.add(ambientLight, dirLight);
36 |
37 | main.createView({ scene, camera, backgroundColor: 0x222222 });
38 |
--------------------------------------------------------------------------------
/docs/src/examples/animation/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2, type InstancedMesh2Params as Params } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | const geo = new TorusKnotGeometry();
5 | const mat = new MeshStandardMaterial()
6 | const options: Params = { createEntities: true };
7 | export const torusKnots = new InstancedMesh2(geo, mat, options);
8 |
9 | torusKnots.addInstances(9, (obj, index) => {
10 | obj.position.x = (index % 3 - 1) * 5;
11 | obj.position.y = (Math.trunc(index / 3) - 1) * 5;
12 | obj.quaternion.random();
13 | obj.color = Math.random() * 0xffffff;
14 | });
15 |
16 | torusKnots.on('animate', (e) => {
17 | torusKnots.updateInstances((instance) => {
18 | instance.rotateZ(e.delta);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/docs/src/examples/animation/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/euler/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2, type InstancedMesh2Params as Params } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | const geo = new TorusKnotGeometry();
5 | const mat = new MeshStandardMaterial()
6 | const options: Params = { createEntities: true, allowsEuler: true };
7 | export const torusKnots = new InstancedMesh2(geo, mat, options);
8 |
9 | torusKnots.addInstances(9, (obj, index) => {
10 | obj.position.x = (index % 3 - 1) * 5;
11 | obj.position.y = (Math.trunc(index / 3) - 1) * 5;
12 | obj.quaternion.random();
13 | obj.color = Math.random() * 0xffffff;
14 | });
15 |
16 | torusKnots.on('animate', (e) => {
17 | torusKnots.updateInstances((instance) => {
18 | instance.rotation.z += e.delta;
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/docs/src/examples/euler/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/first/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | const geo = new TorusKnotGeometry();
5 | const mat = new MeshStandardMaterial()
6 | export const torusKnots = new InstancedMesh2(geo, mat);
7 |
8 | torusKnots.addInstances(9, (obj, index) => {
9 | obj.position.x = (index % 3 - 1) * 5;
10 | obj.position.y = (Math.trunc(index / 3) - 1) * 5;
11 | obj.quaternion.random();
12 | obj.color = Math.random() * 0xffffff;
13 | });
14 |
--------------------------------------------------------------------------------
/docs/src/examples/first/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/frustum-culling/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | const geo = new TorusKnotGeometry();
5 | const mat = new MeshStandardMaterial()
6 | export const torusKnots = new InstancedMesh2(geo, mat);
7 |
8 | torusKnots.perObjectFrustumCulled = true; // default is true
9 |
10 | torusKnots.onFrustumEnter = (index, camera) => {
11 | // render only if not too far away
12 | return torusKnots.getPositionAt(index).distanceTo(camera.position) <= 25;
13 | };
14 |
15 | torusKnots.addInstances(25, (obj, index) => {
16 | obj.position.x = (index % 5 - 2) * 5;
17 | obj.position.y = (Math.trunc(index / 5) - 2) * 5;
18 | obj.quaternion.random();
19 | obj.color = Math.random() * 0xffffff;
20 | });
21 |
--------------------------------------------------------------------------------
/docs/src/examples/frustum-culling/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/instances-array/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | const geo = new TorusKnotGeometry();
5 | const mat = new MeshStandardMaterial()
6 | export const torusKnots = new InstancedMesh2(geo, mat, { createEntities: true });
7 |
8 | torusKnots.addInstances(9, (obj, index) => {
9 | obj.position.x = (index % 3 - 1) * 5;
10 | obj.position.y = (Math.trunc(index / 3) - 1) * 5;
11 | obj.quaternion.random();
12 | });
13 |
14 | torusKnots.instances[2].color = 'red';
15 | torusKnots.instances[4].color = 'green';
16 | torusKnots.instances[6].color = 'blue';
17 |
--------------------------------------------------------------------------------
/docs/src/examples/instances-array/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/instances-custom-data/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, TorusKnotGeometry } from 'three';
3 |
4 | type CustomData = { startScale: number };
5 |
6 | const geo = new TorusKnotGeometry();
7 | const mat = new MeshStandardMaterial()
8 | export const torusKnots = new InstancedMesh2(geo, mat, { createEntities: true });
9 |
10 | torusKnots.addInstances(9, (obj, index) => {
11 | obj.position.x = (index % 3 - 1) * 5;
12 | obj.position.y = (Math.trunc(index / 3) - 1) * 5;
13 | obj.quaternion.random();
14 | obj.color = Math.random() * 0xffffff;
15 |
16 | obj.startScale = Math.random();
17 | obj.scale.setScalar(obj.startScale);
18 | });
19 |
20 | torusKnots.on('animate', (e) => {
21 | torusKnots.updateInstances((instance) => {
22 | instance.rotateZ(e.delta);
23 | instance.scale.setScalar(Math.abs(Math.sin(instance.startScale + e.total)));
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/docs/src/examples/instances-custom-data/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { torusKnots } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(torusKnots);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | const ambientLight = new AmbientLight('white', 0.8);
15 | scene.add(ambientLight);
16 |
17 | const dirLight = new DirectionalLight('white', 2);
18 | dirLight.position.set(0.5, 0.866, 0);
19 | camera.add(dirLight);
20 |
--------------------------------------------------------------------------------
/docs/src/examples/raycasting/index.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { AmbientLight, DirectionalLight, type Intersection, MeshLambertMaterial, Scene, SphereGeometry } from 'three';
4 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene();
9 | main.createView({ scene, camera });
10 |
11 | const spheres = new InstancedMesh2(new SphereGeometry(1, 8, 4), new MeshLambertMaterial());
12 |
13 | spheres.addInstances(50000, (obj, index) => {
14 | obj.position.set(Math.random() * 200 - 100, Math.random() * 200 - 100, Math.random() * 100 - 50);
15 | obj.color = Math.random() * 0xffffff;
16 | });
17 |
18 | spheres.computeBVH();
19 |
20 | const intersections: Intersection[] = [];
21 | spheres.on('animate', () => {
22 | intersections.length = 0;
23 | spheres.raycast(main.raycaster, intersections);
24 |
25 | if (intersections.length > 0) {
26 | spheres.removeInstances(intersections[0].instanceId);
27 | }
28 | });
29 |
30 | // Ignore this, it's from three.ez/main package to avoid auto raycasting.
31 | spheres.interceptByRaycaster = false;
32 | scene.add(spheres);
33 |
34 | const controls = new OrbitControls(camera, main.renderer.domElement);
35 | controls.update();
36 |
37 | const ambientLight = new AmbientLight('white', 0.8);
38 | const dirLight = new DirectionalLight('white', 2);
39 | dirLight.position.set(0.5, 0.866, 0);
40 | camera.add(dirLight, ambientLight);
41 |
--------------------------------------------------------------------------------
/docs/src/examples/sorting/app.ts:
--------------------------------------------------------------------------------
1 | import { createRadixSort, InstancedMesh2 } from '@three.ez/instanced-mesh';
2 | import { MeshStandardMaterial, SphereGeometry } from 'three';
3 |
4 | const geo = new SphereGeometry();
5 | const mat = new MeshStandardMaterial({ transparent: true, depthWrite: false })
6 | export const spheres = new InstancedMesh2(geo, mat);
7 |
8 | spheres.sortObjects = true; // default is false
9 | spheres.customSort = createRadixSort(spheres);
10 |
11 | spheres.addInstances(50, (obj, index) => {
12 | obj.position.random().multiplyScalar(15).subScalar(7.5);
13 | obj.color = Math.random() * 0xffffff;
14 | obj.opacity = 0.4;
15 | });
16 |
--------------------------------------------------------------------------------
/docs/src/examples/sorting/index.ts:
--------------------------------------------------------------------------------
1 | import { Scene, DirectionalLight, AmbientLight } from 'three';
2 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
3 | import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
4 | import { spheres } from './app.js';
5 |
6 | const main = new Main();
7 | const camera = new PerspectiveCameraAuto().translateZ(20);
8 | const scene = new Scene().add(spheres);
9 | main.createView({ scene, camera });
10 |
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.autoRotate = true;
13 | controls.autoRotateSpeed = 10;
14 |
15 | scene.on('animate', (e) => {
16 | controls.update(e.delta);
17 | });
18 |
19 | const ambientLight = new AmbientLight('white', 0.8);
20 | scene.add(ambientLight);
21 |
22 | const dirLight = new DirectionalLight('white', 2);
23 | dirLight.position.set(0.5, 0.866, 0);
24 | camera.add(dirLight);
25 |
--------------------------------------------------------------------------------
/docs/src/examples/tween/app.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2, type InstancedMesh2Params as Params } from '@three.ez/instanced-mesh';
2 | import { Tween } from '@three.ez/main';
3 | import { Color, Euler, MeshBasicMaterial, PlaneGeometry } from 'three';
4 |
5 | const geo = new PlaneGeometry();
6 | const mat = new MeshBasicMaterial()
7 | const options: Params = { createEntities: true, allowsEuler: true };
8 | export const planes = new InstancedMesh2(geo, mat, options);
9 |
10 | planes.addInstances(20, (obj, index) => {
11 | obj.position.z -= index;
12 | obj.scale.setScalar(2 + index * 2);
13 | obj.color = new Color().setHSL(0, 0, index % 2 === 1 ? index / 19 : (19 - index) / 19);
14 |
15 | const rotation = new Euler(0, 0, (Math.PI / 2) * (19 - index));
16 |
17 | new Tween(obj)
18 | .to(4000, { rotation }, { easing: 'easeInOutCubic', onUpdate: obj.updateMatrix })
19 | .yoyoForever()
20 | .start();
21 | });
22 |
--------------------------------------------------------------------------------
/docs/src/examples/tween/index.ts:
--------------------------------------------------------------------------------
1 | import { Main, OrthographicCameraAuto } from '@three.ez/main';
2 | import { Scene } from 'three';
3 | import { planes } from './app.js';
4 |
5 | const main = new Main();
6 | const camera = new OrthographicCameraAuto(70, false).translateZ(2);
7 | const scene = new Scene().add(planes);
8 | main.createView({ scene, camera, backgroundColor: 0x222222 });
9 |
--------------------------------------------------------------------------------
/docs/src/pages/examples/Loader.astro:
--------------------------------------------------------------------------------
1 | ---
2 | // Define the Props interface
3 | interface Props {
4 | slug: string;
5 | }
6 |
7 | // Destructure the fileName prop from Astro.props
8 | const isDev = import.meta.env.DEV;
9 | const { slug } = Astro.props as Props;
10 | const src = isDev ? `/src/examples/${slug}/index.ts` : `index.js`;
11 | ---
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/docs/src/pages/examples/[...slug].astro:
--------------------------------------------------------------------------------
1 | ---
2 | import Loader from './Loader.astro';
3 |
4 | export async function getStaticPaths() {
5 | const examples = import.meta.glob('../../examples/**/index.ts');
6 | const keys = Object.keys(examples) as string[];
7 | const slugs = keys.map((key) => key.replace('../../examples/', '').replace('/index.ts', ''));
8 |
9 | return slugs.map((slug) => {
10 | return {
11 | params: { slug },
12 | };
13 | });
14 | }
15 |
16 | const slug = Astro.params.slug;
17 | ---
18 |
19 |
20 |
21 |
22 |
33 |
34 |
35 |
36 |
37 |
38 |
53 |
--------------------------------------------------------------------------------
/docs/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "strict": false,
7 | "noImplicitOverride": true,
8 | "strictNullChecks": false,
9 | "stripInternal": true,
10 | "outDir": "public/examples",
11 | "noImplicitAny": false,
12 | "noUnusedLocals": false,
13 | "skipLibCheck": true,
14 | },
15 | "include": [
16 | "src/examples/**/*.ts"
17 | ],
18 | }
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict",
3 | "compilerOptions": {
4 | "strictNullChecks": false,
5 | },
6 | "exclude": [
7 | "public/examples",
8 | "src/examples",
9 | "src/components/Example/stackblitz-files",
10 | "src/components/Example/stackblitz-template.ts",
11 | "dist"
12 | ]
13 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js';
2 | import tseslint from 'typescript-eslint';
3 | import stylistic from '@stylistic/eslint-plugin';
4 |
5 | export default [
6 | js.configs.recommended,
7 | ...tseslint.configs.strict,
8 | stylistic.configs.customize({
9 | jsx: false,
10 | semi: true,
11 | commaDangle: 'never',
12 | arrowParens: true,
13 | braceStyle: '1tbs',
14 | blockSpacing: true,
15 | indent: 2,
16 | quoteProps: 'as-needed',
17 | quotes: 'single'
18 | }),
19 | {
20 | ignores: ['dist', 'docs']
21 | },
22 | {
23 | rules: {
24 | 'no-unused-vars': 'off',
25 | 'no-undef': 'off',
26 | 'prefer-rest-params': 'off',
27 | '@typescript-eslint/no-explicit-any': 'off',
28 | '@typescript-eslint/no-unused-vars': 'off',
29 | '@typescript-eslint/no-empty-object-type': 'off',
30 | '@typescript-eslint/no-wrapper-object-types': 'off',
31 | '@typescript-eslint/no-dynamic-delete': 'off',
32 | '@typescript-eslint/no-invalid-void-type': 'off',
33 | '@typescript-eslint/no-this-alias': 'off',
34 | '@typescript-eslint/explicit-function-return-type': [
35 | 'error', {
36 | allowExpressions: true
37 | }
38 | ]
39 | }
40 | }
41 | ];
42 |
--------------------------------------------------------------------------------
/examples/LOD.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AmbientLight, Color, DirectionalLight, MeshLambertMaterial, Scene, SphereGeometry } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 | import { PRNG } from './objects/random.js';
6 |
7 | const spawnRange = 10000;
8 |
9 | const random = new PRNG(10000);
10 | const camera = new PerspectiveCameraAuto().translateZ(100).translateY(20);
11 | const scene = new Scene();
12 | const main = new Main();
13 | main.createView({ scene, camera });
14 | const controls = new OrbitControls(camera, main.renderer.domElement);
15 | controls.update();
16 |
17 | const instancedMeshLOD = new InstancedMesh2(new SphereGeometry(5, 30, 15), new MeshLambertMaterial(), { capacity: 1000000 });
18 |
19 | instancedMeshLOD.addLOD(new SphereGeometry(5, 20, 10), new MeshLambertMaterial(), 50);
20 | instancedMeshLOD.addLOD(new SphereGeometry(5, 10, 5), new MeshLambertMaterial(), 500);
21 | instancedMeshLOD.addLOD(new SphereGeometry(5, 5, 3), new MeshLambertMaterial(), 1000);
22 |
23 | instancedMeshLOD.addInstances(1000000, (object, index) => {
24 | object.position.x = random.range(-spawnRange, spawnRange);
25 | object.position.z = random.range(-spawnRange, spawnRange);
26 | object.color = 'white';
27 | });
28 |
29 | instancedMeshLOD.computeBVH();
30 |
31 | const white = new Color('white');
32 | instancedMeshLOD.on('pointermove', (e) => {
33 | const id = e.intersection.instanceId;
34 | if (instancedMeshLOD.getColorAt(id).equals(white)) {
35 | instancedMeshLOD.setColorAt(id, Math.random() * 0xffffff);
36 | }
37 | });
38 |
39 | scene.add(instancedMeshLOD, new AmbientLight(), new DirectionalLight().translateZ(3.5));
40 |
--------------------------------------------------------------------------------
/examples/createEntities.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { BufferGeometry, MeshNormalMaterial, Scene, SphereGeometry, Vector3 } from 'three';
3 | import { FlyControls } from 'three/examples/jsm/Addons.js';
4 | import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
5 | import { InstancedMesh2 } from '../src/index.js';
6 |
7 | const count = 50000;
8 | const spawnSize = 2000;
9 | const rand = Math.random;
10 |
11 | const main = new Main();
12 | const scene = new Scene();
13 | const camera = new PerspectiveCameraAuto();
14 | const controls = new FlyControls(camera, main.renderer.domElement);
15 | controls.rollSpeed = 0.2;
16 | controls.movementSpeed = 50;
17 | scene.on('animate', (e) => controls.update(e.delta));
18 |
19 | const spheres = new InstancedMesh2<{ dir: Vector3 }, BufferGeometry, MeshNormalMaterial>(new SphereGeometry(1, 32, 16), new MeshNormalMaterial(), { capacity: count, createEntities: true });
20 | spheres.addLOD(new SphereGeometry(1, 16, 8), spheres.material, 50);
21 | spheres.addLOD(new SphereGeometry(1, 8, 4), spheres.material, 200);
22 | spheres.addLOD(new SphereGeometry(1, 4, 2), spheres.material, 500);
23 |
24 | spheres.addInstances(count, (obj, index) => {
25 | obj.position.setX(rand() * spawnSize).setY(rand() * spawnSize).setZ(rand() * spawnSize).subScalar(spawnSize / 2);
26 | obj.dir = new Vector3().randomDirection();
27 | });
28 |
29 | spheres.on('animate', (e) => {
30 | spheres.updateInstancesPosition((mesh) => {
31 | mesh.position.add(mesh.dir.setLength((e.delta || 0.01) * 10));
32 | });
33 | });
34 |
35 | scene.add(spheres);
36 |
37 | const gui = new GUI();
38 | gui.add(spheres, 'instancesCount').name('instances total').disable();
39 | const spheresCount1 = gui.add(spheres.LODinfo.objects[0], 'count').name('instances rendered LOD 1').disable();
40 | const spheresCount2 = gui.add(spheres.LODinfo.objects[1], 'count').name('instances rendered LOD 2').disable();
41 | const spheresCount3 = gui.add(spheres.LODinfo.objects[2], 'count').name('instances rendered LOD 3').disable();
42 | const spheresCount4 = gui.add(spheres.LODinfo.objects[3], 'count').name('instances rendered LOD 4').disable();
43 | gui.add(camera, 'far', 100, 4000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix());
44 |
45 | main.createView({
46 | scene, camera, enabled: false, onAfterRender: () => {
47 | spheresCount1.updateDisplay();
48 | spheresCount2.updateDisplay();
49 | spheresCount3.updateDisplay();
50 | spheresCount4.updateDisplay();
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/examples/customMaterial.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { createNoise2D } from 'simplex-noise';
3 | import { AmbientLight, BoxGeometry, Color, DirectionalLight, NearestFilter, NearestMipMapLinearFilter, PlaneGeometry, Scene, Texture, TextureLoader, Vector2 } from 'three';
4 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
5 | import { InstancedMesh2 } from '../src/index.js';
6 | import { TileLambertMaterial } from './objects/tileLambertMaterial.js';
7 |
8 | const noise2D = createNoise2D();
9 | const main = new Main({ rendererParameters: { antialias: false } });
10 | const camera = new PerspectiveCameraAuto();
11 | const scene = new Scene();
12 | scene.continuousRaycasting = true;
13 | main.createView({ scene, camera, backgroundColor: 0xbdddf1 });
14 |
15 | const controls = new OrbitControls(camera, main.renderer.domElement);
16 | camera.position.set(0, 15, 0);
17 | controls.target.set(30, 0, -30);
18 | controls.autoRotate = true;
19 | scene.on('animate', () => controls.update());
20 |
21 | const map = await Asset.load(TextureLoader, 'texture.png');
22 | map.magFilter = NearestFilter;
23 | map.minFilter = NearestMipMapLinearFilter;
24 |
25 | const dirLight = new DirectionalLight('white', 2);
26 | dirLight.position.set(0.5, 0.866, 0);
27 | const ambientLight = new AmbientLight('white', 0.8);
28 |
29 | const side = 256;
30 | const count = side ** 2;
31 |
32 | const cubeOffset = {
33 | grass: new Vector2(0, 15), stone: new Vector2(1, 15), snow: new Vector2(2, 11),
34 | plant: new Vector2(14, 10), flower1: new Vector2(12, 15), flower2: new Vector2(13, 15)
35 | };
36 |
37 | const boxes = new InstancedMesh2(new BoxGeometry(), new TileLambertMaterial(32, 32, { map }), { capacity: count });
38 | const plants = new InstancedMesh2(new PlaneGeometry(), new TileLambertMaterial(32, 32, { map, alphaTest: 0.9 }));
39 |
40 | scene.add(boxes, plants, ambientLight, dirLight);
41 |
42 | boxes.initUniformsPerInstance({ fragment: { offset: 'vec2' } });
43 | plants.initUniformsPerInstance({ fragment: { offset: 'vec2' } });
44 |
45 | boxes.addInstances(count, (box, index) => {
46 | box.color = 'white';
47 | box.position.x = (index % side) - side / 2;
48 | box.position.z = Math.floor(index / side) - side / 2;
49 | const noiseY = noise2D(box.position.x / 150, box.position.z / 150);
50 | box.position.y = Math.floor(noiseY * 20);
51 |
52 | const noiseOffset = box.position.y + noise2D(box.position.x / 50, box.position.y / 50) * 2;
53 | const boxOffset = noiseOffset > 10 ? cubeOffset.snow : (noiseOffset > -5 ? cubeOffset.stone : cubeOffset.grass);
54 | box.setUniform('offset', boxOffset);
55 |
56 | if (boxOffset === cubeOffset.grass && Math.random() <= 0.1) {
57 | const rand = Math.random();
58 | const plantOffset = rand > 0.5 ? cubeOffset.plant : rand > 0.25 ? cubeOffset.flower1 : cubeOffset.flower2;
59 |
60 | plants.addInstances(4, (plant, index) => { // this is just a demo, is not the best way
61 | plant.position.copy(box.position);
62 | plant.position.y += 1;
63 | plant.rotateY(Math.PI / 2 * index);
64 | plant.setUniform('offset', plantOffset);
65 | });
66 | }
67 | });
68 |
69 | boxes.computeBVH();
70 | plants.computeBVH();
71 |
72 | plants.interceptByRaycaster = false;
73 |
74 | const white = new Color('white');
75 | boxes.on('pointerintersection', (e) => {
76 | const id = e.intersection.instanceId;
77 | if (boxes.getColorAt(id).equals(white)) {
78 | boxes.setColorAt(id, Math.random() * 0xffffff);
79 | }
80 | });
81 |
--------------------------------------------------------------------------------
/examples/customMaterialTextureArray.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '../src/index.js';
2 |
3 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
4 | import { DataArrayTexture, LinearFilter, LinearMipMapLinearFilter, Scene, ShaderMaterial, SphereGeometry, Texture, TextureLoader } from 'three';
5 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
6 |
7 | export class TextureArrayMaterial extends ShaderMaterial {
8 | public dataTextureArray: DataArrayTexture;
9 |
10 | public override vertexShader = /* glsl */`
11 | #include
12 | #include
13 | varying vec2 vUv;
14 |
15 | void main() {
16 | #include
17 | #include
18 | vUv = uv;
19 | gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
20 | }`;
21 |
22 | public override fragmentShader = /* glsl */`
23 | uniform float textureIndex;
24 | uniform sampler2DArray textureArray;
25 | varying vec2 vUv;
26 |
27 | void main() {
28 | vec4 texelColor = texture(textureArray, vec3(vUv, textureIndex));
29 | gl_FragColor = texelColor;
30 | }`;
31 |
32 | constructor(textureArray: Texture[]) {
33 | super();
34 | const maxWidth = Math.max(...textureArray.map((v) => v.image.width));
35 | const maxHeight = Math.max(...textureArray.map((v) => v.image.height));
36 | this.dataTextureArray = this.createDataArrayTexture(textureArray, maxWidth, maxHeight);
37 | this.uniforms.textureArray = { value: this.dataTextureArray };
38 | }
39 |
40 | protected createDataArrayTexture(textures: Texture[], width: number, height: number): DataArrayTexture {
41 | const textureCount = textures.length;
42 | const canvas = document.createElement('canvas');
43 | canvas.width = width;
44 | canvas.height = height;
45 | const ctx = canvas.getContext('2d', { willReadFrequently: true });
46 | const pixels = new Uint8Array(width * height * 4 * textureCount);
47 |
48 | for (let i = 0; i < textureCount; i++) {
49 | ctx.clearRect(0, 0, width, height);
50 | ctx.drawImage(textures[i].image, 0, 0, width, height);
51 | const imgData = ctx.getImageData(0, 0, width, height);
52 | pixels.set(imgData.data, i * width * height * 4);
53 | }
54 |
55 | const dataArrayTex = new DataArrayTexture(pixels, width, height, textureCount);
56 | dataArrayTex.minFilter = LinearMipMapLinearFilter;
57 | dataArrayTex.magFilter = LinearFilter;
58 | dataArrayTex.generateMipmaps = true;
59 | dataArrayTex.needsUpdate = true;
60 |
61 | return dataArrayTex;
62 | }
63 | }
64 |
65 | const textures = [
66 | await Asset.load(TextureLoader, 'planks.jpg'),
67 | await Asset.load(TextureLoader, 'wall.jpg'),
68 | await Asset.load(TextureLoader, 'pattern.jpg')
69 | ];
70 |
71 | const instancedMesh = new InstancedMesh2(new SphereGeometry(0.25, 16, 16), new TextureArrayMaterial(textures));
72 | instancedMesh.initUniformsPerInstance({ fragment: { textureIndex: 'float' } });
73 |
74 | instancedMesh.addInstances(1000, (obj, index) => {
75 | obj.position.copy(obj.position.random().subScalar(0.5).multiplyScalar(20));
76 | obj.setUniform('textureIndex', Math.round(Math.random() * (textures.length - 1)));
77 | });
78 |
79 | const scene = new Scene().add(instancedMesh);
80 | const main = new Main();
81 | const camera = new PerspectiveCameraAuto(70).translateZ(10);
82 | const controls = new OrbitControls(camera, main.renderer.domElement);
83 | controls.update();
84 | main.createView({ scene, camera });
85 |
--------------------------------------------------------------------------------
/examples/dynamicBVH.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { DirectionalLight, MeshLambertMaterial, OctahedronGeometry, Scene, SpotLight, Vector3 } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
5 | import { InstancedMesh2 } from '../src/index.js';
6 | import { PRNG } from './objects/random.js';
7 |
8 | const config = {
9 | count: 50000,
10 | animatedCount: 50000,
11 | spawnRadius: 3000,
12 | marginBVH: 75
13 | };
14 |
15 | const random = new PRNG(config.count);
16 | const main = new Main();
17 | const camera = new PerspectiveCameraAuto(70, 0.1, config.spawnRadius / 2).translateZ(10);
18 | const scene = new Scene();
19 |
20 | scene.continuousRaycasting = true;
21 |
22 | const geometry = new OctahedronGeometry(1, 2);
23 | const material = new MeshLambertMaterial({ flatShading: true });
24 | const instancedMesh = new InstancedMesh2<{ dir: Vector3 }>(geometry, material, { capacity: config.count, createEntities: true });
25 |
26 | instancedMesh.addInstances(config.count, (object) => {
27 | object.dir = new Vector3().randomDirection();
28 | object.position.randomDirection().multiplyScalar(random.range(0.05, 1) * config.spawnRadius);
29 | object.scale.multiplyScalar(random.range(1, 5));
30 | });
31 |
32 | instancedMesh.computeBVH({ margin: config.marginBVH, getBBoxFromBSphere: true });
33 |
34 | instancedMesh.on('click', (e) => {
35 | instancedMesh.instances[e.intersection.instanceId].visible = false;
36 | });
37 |
38 | instancedMesh.cursor = 'pointer';
39 |
40 | const dirLight = new DirectionalLight('white', 0.1);
41 | const spotLight = new SpotLight('white', 3000, 0, Math.PI / 6, 0.5, 1.4);
42 | camera.add(dirLight, spotLight);
43 |
44 | scene.add(instancedMesh, dirLight.target, spotLight.target);
45 |
46 | scene.on('animate', (e) => {
47 | controls.update(e.delta);
48 |
49 | camera.getWorldDirection(spotLight.target.position).multiplyScalar(100).add(camera.position);
50 | camera.getWorldDirection(dirLight.target.position).multiplyScalar(100).add(camera.position);
51 |
52 | if (e.delta === 0) return;
53 |
54 | const speed = e.delta * 10;
55 | for (let i = 0; i < config.animatedCount; i++) {
56 | const mesh = instancedMesh.instances[i];
57 | mesh.position.add(mesh.dir.setLength(speed));
58 | mesh.updateMatrixPosition();
59 | }
60 | });
61 |
62 | const controls = new OrbitControls(camera, main.renderer.domElement);
63 | controls.panSpeed = 100;
64 |
65 | main.createView({ scene, camera, onAfterRender: () => spheresCount.updateDisplay() });
66 |
67 | const gui = new GUI();
68 | gui.add(instancedMesh, 'capacity').name('instances max count').disable();
69 | const spheresCount = gui.add(instancedMesh, 'count').name('instances rendered').disable();
70 | gui.add(config, 'animatedCount', 0, 50000).name('instances animated');
71 | gui.add(camera, 'far', 100, config.spawnRadius, 20).name('camera far').onChange(() => camera.updateProjectionMatrix());
72 |
--------------------------------------------------------------------------------
/examples/fastRaycasting.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AmbientLight, BoxGeometry, Color, DirectionalLight, MeshLambertMaterial, Scene } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
5 | import { InstancedMesh2 } from '../src/index.js';
6 | import { PRNG } from './objects/random.js';
7 |
8 | const config = { useBVH: true };
9 |
10 | const main = new Main();
11 | const random = new PRNG(150000);
12 | const white = new Color('white');
13 | const camera = new PerspectiveCameraAuto(70, 0.1, 30).translateZ(3);
14 | const scene = new Scene();
15 | scene.continuousRaycasting = true;
16 |
17 | const geometry = new BoxGeometry(0.1, 0.1, 0.1);
18 | const material = new MeshLambertMaterial();
19 | const instancedMesh = new InstancedMesh2(geometry, material);
20 |
21 | instancedMesh.addInstances(150000, (object, index) => {
22 | object.position.setFromSphericalCoords(random.range(0.5, 30), random.range(0, Math.PI * 2), random.range(0, Math.PI * 2));
23 | object.quaternion.random();
24 | object.color = 'white';
25 | });
26 |
27 | instancedMesh.on('pointerintersection', (e) => {
28 | const id = e.intersection.instanceId;
29 |
30 | if (instancedMesh.getColorAt(id).equals(white)) {
31 | instancedMesh.setColorAt(id, random.next() * 0xffffff);
32 | }
33 | });
34 |
35 | instancedMesh.raycastOnlyFrustum = true;
36 | instancedMesh.computeBVH();
37 |
38 | const dirLight = new DirectionalLight();
39 | camera.add(dirLight);
40 |
41 | scene.add(instancedMesh, new AmbientLight());
42 |
43 | const controls = new OrbitControls(camera, main.renderer.domElement);
44 | controls.autoRotate = true;
45 |
46 | scene.on('animate', (e) => controls.update());
47 |
48 | main.createView({ scene, camera, backgroundColor: 'white', onAfterRender: () => spheresCount.updateDisplay() });
49 |
50 | const bvh = instancedMesh.bvh;
51 |
52 | const gui = new GUI();
53 | gui.add(instancedMesh, 'capacity').disable();
54 | const spheresCount = gui.add(instancedMesh, 'count').name('instances rendered').disable();
55 | gui.add(config, 'useBVH').name('use BVH').onChange((value) => instancedMesh.bvh = value ? bvh : null);
56 | gui.add(instancedMesh, 'raycastOnlyFrustum').name('raycastOnlyFrustum (if no BVH)');
57 |
--------------------------------------------------------------------------------
/examples/instancedMeshParse.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { Color, IcosahedronGeometry, InstancedMesh, Matrix4, MeshBasicMaterial, Scene } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
4 | import { createInstancedMesh2From } from '../src/index.js';
5 |
6 | const camera = new PerspectiveCameraAuto().translateZ(1);
7 | const scene = new Scene();
8 | const main = new Main(); // init renderer and other stuff
9 | main.createView({ scene, camera });
10 | const controls = new OrbitControls(camera, main.renderer.domElement);
11 | controls.update();
12 |
13 | const amount = 20;
14 | const count = Math.pow(amount, 3);
15 | const matrix = new Matrix4();
16 | const color = new Color();
17 | const geometry = new IcosahedronGeometry(0.5, 3);
18 | const material = new MeshBasicMaterial();
19 | const im = new InstancedMesh(geometry, material, count);
20 |
21 | let i = 0;
22 | const offset = (amount - 1) / 2;
23 |
24 | for (let x = 0; x < amount; x++) {
25 | for (let y = 0; y < amount; y++) {
26 | for (let z = 0; z < amount; z++) {
27 | color.setHex(Math.random() * 0xffffff);
28 | matrix.setPosition(offset - x, offset - y, offset - z);
29 | im.setMatrixAt(i, matrix);
30 | im.setColorAt(i, color);
31 | i++;
32 | }
33 | }
34 | }
35 |
36 | scene.add(im);
37 |
38 | function parseToInstancedMesh2(): void {
39 | if (im.parent !== scene) return;
40 | im.removeFromParent();
41 | const im2 = createInstancedMesh2From(im);
42 | scene.add(im2);
43 | }
44 |
45 | setTimeout(parseToInstancedMesh2, 1000);
46 |
--------------------------------------------------------------------------------
/examples/morph.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AnimationMixer, DirectionalLight, Fog, HemisphereLight, Mesh, MeshStandardMaterial, PlaneGeometry, Scene } from 'three';
3 | import { GLTF, GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const count = 1024;
7 | const spawnSize = 10000;
8 | const timeOffsets = new Float32Array(count);
9 | for (let i = 0; i < count; i++) {
10 | timeOffsets[i] = Math.random() * 3;
11 | }
12 |
13 | const camera = new PerspectiveCameraAuto(50, 10, 20000);
14 | const scene = new Scene();
15 | const main = new Main();
16 | main.createView({ scene, camera, enabled: false, backgroundColor: 0x99DDFF });
17 |
18 | const hemi = new HemisphereLight(0x99DDFF, 0x669933, 1 / 3);
19 | const light = new DirectionalLight(0xffffff, 1);
20 | light.position.set(200, 1000, 50);
21 |
22 | const ground = new Mesh(new PlaneGeometry(1000000, 1000000), new MeshStandardMaterial({ color: 0x669933 }));
23 | ground.rotation.x = -Math.PI / 2;
24 |
25 | const glb = await Asset.load(GLTFLoader, 'https://threejs.org/examples/models/gltf/Horse.glb');
26 | const dummy = glb.scene.children[0] as Mesh;
27 |
28 | const horses = new InstancedMesh2(dummy.geometry, dummy.material, { capacity: count });
29 |
30 | horses.addInstances(count, (obj, index) => {
31 | obj.position.set(spawnSize * Math.random() - spawnSize / 2, 0, spawnSize * Math.random() - spawnSize / 2);
32 | obj.color = `hsl(${Math.random() * 360}, 50%, 66%)`;
33 | });
34 |
35 | const mixer = new AnimationMixer(glb.scene);
36 | const action = mixer.clipAction(glb.animations[0]);
37 | action.play();
38 |
39 | scene.on('animate', (e) => {
40 | const time = e.total * 2;
41 | const r = 3000;
42 | camera.position.set(Math.sin(time / 10) * r, 1500 + 1000 * Math.cos(time / 5), Math.cos(time / 10) * r);
43 | camera.lookAt(0, 0, 0);
44 |
45 | for (let i = 0; i < horses.instancesCount; i++) {
46 | mixer.setTime(time + timeOffsets[i]);
47 | horses.setMorphAt(i, dummy);
48 | }
49 | });
50 |
51 | scene.add(light, hemi, horses, ground);
52 | scene.fog = new Fog(0x99DDFF, 5000, 10000);
53 |
54 | // in più.. animare solo quelli nella view. capire lod e ombre
55 |
--------------------------------------------------------------------------------
/examples/multimaterial.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { BoxGeometry, MeshBasicMaterial, MeshNormalMaterial, Scene } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const camera = new PerspectiveCameraAuto().translateZ(10);
7 | const scene = new Scene();
8 | const main = new Main(); // init renderer and other stuff
9 | main.createView({ scene, camera, enabled: false });
10 | const controls = new OrbitControls(camera, main.renderer.domElement);
11 | controls.update();
12 |
13 | const materials = [
14 | new MeshBasicMaterial({ color: 'red' }),
15 | new MeshNormalMaterial(),
16 | new MeshNormalMaterial(),
17 | new MeshNormalMaterial(),
18 | new MeshNormalMaterial(),
19 | new MeshNormalMaterial()
20 | ];
21 | const boxes = new InstancedMesh2(new BoxGeometry(), materials);
22 | scene.add(boxes);
23 |
24 | boxes.addInstances(100000, (o) => {
25 | o.position.randomDirection().multiplyScalar(Math.random() * 1000 + 20);
26 | });
27 |
--------------------------------------------------------------------------------
/examples/multimaterial_tree.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AmbientLight, DirectionalLight, Material, Mesh, MeshStandardMaterial, PlaneGeometry, RepeatWrapping, Scene, Texture, TextureLoader } from 'three';
3 | import 'three-hex-tiling';
4 | import { GLTF, GLTFLoader, OrbitControls } from 'three/examples/jsm/Addons.js';
5 | import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
6 | import { InstancedMesh2 } from '../src/index.js';
7 |
8 | const camera = new PerspectiveCameraAuto().translateY(2).translateZ(10);
9 | const scene = new Scene();
10 | const main = new Main(); // init renderer and other stuff
11 | main.createView({ scene, camera, enabled: false, backgroundColor: 'skyblue' });
12 |
13 | const controls = new OrbitControls(camera, main.renderer.domElement);
14 | controls.maxPolarAngle = Math.PI / 2.2;
15 | controls.update();
16 |
17 | const ambientLight = new AmbientLight();
18 | const dirLight = new DirectionalLight('white', 3);
19 | scene.add(ambientLight, dirLight);
20 |
21 | const pineGltf = await Asset.load(GLTFLoader, 'pine.glb');
22 | const pineGroup = pineGltf.scene.children[0];
23 | const pineMergedGeo = mergeGeometries(pineGroup.children.map((x) => (x as Mesh).geometry), true);
24 |
25 | const pineLowGltf = await Asset.load(GLTFLoader, 'pine_low.glb');
26 | const pineLowGroup = pineLowGltf.scene.children[0];
27 | const pineLowMergedGeo = mergeGeometries(pineLowGroup.children.map((x) => (x as Mesh).geometry), true);
28 | // REMOVE THIS FIX AND FIX THE MODEL
29 | pineLowMergedGeo.scale(pineLowGroup.scale.x, pineLowGroup.scale.y, pineLowGroup.scale.z);
30 | pineLowMergedGeo.rotateX(pineLowGroup.rotation.x);
31 | pineLowMergedGeo.rotateY(pineLowGroup.rotation.y);
32 | pineLowMergedGeo.rotateZ(pineLowGroup.rotation.z);
33 | pineLowMergedGeo.translate(pineLowGroup.position.x, pineLowGroup.position.y, pineLowGroup.position.z);
34 |
35 | const trees = new InstancedMesh2(pineMergedGeo, pineGroup.children.map((x) => (x as Mesh).material as Material), { capacity: 2000 });
36 | trees.addLOD(pineLowMergedGeo, pineLowGroup.children.map((x) => (x as Mesh).material as Material), 10);
37 |
38 | scene.add(trees);
39 |
40 | trees.addInstances(2000, (obj, index) => {
41 | obj.position.x = Math.random() * 400 - 200;
42 | obj.position.z = Math.random() * 400 - 200;
43 | });
44 |
45 | trees.computeBVH();
46 |
47 | const grassNormalMap = await Asset.load(TextureLoader, 'grass_normal.jpg');
48 | grassNormalMap.wrapS = RepeatWrapping;
49 | grassNormalMap.wrapT = RepeatWrapping;
50 | grassNormalMap.repeat.set(500, 500);
51 |
52 | const grassMap = await Asset.load(TextureLoader, 'grass.jpg');
53 | grassMap.wrapS = RepeatWrapping;
54 | grassMap.wrapT = RepeatWrapping;
55 | grassMap.repeat.set(500, 500);
56 |
57 | const ground = new Mesh(new PlaneGeometry(1000, 1000, 10, 10), new MeshStandardMaterial({ color: 0xbbbbbb, map: grassMap, normalMap: grassNormalMap, hexTiling: {} }));
58 | ground.rotateX(-Math.PI / 2);
59 | scene.add(ground);
60 |
--------------------------------------------------------------------------------
/examples/objects/createSimplifiedGeometry.ts:
--------------------------------------------------------------------------------
1 | import { Flags, MeshoptSimplifier } from 'meshoptimizer';
2 | import { BufferGeometry } from 'three';
3 |
4 | export interface SimplifyParams {
5 | ratio: number;
6 | error: number;
7 | lockBorder?: boolean;
8 | errorAbsolute?: boolean;
9 | sparse?: boolean;
10 | prune?: boolean;
11 | }
12 |
13 | export async function createSimplifiedGeometry(geometry: BufferGeometry, params: SimplifyParams): Promise {
14 | await MeshoptSimplifier.ready;
15 | const simplifiedGeometry = geometry.clone();
16 | const srcIndexArray = simplifiedGeometry.index.array as Uint32Array;
17 | const srcPositionArray = simplifiedGeometry.attributes.position.array as Float32Array;
18 | const targetCount = 3 * Math.floor((params.ratio * srcIndexArray.length) / 3);
19 |
20 | const flags: Flags[] = [];
21 | if (params.lockBorder) flags.push('LockBorder');
22 | if (params.sparse) flags.push('Sparse');
23 | if (params.errorAbsolute) flags.push('ErrorAbsolute');
24 | if (params.prune) {
25 | flags.push('Prune');
26 | }
27 |
28 | const [dstIndexArray] = MeshoptSimplifier.simplify(
29 | srcIndexArray,
30 | srcPositionArray,
31 | 3,
32 | targetCount,
33 | params.error,
34 | flags
35 | );
36 |
37 | simplifiedGeometry.index.array.set(dstIndexArray);
38 | simplifiedGeometry.setDrawRange(0, dstIndexArray.length);
39 |
40 | return simplifiedGeometry;
41 | }
42 |
--------------------------------------------------------------------------------
/examples/objects/random.ts:
--------------------------------------------------------------------------------
1 | export class PRNG {
2 | protected _seed: number;
3 |
4 | constructor(seed: number) {
5 | this._seed = seed;
6 | }
7 |
8 | public next(): number {
9 | let t = (this._seed += 0x6d2b79f5);
10 | t = Math.imul(t ^ (t >>> 15), t | 1);
11 | t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
12 | return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
13 | }
14 |
15 | public range(min: number, max: number): number {
16 | return min + (max - min) * this.next();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/examples/objects/tileLambertMaterial.ts:
--------------------------------------------------------------------------------
1 | import { MeshLambertMaterial, MeshLambertMaterialParameters, Vector2, WebGLProgramParametersWithUniforms, WebGLRenderer } from 'three';
2 |
3 | export class TileLambertMaterial extends MeshLambertMaterial {
4 | constructor(protected tileSizeX: number, protected tileSizeY: number, parameters?: MeshLambertMaterialParameters) {
5 | super(parameters);
6 | }
7 |
8 | public override onBeforeCompile(p: WebGLProgramParametersWithUniforms, r: WebGLRenderer): void {
9 | p.uniforms.tileSize = { value: new Vector2(this.tileSizeX / this.map.image.width, this.tileSizeY / this.map.image.height) };
10 |
11 | p.fragmentShader = p.fragmentShader.replace('void main() {', /* glsl */`
12 | uniform vec2 offset;
13 | uniform vec2 tileSize;
14 |
15 | void main() {
16 | `);
17 |
18 | p.fragmentShader = p.fragmentShader.replace('#include ', /* glsl */`
19 | diffuseColor *= texture2D(map, vMapUv * tileSize + offset * tileSize);
20 | `);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/objects/tileMaterial.ts:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial, ShaderMaterialParameters, Texture, Vector2 } from 'three';
2 |
3 | export class TileMaterial extends ShaderMaterial {
4 | public override vertexShader = /* glsl */`
5 | #include
6 | varying vec2 vUv;
7 |
8 | void main() {
9 | #include
10 | vUv = uv;
11 | gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
12 | }`;
13 |
14 | public override fragmentShader = /* glsl */`
15 | uniform vec2 offset;
16 | uniform sampler2D map;
17 | uniform vec2 tileSize;
18 | varying vec2 vUv;
19 |
20 | void main() {
21 | vec4 color = texture2D(map, vUv * tileSize + offset * tileSize);
22 | if (color.a < .9) discard;
23 | gl_FragColor = color;
24 | }`;
25 |
26 | constructor(tilemap: Texture, tileSizeX: number, tileSizeY: number, parameters?: ShaderMaterialParameters) {
27 | super(parameters);
28 | this.uniforms.map = { value: tilemap };
29 | this.uniforms.tileSize = { value: new Vector2(tileSizeX / tilemap.image.width, tileSizeY / tilemap.image.height) };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/examples/overrideMaterial.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AmbientLight, BoxGeometry, Mesh, MeshBasicMaterial, MeshLambertMaterial, Scene } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const camera = new PerspectiveCameraAuto().translateZ(10);
7 | const scene = new Scene();
8 | const main = new Main(); // init renderer and other stuff
9 | main.createView({ scene, camera });
10 | const controls = new OrbitControls(camera, main.renderer.domElement);
11 | controls.update();
12 |
13 | const material = new MeshLambertMaterial();
14 | const material2 = new MeshLambertMaterial();
15 |
16 | const boxes = new InstancedMesh2(new BoxGeometry(), material, { capacity: 1 });
17 | const boxes2 = new InstancedMesh2(new BoxGeometry(), material2);
18 | const boxes3 = new InstancedMesh2(new BoxGeometry(), material2);
19 |
20 | boxes.addInstances(1, (obj, index) => {
21 | obj.position.x = index - 5;
22 | obj.position.y = -1.5;
23 | });
24 |
25 | boxes2.addInstances(5, (obj, index) => {
26 | obj.position.x = index - 5;
27 | obj.position.y = 1.5;
28 | });
29 |
30 | boxes3.addInstances(5, (obj, index) => {
31 | obj.position.x = index - 5;
32 | obj.position.y = 0;
33 | });
34 |
35 | const box = new Mesh(new BoxGeometry(), material).translateX(2).translateY(2);
36 | const box2 = new Mesh(new BoxGeometry(), material2).translateX(2).translateY(0.5);
37 | const box3 = new Mesh(new BoxGeometry(), material2).translateX(2).translateY(-1);
38 | const overrideMaterial = new MeshBasicMaterial();
39 |
40 | scene.add(box, boxes, box2, boxes2, boxes3, box3, new AmbientLight());
41 |
42 | setInterval(() => {
43 | scene.overrideMaterial = scene.overrideMaterial !== overrideMaterial ? overrideMaterial : null;
44 | boxes.addInstances(1, (obj, index) => {
45 | obj.position.x = index - 5;
46 | obj.position.y = -1.5;
47 | });
48 | }, 1000);
49 |
--------------------------------------------------------------------------------
/examples/remove-instance.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '../src/index.js';
2 | import { Main, OrthographicCameraAuto } from '@three.ez/main';
3 | import { BoxGeometry, MeshBasicMaterial, Scene } from 'three';
4 |
5 | const count = 5;
6 | const camera = new OrthographicCameraAuto(count).translateZ(10);
7 | const scene = new Scene();
8 | const main = new Main();
9 | main.createView({ scene, camera, backgroundColor: 'skyblue' });
10 |
11 | const boxes = new InstancedMesh2(new BoxGeometry(), new MeshBasicMaterial(), { capacity: count, createEntities: true });
12 | scene.add(boxes);
13 |
14 | boxes.addInstances(count, (obj, index) => obj.position.setX(index));
15 |
16 | setTimeout(() => {
17 | const instanceSecond = boxes.instances[1];
18 | instanceSecond.remove();
19 | }, 500);
20 |
21 | setTimeout(() => {
22 | const instanceThird = boxes.instances[2];
23 | instanceThird.remove();
24 | }, 1000);
25 |
--------------------------------------------------------------------------------
/examples/remove-instances.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '../src/index.js';
2 | import { Main, OrthographicCameraAuto } from '@three.ez/main';
3 | import { BoxGeometry, MeshBasicMaterial, Scene } from 'three';
4 |
5 | const count = 100;
6 |
7 | const camera = new OrthographicCameraAuto(count + count * 0.1).translateZ(10);
8 | const scene = new Scene();
9 | const main = new Main(); // init renderer and other stuff
10 | main.createView({ scene, camera, backgroundColor: 'skyblue' });
11 |
12 | const boxes = new InstancedMesh2(new BoxGeometry(), new MeshBasicMaterial(), { capacity: count });
13 | scene.add(boxes);
14 |
15 | boxes.addInstances(count, (obj, index) => obj.position.setX(index - count / 2));
16 |
17 | setTimeout(() => {
18 | boxes.removeInstances(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
19 | }, 200);
20 |
21 | setTimeout(() => {
22 | boxes.addInstances(5, (obj, index) => obj.position.setX(index - count / 2).setY(10));
23 | }, 400);
24 |
25 | setTimeout(() => {
26 | boxes.removeInstances(20, 21, 22, 23, 24, 25, 26, 27, 28, 29);
27 | }, 600);
28 |
29 | setTimeout(() => {
30 | boxes.addInstances(5, (obj, index) => obj.position.setX(index - count / 2).setY(10));
31 | }, 800);
32 |
33 | boxes.on('click', (e) => {
34 | boxes.removeInstances(e.intersection.instanceId);
35 | });
36 |
37 | boxes.computeBVH();
38 |
--------------------------------------------------------------------------------
/examples/shadowLOD.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AmbientLight, BoxGeometry, DirectionalLight, Mesh, MeshBasicMaterial, MeshNormalMaterial, MeshPhongMaterial, PlaneGeometry, Scene, SphereGeometry, TorusGeometry, TorusKnotGeometry } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const count = 200000;
7 | const terrainSize = 3000;
8 |
9 | const main = new Main(); // init renderer and other stuff
10 | main.renderer.shadowMap.enabled = true;
11 |
12 | const camera = new PerspectiveCameraAuto(50).translateX(100).translateY(100).translateZ(100);
13 | const scene = new Scene();
14 |
15 | const materials = [
16 | new MeshBasicMaterial({ color: 'red' }),
17 | new MeshNormalMaterial(),
18 | new MeshNormalMaterial(),
19 | new MeshNormalMaterial(),
20 | new MeshNormalMaterial(),
21 | new MeshNormalMaterial()
22 | ];
23 |
24 | const instancedMesh = new InstancedMesh2(new BoxGeometry(), materials, { capacity: count });
25 | instancedMesh.castShadow = true;
26 |
27 | instancedMesh.addLOD(new SphereGeometry(1, 8, 4), new MeshPhongMaterial({ color: 0x00e6e6 }), 100);
28 | instancedMesh.addShadowLOD(new BoxGeometry(2, 2, 2));
29 | instancedMesh.addShadowLOD(new TorusKnotGeometry(0.8, 0.2, 32, 8), 80);
30 | instancedMesh.addShadowLOD(new TorusGeometry(0.8, 0.2, 32, 8), 110);
31 |
32 | instancedMesh.addInstances(count, (obj, index) => {
33 | obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);
34 | });
35 |
36 | instancedMesh.computeBVH();
37 |
38 | const ground = new Mesh(new PlaneGeometry(terrainSize, terrainSize, 10, 10), new MeshPhongMaterial());
39 | ground.receiveShadow = true;
40 | ground.translateY(-1);
41 | ground.rotateX(Math.PI / -2);
42 |
43 | const dirLight = new DirectionalLight();
44 | dirLight.castShadow = true;
45 | dirLight.shadow.mapSize.set(2048, 2048);
46 | dirLight.shadow.camera.left = -100;
47 | dirLight.shadow.camera.right = 100;
48 | dirLight.shadow.camera.top = 100;
49 | dirLight.shadow.camera.bottom = -100;
50 | dirLight.shadow.camera.updateProjectionMatrix();
51 |
52 | dirLight.position.set(0, 30, 50);
53 |
54 | scene.on('animate', (e) => {
55 | controls.update();
56 | });
57 |
58 | scene.add(instancedMesh, ground, new AmbientLight(), dirLight);
59 |
60 | main.createView({ scene, camera, enabled: false, backgroundColor: 0xd3d3d3 });
61 |
62 | const controls = new OrbitControls(camera, main.renderer.domElement);
63 | controls.maxPolarAngle = Math.PI / 2.1;
64 | controls.minDistance = 10;
65 | controls.maxDistance = 100;
66 | controls.target.set(0, 0, 0);
67 | controls.update();
68 | controls.autoRotate = true;
69 |
--------------------------------------------------------------------------------
/examples/skeleton.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { AnimationMixer, BufferGeometry, DirectionalLight, Fog, HemisphereLight, Interpolant, Matrix4, Mesh, MeshStandardMaterial, PlaneGeometry, PropertyMixer, Scene, Vector3 } from 'three';
3 | import { GLTF, GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
4 | import { createSimplifiedGeometry } from './objects/createSimplifiedGeometry.js';
5 | import { createInstancedMesh2From } from '../src/index.js';
6 |
7 | const excludedBones = new Set([
8 | 'mixamorigLeftHand', 'mixamorigLeftHandThumb1', 'mixamorigLeftHandThumb2', 'mixamorigLeftHandThumb3',
9 | 'mixamorigLeftHandIndex1', 'mixamorigLeftHandIndex2', 'mixamorigLeftHandIndex3',
10 | 'mixamorigLeftHandMiddle1', 'mixamorigLeftHandMiddle2', 'mixamorigLeftHandMiddle3',
11 | 'mixamorigLeftHandRing1', 'mixamorigLeftHandRing2', 'mixamorigLeftHandRing3',
12 | 'mixamorigLeftHandPinky1', 'mixamorigLeftHandPinky2', 'mixamorigLeftHandPinky3',
13 | 'mixamorigRightHand', 'mixamorigRightHandThumb1', 'mixamorigRightHandThumb2', 'mixamorigRightHandThumb3',
14 | 'mixamorigRightHandIndex1', 'mixamorigRightHandIndex2', 'mixamorigRightHandIndex3',
15 | 'mixamorigRightHandMiddle1', 'mixamorigRightHandMiddle2', 'mixamorigRightHandMiddle3',
16 | 'mixamorigRightHandRing1', 'mixamorigRightHandRing2', 'mixamorigRightHandRing3',
17 | 'mixamorigRightHandPinky1', 'mixamorigRightHandPinky2', 'mixamorigRightHandPinky3',
18 | 'mixamorigLeftFoot', 'mixamorigLeftToeBase', 'mixamorigRightFoot', 'mixamorigRightToeBase'
19 | ]);
20 |
21 | const camera = new PerspectiveCameraAuto(50, 0.1, 100);
22 | const scene = new Scene();
23 | const main = new Main();
24 | main.createView({ scene, camera, enabled: false, backgroundColor: 0x99ddff });
25 |
26 | const glb = await Asset.load(GLTFLoader, 'https://threejs.org/examples/models/gltf/Soldier.glb');
27 | const soldierGroup = glb.scene.children[0];
28 | const soldierScale = soldierGroup.scale.x;
29 | const dummy = soldierGroup.children[0] as Mesh;
30 | soldierGroup.children[1].visible = false;
31 | dummy.removeFromParent();
32 |
33 | const mixer = new AnimationMixer(glb.scene);
34 | const action = mixer.clipAction(glb.animations[1]).play();
35 |
36 | // SIMPLIFY ACTION FOR LODs
37 | const propertyBindings = (action as any)._propertyBindings as PropertyMixer[];
38 | const interpolants = (action as any)._interpolants as Interpolant[];
39 | const propertyBindingsLOD: PropertyMixer[] = [];
40 | const interpolantsLOD: Interpolant[] = [];
41 |
42 | for (let i = 0; i < propertyBindings.length; i++) {
43 | const boneName = propertyBindings[i].binding.node.name as string;
44 |
45 | if (!excludedBones.has(boneName)) {
46 | propertyBindingsLOD.push(propertyBindings[i]);
47 | interpolantsLOD.push(interpolants[i]);
48 | }
49 | }
50 |
51 | const geometry = dummy.geometry;
52 | dummy.geometry = await createSimplifiedGeometry(geometry, { ratio: 0.1, error: 1, lockBorder: true });
53 |
54 | // CREATE INSTANCEDMESH2 AND LODS
55 | const count = 3000;
56 | const soldiers = createInstancedMesh2From<{ time: number; speed: number; offset: number }>(dummy, { capacity: count, createEntities: true });
57 | soldiers.boneTexture.partialUpdate = false;
58 |
59 | soldiers.addLOD(await createSimplifiedGeometry(geometry, { ratio: 0.07, error: 1 }), dummy.material.clone(), (1 / soldierScale) * 10);
60 | soldiers.addLOD(await createSimplifiedGeometry(geometry, { ratio: 0.05, error: 1 }), dummy.material.clone(), (1 / soldierScale) * 30);
61 | soldiers.addLOD(await createSimplifiedGeometry(geometry, { ratio: 0.03, error: 1 }), dummy.material.clone(), (1 / soldierScale) * 50);
62 | soldiers.addLOD(await createSimplifiedGeometry(geometry, { ratio: 0.02, error: 1, prune: true }), dummy.material.clone(), (1 / soldierScale) * 70);
63 |
64 | // ADD INSTANCES
65 | soldiers.addInstances(count, (obj, index) => {
66 | obj.position.set(Math.random() * 100 - 50, Math.random() * -200 + 100, 0).divideScalar(soldierScale);
67 | obj.color = `hsl(${Math.random() * 360}, 50%, 75%)`;
68 | obj.time = 0;
69 | obj.offset = Math.random() * 5;
70 | obj.speed = Math.random() * 0.5 + 1;
71 | });
72 |
73 | // INIT SKELETON DATA
74 | for (const soldier of soldiers.instances) {
75 | mixer.setTime(soldier.offset);
76 | soldier.updateBones();
77 | }
78 |
79 | // ANIMATE INSTANCES
80 | let delta = 0;
81 | let total = 0;
82 | const radiusMovement = 15;
83 | const invMatrixWorld = new Matrix4();
84 | const cameraLocalPosition = new Vector3();
85 | scene.on('animate', (e) => {
86 | delta = e.delta;
87 | total = e.total;
88 | const time = e.total * 2 + 30;
89 | camera.position.set(Math.sin(time / 10) * radiusMovement, 3 + Math.cos(time / 5), Math.cos(time / 10) * radiusMovement);
90 | camera.lookAt(0, 0, 0);
91 | camera.updateMatrixWorld();
92 |
93 | invMatrixWorld.copy(soldiers.matrixWorld).invert();
94 | cameraLocalPosition.setFromMatrixPosition(camera.matrixWorld).applyMatrix4(invMatrixWorld);
95 | });
96 |
97 | // UPDATE ONLY INSTANCES INSIDE FRUSTUM SETTINGS FPS BASED ON CAMERA DISTANCE
98 | const maxFps = 60;
99 | const minFps = 5;
100 | soldiers.onFrustumEnter = (index, camera, cameraLOD, LODindex) => {
101 | const soldier = soldiers.instances[index];
102 | const cameraDistance = cameraLocalPosition.distanceTo(soldier.position) * soldierScale;
103 | const fps = Math.min(maxFps, Math.max(minFps, 70 - cameraDistance));
104 | soldier.time += delta;
105 |
106 | if (soldier.time >= 1 / fps) {
107 | soldier.time %= 1 / fps;
108 |
109 | if (LODindex === 0) {
110 | (mixer as any)._bindings = propertyBindings;
111 | (mixer as any)._nActiveBindings = propertyBindings.length;
112 | (action as any)._propertyBindings = propertyBindings;
113 | (action as any)._interpolants = interpolants;
114 | mixer.setTime(total * soldier.speed + soldier.offset);
115 | soldier.updateBones();
116 | } else {
117 | // use simplified action
118 | (mixer as any)._bindings = propertyBindingsLOD;
119 | (mixer as any)._nActiveBindings = propertyBindingsLOD.length;
120 | (action as any)._propertyBindings = propertyBindingsLOD;
121 | (action as any)._interpolants = interpolantsLOD;
122 | mixer.setTime(total * soldier.speed + soldier.offset);
123 | soldier.updateBones(true, excludedBones);
124 | }
125 | }
126 |
127 | return true;
128 | };
129 |
130 | const hemi = new HemisphereLight(0x99ddff, 0x669933, 5);
131 | const dirLight = new DirectionalLight('white', 5);
132 | const ground = new Mesh(new PlaneGeometry(200, 200), new MeshStandardMaterial({ color: 0x082000 }));
133 | ground.rotation.x = -Math.PI / 2;
134 |
135 | soldierGroup.add(soldiers);
136 | scene.add(hemi, dirLight, glb.scene, ground);
137 | scene.fog = new Fog(0x99ddff, 90, 100);
138 |
--------------------------------------------------------------------------------
/examples/sorting.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { MeshNormalMaterial, Scene, SphereGeometry } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
4 | import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
5 | import { createRadixSort, InstancedMesh2 } from '../src/index.js';
6 |
7 | const config = {
8 | count: 20000,
9 | customSort: true
10 | };
11 |
12 | const main = new Main();
13 | const camera = new PerspectiveCameraAuto().translateZ(10);
14 | const scene = new Scene();
15 |
16 | const material = new MeshNormalMaterial({ transparent: true, opacity: 0.5, depthWrite: false });
17 | const instancedMesh = new InstancedMesh2(new SphereGeometry(1, 16, 8), material, { capacity: config.count, createEntities: true });
18 |
19 | instancedMesh.addInstances(config.count, (object) => {
20 | object.position.random().multiplyScalar(100).subScalar(50);
21 | });
22 |
23 | instancedMesh.sortObjects = true;
24 | const radixSort = createRadixSort(instancedMesh);
25 | instancedMesh.customSort = radixSort;
26 |
27 | scene.add(instancedMesh);
28 |
29 | const controls = new OrbitControls(camera, main.renderer.domElement);
30 | controls.autoRotate = true;
31 |
32 | scene.on('animate', (e) => controls.update());
33 |
34 | main.createView({ scene, camera, enabled: false, backgroundColor: 'white', onAfterRender: () => spheresCount.updateDisplay() });
35 |
36 | const gui = new GUI();
37 | gui.add(instancedMesh, 'capacity').name('instances capacity').disable();
38 | const spheresCount = gui.add(instancedMesh, 'count').name('instances rendered').disable();
39 | gui.add(instancedMesh, 'perObjectFrustumCulled');
40 | gui.add(instancedMesh, 'sortObjects');
41 | gui.add(instancedMesh.material, 'opacity', 0, 1).onChange((v) => {
42 | instancedMesh.material.transparent = v < 1;
43 | instancedMesh.material.depthWrite = v === 1;
44 | instancedMesh.material.opacity = v;
45 | instancedMesh.material.needsUpdate = true;
46 | });
47 | gui.add(config, 'customSort').name('custom sort').onChange((v) => {
48 | instancedMesh.customSort = v ? radixSort : null;
49 | });
50 |
--------------------------------------------------------------------------------
/examples/template.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { BoxGeometry, MeshNormalMaterial, Scene } from 'three';
3 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const camera = new PerspectiveCameraAuto().translateZ(10);
7 | const scene = new Scene();
8 | const main = new Main(); // init renderer and other stuff
9 | main.createView({ scene, camera });
10 | const controls = new OrbitControls(camera, main.renderer.domElement);
11 | controls.update();
12 |
13 | const boxes = new InstancedMesh2(new BoxGeometry(), new MeshNormalMaterial());
14 | scene.add(boxes);
15 |
16 | boxes.addInstances(100, (obj, index) => {
17 | obj.position.randomDirection().multiplyScalar(Math.random() * 5);
18 | });
19 |
--------------------------------------------------------------------------------
/examples/test.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { BoxGeometry, MeshNormalMaterial, Scene } from 'three';
3 | import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
4 | import { OrbitControls } from 'three/examples/jsm/Addons.js';
5 | import { InstancedMesh2 } from '../src/index.js';
6 |
7 | const maxCount = 10000000;
8 | const main = new Main(); // init renderer and other stuff
9 | const camera = new PerspectiveCameraAuto(50, 0.1, 5000).translateZ(200);
10 | const scene = new Scene();
11 | const controls = new OrbitControls(camera, main.renderer.domElement);
12 | controls.update();
13 |
14 | main.createView({
15 | scene, camera, onAfterRender: () => {
16 | capacity.updateDisplay();
17 | instancesCount.updateDisplay();
18 | renderedCount.updateDisplay();
19 | }
20 | });
21 |
22 | const boxes = new InstancedMesh2(new BoxGeometry(), new MeshNormalMaterial());
23 | boxes.on('click', (e) => boxes.setVisibilityAt(e.intersection.instanceId, false));
24 | boxes.computeBVH({ getBBoxFromBSphere: true });
25 | scene.add(boxes);
26 |
27 | const event = boxes.on('animate', () => {
28 | boxes.addInstances(625, (obj, index) => {
29 | obj.position.randomDirection().multiplyScalar(Math.random() * 1000000 + 200);
30 | obj.scale.random().multiplyScalar(Math.random() * 10 + 5);
31 | obj.quaternion.random();
32 | });
33 |
34 | if (boxes.instancesCount >= maxCount) boxes.off('animate', event);
35 | });
36 |
37 | const gui = new GUI();
38 | const capacity = gui.add(boxes, 'capacity').name('capacity').disable();
39 | const instancesCount = gui.add(boxes, 'instancesCount').name('instances count').disable();
40 | const renderedCount = gui.add(boxes, 'count').name('instances rendered').disable();
41 |
--------------------------------------------------------------------------------
/examples/trees.ts:
--------------------------------------------------------------------------------
1 | import { Asset, Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { ACESFilmicToneMapping, AmbientLight, BoxGeometry, BufferGeometry, DirectionalLight, FogExp2, Mesh, MeshLambertMaterial, MeshStandardMaterial, PCFSoftShadowMap, PlaneGeometry, Scene, Vector3 } from 'three';
3 | import { MapControls } from 'three/examples/jsm/controls/MapControls.js';
4 | import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
5 | import { GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
6 | import { Sky } from 'three/examples/jsm/objects/Sky.js';
7 | import { InstancedMesh2 } from '../src/index.js';
8 |
9 | const count = 1000000;
10 | const terrainSize = 20000;
11 |
12 | const main = new Main(); // init renderer and other stuff
13 | main.renderer.toneMapping = ACESFilmicToneMapping;
14 | main.renderer.toneMappingExposure = 0.5;
15 | main.renderer.shadowMap.enabled = true;
16 | main.renderer.shadowMap.type = PCFSoftShadowMap;
17 |
18 | const camera = new PerspectiveCameraAuto(50, 0.1, 1000).translateY(1);
19 | const scene = new Scene();
20 |
21 | const treeGLTF = (await Asset.load(GLTFLoader, 'tree.glb')).scene.children[0] as Mesh;
22 |
23 | const trees = new InstancedMesh2(treeGLTF.geometry, treeGLTF.material, { capacity: count });
24 | trees.castShadow = true;
25 | trees.cursor = 'pointer';
26 |
27 | trees.addLOD(new BoxGeometry(100, 1000, 100), new MeshLambertMaterial({ color: 'darkgreen' }), 1000);
28 | trees.addShadowLOD(trees.geometry);
29 | trees.addShadowLOD(new BoxGeometry(100, 1000, 100), 200);
30 |
31 | trees.addInstances(count, (obj) => {
32 | obj.position.setX(Math.random() * terrainSize - terrainSize / 2).setZ(Math.random() * terrainSize - terrainSize / 2);
33 | obj.scale.setScalar(Math.random() * 0.01 + 0.01);
34 | obj.rotateY(Math.random() * Math.PI * 2).rotateZ(Math.random() * 0.3 - 0.15);
35 | });
36 |
37 | trees.computeBVH();
38 |
39 | trees.on('click', (e) => {
40 | trees.instances[e.intersection.instanceId].visible = false;
41 | });
42 |
43 | const ground = new Mesh(new PlaneGeometry(terrainSize, terrainSize, 10, 10), new MeshLambertMaterial({ color: 0x004622 }));
44 | ground.interceptByRaycaster = false;
45 | ground.receiveShadow = true;
46 | ground.rotateX(Math.PI / -2);
47 |
48 | const sun = new Vector3();
49 | const sky = new Sky();
50 | sky.scale.setScalar(450000);
51 | const uniforms = sky.material.uniforms;
52 | uniforms['turbidity'].value = 5;
53 | uniforms['rayleigh'].value = 2;
54 |
55 | sky.on('animate', (e) => {
56 | sun.setFromSphericalCoords(1, Math.PI / -1.9 + e.total * 0.02, Math.PI / 1.4);
57 | uniforms['sunPosition'].value.copy(sun);
58 | });
59 |
60 | scene.fog = new FogExp2('white', 0.0004);
61 | scene.on('animate', (e) => scene.fog.color.setHSL(0, 0, sun.y));
62 |
63 | const dirLight = new DirectionalLight();
64 | dirLight.castShadow = true;
65 | dirLight.shadow.mapSize.set(2048, 2048);
66 | dirLight.shadow.camera.left = -200;
67 | dirLight.shadow.camera.right = 200;
68 | dirLight.shadow.camera.top = 200;
69 | dirLight.shadow.camera.bottom = -200;
70 | dirLight.shadow.camera.far = 2000;
71 | dirLight.shadow.camera.updateProjectionMatrix();
72 |
73 | const sunOffset = new Vector3();
74 | dirLight.on('animate', (e) => {
75 | dirLight.intensity = sun.y > 0.05 ? 10 : Math.max(0, (sun.y / 0.05) * 10);
76 | sunOffset.copy(sun).multiplyScalar(1000);
77 | dirLight.position.copy(camera.position).add(sunOffset);
78 | dirLight.target.position.copy(camera.position).sub(sunOffset);
79 | });
80 |
81 | scene.add(sky, trees, ground, new AmbientLight(), dirLight, dirLight.target);
82 |
83 | main.createView({
84 | scene, camera, enabled: false, onAfterRender: () => {
85 | treeCount.updateDisplay();
86 | treeLODCount.updateDisplay();
87 | }
88 | });
89 |
90 | const controls = new MapControls(camera, main.renderer.domElement);
91 | controls.maxPolarAngle = Math.PI / 2.1;
92 | controls.minDistance = 10;
93 | controls.maxDistance = 100;
94 | controls.panSpeed = 10;
95 | controls.target.set(-25, 10, 10);
96 | controls.update();
97 |
98 | const gui = new GUI();
99 | gui.add(trees, 'instancesCount').name('instances total').disable();
100 | const treeCount = gui.add(trees, 'count').name('instances rendered').disable();
101 | const treeLODCount = gui.add(trees.LODinfo.render.levels[1].object, 'count').name('instances rendered').disable();
102 | gui.add(camera, 'far', 1000, 4000, 100).name('camera far').onChange(() => camera.updateProjectionMatrix());
103 |
--------------------------------------------------------------------------------
/examples/tween.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto, Tween } from '@three.ez/main';
2 | import { Color, Euler, MeshBasicMaterial, PlaneGeometry, Scene, Vector3 } from 'three';
3 | import { InstancedMesh2 } from '../src/index.js';
4 |
5 | const main = new Main();
6 | const scene = new Scene();
7 | main.createView({ scene, camera: new PerspectiveCameraAuto().translateZ(2), enabled: false });
8 | const tempColor = new Color();
9 |
10 | const planes = new InstancedMesh2(new PlaneGeometry(), new MeshBasicMaterial(), { createEntities: true, allowsEuler: true, capacity: 20 });
11 | scene.add(planes);
12 |
13 | planes.addInstances(20, (obj, index) => {
14 | obj.scale.multiplyScalar(1 - index * 0.05);
15 | obj.color = index % 2 === 0 ? 'red' : 'blue';
16 |
17 | const rotation = new Euler(0, 0, (Math.PI / 2) * index);
18 | const position = new Vector3(0, 0, 0.1 * index);
19 | const color = index % 2 === 0 ? 'yellow' : 'violet';
20 |
21 | new Tween(obj)
22 | .to(5000, { rotation, position, color }, {
23 | onUpdate: () => obj.updateMatrix(),
24 | onProgress: (t, k, s, e, a) => {
25 | if (k === 'color') obj.color = tempColor.lerpColors(s as Color, e as Color, a);
26 | }
27 | })
28 | .yoyoForever()
29 | .start();
30 | });
31 |
32 | // TODO: FIX COLOR AND TWEEN TIME 0
33 |
--------------------------------------------------------------------------------
/examples/uniforms.ts:
--------------------------------------------------------------------------------
1 | import { Main, PerspectiveCameraAuto } from '@three.ez/main';
2 | import { Color, EquirectangularReflectionMapping, MeshStandardMaterial, Scene, TorusKnotGeometry } from 'three';
3 | import { RGBELoader } from 'three/examples/jsm/Addons.js';
4 | import { InstancedMesh2 } from '../src/index.js';
5 |
6 | const main = new Main({ showStats: false }); // FIX stats
7 | const scene = new Scene();
8 |
9 | const url = 'https://raw.githubusercontent.com/mrdoob/three.js/dev/examples/textures/equirectangular/royal_esplanade_1k.hdr';
10 | new RGBELoader().load(url, (texture) => {
11 | texture.mapping = EquirectangularReflectionMapping;
12 | scene.environment = texture;
13 | });
14 |
15 | const instancedMesh = new InstancedMesh2(new TorusKnotGeometry(1, 0.4, 128, 32), new MeshStandardMaterial(), { createEntities: true });
16 | scene.add(instancedMesh);
17 |
18 | instancedMesh.initUniformsPerInstance({ fragment: { metalness: 'float', roughness: 'float', emissive: 'vec3' } });
19 |
20 | instancedMesh.addInstances(50, (obj, index) => {
21 | obj.position.random().multiplyScalar(20).subScalar(10);
22 | obj.quaternion.random();
23 | obj.setUniform('metalness', Math.random());
24 | obj.setUniform('roughness', Math.random());
25 | obj.setUniform('emissive', new Color(0xffffff * Math.random()));
26 | });
27 |
28 | instancedMesh.on('animate', (e) => {
29 | instancedMesh.updateInstances((obj) => obj.rotateX(e.delta));
30 | });
31 |
32 | main.createView({ scene, camera: new PerspectiveCameraAuto().translateZ(50) });
33 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | InstancedMesh2 example
8 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@three.ez/instanced-mesh",
3 | "version": "0.3.4",
4 | "description": "Enhanced InstancedMesh with frustum culling, fast raycasting (using BVH), sorting, visibility management and more.",
5 | "author": "Andrea Gargaro ",
6 | "license": "MIT",
7 | "type": "module",
8 | "module": "build/index.js",
9 | "main": "build/index.cjs",
10 | "types": "src/index.d.ts",
11 | "homepage": "https://agargaro.github.io/instanced-mesh",
12 | "repository": "https://github.com/three-ez/instanced-mesh",
13 | "exports": {
14 | ".": {
15 | "import": {
16 | "types": "./src/index.d.ts",
17 | "default": "./build/index.js"
18 | },
19 | "require": {
20 | "types": "./src/index.d.ts",
21 | "default": "./build/index.cjs"
22 | }
23 | }
24 | },
25 | "keywords": [
26 | "three",
27 | "three.js",
28 | "threejs",
29 | "instancedMesh",
30 | "frustum-culling",
31 | "instance-management",
32 | "sorting",
33 | "performance",
34 | "BVH",
35 | "acceleration",
36 | "raycasting",
37 | "LOD",
38 | "shadow-LOD",
39 | "uniform-per-instance",
40 | "instancedMesh2",
41 | "skinning"
42 | ],
43 | "scripts": {
44 | "start": "vite",
45 | "build": "vite build && tsc --build tsconfig.build.json",
46 | "lint": "npx eslint --fix",
47 | "test": "echo todo add tests",
48 | "publish-alpha": "npm version prerelease --preid=alpha --git-tag-version false && npm run build && cd dist && npm publish --access public",
49 | "publish-patch": "npm version patch --git-tag-version false && npm run build && cd dist && npm publish --access public",
50 | "publish-minor": "npm version minor --git-tag-version false && npm run build && cd dist && npm publish --access public",
51 | "publish-major": "npm version major --git-tag-version false && npm run build && cd dist && npm publish --access public"
52 | },
53 | "devDependencies": {
54 | "@eslint/js": "^9.24.0",
55 | "@stylistic/eslint-plugin": "^4.2.0",
56 | "@three.ez/main": "^0.5.9",
57 | "@types/three": "^0.175.0",
58 | "eslint": "^9.24.0",
59 | "meshoptimizer": "^0.23.0",
60 | "simplex-noise": "^4.0.3",
61 | "three-hex-tiling": "^0.1.5",
62 | "typescript": "^5.8.3",
63 | "typescript-eslint": "^8.29.1",
64 | "vite": "^6.2.5",
65 | "vite-plugin-externalize-deps": "^0.9.0",
66 | "vite-plugin-glsl": "^1.4.1",
67 | "vite-plugin-static-copy": "^2.3.0"
68 | },
69 | "peerDependencies": {
70 | "three": ">=0.159.0"
71 | },
72 | "dependencies": {
73 | "bvh.js": "^0.0.13"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/public/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/banner.png
--------------------------------------------------------------------------------
/public/grass.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/grass.jpg
--------------------------------------------------------------------------------
/public/grass_normal.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/grass_normal.jpg
--------------------------------------------------------------------------------
/public/js.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/js.png
--------------------------------------------------------------------------------
/public/pattern.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/pattern.jpg
--------------------------------------------------------------------------------
/public/pine.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/pine.glb
--------------------------------------------------------------------------------
/public/pine_low.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/pine_low.glb
--------------------------------------------------------------------------------
/public/planks.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/planks.jpg
--------------------------------------------------------------------------------
/public/texture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/texture.png
--------------------------------------------------------------------------------
/public/tree.glb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/tree.glb
--------------------------------------------------------------------------------
/public/ts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/ts.png
--------------------------------------------------------------------------------
/public/wall.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/agargaro/instanced-mesh/d1fce71da8552395a10f5e48e4a37d1d28218eca/public/wall.jpg
--------------------------------------------------------------------------------
/src/core/feature/Capacity.ts:
--------------------------------------------------------------------------------
1 | import { DataTexture, FloatType, RedFormat, TypedArray } from 'three';
2 | import { InstancedMesh2 } from '../InstancedMesh2.js';
3 |
4 | // TODO: add optimize method to reduce buffer size and remove instances objects
5 |
6 | declare module '../InstancedMesh2.js' {
7 | interface InstancedMesh2 {
8 | /**
9 | * Resizes internal buffers to accommodate the specified capacity.
10 | * This ensures that the buffers are large enough to handle the required number of instances.
11 | * @param capacity The new capacity of the buffers.
12 | * @returns The current `InstancedMesh2` instance.
13 | */
14 | resizeBuffers(capacity: number): this;
15 | /** @internal */ setInstancesArrayCount(count: number): void;
16 | }
17 | }
18 |
19 | InstancedMesh2.prototype.resizeBuffers = function (capacity: number): InstancedMesh2 {
20 | const oldCapacity = this._capacity;
21 | this._capacity = capacity;
22 | const minCapacity = Math.min(capacity, oldCapacity);
23 |
24 | if (this.instanceIndex) {
25 | const indexArray = new Uint32Array(capacity);
26 | indexArray.set(new Uint32Array(this.instanceIndex.array.buffer, 0, minCapacity)); // safely copy TODO method
27 | this.instanceIndex.array = indexArray;
28 | }
29 |
30 | if (this.LODinfo) {
31 | for (const obj of this.LODinfo.objects) {
32 | obj._capacity = capacity;
33 |
34 | if (obj.instanceIndex) {
35 | const indexArray = new Uint32Array(capacity);
36 | indexArray.set(new Uint32Array(obj.instanceIndex.array.buffer, 0, minCapacity)); // safely copy TODO method
37 | obj.instanceIndex.array = indexArray;
38 | }
39 | }
40 | }
41 |
42 | this.availabilityArray.length = capacity * 2;
43 |
44 | this.matricesTexture.resize(capacity);
45 |
46 | if (this.colorsTexture) {
47 | this.colorsTexture.resize(capacity);
48 | if (capacity > oldCapacity) {
49 | this.colorsTexture._data.fill(1, oldCapacity * 4);
50 | }
51 | }
52 |
53 | if (this.morphTexture) { // test it
54 | const oldArray = this.morphTexture.image.data as TypedArray; // TODO check if they fix d.ts
55 | const size = oldArray.length / oldCapacity;
56 | this.morphTexture.dispose();
57 | this.morphTexture = new DataTexture(new Float32Array(size * capacity), size, capacity, RedFormat, FloatType);
58 | (this.morphTexture.image.data as TypedArray).set(oldArray); // FIX if reduce
59 | }
60 |
61 | this.uniformsTexture?.resize(capacity);
62 |
63 | return this;
64 | };
65 |
66 | InstancedMesh2.prototype.setInstancesArrayCount = function (count: number): void {
67 | if (count < this._instancesArrayCount) {
68 | const bvh = this.bvh;
69 | if (bvh) {
70 | for (let i = this._instancesArrayCount - 1; i >= count; i--) {
71 | if (!this.getActiveAt(i)) continue;
72 | bvh.delete(i);
73 | }
74 | }
75 |
76 | this._instancesArrayCount = count;
77 | return;
78 | }
79 |
80 | if (count > this._capacity) {
81 | let newCapacity = this._capacity + (this._capacity >> 1) + 512;
82 | while (newCapacity < count) {
83 | newCapacity += (newCapacity >> 1) + 512;
84 | }
85 |
86 | this.resizeBuffers(newCapacity);
87 | }
88 |
89 | const start = this._instancesArrayCount;
90 | this._instancesArrayCount = count;
91 | if (this._createEntities) this.createEntities(start);
92 | };
93 |
--------------------------------------------------------------------------------
/src/core/feature/Instances.ts:
--------------------------------------------------------------------------------
1 | import { InstancedEntity } from '../InstancedEntity.js';
2 | import { InstancedMesh2 } from '../InstancedMesh2.js';
3 |
4 | // TODO: optimize method to fill 'holes'.
5 |
6 | /**
7 | * Represents an extended entity type with custom data.
8 | */
9 | export type Entity = InstancedEntity & T;
10 | /**
11 | * A callback function used to update or initialize an entity.
12 | */
13 | export type UpdateEntityCallback = (obj: Entity, index: number) => void;
14 |
15 | declare module '../InstancedMesh2.js' {
16 | interface InstancedMesh2 {
17 | /**
18 | * Updates instances by applying a callback function to each instance. It calls `updateMatrix` for each instance.
19 | * @param onUpdate A callback function to update each entity.
20 | * @returns The current `InstancedMesh2` instance.
21 | */
22 | updateInstances(onUpdate: UpdateEntityCallback>): this;
23 | /**
24 | * Updates instances position by applying a callback function to each instance. It calls `updateMatrixPosition` for each instance.
25 | * This method updates only the position attributes of the matrix.
26 | * @param onUpdate A callback function to update each entity.
27 | * @returns The current `InstancedMesh2` instance.
28 | */
29 | updateInstancesPosition(onUpdate: UpdateEntityCallback>): this;
30 | /**
31 | * Adds new instances and optionally initializes them using a callback function.
32 | * @param count The number of new instances to add.
33 | * @param onCreation An optional callback to initialize each new entity.
34 | * @returns The current `InstancedMesh2` instance.
35 | */
36 | addInstances(count: number, onCreation?: UpdateEntityCallback>): this;
37 | /**
38 | * Removes instances by their ids.
39 | * @param ids The ids of the instances to remove.
40 | * @returns The current `InstancedMesh2` instance.
41 | */
42 | removeInstances(...ids: number[]): this;
43 | /**
44 | * Clears all instances and resets the instance count.
45 | * @returns The current `InstancedMesh2` instance.
46 | */
47 | clearInstances(): this;
48 | /** @internal */ clearTempInstance(index: number): InstancedEntity;
49 | /** @internal */ clearTempInstancePosition(index: number): InstancedEntity;
50 | /** @internal */ clearInstance(instance: InstancedEntity): InstancedEntity;
51 | /** @internal */ createEntities(start: number): this;
52 | /** @internal */ addInstance(id: number, onCreation?: UpdateEntityCallback): void;
53 | }
54 | }
55 |
56 | InstancedMesh2.prototype.clearTempInstance = function (index: number) {
57 | const instance = this._tempInstance;
58 | (instance as any).id = index;
59 | return this.clearInstance(instance);
60 | };
61 |
62 | InstancedMesh2.prototype.clearTempInstancePosition = function (index: number) {
63 | const instance = this._tempInstance;
64 | (instance as any).id = index;
65 | instance.position.set(0, 0, 0);
66 | return instance;
67 | };
68 |
69 | InstancedMesh2.prototype.clearInstance = function (instance: InstancedEntity) {
70 | instance.position.set(0, 0, 0);
71 | instance.scale.set(1, 1, 1);
72 | instance.quaternion.identity();
73 | return instance;
74 | };
75 |
76 | InstancedMesh2.prototype.updateInstances = function (this: InstancedMesh2, onUpdate: UpdateEntityCallback) {
77 | const end = this._instancesArrayCount;
78 | const instances = this.instances;
79 |
80 | for (let i = 0; i < end; i++) {
81 | if (!this.getActiveAt(i)) continue;
82 | const instance = instances ? instances[i] : this.clearTempInstance(i);
83 | onUpdate(instance, i);
84 | instance.updateMatrix();
85 | }
86 |
87 | return this;
88 | };
89 |
90 | InstancedMesh2.prototype.updateInstancesPosition = function (this: InstancedMesh2, onUpdate: UpdateEntityCallback) {
91 | const end = this._instancesArrayCount;
92 | const instances = this.instances;
93 |
94 | for (let i = 0; i < end; i++) {
95 | if (!this.getActiveAt(i)) continue;
96 | const instance = instances ? instances[i] : this.clearTempInstancePosition(i);
97 | onUpdate(instance, i);
98 | instance.updateMatrixPosition();
99 | }
100 |
101 | return this;
102 | };
103 |
104 | InstancedMesh2.prototype.createEntities = function (this: InstancedMesh2, start: number) {
105 | const end = this._instancesArrayCount;
106 |
107 | if (!this.instances) {
108 | this.instances = new Array(end);
109 | } else if (this.instances.length < end) {
110 | this.instances.length = end;
111 | } else {
112 | return this;
113 | }
114 |
115 | // we can also revert this for and put 'break' instead of 'continue' but no it's memory consecutive
116 | const instances = this.instances;
117 | for (let i = start; i < end; i++) {
118 | if (instances[i]) continue;
119 | instances[i] = new InstancedEntity(this, i, this._allowsEuler);
120 | }
121 |
122 | return this;
123 | };
124 |
125 | InstancedMesh2.prototype.addInstances = function (count: number, onCreation?: UpdateEntityCallback) {
126 | if (!onCreation && this.bvh) {
127 | console.warn('InstancedMesh2: if `computeBVH()` has already been called, it is better to valorize the instances in the `onCreation` callback for better performance.');
128 | }
129 |
130 | // refill holes created from removeInstances
131 | const freeIds = this._freeIds;
132 | if (freeIds.length > 0) {
133 | let maxId = -1;
134 | const freeIdsUsed = Math.min(freeIds.length, count);
135 | const freeidsEnd = freeIds.length - freeIdsUsed;
136 |
137 | for (let i = freeIds.length - 1; i >= freeidsEnd; i--) {
138 | const id = freeIds[i];
139 | if (id > maxId) maxId = id;
140 | this.addInstance(id, onCreation);
141 | }
142 |
143 | freeIds.length -= freeIdsUsed;
144 | count -= freeIdsUsed;
145 | this._instancesArrayCount = Math.max(maxId + 1, this._instancesArrayCount);
146 | }
147 |
148 | const start = this._instancesArrayCount;
149 | const end = start + count;
150 | this.setInstancesArrayCount(end);
151 |
152 | for (let i = start; i < end; i++) {
153 | this.addInstance(i, onCreation);
154 | }
155 |
156 | return this;
157 | };
158 |
159 | InstancedMesh2.prototype.addInstance = function (id: number, onCreation?: UpdateEntityCallback) {
160 | this._instancesCount++;
161 | this.setActiveAndVisibilityAt(id, true);
162 | const instance = this.instances ? this.clearInstance(this.instances[id]) : this.clearTempInstance(id);
163 |
164 | if (onCreation) {
165 | onCreation(instance, id);
166 | instance.updateMatrix();
167 | } else {
168 | instance.setMatrixIdentity();
169 | }
170 |
171 | this.bvh?.insert(id);
172 | };
173 |
174 | InstancedMesh2.prototype.removeInstances = function (...ids: number[]) {
175 | const freeIds = this._freeIds;
176 | const bvh = this.bvh;
177 |
178 | for (const id of ids) {
179 | if (id < this._instancesArrayCount && this.getActiveAt(id)) {
180 | this.setActiveAt(id, false);
181 | freeIds.push(id);
182 | bvh?.delete(id);
183 | this._instancesCount--;
184 | }
185 | }
186 |
187 | for (let i = this._instancesArrayCount - 1; i >= 0; i--) {
188 | if (this.getActiveAt(i)) break;
189 | this._instancesArrayCount--;
190 | }
191 |
192 | return this;
193 | };
194 |
195 | InstancedMesh2.prototype.clearInstances = function () {
196 | this._instancesCount = 0;
197 | this._instancesArrayCount = 0;
198 | this._freeIds.length = 0;
199 |
200 | this.bvh?.clear();
201 |
202 | if (this.LODinfo) {
203 | for (const obj of this.LODinfo.objects) {
204 | obj._count = 0;
205 | }
206 | }
207 |
208 | return this;
209 | };
210 |
--------------------------------------------------------------------------------
/src/core/feature/LOD.ts:
--------------------------------------------------------------------------------
1 | import { BufferGeometry, Material, ShaderMaterial } from 'three';
2 | import { InstancedMesh2, InstancedMesh2Params } from '../InstancedMesh2.js';
3 |
4 | // TODO check squaured distance in comments and code
5 |
6 | /**
7 | * Represents information about Level of Detail (LOD).
8 | * @template TData Type for additional instance data.
9 | */
10 | export interface LODInfo {
11 | /**
12 | * Render settings for the LOD.
13 | */
14 | render: LODRenderList;
15 | /**
16 | * Shadow rendering settings for the LOD.
17 | */
18 | shadowRender: LODRenderList;
19 | /**
20 | * List of `InstancedMesh2` associated to LODs.
21 | */
22 | objects: InstancedMesh2[];
23 | }
24 |
25 | /**
26 | * Represents a list of render levels for LOD.
27 | * @template TData Type for additional instance data.
28 | */
29 | export interface LODRenderList {
30 | /**
31 | * Array of LOD levels.
32 | */
33 | levels: LODLevel[];
34 | /**
35 | * Array of instance counts per LOD level, used internally.
36 | */
37 | count: number[];
38 | }
39 |
40 | /**
41 | * Represents a single LOD level.
42 | * @template TData Type for additional instance data.
43 | */
44 | export interface LODLevel {
45 | /**
46 | * The squared distance at which this LOD level becomes active.
47 | */
48 | distance: number;
49 | /**
50 | * Hysteresis value to prevent LOD flickering when transitioning.
51 | */
52 | hysteresis: number;
53 | /**
54 | * The `InstancedMesh2` object associated with this LOD level.
55 | */
56 | object: InstancedMesh2;
57 | }
58 |
59 | declare module '../InstancedMesh2.js' {
60 | interface InstancedMesh2 {
61 | /**
62 | * Retrieves the index of the LOD level for a given distance.
63 | * @param levels The array of LOD levels.
64 | * @param distance The squared distance from the camera to the object.
65 | * @returns The index of the LOD level that should be used.
66 | */
67 | getObjectLODIndexForDistance(levels: LODLevel[], distance: number): number;
68 | /**
69 | * Sets the first LOD (using current geometry) distance and hysteresis.
70 | * @param distance The distance for the first LOD.
71 | * @param hysteresis The hysteresis value for the first LOD.
72 | * @returns The current `InstancedMesh2` instance.
73 | */
74 | setFirstLODDistance(distance?: number, hysteresis?: number): this;
75 | /**
76 | * Adds a new LOD level with the given geometry, material, and distance.
77 | * @param geometry The geometry for the LOD level.
78 | * @param material The material for the LOD level.
79 | * @param distance The distance for this LOD level.
80 | * @param hysteresis The hysteresis value for this LOD level.
81 | * @returns The current `InstancedMesh2` instance.
82 | */
83 | addLOD(geometry: BufferGeometry, material: Material | Material[], distance?: number, hysteresis?: number): this;
84 | /**
85 | * Adds a shadow-specific LOD level with the given geometry and distance.
86 | * @param geometry The geometry for the shadow LOD.
87 | * @param distance The distance for this LOD level.
88 | * @param hysteresis The hysteresis value for this LOD level.
89 | * @returns The current `InstancedMesh2` instance.
90 | */
91 | addShadowLOD(geometry: BufferGeometry, distance?: number, hysteresis?: number): this;
92 | /** @internal */ addLevel(renderList: LODRenderList, geometry: BufferGeometry, material: Material | Material[], distance: number, hysteresis: number): InstancedMesh2;
93 | /** @internal */ patchLevel(obj: InstancedMesh2): void;
94 | }
95 | }
96 |
97 | InstancedMesh2.prototype.getObjectLODIndexForDistance = function (levels: LODLevel[], distance: number): number {
98 | for (let i = levels.length - 1; i > 0; i--) {
99 | const level = levels[i];
100 | const levelDistance = level.distance - (level.distance * level.hysteresis);
101 | if (distance >= levelDistance) return i;
102 | }
103 |
104 | return 0;
105 | };
106 |
107 | InstancedMesh2.prototype.setFirstLODDistance = function (distance = 0, hysteresis = 0): InstancedMesh2 {
108 | if (this._parentLOD) {
109 | throw new Error('Cannot create LOD for this InstancedMesh2.');
110 | }
111 |
112 | if (!this.LODinfo) {
113 | this.LODinfo = { render: null, shadowRender: null, objects: [this] };
114 | }
115 |
116 | if (!this.LODinfo.render) {
117 | this.LODinfo.render = {
118 | levels: [{ distance, hysteresis, object: this }],
119 | count: [0]
120 | };
121 | }
122 |
123 | return this;
124 | };
125 |
126 | InstancedMesh2.prototype.addLOD = function (geometry: BufferGeometry, material: Material | Material[], distance = 0, hysteresis = 0): InstancedMesh2 {
127 | if (this._parentLOD) {
128 | throw new Error('Cannot create LOD for this InstancedMesh2.');
129 | }
130 |
131 | if (!this.LODinfo?.render && distance === 0) {
132 | throw new Error('Cannot set distance to 0 for the first LOD. Use "setFirstLODDistance" before use "addLOD".');
133 | }
134 |
135 | this.setFirstLODDistance(0, hysteresis);
136 |
137 | this.addLevel(this.LODinfo.render, geometry, material, distance, hysteresis);
138 |
139 | return this;
140 | };
141 |
142 | InstancedMesh2.prototype.addShadowLOD = function (geometry: BufferGeometry, distance = 0, hysteresis = 0): InstancedMesh2 {
143 | if (this._parentLOD) {
144 | throw new Error('Cannot create LOD for this InstancedMesh2.');
145 | }
146 |
147 | if (!this.LODinfo) {
148 | this.LODinfo = { render: null, shadowRender: null, objects: [this] };
149 | }
150 |
151 | if (!this.LODinfo.shadowRender) {
152 | this.LODinfo.shadowRender = { levels: [], count: [] };
153 | }
154 |
155 | const object = this.addLevel(this.LODinfo.shadowRender, geometry, null, distance, hysteresis);
156 | object.castShadow = true;
157 | this.castShadow = true;
158 |
159 | return this;
160 | };
161 |
162 | InstancedMesh2.prototype.addLevel = function (renderList: LODRenderList, geometry: BufferGeometry, material: Material, distance: number, hysteresis: number): InstancedMesh2 {
163 | const objectsList = this.LODinfo.objects;
164 | const levels = renderList.levels;
165 | let index;
166 | let object: InstancedMesh2;
167 | distance = distance ** 2; // to avoid to use Math.sqrt every time
168 |
169 | const objIndex = objectsList.findIndex((e) => e.geometry === geometry);
170 | if (objIndex === -1) {
171 | const params: InstancedMesh2Params = { capacity: this._capacity, renderer: this._renderer };
172 | object = new InstancedMesh2(geometry, material ?? new ShaderMaterial(), params, this);
173 | object.frustumCulled = false;
174 | this.patchLevel(object);
175 | objectsList.push(object);
176 | this.add(object); // TODO handle render order?
177 | } else {
178 | object = objectsList[objIndex];
179 | if (material) object.material = material;
180 | }
181 |
182 | for (index = 0; index < levels.length; index++) {
183 | if (distance < levels[index].distance) break;
184 | }
185 |
186 | levels.splice(index, 0, { distance, hysteresis, object });
187 | renderList.count.push(0);
188 |
189 | return object;
190 | };
191 |
192 | InstancedMesh2.prototype.patchLevel = function (obj: InstancedMesh2): void {
193 | Object.defineProperty(obj, 'matricesTexture', {
194 | get(this: InstancedMesh2) {
195 | return this._parentLOD.matricesTexture;
196 | }
197 | });
198 |
199 | Object.defineProperty(obj, 'colorsTexture', {
200 | get(this: InstancedMesh2) {
201 | return this._parentLOD.colorsTexture;
202 | }
203 | });
204 |
205 | Object.defineProperty(obj, 'uniformsTexture', {
206 | get(this: InstancedMesh2) {
207 | return this._parentLOD.uniformsTexture;
208 | }
209 | });
210 |
211 | Object.defineProperty(obj, 'morphTexture', { // TODO check if it's correct
212 | get(this: InstancedMesh2) {
213 | return this._parentLOD.morphTexture;
214 | }
215 | });
216 |
217 | Object.defineProperty(obj, 'boneTexture', {
218 | get(this: InstancedMesh2) {
219 | return this._parentLOD.boneTexture;
220 | }
221 | });
222 |
223 | Object.defineProperty(obj, 'skeleton', {
224 | get(this: InstancedMesh2) {
225 | return this._parentLOD.skeleton;
226 | }
227 | });
228 |
229 | Object.defineProperty(obj, 'bindMatrixInverse', {
230 | get(this: InstancedMesh2) {
231 | return this._parentLOD.bindMatrixInverse;
232 | }
233 | });
234 |
235 | Object.defineProperty(obj, 'bindMatrix', {
236 | get(this: InstancedMesh2) {
237 | return this._parentLOD.bindMatrix;
238 | }
239 | });
240 | };
241 |
--------------------------------------------------------------------------------
/src/core/feature/Morph.ts:
--------------------------------------------------------------------------------
1 | import { DataTexture, FloatType, Mesh, RedFormat } from 'three';
2 | import { InstancedMesh2 } from '../InstancedMesh2.js';
3 |
4 | declare module '../InstancedMesh2.js' {
5 | interface InstancedMesh2 {
6 | /**
7 | * Gets the morph target data for a specific instance.
8 | * @param id The index of the instance.
9 | * @param object Optional `Mesh` to store the morph target data.
10 | * @returns The mesh object with updated morph target influences.
11 | */
12 | getMorphAt(id: number, object?: Mesh): Mesh;
13 | /**
14 | * Sets the morph target influences for a specific instance.
15 | * @param id The index of the instance.
16 | * @param object The `Mesh` containing the morph target influences to apply.
17 | */
18 | setMorphAt(id: number, object: Mesh): void;
19 | }
20 | }
21 |
22 | const _tempMesh = new Mesh();
23 |
24 | InstancedMesh2.prototype.getMorphAt = function (id: number, object = _tempMesh): Mesh {
25 | const objectInfluences = object.morphTargetInfluences;
26 | const array = this.morphTexture.source.data.data;
27 | const len = objectInfluences.length + 1; // All influences + the baseInfluenceSum
28 | const dataIndex = id * len + 1; // Skip the baseInfluenceSum at the beginning
29 |
30 | for (let i = 0; i < objectInfluences.length; i++) {
31 | objectInfluences[i] = array[dataIndex + i];
32 | }
33 |
34 | return object;
35 | };
36 |
37 | InstancedMesh2.prototype.setMorphAt = function (id: number, object: Mesh): void {
38 | const objectInfluences = object.morphTargetInfluences;
39 | const len = objectInfluences.length + 1;
40 |
41 | if (this.morphTexture === null && !this._parentLOD) {
42 | this.morphTexture = new DataTexture(new Float32Array(len * this._capacity), len, this._capacity, RedFormat, FloatType);
43 | }
44 |
45 | const array = this.morphTexture.source.data.data;
46 | let morphInfluencesSum = 0;
47 |
48 | for (const objectInfluence of objectInfluences) {
49 | morphInfluencesSum += objectInfluence;
50 | }
51 |
52 | const morphBaseInfluence = this._geometry.morphTargetsRelative ? 1 : 1 - morphInfluencesSum;
53 | const dataIndex = len * id;
54 | array[dataIndex] = morphBaseInfluence;
55 | array.set(objectInfluences, dataIndex + 1);
56 | this.morphTexture.needsUpdate = true;
57 | };
58 |
--------------------------------------------------------------------------------
/src/core/feature/Raycasting.ts:
--------------------------------------------------------------------------------
1 | import { Intersection, Matrix4, Mesh, Ray, Raycaster, Sphere, Vector3 } from 'three';
2 | import { InstancedMesh2 } from '../InstancedMesh2.js';
3 |
4 | declare module '../InstancedMesh2.js' {
5 | interface InstancedMesh2 {
6 | /** @internal */ raycastInstances(raycaster: Raycaster, result: Intersection[]): void;
7 | /** @internal */ checkObjectIntersection(raycaster: Raycaster, objectIndex: number, result: Intersection[]): void;
8 | }
9 | }
10 |
11 | const _intersections: Intersection[] = [];
12 | const _mesh = new Mesh();
13 | const _ray = new Ray();
14 | const _direction = new Vector3();
15 | const _worldScale = new Vector3();
16 | const _invMatrixWorld = new Matrix4();
17 | const _sphere = new Sphere();
18 |
19 | InstancedMesh2.prototype.raycast = function (raycaster, result) {
20 | if (this._parentLOD || !this.material || this._instancesArrayCount === 0 || !this.instanceIndex) return;
21 |
22 | _mesh.geometry = this._geometry;
23 | _mesh.material = this.material;
24 |
25 | const originalRay = raycaster.ray;
26 | const originalNear = raycaster.near;
27 | const originalFar = raycaster.far;
28 |
29 | _invMatrixWorld.copy(this.matrixWorld).invert();
30 |
31 | _worldScale.setFromMatrixScale(this.matrixWorld);
32 | _direction.copy(raycaster.ray.direction).multiply(_worldScale);
33 | const scaleFactor = _direction.length();
34 |
35 | raycaster.ray = _ray.copy(raycaster.ray).applyMatrix4(_invMatrixWorld);
36 | raycaster.near /= scaleFactor;
37 | raycaster.far /= scaleFactor;
38 |
39 | this.raycastInstances(raycaster, result);
40 |
41 | raycaster.ray = originalRay;
42 | raycaster.near = originalNear;
43 | raycaster.far = originalFar;
44 | };
45 |
46 | InstancedMesh2.prototype.raycastInstances = function (raycaster, result) {
47 | if (this.bvh) {
48 | this.bvh.raycast(raycaster, (instanceId) => this.checkObjectIntersection(raycaster, instanceId, result));
49 | // TODO test with three-mesh-bvh
50 | } else {
51 | if (this.boundingSphere === null) this.computeBoundingSphere();
52 | _sphere.copy(this.boundingSphere);
53 | if (!raycaster.ray.intersectsSphere(_sphere)) return;
54 |
55 | const instancesToCheck = this.instanceIndex.array; // TODO this is unsorted and it's slower to iterate. If raycastFrustum is false, don't use it.
56 | const raycastFrustum = this.raycastOnlyFrustum && this._perObjectFrustumCulled;
57 | const checkCount = raycastFrustum ? this._count : this._instancesArrayCount;
58 |
59 | for (let i = 0; i < checkCount; i++) {
60 | this.checkObjectIntersection(raycaster, instancesToCheck[i], result);
61 | }
62 | }
63 | };
64 |
65 | InstancedMesh2.prototype.checkObjectIntersection = function (raycaster, objectIndex, result) {
66 | if (objectIndex > this._instancesArrayCount || !this.getActiveAndVisibilityAt(objectIndex)) return;
67 |
68 | this.getMatrixAt(objectIndex, _mesh.matrixWorld);
69 |
70 | _mesh.raycast(raycaster, _intersections);
71 |
72 | for (const intersect of _intersections) {
73 | intersect.instanceId = objectIndex;
74 | intersect.object = this;
75 | result.push(intersect);
76 | }
77 |
78 | _intersections.length = 0;
79 | };
80 |
--------------------------------------------------------------------------------
/src/core/feature/Skeleton.ts:
--------------------------------------------------------------------------------
1 | import { Matrix4, Skeleton } from 'three';
2 | import { InstancedMesh2 } from '../InstancedMesh2.js';
3 | import { SquareDataTexture } from '../utils/SquareDataTexture.js';
4 |
5 | declare module '../InstancedMesh2.js' {
6 | interface InstancedMesh2 {
7 | /**
8 | * Initialize the skeleton of the instances.
9 | * @param skeleton The skeleton to initialize.
10 | * @param disableMatrixAutoUpdate Whether to disable the matrix auto update of the bones. Default is `true`.
11 | */
12 | initSkeleton(skeleton: Skeleton, disableMatrixAutoUpdate?: boolean): void;
13 | /**
14 | * Set the bones of the skeleton to the instance at the specified index.
15 | * @param id The index of the instance.
16 | * @param updateBonesMatrices Whether to update the matrices of the bones. Default is `true`.
17 | * @param excludeBonesSet An optional set of bone names to exclude from updates, skipping their local matrix updates.
18 | */
19 | setBonesAt(id: number, updateBonesMatrices?: boolean, excludeBonesSet?: Set): void;
20 | /** internal */ multiplyBoneMatricesAt(instanceIndex: number, boneIndex: number, m1: Matrix4, m2: Matrix4): void;
21 | }
22 | }
23 |
24 | InstancedMesh2.prototype.initSkeleton = function (skeleton: Skeleton, disableMatrixAutoUpdate = true) {
25 | if (skeleton && this.skeleton !== skeleton && !this._parentLOD) { // TODO remove !this._parentLOD
26 | const bones = skeleton.bones;
27 | this.skeleton = skeleton;
28 | this.bindMatrix = new Matrix4();
29 | this.bindMatrixInverse = new Matrix4();
30 | this.boneTexture = new SquareDataTexture(Float32Array, 4, 4 * bones.length, this._capacity);
31 |
32 | if (disableMatrixAutoUpdate) {
33 | for (const bone of bones) {
34 | bone.matrixAutoUpdate = false;
35 | bone.matrixWorldAutoUpdate = false;
36 | }
37 | }
38 |
39 | this.materialsNeedsUpdate(); // TODO this may not work if change already present skeleton
40 | }
41 | };
42 |
43 | InstancedMesh2.prototype.setBonesAt = function (id: number, updateBonesMatrices = true, excludeBonesSet?: Set) {
44 | const skeleton = this.skeleton;
45 | if (!skeleton) {
46 | throw new Error('"setBonesAt" cannot be called before "initSkeleton"');
47 | }
48 |
49 | const bones = skeleton.bones;
50 | const boneInverses = skeleton.boneInverses;
51 |
52 | for (let i = 0, l = bones.length; i < l; i++) {
53 | const bone = bones[i];
54 |
55 | if (updateBonesMatrices) {
56 | if (!excludeBonesSet?.has(bone.name)) {
57 | bone.updateMatrix();
58 | }
59 | bone.matrixWorld.multiplyMatrices(bone.parent.matrixWorld, bone.matrix);
60 | }
61 |
62 | this.multiplyBoneMatricesAt(id, i, bone.matrixWorld, boneInverses[i]);
63 | }
64 |
65 | this.boneTexture.enqueueUpdate(id);
66 | };
67 |
68 | InstancedMesh2.prototype.multiplyBoneMatricesAt = function (instanceIndex: number, boneIndex: number, m1: Matrix4, m2: Matrix4) {
69 | const offset = (instanceIndex * this.skeleton.bones.length + boneIndex) * 16;
70 | const ae = m1.elements;
71 | const be = m2.elements;
72 | const te = this.boneTexture._data;
73 |
74 | const a11 = ae[0], a12 = ae[4], a13 = ae[8], a14 = ae[12];
75 | const a21 = ae[1], a22 = ae[5], a23 = ae[9], a24 = ae[13];
76 | const a31 = ae[2], a32 = ae[6], a33 = ae[10], a34 = ae[14];
77 | const a41 = ae[3], a42 = ae[7], a43 = ae[11], a44 = ae[15];
78 |
79 | const b11 = be[0], b12 = be[4], b13 = be[8], b14 = be[12];
80 | const b21 = be[1], b22 = be[5], b23 = be[9], b24 = be[13];
81 | const b31 = be[2], b32 = be[6], b33 = be[10], b34 = be[14];
82 | const b41 = be[3], b42 = be[7], b43 = be[11], b44 = be[15];
83 |
84 | te[offset + 0] = a11 * b11 + a12 * b21 + a13 * b31 + a14 * b41;
85 | te[offset + 4] = a11 * b12 + a12 * b22 + a13 * b32 + a14 * b42;
86 | te[offset + 8] = a11 * b13 + a12 * b23 + a13 * b33 + a14 * b43;
87 | te[offset + 12] = a11 * b14 + a12 * b24 + a13 * b34 + a14 * b44;
88 |
89 | te[offset + 1] = a21 * b11 + a22 * b21 + a23 * b31 + a24 * b41;
90 | te[offset + 5] = a21 * b12 + a22 * b22 + a23 * b32 + a24 * b42;
91 | te[offset + 9] = a21 * b13 + a22 * b23 + a23 * b33 + a24 * b43;
92 | te[offset + 13] = a21 * b14 + a22 * b24 + a23 * b34 + a24 * b44;
93 |
94 | te[offset + 2] = a31 * b11 + a32 * b21 + a33 * b31 + a34 * b41;
95 | te[offset + 6] = a31 * b12 + a32 * b22 + a33 * b32 + a34 * b42;
96 | te[offset + 10] = a31 * b13 + a32 * b23 + a33 * b33 + a34 * b43;
97 | te[offset + 14] = a31 * b14 + a32 * b24 + a33 * b34 + a34 * b44;
98 |
99 | te[offset + 3] = a41 * b11 + a42 * b21 + a43 * b31 + a44 * b41;
100 | te[offset + 7] = a41 * b12 + a42 * b22 + a43 * b32 + a44 * b42;
101 | te[offset + 11] = a41 * b13 + a42 * b23 + a43 * b33 + a44 * b43;
102 | te[offset + 15] = a41 * b14 + a42 * b24 + a43 * b34 + a44 * b44;
103 | };
104 |
--------------------------------------------------------------------------------
/src/core/feature/Uniforms.ts:
--------------------------------------------------------------------------------
1 | import { InstancedMesh2 } from '../InstancedMesh2.js';
2 | import { ChannelSize, SquareDataTexture, UniformMap, UniformMapType, UniformType, UniformValue, UniformValueObj } from '../utils/SquareDataTexture.js';
3 |
4 | type UniformSchema = { [x: string]: UniformType };
5 | type UniformSchemaShader = { vertex?: UniformSchema; fragment?: UniformSchema };
6 |
7 | type UniformSchemaResult = {
8 | channels: ChannelSize;
9 | pixelsPerInstance: number;
10 | uniformMap: UniformMap;
11 | fetchInFragmentShader: boolean;
12 | };
13 |
14 | declare module '../InstancedMesh2.js' {
15 | interface InstancedMesh2 {
16 | /**
17 | * Retrieves a uniform value for a specific instance.
18 | * @param id The index of the instance.
19 | * @param name The name of the uniform.
20 | * @param target Optional target object to store the uniform value.
21 | * @returns The uniform value for the specified instance.
22 | */
23 | getUniformAt(id: number, name: string, target?: UniformValueObj): UniformValue;
24 | /**
25 | * Sets a uniform value for a specific instance.
26 | * @param id The index of the instance.
27 | * @param name The name of the uniform.
28 | * @param value The value to set for the uniform.
29 | */
30 | setUniformAt(id: number, name: string, value: UniformValue): void;
31 | /**
32 | * Initializes per-instance uniforms using a schema.
33 | * @param schema The schema defining the uniforms.
34 | */
35 | initUniformsPerInstance(schema: UniformSchemaShader): void;
36 | /** @internal */ getUniformSchemaResult(schema: UniformSchemaShader): UniformSchemaResult;
37 | /** @internal */ getUniformOffset(size: number, tempOffset: number[]): number;
38 | /** @internal */ getUniformSize(type: UniformType): number;
39 | }
40 | }
41 |
42 | InstancedMesh2.prototype.getUniformAt = function (id: number, name: string, target?: UniformValueObj): UniformValue {
43 | if (!this.uniformsTexture) {
44 | throw new Error('Before get/set uniform, it\'s necessary to use "initUniformsPerInstance".');
45 | }
46 | return this.uniformsTexture.getUniformAt(id, name, target);
47 | };
48 |
49 | InstancedMesh2.prototype.setUniformAt = function (id: number, name: string, value: UniformValue): void {
50 | if (!this.uniformsTexture) {
51 | throw new Error('Before get/set uniform, it\'s necessary to use "initUniformsPerInstance".');
52 | }
53 | this.uniformsTexture.setUniformAt(id, name, value);
54 | this.uniformsTexture.enqueueUpdate(id);
55 | };
56 |
57 | InstancedMesh2.prototype.initUniformsPerInstance = function (schema: UniformSchemaShader): void {
58 | if (!this._parentLOD) {
59 | const { channels, pixelsPerInstance, uniformMap, fetchInFragmentShader } = this.getUniformSchemaResult(schema);
60 | this.uniformsTexture = new SquareDataTexture(Float32Array, channels, pixelsPerInstance, this._capacity, uniformMap, fetchInFragmentShader);
61 | this.materialsNeedsUpdate();
62 | }
63 | };
64 |
65 | InstancedMesh2.prototype.getUniformSchemaResult = function (schema: UniformSchemaShader): UniformSchemaResult {
66 | let totalSize = 0;
67 | const uniformMap = new Map();
68 | const uniforms: { type: UniformType; name: string; size: number }[] = [];
69 | const vertexSchema = schema.vertex ?? {};
70 | const fragmentSchema = schema.fragment ?? {};
71 | let fetchInFragmentShader = true;
72 |
73 | for (const name in vertexSchema) {
74 | const type = vertexSchema[name];
75 | const size = this.getUniformSize(type);
76 | totalSize += size;
77 | uniforms.push({ name, type, size });
78 | fetchInFragmentShader = false;
79 | }
80 |
81 | for (const name in fragmentSchema) {
82 | if (!vertexSchema[name]) {
83 | const type = fragmentSchema[name];
84 | const size = this.getUniformSize(type);
85 | totalSize += size;
86 | uniforms.push({ name, type, size });
87 | }
88 | }
89 |
90 | uniforms.sort((a, b) => b.size - a.size);
91 |
92 | const tempOffset = [];
93 | for (const { name, size, type } of uniforms) {
94 | const offset = this.getUniformOffset(size, tempOffset);
95 | uniformMap.set(name, { offset, size, type });
96 | }
97 |
98 | const pixelsPerInstance = Math.ceil(totalSize / 4);
99 | const channels = Math.min(totalSize, 4) as ChannelSize;
100 |
101 | return { channels, pixelsPerInstance, uniformMap, fetchInFragmentShader };
102 | };
103 |
104 | InstancedMesh2.prototype.getUniformOffset = function (size: number, tempOffset: number[]): number {
105 | if (size < 4) {
106 | for (let i = 0; i < tempOffset.length; i++) {
107 | if (tempOffset[i] + size <= 4) {
108 | const offset = i * 4 + tempOffset[i];
109 | tempOffset[i] += size;
110 | return offset;
111 | }
112 | }
113 | }
114 |
115 | const offset = tempOffset.length * 4;
116 | for (; size > 0; size -= 4) {
117 | tempOffset.push(size);
118 | }
119 |
120 | return offset;
121 | };
122 |
123 | InstancedMesh2.prototype.getUniformSize = function (type: UniformType): number {
124 | switch (type) {
125 | case 'float': return 1;
126 | case 'vec2': return 2;
127 | case 'vec3': return 3;
128 | case 'vec4': return 4;
129 | case 'mat3': return 9;
130 | case 'mat4': return 16;
131 | default:
132 | throw new Error(`Invalid uniform type: ${type}`);
133 | }
134 | };
135 |
--------------------------------------------------------------------------------
/src/core/utils/GLInstancedBufferAttribute.ts:
--------------------------------------------------------------------------------
1 | import { GLBufferAttribute, TypedArray, WebGLRenderer } from 'three';
2 |
3 | /**
4 | * A class that extends `GLBufferAttribute` to handle instanced buffer attributes.
5 | * This class was specifically created to allow updating instanced buffer attributes during the `onBeforeRender` callback,
6 | * providing an efficient way to modify the buffer data dynamically before rendering.
7 | */
8 | export class GLInstancedBufferAttribute extends GLBufferAttribute {
9 | /**
10 | * Indicates if this is an `isGLInstancedBufferAttribute`.
11 | */
12 | public isGLInstancedBufferAttribute = true;
13 | /**
14 | * The number of meshes that share the same attribute data.
15 | */
16 | public meshPerAttribute: number;
17 | /**
18 | * The data array that holds the attribute values.
19 | */
20 | public array: TypedArray;
21 | protected _cacheArray: TypedArray;
22 | /** @internal */ _needsUpdate = false;
23 |
24 | // HACK TO MAKE IT WORK WITHOUT UPDATE CORE
25 | /** @internal */ isInstancedBufferAttribute = true;
26 |
27 | /**
28 | * @param gl The WebGL2RenderingContext used to create the buffer.
29 | * @param type The type of data in the attribute.
30 | * @param itemSize The number of elements per attribute.
31 | * @param elementSize The size of individual elements in the array.
32 | * @param array The data array that holds the attribute values.
33 | * @param meshPerAttribute The number of meshes that share the same attribute data.
34 | */
35 | constructor(gl: WebGL2RenderingContext, type: GLenum, itemSize: number, elementSize: 1 | 2 | 4, array: TypedArray, meshPerAttribute = 1) {
36 | const buffer = gl.createBuffer();
37 | super(buffer, type, itemSize, elementSize, array.length / itemSize);
38 |
39 | this.meshPerAttribute = meshPerAttribute;
40 | this.array = array;
41 | this._cacheArray = array;
42 |
43 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
44 | gl.bufferData(gl.ARRAY_BUFFER, array, gl.DYNAMIC_DRAW);
45 | }
46 |
47 | /**
48 | * Updates the buffer data.
49 | * This method is designed to be called during the `onBeforeRender` callback.
50 | * It ensures that the attribute data is updated just before the rendering process begins.
51 | * @param renderer The WebGLRenderer used to render the scene.
52 | * @param count The number of elements to update in the buffer.
53 | */
54 | public update(renderer: WebGLRenderer, count: number): void {
55 | if (!this._needsUpdate || count === 0) return;
56 |
57 | const gl = renderer.getContext();
58 | gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
59 |
60 | if (this.array === this._cacheArray) {
61 | gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.array, 0, count);
62 | } else {
63 | gl.bufferData(gl.ARRAY_BUFFER, this.array, gl.DYNAMIC_DRAW);
64 | this._cacheArray = this.array;
65 | }
66 |
67 | this._needsUpdate = false;
68 | }
69 |
70 | /** @internal */
71 | public clone(): this {
72 | // This method is intentionally empty but necessary to avoid exceptions when cloning geometry.
73 | return this;
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/core/utils/InstancedRenderList.ts:
--------------------------------------------------------------------------------
1 | export type InstancedRenderItem = { index: number; depth: number; depthSort: number };
2 |
3 | /**
4 | * A class that creates and manages a list of render items, used to determine the rendering order based on depth.
5 | */
6 | export class InstancedRenderList {
7 | /**
8 | * The main array that holds the list of render items for instanced rendering.
9 | */
10 | public array: InstancedRenderItem[] = [];
11 | protected pool: InstancedRenderItem[] = [];
12 |
13 | /**
14 | * Adds a new render item to the list.
15 | * @param depth The depth value used for sorting or determining the rendering order.
16 | * @param index The unique instance id of the render item.
17 | */
18 | public push(depth: number, index: number): void {
19 | const pool = this.pool;
20 | const list = this.array;
21 | const count = list.length;
22 |
23 | if (count >= pool.length) {
24 | pool.push({ depth: null, index: null, depthSort: null });
25 | }
26 |
27 | const item = pool[count];
28 | item.depth = depth;
29 | item.index = index;
30 |
31 | list.push(item);
32 | }
33 |
34 | /**
35 | * Resets the render list by clearing the array.
36 | */
37 | public reset(): void {
38 | this.array.length = 0;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './core/InstancedEntity.js';
2 | export * from './core/InstancedMesh2.js';
3 | export * from './core/InstancedMeshBVH.js';
4 |
5 | export * from './core/feature/Capacity.js';
6 | export * from './core/feature/FrustumCulling.js';
7 | export * from './core/feature/Instances.js';
8 | export * from './core/feature/LOD.js';
9 | export * from './core/feature/Morph.js';
10 | export * from './core/feature/Raycasting.js';
11 | export * from './core/feature/Skeleton.js';
12 | export * from './core/feature/Uniforms.js';
13 |
14 | export * from './core/utils/GLInstancedBufferAttribute.js';
15 | export * from './core/utils/InstancedRenderList.js';
16 | export * from './core/utils/SquareDataTexture.js';
17 |
18 | export * from './shaders/ShaderChunk.js';
19 | export * from './shaders/chunks/instanced_color_pars_vertex.glsl';
20 | export * from './shaders/chunks/instanced_color_vertex.glsl';
21 | export * from './shaders/chunks/instanced_pars_vertex.glsl';
22 | export * from './shaders/chunks/instanced_skinning_pars_vertex.glsl';
23 | export * from './shaders/chunks/instanced_vertex.glsl';
24 |
25 | export * from './utils/SortingUtils.js';
26 | export * from './utils/CreateFrom.js';
27 |
--------------------------------------------------------------------------------
/src/shaders/ShaderChunk.ts:
--------------------------------------------------------------------------------
1 | import { ShaderChunk } from 'three';
2 | import instanced_pars_vertex from './chunks/instanced_pars_vertex.glsl';
3 | import instanced_color_pars_vertex from './chunks/instanced_color_pars_vertex.glsl';
4 | import instanced_vertex from './chunks/instanced_vertex.glsl';
5 | import instanced_color_vertex from './chunks/instanced_color_vertex.glsl';
6 | import instanced_skinning_pars_vertex from './chunks/instanced_skinning_pars_vertex.glsl';
7 |
8 | ShaderChunk['instanced_pars_vertex'] = instanced_pars_vertex;
9 | ShaderChunk['instanced_color_pars_vertex'] = instanced_color_pars_vertex;
10 | ShaderChunk['instanced_vertex'] = instanced_vertex;
11 | ShaderChunk['instanced_color_vertex'] = instanced_color_vertex;
12 |
13 | /**
14 | * Patches the given shader string by adding a condition for indirect instancing support.
15 | * @param shader The shader code to modify.
16 | * @returns The modified shader code with the additional instancing condition.
17 | */
18 | export function patchShader(shader: string): string {
19 | return shader.replace('#ifdef USE_INSTANCING', '#if defined USE_INSTANCING || defined USE_INSTANCING_INDIRECT');
20 | }
21 |
22 | ShaderChunk.project_vertex = patchShader(ShaderChunk.project_vertex);
23 | ShaderChunk.worldpos_vertex = patchShader(ShaderChunk.worldpos_vertex);
24 | ShaderChunk.defaultnormal_vertex = patchShader(ShaderChunk.defaultnormal_vertex);
25 |
26 | ShaderChunk.batching_pars_vertex = ShaderChunk.batching_pars_vertex.concat('\n#include ');
27 | ShaderChunk.color_pars_vertex = ShaderChunk.color_pars_vertex.concat('\n#include ');
28 | ShaderChunk['batching_vertex'] = ShaderChunk['batching_vertex'].concat('\n#include ');
29 |
30 | ShaderChunk.skinning_pars_vertex = instanced_skinning_pars_vertex;
31 |
32 | // TODO FIX don't override like this, create a new shaderChunk to make it works also with older three.js version
33 | if (ShaderChunk['morphinstance_vertex']) {
34 | ShaderChunk['morphinstance_vertex'] = ShaderChunk['morphinstance_vertex'].replaceAll('gl_InstanceID', 'instanceIndex');
35 | }
36 |
37 | // use 'getPatchedShader' function to make these example works
38 | // examples/jsm/modifiers/CurveModifier.js
39 | // examples/jsm/postprocessing/OutlinePass.js
40 |
--------------------------------------------------------------------------------
/src/shaders/chunks/instanced_color_pars_vertex.glsl:
--------------------------------------------------------------------------------
1 | #ifdef USE_INSTANCING_COLOR_INDIRECT
2 | uniform highp sampler2D colorsTexture;
3 |
4 | #ifdef USE_COLOR_ALPHA
5 | vec4 getColorTexture() {
6 | int size = textureSize( colorsTexture, 0 ).x;
7 | int j = int( instanceIndex );
8 | int x = j % size;
9 | int y = j / size;
10 | return texelFetch( colorsTexture, ivec2( x, y ), 0 );
11 | }
12 | #else
13 | vec3 getColorTexture() {
14 | int size = textureSize( colorsTexture, 0 ).x;
15 | int j = int( instanceIndex );
16 | int x = j % size;
17 | int y = j / size;
18 | return texelFetch( colorsTexture, ivec2( x, y ), 0 ).rgb;
19 | }
20 | #endif
21 | #endif
22 |
--------------------------------------------------------------------------------
/src/shaders/chunks/instanced_color_vertex.glsl:
--------------------------------------------------------------------------------
1 | #ifdef USE_INSTANCING_COLOR_INDIRECT
2 | #ifdef USE_VERTEX_COLOR
3 | vColor = color;
4 | #else
5 | #ifdef USE_COLOR_ALPHA
6 | vColor = vec4( 1.0 );
7 | #else
8 | vColor = vec3( 1.0 );
9 | #endif
10 | #endif
11 | #endif
12 |
--------------------------------------------------------------------------------
/src/shaders/chunks/instanced_pars_vertex.glsl:
--------------------------------------------------------------------------------
1 | #ifdef USE_INSTANCING_INDIRECT
2 | attribute uint instanceIndex;
3 | uniform highp sampler2D matricesTexture;
4 |
5 | mat4 getInstancedMatrix() {
6 | int size = textureSize( matricesTexture, 0 ).x;
7 | int j = int( instanceIndex ) * 4;
8 | int x = j % size;
9 | int y = j / size;
10 | vec4 v1 = texelFetch( matricesTexture, ivec2( x, y ), 0 );
11 | vec4 v2 = texelFetch( matricesTexture, ivec2( x + 1, y ), 0 );
12 | vec4 v3 = texelFetch( matricesTexture, ivec2( x + 2, y ), 0 );
13 | vec4 v4 = texelFetch( matricesTexture, ivec2( x + 3, y ), 0 );
14 | return mat4( v1, v2, v3, v4 );
15 | }
16 | #endif
17 |
--------------------------------------------------------------------------------
/src/shaders/chunks/instanced_skinning_pars_vertex.glsl:
--------------------------------------------------------------------------------
1 | #ifdef USE_SKINNING
2 | uniform mat4 bindMatrix;
3 | uniform mat4 bindMatrixInverse;
4 | uniform highp sampler2D boneTexture;
5 |
6 | #ifdef USE_INSTANCING_SKINNING
7 | uniform int bonesPerInstance;
8 | #endif
9 |
10 | mat4 getBoneMatrix( const in float i ) {
11 | int size = textureSize( boneTexture, 0 ).x;
12 |
13 | #ifdef USE_INSTANCING_SKINNING
14 | int j = ( bonesPerInstance * int( instanceIndex ) + int( i ) ) * 4;
15 | #else
16 | int j = int( i ) * 4;
17 | #endif
18 |
19 | int x = j % size;
20 | int y = j / size;
21 | vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 );
22 | vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 );
23 | vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 );
24 | vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 );
25 | return mat4( v1, v2, v3, v4 );
26 | }
27 | #endif
28 |
--------------------------------------------------------------------------------
/src/shaders/chunks/instanced_vertex.glsl:
--------------------------------------------------------------------------------
1 | #ifdef USE_INSTANCING_INDIRECT
2 | mat4 instanceMatrix = getInstancedMatrix();
3 |
4 | #ifdef USE_INSTANCING_COLOR_INDIRECT
5 | vColor *= getColorTexture();
6 | #endif
7 | #endif
8 |
--------------------------------------------------------------------------------
/src/utils/CreateFrom.ts:
--------------------------------------------------------------------------------
1 | import { InstancedBufferAttribute, InstancedMesh, Mesh, SkinnedMesh } from 'three';
2 | import { InstancedMesh2, InstancedMesh2Params } from '../core/InstancedMesh2.js';
3 |
4 | /**
5 | * Create an `InstancedMesh2` instance from an existing `Mesh` or `InstancedMesh`.
6 | * @param mesh The `Mesh` or `InstancedMesh` to create an `InstanceMesh2` from.
7 | * @param params Optional configuration parameters object. See `InstancedMesh2Params` for details.
8 | * @returns The created `InstancedMesh2` instance.
9 | */
10 | export function createInstancedMesh2From(mesh: Mesh, params: InstancedMesh2Params = {}): InstancedMesh2 {
11 | if ((mesh as SkinnedMesh).isSkinnedMesh) return createFromSkinnedMesh(mesh as SkinnedMesh);
12 | if ((mesh as InstancedMesh).isInstancedMesh) return createFromInstancedMesh(mesh as InstancedMesh);
13 | // TODO add morph support
14 | return new InstancedMesh2(mesh.geometry, mesh.material, params);
15 |
16 | function createFromSkinnedMesh(mesh: SkinnedMesh): InstancedMesh2 {
17 | const instancedMesh = new InstancedMesh2(mesh.geometry, mesh.material, params);
18 | instancedMesh.initSkeleton(mesh.skeleton);
19 | return instancedMesh;
20 | }
21 |
22 | function createFromInstancedMesh(mesh: InstancedMesh): InstancedMesh2 {
23 | params.capacity = Math.max(mesh.count, params.capacity);
24 |
25 | const geometry = mesh.geometry.clone();
26 | geometry.deleteAttribute('instanceIndex');
27 | warnIfInstancedAttribute();
28 |
29 | const instancedMesh = new InstancedMesh2(geometry, mesh.material, params);
30 |
31 | instancedMesh.position.copy(mesh.position);
32 | instancedMesh.quaternion.copy(mesh.quaternion);
33 | instancedMesh.scale.copy(mesh.scale);
34 |
35 | copyInstances();
36 | copyMatrices();
37 | copyColors();
38 | // TODO copy morph target?
39 |
40 | return instancedMesh;
41 |
42 | function copyInstances(): void {
43 | instancedMesh.setInstancesArrayCount(mesh.count);
44 | instancedMesh._instancesCount = mesh.count;
45 | instancedMesh.availabilityArray.fill(true, 0, mesh.count * 2);
46 | }
47 |
48 | function copyMatrices(): void {
49 | (instancedMesh.matricesTexture.image.data as Float32Array).set(mesh.instanceMatrix.array);
50 | }
51 |
52 | function copyColors(): void {
53 | if (mesh.instanceColor) {
54 | (instancedMesh as any).initColorsTexture();
55 |
56 | const rgbArray = mesh.instanceColor.array;
57 | const rgbaArray = instancedMesh.colorsTexture.image.data as Float32Array;
58 |
59 | for (let i = 0, j = 0; i < rgbArray.length; i += 3, j += 4) {
60 | rgbaArray[j] = rgbArray[i];
61 | rgbaArray[j + 1] = rgbArray[i + 1];
62 | rgbaArray[j + 2] = rgbArray[i + 2];
63 | rgbaArray[j + 3] = 1;
64 | }
65 | }
66 | }
67 |
68 | function warnIfInstancedAttribute(): void {
69 | const attributes = geometry.attributes;
70 | for (const name in attributes) {
71 | if ((attributes[name] as InstancedBufferAttribute).isInstancedBufferAttribute) {
72 | console.warn(`InstancedBufferAttribute "${name}" is not supported. It will be ignored.`);
73 | }
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/utils/SortingUtils.ts:
--------------------------------------------------------------------------------
1 | import { Material } from 'three';
2 | import { radixSort, RadixSortOptions } from 'three/addons/utils/SortUtils.js';
3 | import { InstancedMesh2 } from '../core/InstancedMesh2.js';
4 | import { InstancedRenderItem } from '../core/utils/InstancedRenderList.js';
5 |
6 | /**
7 | * Creates a radix sort function specifically for sorting `InstancedMesh2` instances.
8 | * The sorting is based on the `depth` property of each `InstancedRenderItem`.
9 | * This function dynamically adjusts for transparent materials by reversing the sort order if necessary.
10 | * @param target The `InstancedMesh2` instance that contains the instances to be sorted.
11 | * @returns A radix sort function.
12 | */
13 | // Reference: https://github.com/mrdoob/three.js/blob/master/examples/webgl_mesh_batch.html#L291
14 | export function createRadixSort(target: InstancedMesh2): typeof radixSort {
15 | const options: RadixSortOptions = {
16 | get: (el) => el.depthSort,
17 | aux: new Array(target._capacity),
18 | reversed: null
19 | };
20 |
21 | return function sortFunction(list: InstancedRenderItem[]): void {
22 | options.reversed = !!(target.material as Material)?.transparent;
23 |
24 | if (target._capacity > options.aux.length) {
25 | options.aux.length = target._capacity;
26 | }
27 |
28 | let minZ = Infinity;
29 | let maxZ = -Infinity;
30 |
31 | for (const { depth } of list) {
32 | if (depth > maxZ) maxZ = depth;
33 | if (depth < minZ) minZ = depth;
34 | }
35 |
36 | const depthDelta = maxZ - minZ;
37 | const factor = (2 ** 32 - 1) / depthDelta;
38 |
39 | for (const item of list) {
40 | item.depthSort = (item.depth - minZ) * factor;
41 | }
42 |
43 | radixSort(list, options);
44 | };
45 | }
46 |
47 | /** @internal */
48 | export function sortOpaque(a: InstancedRenderItem, b: InstancedRenderItem): number {
49 | return a.depth - b.depth;
50 | }
51 |
52 | /** @internal */
53 | export function sortTransparent(a: InstancedRenderItem, b: InstancedRenderItem): number {
54 | return b.depth - a.depth;
55 | }
56 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | "src/**/*.ts"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "NodeNext",
5 | "moduleResolution": "NodeNext",
6 | "strict": true,
7 | "declaration": true,
8 | "sourceMap": true,
9 | "declarationMap": true,
10 | "noImplicitOverride": true,
11 | "strictNullChecks": false,
12 | "stripInternal": true,
13 | "outDir": "dist/src",
14 | "esModuleInterop": true,
15 | "noImplicitAny": false,
16 | "skipLibCheck": true,
17 | "types": [
18 | "vite-plugin-glsl/ext"
19 | ]
20 | }
21 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path';
2 | import { defineConfig } from 'vite';
3 | import { viteStaticCopy } from 'vite-plugin-static-copy';
4 | import { externalizeDeps } from 'vite-plugin-externalize-deps';
5 | import glsl from 'vite-plugin-glsl';
6 |
7 | export default defineConfig(({ command }) => ({
8 | publicDir: command === 'build' ? false : 'public',
9 | build: {
10 | sourcemap: true,
11 | lib: {
12 | entry: resolve(__dirname, 'src/index.ts'),
13 | fileName: 'build/index',
14 | formats: ['es', 'cjs']
15 | }
16 | },
17 | plugins: [
18 | glsl(),
19 | externalizeDeps(),
20 | viteStaticCopy({
21 | targets: [{
22 | src: ['LICENSE', 'package.json', 'README.md'],
23 | dest: './'
24 | }]
25 | })
26 | ]
27 | }));
28 |
--------------------------------------------------------------------------------