├── .editorconfig
├── .eslintrc.json
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── codeql
│ └── codeql-config.yml
└── workflows
│ ├── codeql.yml
│ ├── examples-build.yml
│ └── node.js.yml
├── .gitignore
├── .nojekyll
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── benchmark
├── compare-bench-json.js
├── lib
│ ├── bench.js
│ └── logTable.js
├── run-bench-compare.js
├── run-benchmark.js
└── utils.js
├── docs
├── banner.png
├── example-sm.gif
└── screenshot.png
├── example
├── 3dtex_repro.html
├── 3dtex_repro.js
├── asyncGenerate.html
├── asyncGenerate.js
├── batchedMesh.html
├── batchedMesh.js
├── characterMovement.html
├── characterMovement.js
├── clippedEdges.html
├── clippedEdges.js
├── coi-serviceworker.js
├── collectTriangles.html
├── collectTriangles.js
├── cpuPathTracing.html
├── cpuPathTracing.js
├── diamond.html
├── diamond.js
├── distancecast.html
├── distancecast.js
├── edgeIntersect.html
├── edgeIntersect.js
├── gpuPathTracing.html
├── gpuPathTracing.js
├── gpuPathTracingSimple.html
├── gpuPathTracingSimple.js
├── inspector.html
├── inspector.js
├── lib
│ └── MarchingCubes.js
├── pathtracing
│ ├── ggxSampling.js
│ ├── materialSampling.js
│ └── utils.js
├── physics.html
├── physics.js
├── pointCloudIntersection.html
├── pointCloudIntersection.js
├── randomSampleDebug.html
├── randomSampleDebug.js
├── raycast.html
├── raycast.js
├── sculpt.html
├── sculpt.js
├── sdfGeneration.html
├── sdfGeneration.js
├── selection.html
├── selection.js
├── shapecast.html
├── shapecast.js
├── skinnedMesh.html
├── skinnedMesh.js
├── src
│ ├── Selection.js
│ └── computeSelectedTriangles.js
├── textures
│ ├── 3B6E10_E3F2C3_88AC2E_99CE51-256px.png
│ ├── 763C39_431510_210504_55241C-256px.png
│ ├── 7877EE_D87FC5_75D9C7_1C78C0-256px.png
│ └── B67F6B_4B2E2A_6C3A34_F3DBC6-256px.png
├── triangleIntersect.html
├── triangleIntersect.js
├── utils
│ ├── GenerateSDFMaterial.js
│ ├── RayMarchSDFMaterial.js
│ ├── RenderSDFLayerMaterial.js
│ ├── edgeUtils.js
│ └── math
│ │ ├── getConvexHull.js
│ │ ├── lineCrossesLine.js
│ │ └── pointRayCrossesSegments.js
├── voxelize.html
└── voxelize.js
├── jest.config.json
├── package-lock.json
├── package.json
├── rollup-templating.config.js
├── rollup.config.js
├── src
├── core
│ ├── Constants.js
│ ├── MeshBVH.js
│ ├── MeshBVHNode.js
│ ├── build
│ │ ├── buildTree.js
│ │ ├── buildUtils.js
│ │ ├── computeBoundsUtils.js
│ │ ├── geometryUtils.js
│ │ ├── sortUtils.template.js
│ │ └── splitUtils.js
│ ├── cast
│ │ ├── bvhcast.js
│ │ ├── closestPointToGeometry.template.js
│ │ ├── closestPointToPoint.js
│ │ ├── intersectsGeometry.template.js
│ │ ├── raycast.template.js
│ │ ├── raycastFirst.template.js
│ │ ├── refit.template.js
│ │ └── shapecast.js
│ └── utils
│ │ ├── BufferStack.js
│ │ ├── intersectUtils.js
│ │ ├── iterationUtils.template.js
│ │ └── nodeBufferUtils.js
├── debug
│ └── Debug.js
├── gpu
│ ├── BVHShaderGLSL.js
│ ├── MeshBVHUniformStruct.js
│ ├── VertexAttributeTexture.js
│ └── glsl
│ │ ├── bvh_distance_functions.glsl.js
│ │ ├── bvh_ray_functions.glsl.js
│ │ ├── bvh_struct_definitions.glsl.js
│ │ └── common_functions.glsl.js
├── index.d.ts
├── index.js
├── math
│ ├── ExtendedTriangle.js
│ ├── MathUtilities.js
│ ├── OrientedBox.js
│ └── SeparatingAxisBounds.js
├── objects
│ └── MeshBVHHelper.js
├── utils
│ ├── ArrayBoxUtilities.js
│ ├── BufferUtils.js
│ ├── ExtendedTrianglePool.js
│ ├── ExtensionUtilities.js
│ ├── GeometryRayIntersectUtilities.js
│ ├── PrimitivePool.js
│ ├── StaticGeometryGenerator.js
│ ├── ThreeRayIntersectUtilities.js
│ └── TriangleUtilities.js
└── workers
│ ├── GenerateMeshBVHWorker.js
│ ├── ParallelMeshBVHWorker.js
│ ├── generateMeshBVH.worker.js
│ ├── parallelMeshBVH.worker.js
│ └── utils
│ ├── WorkerBase.js
│ └── WorkerPool.js
├── test
├── Math.OBB.test.js
├── Math.SphereIntersections.test.js
├── Math.TriangleIntersections.test.js
├── MeshBVH.options.test.js
├── MeshBVH.serialize.test.js
├── MeshBVH.test.js
├── MeshBVHUniformStruct.test.js
├── RandomRaycasts.test.js
├── ShapeCasts.test.js
├── TypescriptImportTest.ts
├── Utils.geometryUtils.test.js
├── VertexAttributeTexture.test.js
├── babel.config.json
├── data
│ └── points.bin
├── repro
│ ├── FloatingPointBoundsError.test.js
│ └── RaycastsRepro.test.js
└── utils.js
├── tsconfig.json
└── vite.config.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = tab
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [ "eslint:recommended", "./node_modules/eslint-config-mdcs/index.js" ],
4 | "plugins": [ "jest" ],
5 | "ignorePatterns": [ "**/*.generated.js" ],
6 | "rules": {
7 | "no-inner-declarations": "off",
8 | "no-constant-condition": "off",
9 | // "no-constant-condition": [ "error", { "checkLoops": "exceptWhileTrue" } ],
10 |
11 | "jest/no-disabled-tests": "warn",
12 | "jest/no-focused-tests": "error",
13 | "jest/no-identical-title": "error",
14 | "jest/prefer-to-have-length": "warn",
15 | "jest/valid-expect": "error"
16 | },
17 | "env": {
18 | "jest/globals": true
19 | },
20 | "overrides": [
21 | {
22 | "files": ["*.ts"],
23 | "parser": "@typescript-eslint/parser",
24 | "plugins": [
25 | "@typescript-eslint"
26 | ],
27 | "rules": {
28 | "no-unused-vars": ["error", { "args": "none" }],
29 | "indent": [2, 2]
30 | }
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: gkjohnson
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Report a reproducible bug or regression.
4 | title: ''
5 | labels: 'bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 | **Describe the bug**
13 |
14 | A clear and concise description of what the bug is. Before submitting, please remove unnecessary sections.
15 |
16 | **To Reproduce**
17 |
18 | Steps to reproduce the behavior:
19 | 1. Go to '...'
20 | 2. Click on '....'
21 | 3. See error
22 |
23 | ***Code***
24 |
25 | ```js
26 | // code goes here
27 | ```
28 |
29 | ***Live example***
30 |
31 | -
32 |
33 | **Expected behavior**
34 |
35 | A clear and concise description of what you expected to happen.
36 |
37 | **Screenshots**
38 |
39 | If applicable, add screenshots to help explain your problem (drag and drop the image).
40 |
41 | **Platform:**
42 |
43 | - Device: [Desktop, Mobile, ...]
44 | - OS: [Windows, MacOS, Linux, Android, iOS, ...]
45 | - Browser: [Chrome, Firefox, Safari, Edge, ...]
46 | - Three.js version: [r???]
47 | - Library version: [v???]
48 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for the project.
4 | title: ''
5 | labels: 'enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 |
11 |
12 | **Is your feature request related to a problem? Please describe.**
13 |
14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
15 |
16 | **Describe the solution you'd like**
17 |
18 | A clear and concise description of what you want to happen.
19 |
20 | **Describe alternatives you've considered**
21 |
22 | A clear and concise description of any alternative solutions or features you've considered.
23 |
24 | **Additional context**
25 |
26 | Add any other context or screenshots about the feature request here.
27 |
--------------------------------------------------------------------------------
/.github/codeql/codeql-config.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL config"
2 |
3 | paths-ignore:
4 | - "node_modules"
5 | - "**/*.template.js"
6 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "master" ]
6 | pull_request:
7 | branches: [ "*" ]
8 |
9 | schedule:
10 | - cron: "57 15 * * 0"
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 | permissions:
17 | actions: read
18 | contents: read
19 | security-events: write
20 |
21 | strategy:
22 | fail-fast: false
23 | matrix:
24 | language: [ javascript ]
25 |
26 | steps:
27 | - name: Checkout
28 | uses: actions/checkout@v3
29 | - run: npm ci
30 | - run: npm run build
31 |
32 | - name: Initialize CodeQL
33 | uses: github/codeql-action/init@v2
34 | with:
35 | languages: ${{ matrix.language }}
36 | queries: +security-and-quality
37 |
38 | - name: Autobuild
39 | uses: github/codeql-action/autobuild@v2
40 |
41 | - name: Perform CodeQL Analysis
42 | uses: github/codeql-action/analyze@v2
43 | with:
44 | category: "/language:${{ matrix.language }}"
45 |
--------------------------------------------------------------------------------
/.github/workflows/examples-build.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3 |
4 | name: Build Examples
5 |
6 | on:
7 | push:
8 | branches: [ master ]
9 |
10 | jobs:
11 | build:
12 |
13 | runs-on: ubuntu-latest
14 |
15 | strategy:
16 | matrix:
17 | node-version: [20.x]
18 |
19 | steps:
20 | - uses: actions/checkout@v2
21 |
22 | - name: Use Node.js ${{ matrix.node-version }}
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: ${{ matrix.node-version }}
26 | cache: 'npm'
27 | - run: npm ci
28 | - run: npm run build-examples
29 |
30 | - name: Commit Examples
31 | uses: EndBug/add-and-commit@v7
32 | with:
33 | add: 'example/bundle'
34 | message: 'update builds'
35 | push: 'origin HEAD:examples --force'
36 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
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 | three-version: [0.159.0, 0.168.0, latest]
21 |
22 | steps:
23 | - uses: actions/checkout@v2
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 install three@${{ matrix.three-version }}
31 | - run: npm run build
32 | - run: npm run benchmark
33 | - run: npm run lint
34 | - run: npm test
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # MacOS
2 | *.DS_Store
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (http://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # Typescript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | example/dev-bundle
64 | .parcel-cache
65 |
66 | *.generated.js
67 |
68 | build/**
69 |
--------------------------------------------------------------------------------
/.nojekyll:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in contributing to the project!
4 |
5 | Contributions of all kinds are welcome including pull requests, issues, and reports of or links to repos using the project!
6 |
7 | ## Filing Issues
8 |
9 | When submitting a bug report try to include a clear, minimal repro case along with the issue. More information means the problem can be fixed faster and better!
10 |
11 | When submitting a feature request please include a well-defined use case and even better if you include code modeling how the new feature could be used with a proposed API!
12 |
13 | Promote discussion! Let's talk about the change and discuss what the best, most flexible option might be.
14 |
15 | ## Developer setup
16 |
17 | To develop and test changes to this library, make sure you have Node and NPM installed.
18 | Check the supported versions in [the test configuration](./.github/workflows/node.js.yml).
19 |
20 | In order to install dependencies, you will need `make` and a C++ compiler available.
21 | On Debian or Ubuntu, run `sudo apt install build-essential`.
22 |
23 | - To install dependencies, run `npm install`
24 | - Once that has successfully completed, make sure all tests pass by running `npm test`. If any tests failed, you'll see red output indicating the number of failures. This may indicate a problem with your setup
25 | - If you want to check the library's performance on different systems or after making changes, run `npm run benchmark`
26 |
27 | ## Pull Requests
28 |
29 | Keep it simple! Code clean up and linting changes should be submitted as separate PRS from logic changes so the impact to the codebase is clear.
30 |
31 | Keep PRs with logic changes to the essential modifications if possible -- people have to read it!
32 |
33 | Open an issue for discussion first so we can have consensus on the change and be sure to reference the issue that the PR is addressing.
34 |
35 | Keep commit messages descriptive. "Update" and "oops" doesn't tell anyone what happened there!
36 |
37 | Don't modify existing commits when responding to PR comments. New commits make it easier to follow what changed.
38 |
39 | ## Code Style
40 |
41 | Follow the `.editorconfig`, `.babelrc`, `.stylelintrc`, and `.htmlhintrc` style configurations included in the repo to keep the code looking consistent.
42 |
43 | Try to keep code as clear as possible! Code for readability! For example longer, descriptive variable names are preferred to short ones. If a line of code includes a lot of nested statements (even just one or two) consider breaking the line up into multiple variables to improve the clarity of what's happening.
44 |
45 | Include comments describing _why_ a change was made. If code was moved from one part of a function to another then tell what happened and why the change got made so it doesn't get moved back. Comments aren't just for others, they're for your future self, too!
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Garrett Johnson
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 |
--------------------------------------------------------------------------------
/benchmark/compare-bench-json.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import { fileURLToPath } from 'url';
3 | import { dirname, join } from 'path';
4 |
5 | const CRITICAL_ONLY = process.argv.includes( '--critical' );
6 | const __filename = fileURLToPath( import.meta.url );
7 | const __dirname = dirname( __filename );
8 |
9 | const prData = JSON.parse( fs.readFileSync( join( __dirname, '../pr-benchmark.json' ) ) );
10 | const maData = JSON.parse( fs.readFileSync( join( __dirname, '../master-benchmark.json' ) ) );
11 |
12 | const exclude = [ 'iterations', 'name', 'min', 'max' ];
13 | for ( let i = 0; i < prData.length; i ++ ) {
14 |
15 | const prInfo = prData[ i ];
16 | const maInfo = maData[ i ];
17 |
18 | const prResults = prInfo.results;
19 | const maResults = maInfo.results;
20 |
21 | let finalTable = '';
22 | for ( let j = 0; j < prResults.length; j ++ ) {
23 |
24 | const prData = prResults[ j ];
25 | const maData = maResults[ j ];
26 |
27 | let result = '';
28 | for ( const key in prData ) {
29 |
30 | if ( exclude.includes( key ) ) continue;
31 |
32 | const prValue = prData[ key ];
33 | const maValue = maData[ key ];
34 | const delta = prValue - maValue;
35 | const perc = delta === 0 ? 0 : delta / maValue;
36 |
37 | if ( CRITICAL_ONLY && perc > 0.03 || ! CRITICAL_ONLY ) {
38 |
39 | const star = perc > 0.03 ? '* ' : ' ';
40 | result += `| ${ star } ${ key } | ${ maValue.toFixed( 5 ) } ms | ${ prValue.toFixed( 5 ) } ms | ${ delta.toFixed( 5 ) } ms | ${ ( perc * 100 ).toFixed( 5 ) } % |\n`;
41 |
42 | }
43 |
44 | }
45 |
46 | if ( result ) {
47 |
48 | finalTable += `| ${ prData.name } | | | | |\n`;
49 | finalTable += result;
50 |
51 | }
52 |
53 | }
54 |
55 | if ( finalTable ) {
56 |
57 | console.log( `\n**${ prInfo.name }**` );
58 | console.log( '| | before | after | delta | increase |' );
59 | console.log( '|---|---|---|---|---|' );
60 | console.log( finalTable );
61 |
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/benchmark/lib/bench.js:
--------------------------------------------------------------------------------
1 | import { logTable } from './logTable.js';
2 |
3 | const LOG_JSON = process.argv.includes( '--json' );
4 | const LONG_RUNNING = process.argv.includes( '--long' );
5 | let _maxTime = LONG_RUNNING ? 15000 : 3000;
6 | let _maxIterations = LONG_RUNNING ? 1000 : 100;
7 | let _prewarmIterations = LONG_RUNNING ? 15 : 5;
8 |
9 | const _beforeAll = [];
10 | const _beforeEach = [];
11 | const _afterAll = [];
12 | const _afterEach = [];
13 | const _bench = [];
14 |
15 | const _suites = [];
16 | let _current = null;
17 |
18 | process.on( 'exit', () => {
19 |
20 | if ( LOG_JSON ) {
21 |
22 | console.log( JSON.stringify( _suites, undefined, ' ' ) );
23 |
24 | }
25 |
26 | } );
27 |
28 | function findMedian( values ) {
29 |
30 | values.sort( ( a, b ) => a - b );
31 | const length = values.length;
32 | if ( length % 2 === 1 ) {
33 |
34 | return values[ Math.floor( length / 2 ) ];
35 |
36 | } else {
37 |
38 | const v1 = values[ length / 2 - 1 ];
39 | const v2 = values[ length / 2 ];
40 | return ( v1 + v2 ) / 2;
41 |
42 | }
43 |
44 | }
45 |
46 | export function suite( name, cb ) {
47 |
48 | name = name.trim();
49 |
50 | cb();
51 |
52 | _beforeAll.forEach( cb => cb() );
53 | _current = [];
54 | for ( let i = 0, l = _bench.length; i < l; i ++ ) {
55 |
56 | _beforeEach.forEach( cb => cb() );
57 |
58 | let iterations = 0;
59 | let elapsed = 0;
60 | let minTime = Infinity;
61 | let maxTime = - Infinity;
62 | let times = [];
63 | let delta, start;
64 | const { name, run, prerun } = _bench[ i ];
65 |
66 | for ( let j = 0; j < _prewarmIterations; j ++ ) {
67 |
68 | if ( prerun ) prerun();
69 | run();
70 |
71 | }
72 |
73 | while ( elapsed < _maxTime ) {
74 |
75 | if ( prerun ) prerun();
76 | start = performance.now();
77 | run();
78 | delta = performance.now() - start;
79 | elapsed += delta;
80 |
81 | iterations ++;
82 | maxTime = Math.max( maxTime, delta );
83 | minTime = Math.min( minTime, delta );
84 | times.push( delta );
85 |
86 | if ( iterations >= _maxIterations ) break;
87 |
88 | }
89 |
90 | _afterEach.forEach( cb => cb() );
91 |
92 | _current.push( {
93 | name,
94 | mean: elapsed / iterations,
95 | median: findMedian( times ),
96 | min: minTime,
97 | max: maxTime,
98 | iterations,
99 | } );
100 |
101 | }
102 |
103 | _afterAll.forEach( cb => cb() );
104 |
105 | _suites.push( { name, results: _current } );
106 | _current = null;
107 |
108 | if ( ! LOG_JSON ) {
109 |
110 | logTable( _suites[ 0 ], [ 'mean', 'median', 'min', 'max' ] );
111 | _suites.length = 0;
112 |
113 | }
114 |
115 | _afterAll.length = 0;
116 | _afterEach.length = 0;
117 | _beforeAll.length = 0;
118 | _beforeEach.length = 0;
119 | _bench.length = 0;
120 |
121 | }
122 |
123 | export function bench( name, prerun, run ) {
124 |
125 | name = name.trim();
126 |
127 | if ( run === undefined ) {
128 |
129 | run = prerun;
130 | prerun = undefined;
131 |
132 | }
133 |
134 | _bench.push( { prerun, run, name } );
135 |
136 | }
137 |
138 | export function beforeAll( cb ) {
139 |
140 | _beforeAll.push( cb );
141 |
142 | }
143 |
144 | export function beforeEach( cb ) {
145 |
146 | _beforeEach.push( cb );
147 |
148 | }
149 |
150 | export function afterEach( cb ) {
151 |
152 | _afterEach.push( cb );
153 |
154 | }
155 |
156 | export function afterAll( cb ) {
157 |
158 | _afterAll.push( cb );
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/benchmark/lib/logTable.js:
--------------------------------------------------------------------------------
1 |
2 | const NAME_WIDTH = 35;
3 | const COLUMN_WIDTH = 20;
4 | const SEPARATOR = '|';
5 | const SPACE = ' ';
6 |
7 | function pad( str, len, char = ' ' ) {
8 |
9 | let res = str;
10 | while ( res.length < len ) {
11 |
12 | res += char;
13 |
14 | }
15 |
16 | return res;
17 |
18 | }
19 |
20 | export function logObjectAsRows( info, exclude = [ 'name', 'iterations', 'table' ], depth = 1 ) {
21 |
22 | for ( const key in info ) {
23 |
24 | if ( exclude.includes( key ) ) continue;
25 |
26 | if ( typeof info[ key ] === 'object' ) {
27 |
28 | logObjectAsRows( info[ key ], exclude, depth + 1 );
29 |
30 | } else {
31 |
32 | const value = typeof info[ key ] === 'string' ? info[ key ] : `${ info[ key ].toFixed( 5 ) } ms`;
33 | console.log( SEPARATOR + pad( pad( '', depth, SPACE ) + key, NAME_WIDTH ) + SEPARATOR + pad( value, COLUMN_WIDTH ) + SEPARATOR );
34 |
35 | }
36 |
37 | }
38 |
39 | }
40 |
41 | export function logTable( info, columns = [] ) {
42 |
43 | if ( info.name ) {
44 |
45 | console.log( `**${ info.name }**` );
46 |
47 | }
48 |
49 | if ( columns.length > 0 ) {
50 |
51 | let row = SEPARATOR + pad( '', NAME_WIDTH ) + SEPARATOR;
52 | let split = '|---|';
53 | columns.forEach( key => {
54 |
55 | row += pad( key, COLUMN_WIDTH ) + SEPARATOR;
56 | split += '---|';
57 |
58 | } );
59 | console.log( row );
60 | console.log( split );
61 |
62 | } else {
63 |
64 | console.log( '| | Values |' );
65 | console.log( '|---|---|' );
66 |
67 | }
68 |
69 | info.results.forEach( data => {
70 |
71 | if ( data.table ) {
72 |
73 | console.log( SEPARATOR + pad( data.name, NAME_WIDTH ) + SEPARATOR );
74 | logObjectAsRows( data.table );
75 |
76 | } else if ( columns.length > 0 ) {
77 |
78 | let row = SEPARATOR + pad( data.name, NAME_WIDTH ) + SEPARATOR;
79 | columns.forEach( key => {
80 |
81 | if ( ! ( key in data ) ) {
82 |
83 | row += pad( `--`, COLUMN_WIDTH ) + SEPARATOR;
84 |
85 | } else {
86 |
87 | const value = typeof data[ key ] === 'string' ? data[ key ] : `${ data[ key ].toFixed( 5 ) } ms`;
88 | row += pad( value, COLUMN_WIDTH ) + SEPARATOR;
89 |
90 | }
91 |
92 | } );
93 | console.log( row );
94 |
95 | } else {
96 |
97 | console.log( SEPARATOR + pad( data.name, NAME_WIDTH ) + SEPARATOR );
98 | logObjectAsRows( data );
99 |
100 | }
101 |
102 | } );
103 | console.log();
104 |
105 | }
106 |
--------------------------------------------------------------------------------
/benchmark/run-bench-compare.js:
--------------------------------------------------------------------------------
1 | import simpleGit from 'simple-git';
2 | import { exec } from 'child_process';
3 |
4 | ( async() => {
5 |
6 | const git = simpleGit();
7 | const status = await git.status();
8 |
9 | const modified = status.modified.length + status.created.length + status.renamed.length + status.deleted.length;
10 | if ( modified !== 0 ) {
11 |
12 | console.error( 'Current branch is not clean' );
13 | process.exit( 1 );
14 |
15 | }
16 |
17 | const currentBranch = status.current;
18 |
19 | console.log( 'Running Benchmark' );
20 | // await runScript( 'npm run build-silent' );
21 | await runScript( 'node ./benchmark/run-benchmark.js --long --json > pr-benchmark.json' );
22 |
23 | console.log( 'Running Master Benchmark' );
24 | await git.checkout( 'master' );
25 | // await runScript( 'npm run build-silent' );
26 | await runScript( 'node ./benchmark/run-benchmark.js --long --json > master-benchmark.json' );
27 |
28 | console.log( 'Comparing Benchmarks' );
29 | console.log();
30 |
31 | await runScript( 'node ./benchmark/compare-bench-json.js --critical' );
32 | console.log( 'Full Benchmark
' );
33 |
34 | await runScript( 'node ./benchmark/compare-bench-json.js' );
35 | console.log( ' ' );
36 |
37 | await git.checkout( currentBranch );
38 |
39 | } )();
40 |
41 | function runScript( command ) {
42 |
43 | return new Promise( ( resolve, reject ) => {
44 |
45 | const proc = exec( command );
46 | proc.stderr.pipe( process.stderr );
47 | proc.stdout.pipe( process.stdout );
48 | proc.on( 'exit', code => {
49 |
50 | if ( code === 0 ) resolve();
51 | else reject();
52 |
53 | } );
54 |
55 | } );
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/benchmark/utils.js:
--------------------------------------------------------------------------------
1 | import { BoxGeometry, Vector3 } from 'three';
2 |
3 | export function generateGroupGeometry( complexity ) {
4 |
5 | const geometry = new BoxGeometry( 1, 1, 1, complexity, complexity, complexity );
6 | const position = geometry.attributes.position;
7 | const vertCount = position.count;
8 | const vec = new Vector3();
9 | for ( let i = 0; i < vertCount; i ++ ) {
10 |
11 | vec.fromBufferAttribute( position, i );
12 | vec.normalize();
13 | position.setXYZ( i, vec.x, vec.y, vec.z );
14 |
15 | }
16 |
17 | return geometry;
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/docs/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/docs/banner.png
--------------------------------------------------------------------------------
/docs/example-sm.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/docs/example-sm.gif
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/docs/screenshot.png
--------------------------------------------------------------------------------
/example/3dtex_repro.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Fast SDF Generation
5 |
6 |
7 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/example/asyncGenerate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Async BVH Generation
5 |
6 |
7 |
11 |
12 |
52 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/example/batchedMesh.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Complex Geometry Raycasting
5 |
6 |
7 |
30 |
31 |
32 |
33 | BatchedMesh with 3 geometries.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/characterMovement.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Character Movement
5 |
6 |
7 |
33 |
34 |
35 |
36 | Basic character movement example. Click and drag to rotate the camera direction and use WASD to move.
37 |
38 |
39 |
40 |
41 | Model by
Warkarma on Sketchfab.
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/example/clippedEdges.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Clipped Edges
5 |
6 |
7 |
44 |
45 |
46 |
47 |
48 | Using MeshBVH to quickly detect plane-clipped triangle edges on static,
49 |
50 | merged 2 million polygon model. Stencil buffer used for solid clip cap.
51 |
52 |
53 | Model by
T-FLEX CAD on Sketchfab.
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/coi-serviceworker.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // copied from https://github.com/gzuidhof/coi-serviceworker
3 |
4 | /*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
5 | let coepCredentialless = false;
6 | if (typeof window === 'undefined') {
7 | self.addEventListener("install", () => self.skipWaiting());
8 | self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
9 |
10 | self.addEventListener("message", (ev) => {
11 | if (!ev.data) {
12 | return;
13 | } else if (ev.data.type === "deregister") {
14 | self.registration
15 | .unregister()
16 | .then(() => {
17 | return self.clients.matchAll();
18 | })
19 | .then(clients => {
20 | clients.forEach((client) => client.navigate(client.url));
21 | });
22 | } else if (ev.data.type === "coepCredentialless") {
23 | coepCredentialless = ev.data.value;
24 | }
25 | });
26 |
27 | self.addEventListener("fetch", function (event) {
28 | const r = event.request;
29 | if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
30 | return;
31 | }
32 |
33 | const request = (coepCredentialless && r.mode === "no-cors")
34 | ? new Request(r, {
35 | credentials: "omit",
36 | })
37 | : r;
38 | event.respondWith(
39 | fetch(request)
40 | .then((response) => {
41 | if (response.status === 0) {
42 | return response;
43 | }
44 |
45 | const newHeaders = new Headers(response.headers);
46 | newHeaders.set("Cross-Origin-Embedder-Policy",
47 | coepCredentialless ? "credentialless" : "require-corp"
48 | );
49 | if (!coepCredentialless) {
50 | newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
51 | }
52 | newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
53 |
54 | return new Response(response.body, {
55 | status: response.status,
56 | statusText: response.statusText,
57 | headers: newHeaders,
58 | });
59 | })
60 | .catch((e) => console.error(e))
61 | );
62 | });
63 |
64 | } else {
65 | (() => {
66 | // You can customize the behavior of this script through a global `coi` variable.
67 | const coi = {
68 | shouldRegister: () => true,
69 | shouldDeregister: () => false,
70 | coepCredentialless: () => !(window.chrome || window.netscape),
71 | doReload: () => window.location.reload(),
72 | quiet: false,
73 | ...window.coi
74 | };
75 |
76 | const n = navigator;
77 |
78 | if (n.serviceWorker && n.serviceWorker.controller) {
79 | n.serviceWorker.controller.postMessage({
80 | type: "coepCredentialless",
81 | value: coi.coepCredentialless(),
82 | });
83 |
84 | if (coi.shouldDeregister()) {
85 | n.serviceWorker.controller.postMessage({ type: "deregister" });
86 | }
87 | }
88 |
89 | // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
90 | // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
91 | if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
92 |
93 | if (!window.isSecureContext) {
94 | !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
95 | return;
96 | }
97 |
98 | // In some environments (e.g. Chrome incognito mode) this won't be available
99 | if (n.serviceWorker) {
100 | n.serviceWorker.register(window.document.currentScript.src).then(
101 | (registration) => {
102 | !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
103 |
104 | registration.addEventListener("updatefound", () => {
105 | !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
106 | coi.doReload();
107 | });
108 |
109 | // If the registration is active, but it's not controlling the page
110 | if (registration.active && !n.serviceWorker.controller) {
111 | !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
112 | coi.doReload();
113 | }
114 | },
115 | (err) => {
116 | !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
117 | }
118 | );
119 | }
120 | })();
121 | }
122 |
--------------------------------------------------------------------------------
/example/collectTriangles.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Collect Triangles
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/cpuPathTracing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - CPU Path Tracing
5 |
6 |
7 |
47 |
48 |
49 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/example/diamond.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Total Internal Refraction
6 |
7 |
8 |
35 |
36 |
37 |
38 |
39 | Using shader ray tracing to model internal reflection for a diamond material.
40 |
41 | Model from
Sketchfab. Full material implementation available
here.
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/example/distancecast.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Closest Distance
5 |
6 |
7 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/edgeIntersect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Edge Intersection
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/gpuPathTracing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - GPU Path Tracing
5 |
6 |
7 |
42 |
43 |
44 |
45 | Lambertian material Path Tracer implemented using BVH intersections on the GPU.
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/example/gpuPathTracingSimple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - GPU Path Tracing
5 |
6 |
7 |
30 |
31 |
32 |
33 | Simple example of running BVH intersections on an animated geometry on the GPU in a shader.
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/inspector.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - BVH Inspector
5 |
6 |
7 |
49 |
50 |
51 |
52 | BVH inspector showing the number of traversals at a given pixel, various BVH construction stats, and basic raycast benchmark.
53 |
54 |
55 | Pixels with traversal counts above the "traversalTreshold" are highlighted in red.
56 |
57 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/example/pathtracing/ggxSampling.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 |
3 | const _V = new Vector3();
4 | const _T1 = new Vector3();
5 | const _T2 = new Vector3();
6 | const _N = new Vector3();
7 | const _Z_VECTOR = new Vector3( 0, 0, 1 );
8 | const M_PI = Math.PI;
9 |
10 | // The GGX functions provide sampling and distribution information for normals as output so
11 | // in order to get probability of scatter direction the half vector must be computed and provided.
12 | // [0] https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
13 | // [1] https://hal.archives-ouvertes.fr/hal-01509746/document
14 | // [2] http://jcgt.org/published/0007/04/01/
15 | // [4] http://jcgt.org/published/0003/02/03/
16 |
17 | export function ggxDirection( incidentDir, roughnessX, roughnessY, random1, random2, target ) {
18 |
19 | // TODO: try GGXVNDF implementation from reference [2], here. Needs to update ggxDistribution
20 | // function below, as well
21 |
22 | // Implementation from reference [1]
23 | // stretch view
24 | const V = _V.set( roughnessX * incidentDir.x, roughnessY * incidentDir.y, incidentDir.z ).normalize();
25 |
26 | // orthonormal basis
27 | const T1 = ( V.z < 0.9999 ) ? _T1.crossVectors( V, _Z_VECTOR ).normalize() : _T1.set( 1, 0, 0 );
28 | const T2 = _T2.crossVectors( T1, V );
29 |
30 | // sample point with polar coordinates (r, phi)
31 | const a = 1.0 / ( 1.0 + V.z );
32 | const r = Math.sqrt( random1 );
33 | const phi = ( random2 < a ) ? random2 / a * M_PI : M_PI + ( random2 - a ) / ( 1.0 - a ) * M_PI;
34 | const P1 = r * Math.cos( phi );
35 | const P2 = r * Math.sin( phi ) * ( ( random2 < a ) ? 1.0 : V.z );
36 |
37 | // compute normal
38 | T1.multiplyScalar( P1 );
39 | T2.multiplyScalar( P2 );
40 | const N = _N.addVectors( T1, T2 ).addScaledVector( V, Math.sqrt( Math.max( 0.0, 1.0 - P1 * P1 - P2 * P2 ) ) );
41 |
42 | // unstretch
43 | N.x *= roughnessX;
44 | N.y *= roughnessY;
45 | N.z = Math.max( 0.0, N.z );
46 | N.normalize();
47 |
48 | target.copy( N );
49 |
50 | return target;
51 |
52 | }
53 |
54 | // Below are PDF and related functions for use in a Monte Carlo path tracer
55 | // as specified in Appendix B of the following paper
56 | // See equation (2) from reference [2]
57 | function ggxLamda( theta, roughness ) {
58 |
59 | const tanTheta = Math.tan( theta );
60 | const tanTheta2 = tanTheta * tanTheta;
61 | const alpha2 = roughness * roughness;
62 |
63 | const numerator = - 1 + Math.sqrt( 1 + alpha2 * tanTheta2 );
64 | return numerator / 2;
65 |
66 | }
67 |
68 | // See equation (2) from reference [2]
69 | export function ggxShadowMaskG1( theta, roughness ) {
70 |
71 | return 1.0 / ( 1.0 + ggxLamda( theta, roughness ) );
72 |
73 | }
74 |
75 | // See equation (125) from reference [4]
76 | export function ggxShadowMaskG2( wi, wo, roughness ) {
77 |
78 | const incidentTheta = Math.acos( wi.z );
79 | const scatterTheta = Math.acos( wo.z );
80 | return 1.0 / ( 1 + ggxLamda( incidentTheta, roughness ) + ggxLamda( scatterTheta, roughness ) );
81 |
82 | }
83 |
84 | export function ggxDistribution( halfVector, roughness ) {
85 |
86 | // See equation (33) from reference [0]
87 | const a2 = roughness * roughness;
88 | const cosTheta = halfVector.z;
89 | const cosTheta4 = Math.pow( cosTheta, 4 );
90 |
91 | if ( cosTheta === 0 ) return 0;
92 |
93 | const theta = Math.acos( halfVector.z );
94 | const tanTheta = Math.tan( theta );
95 | const tanTheta2 = Math.pow( tanTheta, 2 );
96 |
97 | const denom = Math.PI * cosTheta4 * Math.pow( a2 + tanTheta2, 2 );
98 | return a2 / denom;
99 |
100 | // See equation (1) from reference [2]
101 | // const { x, y, z } = halfVector;
102 | // const a2 = roughness * roughness;
103 | // const mult = x * x / a2 + y * y / a2 + z * z;
104 | // const mult2 = mult * mult;
105 |
106 | // return 1.0 / Math.PI * a2 * mult2;
107 |
108 | }
109 |
110 | // See equation (3) from reference [2]
111 | export function ggxPDF( wi, halfVector, roughness ) {
112 |
113 | const incidentTheta = Math.acos( wi.z );
114 | const D = ggxDistribution( halfVector, roughness );
115 | const G1 = ggxShadowMaskG1( incidentTheta, roughness );
116 |
117 | return D * G1 * Math.max( 0.0, wi.dot( halfVector ) ) / wi.z;
118 |
119 | }
120 |
--------------------------------------------------------------------------------
/example/pathtracing/utils.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 |
3 | const tempVector = new Vector3();
4 | const tempVector1 = new Vector3();
5 | const tempVector2 = new Vector3();
6 |
7 | export const EPSILON = 1e-7;
8 |
9 | // https://docs.microsoft.com/en-us/windows/win32/api/d3d11/ne-d3d11-d3d11_standard_multisample_quality_levels
10 | export const ANTIALIAS_WIDTH = 16;
11 | export const ANTIALIAS_OFFSETS = [
12 | [ 1, 1 ], [ - 1, - 3 ], [ - 3, 2 ], [ 4, - 1 ],
13 | [ - 5, - 2 ], [ 2, 5 ], [ 5, 3 ], [ 3, - 5 ],
14 | [ - 2, 6 ], [ 0, - 7 ], [ - 4, - 6 ], [ - 6, 4 ],
15 | [ - 8, 0 ], [ 7, - 4 ], [ 6, 7 ], [ - 7, - 8 ],
16 | ];
17 |
18 | // https://google.github.io/filament/Filament.md.html#materialsystem/diffusebrdf
19 | export function schlickFresnel( cosine, f0 ) {
20 |
21 | return f0 + ( 1.0 - f0 ) * Math.pow( 1.0 - cosine, 5.0 );
22 |
23 | }
24 |
25 | // https://raytracing.github.io/books/RayTracingInOneWeekend.html#dielectrics/schlickapproximation
26 | export function schlickFresnelFromIor( cosine, iorRatio ) {
27 |
28 | // Schlick approximation
29 | const r0 = Math.pow( ( 1 - iorRatio ) / ( 1 + iorRatio ), 2 );
30 | return schlickFresnel( cosine, r0 );
31 |
32 | }
33 |
34 | export function refract( dir, norm, iorRatio, target ) {
35 |
36 | // snell's law
37 | // ior1 * sin( t1 ) = ior2 * sin( t2 )
38 | let cosTheta = Math.min( - dir.dot( norm ), 1.0 );
39 |
40 | tempVector
41 | .copy( dir )
42 | .addScaledVector( norm, cosTheta )
43 | .multiplyScalar( iorRatio );
44 |
45 | target
46 | .copy( norm )
47 | .multiplyScalar( - Math.sqrt( Math.abs( 1.0 - tempVector.lengthSq() ) ) )
48 | .add( tempVector );
49 |
50 | }
51 |
52 | // forms a basis with the normal vector as Z
53 | export function getBasisFromNormal( normal, targetMatrix ) {
54 |
55 | if ( Math.abs( normal.x ) > 0.5 ) {
56 |
57 | tempVector.set( 0, 1, 0 );
58 |
59 | } else {
60 |
61 | tempVector.set( 1, 0, 0 );
62 |
63 | }
64 |
65 | tempVector1.crossVectors( normal, tempVector ).normalize();
66 | tempVector2.crossVectors( normal, tempVector1 ).normalize();
67 | targetMatrix.makeBasis( tempVector2, tempVector1, normal );
68 |
69 | }
70 |
71 | export function getHalfVector( a, b, target ) {
72 |
73 | return target.addVectors( a, b ).normalize();
74 |
75 | }
76 |
77 | // The discrepancy between interpolated surface normal and geometry normal can cause issues when a ray
78 | // is cast that is on the top side of the geometry normal plane but below the surface normal plane. If
79 | // we find a ray like that we ignore it to avoid artifacts.
80 | // This function returns if the direction is on the same side of both planes.
81 | export function isDirectionValid( direction, surfaceNormal, geometryNormal ) {
82 |
83 | const aboveSurfaceNormal = direction.dot( surfaceNormal ) > 0;
84 | const aboveGeometryNormal = direction.dot( geometryNormal ) > 0;
85 | return aboveSurfaceNormal === aboveGeometryNormal;
86 |
87 | }
88 |
--------------------------------------------------------------------------------
/example/physics.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Rudimentary Sphere Physics
5 |
6 |
7 |
33 |
34 |
35 |
36 | Rudimentary sphere collision physics with an environment using MeshBVH.
37 |
38 |
39 |
40 |
41 | Model by
Lowpolyprincipal on Sketchfab.
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/example/pointCloudIntersection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry point clouds
5 |
6 |
7 |
43 |
44 |
45 |
46 | Point cloud intersection by modeling points as degenerate triangles with MeshBVH.
47 |
48 |
49 |
50 |
51 | Model by
SiteScape on Sketchfab.
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/randomSampleDebug.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Random Sample Debug
5 |
6 |
7 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/example/randomSampleDebug.js:
--------------------------------------------------------------------------------
1 | import * as THREE from 'three';
2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
3 | import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, AVERAGE, MeshBVHHelper } from '..';
4 |
5 | // Code for debugging issue #180 and other random raycast test associated issues.
6 | THREE.Mesh.prototype.raycast = acceleratedRaycast;
7 | THREE.BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
8 | THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
9 |
10 | let renderer, camera, scene;
11 | let meshes = [];
12 |
13 | let _seed = null;
14 | function random() {
15 |
16 | if ( _seed === null ) throw new Error();
17 |
18 | const a = 1103515245;
19 | const c = 12345;
20 | const m = 2e31;
21 |
22 | _seed = ( a * _seed + c ) % m;
23 | return _seed / m;
24 |
25 | }
26 |
27 | init();
28 | render();
29 |
30 | function init() {
31 |
32 | const bgColor = 0x111111;
33 |
34 | // renderer setup
35 | renderer = new THREE.WebGLRenderer( { antialias: true } );
36 | renderer.setPixelRatio( window.devicePixelRatio );
37 | renderer.setSize( window.innerWidth, window.innerHeight );
38 | renderer.setClearColor( bgColor, 1 );
39 | renderer.outputEncoding = THREE.sRGBEncoding;
40 | document.body.appendChild( renderer.domElement );
41 |
42 | // scene setup
43 | scene = new THREE.Scene();
44 |
45 | const light = new THREE.DirectionalLight( 0xffffff, 1 );
46 | light.position.set( 1, 1, 1 );
47 | scene.add( light );
48 | scene.add( new THREE.AmbientLight( 0xb0bec5, 0.8 ) );
49 |
50 | // camera setup
51 | camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 50 );
52 | camera.position.set( 0, 0, 4 );
53 | camera.far = 100;
54 | camera.updateProjectionMatrix();
55 |
56 | new OrbitControls( camera, renderer.domElement );
57 |
58 | const transformSeed = 7830035629;
59 | const raySeed = 4697211981;
60 | const options = { strategy: AVERAGE, packData: false, maxDepth: 1 };
61 |
62 | const geometry = new THREE.TorusGeometry( 1, 1, 40, 10 );
63 | geometry.computeBoundsTree( options );
64 |
65 | // mesh setup
66 | _seed = transformSeed;
67 | random(); // call random() to seed with a larger value
68 |
69 | for ( var i = 0; i < 10; i ++ ) {
70 |
71 | let mesh = new THREE.Mesh( geometry, new THREE.MeshStandardMaterial() );
72 | mesh.rotation.x = random() * 10;
73 | mesh.rotation.y = random() * 10;
74 | mesh.rotation.z = random() * 10;
75 |
76 | mesh.position.x = random();
77 | mesh.position.y = random();
78 | mesh.position.z = random();
79 |
80 | // only the mesh at index 2 was causing an issue
81 | if ( i === 2 ) {
82 |
83 | meshes.push( mesh );
84 | scene.add( mesh );
85 |
86 | const wireframe = mesh.clone();
87 | wireframe.material = new THREE.MeshBasicMaterial( { wireframe: true, color: 0xff6666 } );
88 | scene.add( wireframe );
89 |
90 | const helper = new MeshBVHHelper( mesh, 10 );
91 | scene.add( helper );
92 |
93 | // mesh.add( new THREE.AxesHelper( 10 ) );
94 |
95 | }
96 |
97 | mesh.updateMatrix( true );
98 | mesh.updateMatrixWorld( true );
99 |
100 | }
101 |
102 | // raycast
103 | _seed = raySeed;
104 | random(); // call random() to seed with a larger value
105 |
106 | const raycaster = new THREE.Raycaster();
107 | raycaster.firstHitOnly = false;
108 | raycaster.ray.origin.set( random() * 10, random() * 10, random() * 10 );
109 | raycaster.ray.direction.copy( raycaster.ray.origin ).multiplyScalar( - 1 ).normalize();
110 |
111 | // set up raycast points
112 | const sphereGeom = new THREE.SphereGeometry( 0.1 );
113 | const sphereMesh = new THREE.Mesh( sphereGeom );
114 |
115 | sphereMesh.position.copy( raycaster.ray.at( 0, new THREE.Vector3() ) );
116 | scene.add( sphereMesh );
117 |
118 | // perform the hits
119 | const bvhHits = raycaster.intersectObjects( meshes, true );
120 |
121 | raycaster.firstHitOnly = true;
122 | const firstHit = raycaster.intersectObjects( meshes, true );
123 |
124 | geometry.boundsTree = null;
125 | const ogHits = raycaster.intersectObjects( meshes, true );
126 |
127 | console.log( 'FIRST HIT', firstHit );
128 |
129 | console.log( 'BVH HITS', bvhHits );
130 |
131 | console.log( 'OG HITS', ogHits );
132 |
133 | // draw hit points and line
134 | const firstHitSphere = sphereMesh.clone();
135 | firstHitSphere.position.copy( firstHit[ 0 ].point );
136 | scene.add( firstHitSphere );
137 |
138 | const bvhHitSphere = sphereMesh.clone();
139 | bvhHitSphere.position.copy( bvhHits[ 0 ].point );
140 | scene.add( bvhHitSphere );
141 |
142 | const line = new THREE.Line();
143 | line.geometry.setFromPoints(
144 | [
145 | raycaster.ray.at( 0, new THREE.Vector3() ),
146 | raycaster.ray.at( 20, new THREE.Vector3() ),
147 | ]
148 | );
149 | scene.add( line );
150 |
151 | // resize listener
152 | window.addEventListener( 'resize', function () {
153 |
154 | camera.aspect = window.innerWidth / window.innerHeight;
155 | camera.updateProjectionMatrix();
156 |
157 | renderer.setSize( window.innerWidth, window.innerHeight );
158 |
159 | }, false );
160 |
161 | }
162 |
163 | function render() {
164 |
165 | requestAnimationFrame( render );
166 |
167 | renderer.render( scene, camera );
168 |
169 | }
170 |
--------------------------------------------------------------------------------
/example/raycast.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Complex Geometry Raycasting
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/sculpt.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Sculpting
5 |
6 |
7 |
33 |
34 |
35 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/example/sdfGeneration.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Fast SDF Generation
5 |
6 |
7 |
43 |
44 |
45 |
46 | 3D Texture
Signed Distance Field generation on the gpu and raymarching.
47 |
48 | "Surface" sets the distance at which the surface is rendered.
49 |
50 | "Layers" show the raw signed distance values.
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/selection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Collect Triangles
5 |
6 |
7 |
41 |
42 |
43 |
44 | Right click rotate, left click drag selection
45 |
46 | NOTE: Triangles and bounds clipped by the camera not supported
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/example/shapecast.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Lasso Selection
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/skinnedMesh.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Skinned Mesh BVH
5 |
6 |
7 |
44 |
45 |
46 |
47 | SkinnedMesh and morph target BVH support by iteratively generating a static version of
48 | the geometry and refitting the existing BVH.
49 |
50 |
51 |
52 |
Model by DailyArt on Sketchfab
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/example/src/Selection.js:
--------------------------------------------------------------------------------
1 | import * as THREE from "three";
2 |
3 | /** Abstract class representing a selection using a pointer. */
4 | class Selection {
5 |
6 | constructor() {
7 |
8 | this.dragging = false;
9 |
10 | }
11 |
12 | handlePointerDown() {
13 |
14 | this.dragging = true;
15 |
16 | }
17 | handlePointerUp() {
18 |
19 | this.dragging = false;
20 |
21 | }
22 | handlePointerMove() {}
23 |
24 | get points() {
25 |
26 | return [];
27 |
28 | }
29 |
30 | /** Convert absolute screen coordinates `x` and `y` to relative coordinates in range [-1; 1]. */
31 | static normalizePoint( x, y ) {
32 |
33 | return [
34 | ( x / window.innerWidth ) * 2 - 1,
35 | - ( ( y / window.innerHeight ) * 2 - 1 ),
36 | ];
37 |
38 | }
39 |
40 | }
41 |
42 | const tempVec0 = new THREE.Vector2();
43 | const tempVec1 = new THREE.Vector2();
44 | const tempVec2 = new THREE.Vector2();
45 | /** Selection that adds points on drag and connects the start and end points with a straight line. */
46 | export class LassoSelection extends Selection {
47 |
48 | constructor() {
49 |
50 | super();
51 | this.lassoPoints = [];
52 | this.prevX = - Infinity;
53 | this.prevY = - Infinity;
54 |
55 | }
56 |
57 | handlePointerDown( e ) {
58 |
59 | super.handlePointerDown();
60 | this.prevX = e.clientX;
61 | this.prevY = e.clientY;
62 | this.lassoPoints = [];
63 |
64 | }
65 |
66 | handlePointerMove( e ) {
67 |
68 | const ex = e.clientX;
69 | const ey = e.clientY;
70 | const [ nx, ny ] = Selection.normalizePoint( ex, ey );
71 |
72 | // If the mouse hasn't moved a lot since the last point
73 | if ( Math.abs( ex - this.prevX ) >= 3 || Math.abs( ey - this.prevY ) >= 3 ) {
74 |
75 | // Check if the mouse moved in roughly the same direction as the previous point
76 | // and replace it if so.
77 | const i = this.lassoPoints.length / 3 - 1;
78 | const i3 = i * 3;
79 | let doReplace = false;
80 | if ( this.lassoPoints.length > 3 ) {
81 |
82 | // prev segment direction
83 | tempVec0.set( this.lassoPoints[ i3 - 3 ], this.lassoPoints[ i3 - 3 + 1 ] );
84 | tempVec1.set( this.lassoPoints[ i3 ], this.lassoPoints[ i3 + 1 ] );
85 | tempVec1.sub( tempVec0 ).normalize();
86 |
87 | // this segment direction
88 | tempVec0.set( this.lassoPoints[ i3 ], this.lassoPoints[ i3 + 1 ] );
89 | tempVec2.set( nx, ny );
90 | tempVec2.sub( tempVec0 ).normalize();
91 |
92 | const dot = tempVec1.dot( tempVec2 );
93 | doReplace = dot > 0.99;
94 |
95 | }
96 |
97 | if ( doReplace ) {
98 |
99 | this.lassoPoints[ i3 ] = nx;
100 | this.lassoPoints[ i3 + 1 ] = ny;
101 |
102 | } else {
103 |
104 | this.lassoPoints.push( nx, ny, 0 );
105 |
106 | }
107 |
108 | this.prevX = ex;
109 | this.prevY = ey;
110 |
111 | return { changed: true };
112 |
113 | }
114 |
115 | return { changed: false };
116 |
117 | }
118 |
119 | get points() {
120 |
121 | return this.lassoPoints;
122 |
123 | }
124 |
125 | }
126 |
127 | export class BoxSelection extends Selection {
128 |
129 | constructor() {
130 |
131 | super();
132 | this.startX = 0;
133 | this.startY = 0;
134 | this.currentX = 0;
135 | this.currentY = 0;
136 |
137 | }
138 |
139 | handlePointerDown( e ) {
140 |
141 | super.handlePointerDown();
142 | this.prevX = e.clientX;
143 | this.prevY = e.clientY;
144 | const [ nx, ny ] = Selection.normalizePoint( e.clientX, e.clientY );
145 | this.startX = nx;
146 | this.startY = ny;
147 | this.lassoPoints = [];
148 |
149 | }
150 |
151 | handlePointerMove( e ) {
152 |
153 | const ex = e.clientX;
154 | const ey = e.clientY;
155 |
156 | const [ nx, ny ] = Selection.normalizePoint( e.clientX, e.clientY );
157 | this.currentX = nx;
158 | this.currentY = ny;
159 |
160 | if ( ex === this.prevX && ey === this.prevY ) {
161 |
162 | return { changed: false };
163 |
164 | }
165 |
166 | this.prevX = ex;
167 | this.prevY = ey;
168 |
169 | return { changed: true };
170 |
171 | }
172 |
173 | get points() {
174 |
175 | return [
176 | [ this.startX, this.startY, 0 ],
177 | [ this.currentX, this.startY, 0 ],
178 | [ this.currentX, this.currentY, 0 ],
179 | [ this.startX, this.currentY, 0 ],
180 | ].flat();
181 |
182 | }
183 |
184 | }
185 |
--------------------------------------------------------------------------------
/example/textures/3B6E10_E3F2C3_88AC2E_99CE51-256px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/example/textures/3B6E10_E3F2C3_88AC2E_99CE51-256px.png
--------------------------------------------------------------------------------
/example/textures/763C39_431510_210504_55241C-256px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/example/textures/763C39_431510_210504_55241C-256px.png
--------------------------------------------------------------------------------
/example/textures/7877EE_D87FC5_75D9C7_1C78C0-256px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/example/textures/7877EE_D87FC5_75D9C7_1C78C0-256px.png
--------------------------------------------------------------------------------
/example/textures/B67F6B_4B2E2A_6C3A34_F3DBC6-256px.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/example/textures/B67F6B_4B2E2A_6C3A34_F3DBC6-256px.png
--------------------------------------------------------------------------------
/example/triangleIntersect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Triangle-Triangle Intersection
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/example/utils/GenerateSDFMaterial.js:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial, Matrix4 } from 'three';
2 | import { BVHShaderGLSL, MeshBVHUniformStruct } from '../..';
3 |
4 | export class GenerateSDFMaterial extends ShaderMaterial {
5 |
6 | constructor( params ) {
7 |
8 | super( {
9 |
10 | defines: {
11 |
12 | USE_SHADER_RAYCAST: window.location.hash.includes( 'USE_SHADER_RAYCAST' ) ? 1 : 0,
13 |
14 | },
15 |
16 | uniforms: {
17 |
18 | matrix: { value: new Matrix4() },
19 | zValue: { value: 0 },
20 | bvh: { value: new MeshBVHUniformStruct() }
21 |
22 | },
23 |
24 | vertexShader: /* glsl */`
25 |
26 | varying vec2 vUv;
27 |
28 | void main() {
29 |
30 | vUv = uv;
31 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
32 |
33 | }
34 |
35 | `,
36 |
37 | fragmentShader: /* glsl */`
38 |
39 | precision highp isampler2D;
40 | precision highp usampler2D;
41 |
42 | ${ BVHShaderGLSL.common_functions }
43 | ${ BVHShaderGLSL.bvh_struct_definitions }
44 | ${ BVHShaderGLSL.bvh_ray_functions }
45 | ${ BVHShaderGLSL.bvh_distance_functions }
46 |
47 | varying vec2 vUv;
48 |
49 | uniform BVH bvh;
50 | uniform float zValue;
51 | uniform mat4 matrix;
52 |
53 | void main() {
54 |
55 | // compute the point in space to check
56 | vec3 point = vec3( vUv, zValue );
57 | point -= vec3( 0.5 );
58 | point = ( matrix * vec4( point, 1.0 ) ).xyz;
59 |
60 | // retrieve the distance and other values
61 | uvec4 faceIndices;
62 | vec3 faceNormal;
63 | vec3 barycoord;
64 | float side;
65 | float rayDist;
66 | vec3 outPoint;
67 | float dist = bvhClosestPointToPoint( bvh, point.xyz, 100000.0, faceIndices, faceNormal, barycoord, side, outPoint );
68 |
69 | // This currently causes issues on some devices when rendering to 3d textures and texture arrays
70 | #if USE_SHADER_RAYCAST
71 |
72 | side = 1.0;
73 | bvhIntersectFirstHit( bvh, point.xyz, vec3( 0.0, 0.0, 1.0 ), faceIndices, faceNormal, barycoord, side, rayDist );
74 |
75 | #endif
76 |
77 | // if the triangle side is the back then it must be on the inside and the value negative
78 | gl_FragColor = vec4( side * dist, 0, 0, 0 );
79 |
80 | }
81 |
82 | `
83 |
84 | } );
85 |
86 | this.setValues( params );
87 |
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/example/utils/RayMarchSDFMaterial.js:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial, Matrix4, Vector3 } from 'three';
2 |
3 | export class RayMarchSDFMaterial extends ShaderMaterial {
4 |
5 | constructor( params ) {
6 |
7 | super( {
8 |
9 | defines: {
10 |
11 | MAX_STEPS: 500,
12 | SURFACE_EPSILON: 0.001,
13 |
14 | },
15 |
16 | uniforms: {
17 |
18 | surface: { value: 0 },
19 | sdfTex: { value: null },
20 | normalStep: { value: new Vector3() },
21 | projectionInverse: { value: new Matrix4() },
22 | sdfTransformInverse: { value: new Matrix4() }
23 |
24 | },
25 |
26 | vertexShader: /* glsl */`
27 |
28 | varying vec2 vUv;
29 |
30 | void main() {
31 |
32 | vUv = uv;
33 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
34 |
35 | }
36 |
37 | `,
38 |
39 | fragmentShader: /* glsl */`
40 | precision highp sampler3D;
41 |
42 | varying vec2 vUv;
43 |
44 | uniform float surface;
45 | uniform sampler3D sdfTex;
46 | uniform vec3 normalStep;
47 | uniform mat4 projectionInverse;
48 | uniform mat4 sdfTransformInverse;
49 |
50 | #include
51 |
52 | // distance to box bounds
53 | vec2 rayBoxDist( vec3 boundsMin, vec3 boundsMax, vec3 rayOrigin, vec3 rayDir ) {
54 |
55 | vec3 t0 = ( boundsMin - rayOrigin ) / rayDir;
56 | vec3 t1 = ( boundsMax - rayOrigin ) / rayDir;
57 | vec3 tmin = min( t0, t1 );
58 | vec3 tmax = max( t0, t1 );
59 |
60 | float distA = max( max( tmin.x, tmin.y ), tmin.z );
61 | float distB = min( tmax.x, min( tmax.y, tmax.z ) );
62 |
63 | float distToBox = max( 0.0, distA );
64 | float distInsideBox = max( 0.0, distB - distToBox );
65 | return vec2( distToBox, distInsideBox );
66 |
67 | }
68 |
69 | void main() {
70 |
71 | // get the inverse of the sdf box transform
72 | mat4 sdfTransform = inverse( sdfTransformInverse );
73 |
74 | // convert the uv to clip space for ray transformation
75 | vec2 clipSpace = 2.0 * vUv - vec2( 1.0 );
76 |
77 | // get world ray direction
78 | vec3 rayOrigin = vec3( 0.0 );
79 | vec4 homogenousDirection = projectionInverse * vec4( clipSpace, - 1.0, 1.0 );
80 | vec3 rayDirection = normalize( homogenousDirection.xyz / homogenousDirection.w );
81 |
82 | // transform ray into local coordinates of sdf bounds
83 | vec3 sdfRayOrigin = ( sdfTransformInverse * vec4( rayOrigin, 1.0 ) ).xyz;
84 | vec3 sdfRayDirection = normalize( ( sdfTransformInverse * vec4( rayDirection, 0.0 ) ).xyz );
85 |
86 | // find whether our ray hits the box bounds in the local box space
87 | vec2 boxIntersectionInfo = rayBoxDist( vec3( - 0.5 ), vec3( 0.5 ), sdfRayOrigin, sdfRayDirection );
88 | float distToBox = boxIntersectionInfo.x;
89 | float distInsideBox = boxIntersectionInfo.y;
90 | bool intersectsBox = distInsideBox > 0.0;
91 |
92 | gl_FragColor = vec4( 0.0 );
93 | if ( intersectsBox ) {
94 |
95 | // find the surface point in world space
96 | bool intersectsSurface = false;
97 | vec4 localPoint = vec4( sdfRayOrigin + sdfRayDirection * ( distToBox + 1e-5 ), 1.0 );
98 | vec4 point = sdfTransform * localPoint;
99 |
100 | // ray march
101 | for ( int i = 0; i < MAX_STEPS; i ++ ) {
102 |
103 | // sdf box extends from - 0.5 to 0.5
104 | // transform into the local bounds space [ 0, 1 ] and check if we're inside the bounds
105 | vec3 uv = ( sdfTransformInverse * point ).xyz + vec3( 0.5 );
106 | if ( uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0 || uv.z < 0.0 || uv.z > 1.0 ) {
107 |
108 | break;
109 |
110 | }
111 |
112 | // get the distance to surface and exit the loop if we're close to the surface
113 | float distanceToSurface = texture2D( sdfTex, uv ).r - surface;
114 | if ( distanceToSurface < SURFACE_EPSILON ) {
115 |
116 | intersectsSurface = true;
117 | break;
118 |
119 | }
120 |
121 | // step the ray
122 | point.xyz += rayDirection * abs( distanceToSurface );
123 |
124 | }
125 |
126 | // find the surface normal
127 | if ( intersectsSurface ) {
128 |
129 | // compute the surface normal
130 | vec3 uv = ( sdfTransformInverse * point ).xyz + vec3( 0.5 );
131 | float dx = texture( sdfTex, uv + vec3( normalStep.x, 0.0, 0.0 ) ).r - texture( sdfTex, uv - vec3( normalStep.x, 0.0, 0.0 ) ).r;
132 | float dy = texture( sdfTex, uv + vec3( 0.0, normalStep.y, 0.0 ) ).r - texture( sdfTex, uv - vec3( 0.0, normalStep.y, 0.0 ) ).r;
133 | float dz = texture( sdfTex, uv + vec3( 0.0, 0.0, normalStep.z ) ).r - texture( sdfTex, uv - vec3( 0.0, 0.0, normalStep.z ) ).r;
134 | vec3 normal = normalize( vec3( dx, dy, dz ) );
135 |
136 | // compute some basic lighting effects
137 | vec3 lightDirection = normalize( vec3( 1.0 ) );
138 | float lightIntensity =
139 | saturate( dot( normal, lightDirection ) ) +
140 | saturate( dot( normal, - lightDirection ) ) * 0.05 +
141 | 0.1;
142 | gl_FragColor.rgb = vec3( lightIntensity );
143 | gl_FragColor.a = 1.0;
144 |
145 | }
146 |
147 | }
148 |
149 | #include
150 |
151 | }
152 | `
153 |
154 | } );
155 |
156 | this.setValues( params );
157 |
158 | }
159 |
160 | }
161 |
--------------------------------------------------------------------------------
/example/utils/RenderSDFLayerMaterial.js:
--------------------------------------------------------------------------------
1 | import { ShaderMaterial } from 'three';
2 |
3 | export class RenderSDFLayerMaterial extends ShaderMaterial {
4 |
5 | constructor( params ) {
6 |
7 | super( {
8 |
9 | defines: {
10 |
11 | DISPLAY_GRID: 0,
12 |
13 | },
14 |
15 | uniforms: {
16 |
17 | sdfTex: { value: null },
18 | layer: { value: 0 },
19 | layers: { value: 0 },
20 |
21 | },
22 |
23 | vertexShader: /* glsl */`
24 |
25 | varying vec2 vUv;
26 |
27 | void main() {
28 |
29 | vUv = uv;
30 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
31 |
32 | }
33 |
34 | `,
35 |
36 | fragmentShader: /* glsl */`
37 | precision highp sampler3D;
38 |
39 | varying vec2 vUv;
40 | uniform sampler3D sdfTex;
41 | uniform float layer;
42 | uniform float layers;
43 |
44 | void main() {
45 |
46 | #if DISPLAY_GRID
47 |
48 | float dim = ceil( sqrt( layers ) );
49 | vec2 cell = floor( vUv * dim );
50 | vec2 frac = vUv * dim - cell;
51 | float zLayer = ( cell.y * dim + cell.x ) / ( dim * dim );
52 |
53 | float dist = texture( sdfTex, vec3( frac, zLayer ) ).r;
54 | gl_FragColor.rgb = dist > 0.0 ? vec3( 0, dist, 0 ) : vec3( - dist, 0, 0 );
55 | gl_FragColor.a = 1.0;
56 |
57 | #else
58 |
59 | float dist = texture( sdfTex, vec3( vUv, layer ) ).r;
60 | gl_FragColor.rgb = dist > 0.0 ? vec3( 0, dist, 0 ) : vec3( - dist, 0, 0 );
61 | gl_FragColor.a = 1.0;
62 |
63 | #endif
64 |
65 | #include
66 |
67 | }
68 | `
69 |
70 | } );
71 |
72 | this.setValues( params );
73 |
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/example/utils/math/getConvexHull.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Compute a convex hull of the given points.
3 | *
4 | * Source: https://www.geeksforgeeks.org/convex-hull-set-2-graham-scan/
5 | * @param {Array} points
6 | * @returns {Array}
7 | */
8 | export function getConvexHull( points ) {
9 |
10 | function orientation( p, q, r ) {
11 |
12 | const val = ( q.y - p.y ) * ( r.x - q.x ) - ( q.x - p.x ) * ( r.y - q.y );
13 |
14 | if ( val == 0 ) {
15 |
16 | return 0; // colinear
17 |
18 | }
19 |
20 | // clockwise or counterclockwise
21 | return val > 0 ? 1 : 2;
22 |
23 | }
24 |
25 | function distSq( p1, p2 ) {
26 |
27 | return ( p1.x - p2.x ) * ( p1.x - p2.x ) + ( p1.y - p2.y ) * ( p1.y - p2.y );
28 |
29 | }
30 |
31 | function compare( p1, p2 ) {
32 |
33 | // Find orientation
34 | const o = orientation( p0, p1, p2 );
35 | if ( o == 0 ) return distSq( p0, p2 ) >= distSq( p0, p1 ) ? - 1 : 1;
36 |
37 | return o == 2 ? - 1 : 1;
38 |
39 | }
40 |
41 | // find the lowest point in 2d
42 | let lowestY = Infinity;
43 | let lowestIndex = - 1;
44 | for ( let i = 0, l = points.length; i < l; i ++ ) {
45 |
46 | const p = points[ i ];
47 | if ( p.y < lowestY ) {
48 |
49 | lowestIndex = i;
50 | lowestY = p.y;
51 |
52 | }
53 |
54 | }
55 |
56 | // sort the points
57 | const p0 = points[ lowestIndex ];
58 | points[ lowestIndex ] = points[ 0 ];
59 | points[ 0 ] = p0;
60 |
61 | points = points.sort( compare );
62 |
63 | // filter the points
64 | let m = 1;
65 | const n = points.length;
66 | for ( let i = 1; i < n; i ++ ) {
67 |
68 | while ( i < n - 1 && orientation( p0, points[ i ], points[ i + 1 ] ) == 0 ) {
69 |
70 | i ++;
71 |
72 | }
73 |
74 | points[ m ] = points[ i ];
75 | m ++;
76 |
77 | }
78 |
79 | // early out if we don't have enough points for a hull
80 | if ( m < 3 ) return null;
81 |
82 | // generate the hull
83 | const hull = [ points[ 0 ], points[ 1 ], points[ 2 ] ];
84 | for ( let i = 3; i < m; i ++ ) {
85 |
86 | while (
87 | orientation( hull[ hull.length - 2 ], hull[ hull.length - 1 ], points[ i ] ) !== 2
88 | ) {
89 |
90 | hull.pop();
91 |
92 | }
93 |
94 | hull.push( points[ i ] );
95 |
96 | }
97 |
98 | return hull;
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/example/utils/math/lineCrossesLine.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if two line segments intersect.
3 | *
4 | * Source: https://stackoverflow.com/questions/3838329/how-can-i-check-if-two-segments-intersect
5 | * @param {THREE.Line3} l1
6 | * @param {THREE.Line3} l2
7 | * @returns {boolean}
8 | */
9 | export function lineCrossesLine( l1, l2 ) {
10 |
11 | function ccw( A, B, C ) {
12 |
13 | return ( C.y - A.y ) * ( B.x - A.x ) > ( B.y - A.y ) * ( C.x - A.x );
14 |
15 | }
16 |
17 | const A = l1.start;
18 | const B = l1.end;
19 |
20 | const C = l2.start;
21 | const D = l2.end;
22 |
23 | return ccw( A, C, D ) !== ccw( B, C, D ) && ccw( A, B, C ) !== ccw( A, B, D );
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/example/utils/math/pointRayCrossesSegments.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Check if the given point is inside the given polygon.
3 | * @param {THREE.Vector3} point
4 | * @param {Array} polygon
5 | * @returns {boolean}
6 | * @see https://en.wikipedia.org/wiki/Point_in_polygon#Ray_casting_algorithm
7 | */
8 | export function isPointInsidePolygon( point, polygon ) {
9 |
10 | return pointRayCrossesSegments( point, polygon ) % 2 === 1;
11 |
12 | }
13 |
14 | /**
15 | * Count how many times a ray cast from the given point crosses the segments.
16 | * @param {THREE.Vector3} point
17 | * @param {Array} segments
18 | * @returns {number}
19 | */
20 | function pointRayCrossesSegments( point, segments ) {
21 |
22 | let crossings = 0;
23 | const firstSeg = segments[ segments.length - 1 ];
24 | let prevSegmentGoesDown = firstSeg.start.y > firstSeg.end.y;
25 | for ( let s = 0, l = segments.length; s < l; s ++ ) {
26 |
27 | const line = segments[ s ];
28 | const thisSegmentGoesDown = line.start.y > line.end.y;
29 | if (
30 | pointRayCrossesLine( point, line, prevSegmentGoesDown, thisSegmentGoesDown )
31 | ) {
32 |
33 | crossings ++;
34 |
35 | }
36 |
37 | prevSegmentGoesDown = thisSegmentGoesDown;
38 |
39 | }
40 |
41 | return crossings;
42 |
43 | }
44 |
45 | /**
46 | * Check if a ray cast from `point` to the right intersects the line segment.
47 | *
48 | * @param {THREE.Vector3} point
49 | * @param {THREE.Line3} line
50 | * @param {boolean} prevSegmentGoesDown
51 | * @param {boolean} thisSegmentGoesDown
52 | */
53 | function pointRayCrossesLine(
54 | point,
55 | line,
56 | prevSegmentGoesDown,
57 | thisSegmentGoesDown
58 | ) {
59 |
60 | const { start, end } = line;
61 | const px = point.x;
62 | const py = point.y;
63 |
64 | const sy = start.y;
65 | const ey = end.y;
66 |
67 | // If the line segment is parallel to the horizonal ray, then it can never intersect
68 | if ( sy === ey ) return false;
69 |
70 | // If the point is above or below both ends of the line segment, then the ray can't intersect the segment
71 | if ( py > sy && py > ey ) return false;
72 | if ( py < sy && py < ey ) return false;
73 |
74 | const sx = start.x;
75 | const ex = end.x;
76 |
77 | // If the point is to the right of both ends of the line segment, then the ray cast to the right can't intersect the segment
78 | if ( px > sx && px > ex ) return false;
79 | if ( px < sx && px < ex ) {
80 |
81 | // If the ray hits just the "peak" formed by two adjacent segments, then it's not considered an intersection
82 | // This checks only the peak formed with the previous segment, assuming that this function will also be called for the next segment
83 | if ( py === sy && prevSegmentGoesDown !== thisSegmentGoesDown ) {
84 |
85 | return false;
86 |
87 | }
88 |
89 | // The point is to the left of the line segment and vertically in between the two ends of the segment, so the ray must hit the segment
90 | return true;
91 |
92 | }
93 |
94 | // The line segment is a vector (dx; dy)
95 | const dx = ex - sx;
96 | const dy = ey - sy;
97 | // Its clockwise perpendicular vector is (dy; -dx)
98 | const perpx = dy;
99 | const perpy = - dx;
100 |
101 | // The vector from the start of the segment to the point is (pdx; pdy)
102 | const pdx = px - sx;
103 | const pdy = py - sy;
104 |
105 | // The dot product is positive if angle from (pdx; pdy) to (perpx; perpy) is between -90 and 90 degrees
106 | const dot = perpx * pdx + perpy * pdy;
107 |
108 | if ( Math.sign( dot ) !== Math.sign( perpx ) ) {
109 |
110 | return true;
111 |
112 | }
113 |
114 | return false;
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/example/voxelize.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | three-mesh-bvh - Geometry Voxelization
5 |
6 |
7 |
44 |
45 |
46 |
47 | Demonstration of voxelizing a mesh with the ability to detect the inside and outside of a model.
48 |
49 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "verbose": true,
3 | "transformIgnorePatterns": []
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "three-mesh-bvh",
3 | "version": "0.9.0",
4 | "description": "A BVH implementation to speed up raycasting against three.js meshes.",
5 | "module": "src/index.js",
6 | "main": "build/index.umd.cjs",
7 | "type": "module",
8 | "types": "src/index.d.ts",
9 | "sideEffects": false,
10 | "scripts": {
11 | "start": "concurrently \"rollup -w -c rollup-templating.config.js\" \"vite --config ./vite.config.js\"",
12 | "build": "rollup -c rollup-templating.config.js && rollup -c",
13 | "build-silent": "rollup -c rollup-templating.config.js --silent && rollup -c --silent",
14 | "build-examples": "npm run build && vite build --config ./vite.config.js && cp ./example/coi-serviceworker.js ./example/bundle/",
15 | "test": "npm run build-silent && cd test && jest",
16 | "lint": "eslint \"./src/**/*.{js,ts}\" \"./test/**/*.{js,ts}\" \"./example/*.js\" && tsc --noEmit",
17 | "benchmark": "npm run build-silent && node benchmark/run-benchmark.js",
18 | "prepublishOnly": "npm run build"
19 | },
20 | "files": [
21 | "src/*",
22 | "build/*"
23 | ],
24 | "keywords": [
25 | "graphics",
26 | "raycast",
27 | "tree",
28 | "bounds",
29 | "threejs",
30 | "three-js",
31 | "bounds-hierarchy",
32 | "performance",
33 | "raytracing",
34 | "pathtracing",
35 | "geometry",
36 | "mesh",
37 | "distance",
38 | "intersection",
39 | "acceleration",
40 | "bvh",
41 | "webvr",
42 | "webxr"
43 | ],
44 | "repository": {
45 | "type": "git",
46 | "url": "git+https://github.com/gkjohnson/three-mesh-bvh.git"
47 | },
48 | "author": "Garrett Johnson ",
49 | "license": "MIT",
50 | "bugs": {
51 | "url": "https://github.com/gkjohnson/three-mesh-bvh/issues"
52 | },
53 | "homepage": "https://github.com/gkjohnson/three-mesh-bvh#readme",
54 | "peerDependencies": {
55 | "three": ">= 0.159.0"
56 | },
57 | "devDependencies": {
58 | "@babel/core": "^7.15.5",
59 | "@babel/preset-env": "^7.15.4",
60 | "@types/eslint": "^7.28.1",
61 | "@types/jest": "^27.0.2",
62 | "@types/three": "^0.166.0",
63 | "@typescript-eslint/eslint-plugin": "^7.14.1",
64 | "@typescript-eslint/parser": "^7.14.1",
65 | "babel-jest": "^27.2.4",
66 | "concurrently": "^8.2.1",
67 | "eslint": "^8.56.0",
68 | "eslint-config-mdcs": "^5.0.0",
69 | "eslint-plugin-jest": "^28.6.0",
70 | "glob": "^10.3.3",
71 | "jest": "^27.2.4",
72 | "preprocess": "^3.2.0",
73 | "rollup": "^3.28.1",
74 | "script-loader": "^0.7.2",
75 | "simple-git": "^3.19.1",
76 | "simplex-noise": "^2.4.0",
77 | "static-server": "^2.2.1",
78 | "stats.js": "^0.17.0",
79 | "three": "^0.170.0",
80 | "typescript": "^5.1.3",
81 | "vite": "^5.2.13"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/rollup-templating.config.js:
--------------------------------------------------------------------------------
1 | import * as glob from 'glob';
2 | import { preprocess } from 'preprocess';
3 |
4 | // The purpose of this file is to transform any template files into both "direct" and "indirect"
5 | // variants to support retaining performance in the non-indirect bvh construction and casting cases.
6 |
7 | // generates set of stars
8 | function generateStars( num ) {
9 |
10 | let result = '';
11 | for ( let i = 0; i < num; i ++ ) {
12 |
13 | result += '*';
14 |
15 | }
16 |
17 | return result;
18 |
19 | }
20 |
21 | // Runs the preprocess package on the files with the provided env options
22 | const preprocessPlugin = options => {
23 |
24 | return {
25 | name: 'preprocess',
26 | transform: ( code, id ) => {
27 |
28 | const file = id.split( /[/\\]/g ).pop();
29 | const stars = generateStars( file.length );
30 | return {
31 | code:
32 | `/***********************************${ stars }/\n` +
33 | `/* This file is generated from "${ file }". */\n` +
34 | `/***********************************${ stars }/\n` +
35 | preprocess( code, options, { type: 'js' } ),
36 | };
37 |
38 | }
39 |
40 | };
41 |
42 | };
43 |
44 | // Transforms every template.js files into a sibling directory with and without "indirect" flags
45 | export default glob.sync( './src/**/*.template.js' )
46 | .flatMap( input => [ {
47 | input,
48 | plugins: [ preprocessPlugin( { INDIRECT: true, INDIRECT_STRING: '_indirect' } ) ],
49 | external: () => true,
50 | output: {
51 | file: input.replace( /\.template\.js$/, '_indirect.generated.js' ),
52 | },
53 | }, {
54 | input,
55 | plugins: [ preprocessPlugin( { INDIRECT: false, INDIRECT_STRING: '' } ) ],
56 | external: () => true,
57 | output: {
58 | file: input.replace( /\.template\.js$/, '.generated.js' ),
59 | },
60 | } ] );
61 |
62 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | input: './src/index.js',
4 | treeshake: false,
5 | external: p => /^three/.test( p ),
6 |
7 | output: {
8 |
9 | name: 'MeshBVHLib',
10 | extend: true,
11 | format: 'umd',
12 | file: './build/index.umd.cjs',
13 | sourcemap: true,
14 |
15 | globals: p => /^three/.test( p ) ? 'THREE' : null,
16 |
17 | },
18 |
19 | },
20 | {
21 | input: './src/index.js',
22 | treeshake: false,
23 | external: p => /^three/.test( p ),
24 |
25 | output: {
26 |
27 | format: 'esm',
28 | file: './build/index.module.js',
29 | sourcemap: true,
30 |
31 | },
32 |
33 | }
34 | ];
35 |
--------------------------------------------------------------------------------
/src/core/Constants.js:
--------------------------------------------------------------------------------
1 | // Split strategy constants
2 | export const CENTER = 0;
3 | export const AVERAGE = 1;
4 | export const SAH = 2;
5 |
6 | // Traversal constants
7 | export const NOT_INTERSECTED = 0;
8 | export const INTERSECTED = 1;
9 | export const CONTAINED = 2;
10 |
11 | // SAH cost constants
12 | // TODO: hone these costs more. The relative difference between them should be the
13 | // difference in measured time to perform a triangle intersection vs traversing
14 | // bounds.
15 | export const TRIANGLE_INTERSECT_COST = 1.25;
16 | export const TRAVERSAL_COST = 1;
17 |
18 |
19 | // Build constants
20 | export const BYTES_PER_NODE = 6 * 4 + 4 + 4;
21 | export const IS_LEAFNODE_FLAG = 0xFFFF;
22 |
23 | // EPSILON for computing floating point error during build
24 | // https://en.wikipedia.org/wiki/Machine_epsilon#Values_for_standard_hardware_floating_point_arithmetics
25 | export const FLOAT32_EPSILON = Math.pow( 2, - 24 );
26 |
27 | export const SKIP_GENERATION = Symbol( 'SKIP_GENERATION' );
28 |
--------------------------------------------------------------------------------
/src/core/MeshBVHNode.js:
--------------------------------------------------------------------------------
1 | export class MeshBVHNode {
2 |
3 | constructor() {
4 |
5 | // internal nodes have boundingData, left, right, and splitAxis
6 | // leaf nodes have offset and count (referring to primitives in the mesh geometry)
7 |
8 | this.boundingData = new Float32Array( 6 );
9 |
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/src/core/build/buildUtils.js:
--------------------------------------------------------------------------------
1 | import { BYTES_PER_NODE, IS_LEAFNODE_FLAG } from '../Constants.js';
2 | import { IS_LEAF } from '../utils/nodeBufferUtils.js';
3 |
4 | let float32Array, uint32Array, uint16Array, uint8Array;
5 | const MAX_POINTER = Math.pow( 2, 32 );
6 |
7 | export function countNodes( node ) {
8 |
9 | if ( 'count' in node ) {
10 |
11 | return 1;
12 |
13 | } else {
14 |
15 | return 1 + countNodes( node.left ) + countNodes( node.right );
16 |
17 | }
18 |
19 | }
20 |
21 | export function populateBuffer( byteOffset, node, buffer ) {
22 |
23 | float32Array = new Float32Array( buffer );
24 | uint32Array = new Uint32Array( buffer );
25 | uint16Array = new Uint16Array( buffer );
26 | uint8Array = new Uint8Array( buffer );
27 |
28 | return _populateBuffer( byteOffset, node );
29 |
30 | }
31 |
32 | // pack structure
33 | // boundingData : 6 float32
34 | // right / offset : 1 uint32
35 | // splitAxis / isLeaf + count : 1 uint32 / 2 uint16
36 | function _populateBuffer( byteOffset, node ) {
37 |
38 | const stride4Offset = byteOffset / 4;
39 | const stride2Offset = byteOffset / 2;
40 | const isLeaf = 'count' in node;
41 | const boundingData = node.boundingData;
42 | for ( let i = 0; i < 6; i ++ ) {
43 |
44 | float32Array[ stride4Offset + i ] = boundingData[ i ];
45 |
46 | }
47 |
48 | if ( isLeaf ) {
49 |
50 | if ( node.buffer ) {
51 |
52 | const buffer = node.buffer;
53 | uint8Array.set( new Uint8Array( buffer ), byteOffset );
54 |
55 | for ( let offset = byteOffset, l = byteOffset + buffer.byteLength; offset < l; offset += BYTES_PER_NODE ) {
56 |
57 | const offset2 = offset / 2;
58 | if ( ! IS_LEAF( offset2, uint16Array ) ) {
59 |
60 | uint32Array[ ( offset / 4 ) + 6 ] += stride4Offset;
61 |
62 |
63 | }
64 |
65 | }
66 |
67 | return byteOffset + buffer.byteLength;
68 |
69 | } else {
70 |
71 | const offset = node.offset;
72 | const count = node.count;
73 | uint32Array[ stride4Offset + 6 ] = offset;
74 | uint16Array[ stride2Offset + 14 ] = count;
75 | uint16Array[ stride2Offset + 15 ] = IS_LEAFNODE_FLAG;
76 | return byteOffset + BYTES_PER_NODE;
77 |
78 | }
79 |
80 | } else {
81 |
82 | const left = node.left;
83 | const right = node.right;
84 | const splitAxis = node.splitAxis;
85 |
86 | let nextUnusedPointer;
87 | nextUnusedPointer = _populateBuffer( byteOffset + BYTES_PER_NODE, left );
88 |
89 | if ( ( nextUnusedPointer / 4 ) > MAX_POINTER ) {
90 |
91 | throw new Error( 'MeshBVH: Cannot store child pointer greater than 32 bits.' );
92 |
93 | }
94 |
95 | uint32Array[ stride4Offset + 6 ] = nextUnusedPointer / 4;
96 | nextUnusedPointer = _populateBuffer( nextUnusedPointer, right );
97 |
98 | uint32Array[ stride4Offset + 7 ] = splitAxis;
99 | return nextUnusedPointer;
100 |
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/src/core/build/computeBoundsUtils.js:
--------------------------------------------------------------------------------
1 | import { FLOAT32_EPSILON } from '../Constants.js';
2 | import { getTriCount } from './geometryUtils.js';
3 |
4 | // computes the union of the bounds of all of the given triangles and puts the resulting box in "target".
5 | // A bounding box is computed for the centroids of the triangles, as well, and placed in "centroidTarget".
6 | // These are computed together to avoid redundant accesses to bounds array.
7 | export function getBounds( triangleBounds, offset, count, target, centroidTarget ) {
8 |
9 | let minx = Infinity;
10 | let miny = Infinity;
11 | let minz = Infinity;
12 | let maxx = - Infinity;
13 | let maxy = - Infinity;
14 | let maxz = - Infinity;
15 |
16 | let cminx = Infinity;
17 | let cminy = Infinity;
18 | let cminz = Infinity;
19 | let cmaxx = - Infinity;
20 | let cmaxy = - Infinity;
21 | let cmaxz = - Infinity;
22 |
23 | for ( let i = offset * 6, end = ( offset + count ) * 6; i < end; i += 6 ) {
24 |
25 | const cx = triangleBounds[ i + 0 ];
26 | const hx = triangleBounds[ i + 1 ];
27 | const lx = cx - hx;
28 | const rx = cx + hx;
29 | if ( lx < minx ) minx = lx;
30 | if ( rx > maxx ) maxx = rx;
31 | if ( cx < cminx ) cminx = cx;
32 | if ( cx > cmaxx ) cmaxx = cx;
33 |
34 | const cy = triangleBounds[ i + 2 ];
35 | const hy = triangleBounds[ i + 3 ];
36 | const ly = cy - hy;
37 | const ry = cy + hy;
38 | if ( ly < miny ) miny = ly;
39 | if ( ry > maxy ) maxy = ry;
40 | if ( cy < cminy ) cminy = cy;
41 | if ( cy > cmaxy ) cmaxy = cy;
42 |
43 | const cz = triangleBounds[ i + 4 ];
44 | const hz = triangleBounds[ i + 5 ];
45 | const lz = cz - hz;
46 | const rz = cz + hz;
47 | if ( lz < minz ) minz = lz;
48 | if ( rz > maxz ) maxz = rz;
49 | if ( cz < cminz ) cminz = cz;
50 | if ( cz > cmaxz ) cmaxz = cz;
51 |
52 | }
53 |
54 | target[ 0 ] = minx;
55 | target[ 1 ] = miny;
56 | target[ 2 ] = minz;
57 |
58 | target[ 3 ] = maxx;
59 | target[ 4 ] = maxy;
60 | target[ 5 ] = maxz;
61 |
62 | centroidTarget[ 0 ] = cminx;
63 | centroidTarget[ 1 ] = cminy;
64 | centroidTarget[ 2 ] = cminz;
65 |
66 | centroidTarget[ 3 ] = cmaxx;
67 | centroidTarget[ 4 ] = cmaxy;
68 | centroidTarget[ 5 ] = cmaxz;
69 |
70 | }
71 |
72 | // precomputes the bounding box for each triangle; required for quickly calculating tree splits.
73 | // result is an array of size tris.length * 6 where triangle i maps to a
74 | // [x_center, x_delta, y_center, y_delta, z_center, z_delta] tuple starting at index i * 6,
75 | // representing the center and half-extent in each dimension of triangle i
76 | export function computeTriangleBounds( geo, target = null, offset = null, count = null ) {
77 |
78 | const posAttr = geo.attributes.position;
79 | const index = geo.index ? geo.index.array : null;
80 | const triCount = getTriCount( geo );
81 | const normalized = posAttr.normalized;
82 | let triangleBounds;
83 | if ( target === null ) {
84 |
85 | triangleBounds = new Float32Array( triCount * 6 );
86 | offset = 0;
87 | count = triCount;
88 |
89 | } else {
90 |
91 | triangleBounds = target;
92 | offset = offset || 0;
93 | count = count || triCount;
94 |
95 | }
96 |
97 | // used for non-normalized positions
98 | const posArr = posAttr.array;
99 |
100 | // support for an interleaved position buffer
101 | const bufferOffset = posAttr.offset || 0;
102 | let stride = 3;
103 | if ( posAttr.isInterleavedBufferAttribute ) {
104 |
105 | stride = posAttr.data.stride;
106 |
107 | }
108 |
109 | // used for normalized positions
110 | const getters = [ 'getX', 'getY', 'getZ' ];
111 |
112 | for ( let tri = offset; tri < offset + count; tri ++ ) {
113 |
114 | const tri3 = tri * 3;
115 | const tri6 = tri * 6;
116 |
117 | let ai = tri3 + 0;
118 | let bi = tri3 + 1;
119 | let ci = tri3 + 2;
120 |
121 | if ( index ) {
122 |
123 | ai = index[ ai ];
124 | bi = index[ bi ];
125 | ci = index[ ci ];
126 |
127 | }
128 |
129 | // we add the stride and offset here since we access the array directly
130 | // below for the sake of performance
131 | if ( ! normalized ) {
132 |
133 | ai = ai * stride + bufferOffset;
134 | bi = bi * stride + bufferOffset;
135 | ci = ci * stride + bufferOffset;
136 |
137 | }
138 |
139 | for ( let el = 0; el < 3; el ++ ) {
140 |
141 | let a, b, c;
142 |
143 | if ( normalized ) {
144 |
145 | a = posAttr[ getters[ el ] ]( ai );
146 | b = posAttr[ getters[ el ] ]( bi );
147 | c = posAttr[ getters[ el ] ]( ci );
148 |
149 | } else {
150 |
151 | a = posArr[ ai + el ];
152 | b = posArr[ bi + el ];
153 | c = posArr[ ci + el ];
154 |
155 | }
156 |
157 | let min = a;
158 | if ( b < min ) min = b;
159 | if ( c < min ) min = c;
160 |
161 | let max = a;
162 | if ( b > max ) max = b;
163 | if ( c > max ) max = c;
164 |
165 | // Increase the bounds size by float32 epsilon to avoid precision errors when
166 | // converting to 32 bit float. Scale the epsilon by the size of the numbers being
167 | // worked with.
168 | const halfExtents = ( max - min ) / 2;
169 | const el2 = el * 2;
170 | triangleBounds[ tri6 + el2 + 0 ] = min + halfExtents;
171 | triangleBounds[ tri6 + el2 + 1 ] = halfExtents + ( Math.abs( min ) + halfExtents ) * FLOAT32_EPSILON;
172 |
173 | }
174 |
175 | }
176 |
177 | return triangleBounds;
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/src/core/build/geometryUtils.js:
--------------------------------------------------------------------------------
1 | import { BufferAttribute } from 'three';
2 |
3 | export function getVertexCount( geo ) {
4 |
5 | return geo.index ? geo.index.count : geo.attributes.position.count;
6 |
7 | }
8 |
9 | export function getTriCount( geo ) {
10 |
11 | return getVertexCount( geo ) / 3;
12 |
13 | }
14 |
15 | export function getIndexArray( vertexCount, BufferConstructor = ArrayBuffer ) {
16 |
17 | if ( vertexCount > 65535 ) {
18 |
19 | return new Uint32Array( new BufferConstructor( 4 * vertexCount ) );
20 |
21 | } else {
22 |
23 | return new Uint16Array( new BufferConstructor( 2 * vertexCount ) );
24 |
25 | }
26 |
27 | }
28 |
29 | // ensures that an index is present on the geometry
30 | export function ensureIndex( geo, options ) {
31 |
32 | if ( ! geo.index ) {
33 |
34 | const vertexCount = geo.attributes.position.count;
35 | const BufferConstructor = options.useSharedArrayBuffer ? SharedArrayBuffer : ArrayBuffer;
36 | const index = getIndexArray( vertexCount, BufferConstructor );
37 | geo.setIndex( new BufferAttribute( index, 1 ) );
38 |
39 | for ( let i = 0; i < vertexCount; i ++ ) {
40 |
41 | index[ i ] = i;
42 |
43 | }
44 |
45 | }
46 |
47 | }
48 |
49 | // Computes the set of { offset, count } ranges which need independent BVH roots. Each
50 | // region in the geometry index that belongs to a different set of material groups requires
51 | // a separate BVH root, so that triangles indices belonging to one group never get swapped
52 | // with triangle indices belongs to another group. For example, if the groups were like this:
53 | //
54 | // [-------------------------------------------------------------]
55 | // |__________________|
56 | // g0 = [0, 20] |______________________||_____________________|
57 | // g1 = [16, 40] g2 = [41, 60]
58 | //
59 | // we would need four BVH roots: [0, 15], [16, 20], [21, 40], [41, 60].
60 | export function getFullGeometryRange( geo, range ) {
61 |
62 | const triCount = getTriCount( geo );
63 | const drawRange = range ? range : geo.drawRange;
64 | const start = drawRange.start / 3;
65 | const end = ( drawRange.start + drawRange.count ) / 3;
66 |
67 | const offset = Math.max( 0, start );
68 | const count = Math.min( triCount, end ) - offset;
69 | return [ {
70 | offset: Math.floor( offset ),
71 | count: Math.floor( count ),
72 | } ];
73 |
74 | }
75 |
76 | export function getRootIndexRanges( geo, range ) {
77 |
78 | if ( ! geo.groups || ! geo.groups.length ) {
79 |
80 | return getFullGeometryRange( geo, range );
81 |
82 | }
83 |
84 | const ranges = [];
85 | const rangeBoundaries = new Set();
86 |
87 | const drawRange = range ? range : geo.drawRange;
88 | const drawRangeStart = drawRange.start / 3;
89 | const drawRangeEnd = ( drawRange.start + drawRange.count ) / 3;
90 | for ( const group of geo.groups ) {
91 |
92 | const groupStart = group.start / 3;
93 | const groupEnd = ( group.start + group.count ) / 3;
94 | rangeBoundaries.add( Math.max( drawRangeStart, groupStart ) );
95 | rangeBoundaries.add( Math.min( drawRangeEnd, groupEnd ) );
96 |
97 | }
98 |
99 |
100 | // note that if you don't pass in a comparator, it sorts them lexicographically as strings :-(
101 | const sortedBoundaries = Array.from( rangeBoundaries.values() ).sort( ( a, b ) => a - b );
102 | for ( let i = 0; i < sortedBoundaries.length - 1; i ++ ) {
103 |
104 | const start = sortedBoundaries[ i ];
105 | const end = sortedBoundaries[ i + 1 ];
106 |
107 | ranges.push( {
108 | offset: Math.floor( start ),
109 | count: Math.floor( end - start ),
110 | } );
111 |
112 | }
113 |
114 | return ranges;
115 |
116 | }
117 |
118 | export function hasGroupGaps( geometry, range ) {
119 |
120 | const vertexCount = getTriCount( geometry );
121 | const groups = getRootIndexRanges( geometry, range )
122 | .sort( ( a, b ) => a.offset - b.offset );
123 |
124 | const finalGroup = groups[ groups.length - 1 ];
125 | finalGroup.count = Math.min( vertexCount - finalGroup.offset, finalGroup.count );
126 |
127 | let total = 0;
128 | groups.forEach( ( { count } ) => total += count );
129 | return vertexCount !== total;
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/core/build/sortUtils.template.js:
--------------------------------------------------------------------------------
1 | // reorders `tris` such that for `count` elements after `offset`, elements on the left side of the split
2 | // will be on the left and elements on the right side of the split will be on the right. returns the index
3 | // of the first element on the right side, or offset + count if there are no elements on the right side.
4 | export function partition/* @echo INDIRECT_STRING */( indirectBuffer, index, triangleBounds, offset, count, split ) {
5 |
6 | let left = offset;
7 | let right = offset + count - 1;
8 | const pos = split.pos;
9 | const axisOffset = split.axis * 2;
10 |
11 | // hoare partitioning, see e.g. https://en.wikipedia.org/wiki/Quicksort#Hoare_partition_scheme
12 | while ( true ) {
13 |
14 | while ( left <= right && triangleBounds[ left * 6 + axisOffset ] < pos ) {
15 |
16 | left ++;
17 |
18 | }
19 |
20 | // if a triangle center lies on the partition plane it is considered to be on the right side
21 | while ( left <= right && triangleBounds[ right * 6 + axisOffset ] >= pos ) {
22 |
23 | right --;
24 |
25 | }
26 |
27 | if ( left < right ) {
28 |
29 | // we need to swap all of the information associated with the triangles at index
30 | // left and right; that's the verts in the geometry index, the bounds,
31 | // and perhaps the SAH planes
32 | /* @if INDIRECT */
33 |
34 | let t = indirectBuffer[ left ];
35 | indirectBuffer[ left ] = indirectBuffer[ right ];
36 | indirectBuffer[ right ] = t;
37 |
38 | /* @else */
39 |
40 | for ( let i = 0; i < 3; i ++ ) {
41 |
42 | let t0 = index[ left * 3 + i ];
43 | index[ left * 3 + i ] = index[ right * 3 + i ];
44 | index[ right * 3 + i ] = t0;
45 |
46 | }
47 |
48 | /* @endif */
49 |
50 | // swap bounds
51 | for ( let i = 0; i < 6; i ++ ) {
52 |
53 | let tb = triangleBounds[ left * 6 + i ];
54 | triangleBounds[ left * 6 + i ] = triangleBounds[ right * 6 + i ];
55 | triangleBounds[ right * 6 + i ] = tb;
56 |
57 | }
58 |
59 | left ++;
60 | right --;
61 |
62 | } else {
63 |
64 | return left;
65 |
66 | }
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/core/cast/closestPointToPoint.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 |
3 | const temp = /* @__PURE__ */ new Vector3();
4 | const temp1 = /* @__PURE__ */ new Vector3();
5 |
6 | export function closestPointToPoint(
7 | bvh,
8 | point,
9 | target = { },
10 | minThreshold = 0,
11 | maxThreshold = Infinity,
12 | ) {
13 |
14 | // early out if under minThreshold
15 | // skip checking if over maxThreshold
16 | // set minThreshold = maxThreshold to quickly check if a point is within a threshold
17 | // returns Infinity if no value found
18 | const minThresholdSq = minThreshold * minThreshold;
19 | const maxThresholdSq = maxThreshold * maxThreshold;
20 | let closestDistanceSq = Infinity;
21 | let closestDistanceTriIndex = null;
22 | bvh.shapecast(
23 |
24 | {
25 |
26 | boundsTraverseOrder: box => {
27 |
28 | temp.copy( point ).clamp( box.min, box.max );
29 | return temp.distanceToSquared( point );
30 |
31 | },
32 |
33 | intersectsBounds: ( box, isLeaf, score ) => {
34 |
35 | return score < closestDistanceSq && score < maxThresholdSq;
36 |
37 | },
38 |
39 | intersectsTriangle: ( tri, triIndex ) => {
40 |
41 | tri.closestPointToPoint( point, temp );
42 | const distSq = point.distanceToSquared( temp );
43 | if ( distSq < closestDistanceSq ) {
44 |
45 | temp1.copy( temp );
46 | closestDistanceSq = distSq;
47 | closestDistanceTriIndex = triIndex;
48 |
49 | }
50 |
51 | if ( distSq < minThresholdSq ) {
52 |
53 | return true;
54 |
55 | } else {
56 |
57 | return false;
58 |
59 | }
60 |
61 | },
62 |
63 | }
64 |
65 | );
66 |
67 | if ( closestDistanceSq === Infinity ) return null;
68 |
69 | const closestDistance = Math.sqrt( closestDistanceSq );
70 |
71 | if ( ! target.point ) target.point = temp1.clone();
72 | else target.point.copy( temp1 );
73 | target.distance = closestDistance,
74 | target.faceIndex = closestDistanceTriIndex;
75 |
76 | return target;
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/core/cast/intersectsGeometry.template.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | import { Box3, Matrix4 } from 'three';
3 | import { OrientedBox } from '../../math/OrientedBox.js';
4 | import { ExtendedTriangle } from '../../math/ExtendedTriangle.js';
5 | import { setTriangle } from '../../utils/TriangleUtilities.js';
6 | import { arrayToBox } from '../../utils/ArrayBoxUtilities.js';
7 | import { COUNT, OFFSET, IS_LEAF, BOUNDING_DATA_INDEX } from '../utils/nodeBufferUtils.js';
8 | import { BufferStack } from '../utils/BufferStack.js';
9 |
10 | const boundingBox = /* @__PURE__ */ new Box3();
11 | const triangle = /* @__PURE__ */ new ExtendedTriangle();
12 | const triangle2 = /* @__PURE__ */ new ExtendedTriangle();
13 | const invertedMat = /* @__PURE__ */ new Matrix4();
14 |
15 | const obb = /* @__PURE__ */ new OrientedBox();
16 | const obb2 = /* @__PURE__ */ new OrientedBox();
17 |
18 | export function intersectsGeometry/* @echo INDIRECT_STRING */( bvh, root, otherGeometry, geometryToBvh ) {
19 |
20 | BufferStack.setBuffer( bvh._roots[ root ] );
21 | const result = _intersectsGeometry( 0, bvh, otherGeometry, geometryToBvh );
22 | BufferStack.clearBuffer();
23 |
24 | return result;
25 |
26 | }
27 |
28 | function _intersectsGeometry( nodeIndex32, bvh, otherGeometry, geometryToBvh, cachedObb = null ) {
29 |
30 | const { float32Array, uint16Array, uint32Array } = BufferStack;
31 | let nodeIndex16 = nodeIndex32 * 2;
32 |
33 | if ( cachedObb === null ) {
34 |
35 | if ( ! otherGeometry.boundingBox ) {
36 |
37 | otherGeometry.computeBoundingBox();
38 |
39 | }
40 |
41 | obb.set( otherGeometry.boundingBox.min, otherGeometry.boundingBox.max, geometryToBvh );
42 | cachedObb = obb;
43 |
44 | }
45 |
46 | const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
47 | if ( isLeaf ) {
48 |
49 | const thisGeometry = bvh.geometry;
50 | const thisIndex = thisGeometry.index;
51 | const thisPos = thisGeometry.attributes.position;
52 |
53 | const index = otherGeometry.index;
54 | const pos = otherGeometry.attributes.position;
55 |
56 | const offset = OFFSET( nodeIndex32, uint32Array );
57 | const count = COUNT( nodeIndex16, uint16Array );
58 |
59 | // get the inverse of the geometry matrix so we can transform our triangles into the
60 | // geometry space we're trying to test. We assume there are fewer triangles being checked
61 | // here.
62 | invertedMat.copy( geometryToBvh ).invert();
63 |
64 | if ( otherGeometry.boundsTree ) {
65 |
66 | // if there's a bounds tree
67 | arrayToBox( BOUNDING_DATA_INDEX( nodeIndex32 ), float32Array, obb2 );
68 | obb2.matrix.copy( invertedMat );
69 | obb2.needsUpdate = true;
70 |
71 | // TODO: use a triangle iteration function here
72 | const res = otherGeometry.boundsTree.shapecast( {
73 |
74 | intersectsBounds: box => obb2.intersectsBox( box ),
75 |
76 | intersectsTriangle: tri => {
77 |
78 | tri.a.applyMatrix4( geometryToBvh );
79 | tri.b.applyMatrix4( geometryToBvh );
80 | tri.c.applyMatrix4( geometryToBvh );
81 | tri.needsUpdate = true;
82 |
83 | /* @if INDIRECT */
84 |
85 | for ( let i = offset, l = count + offset; i < l; i ++ ) {
86 |
87 | // this triangle needs to be transformed into the current BVH coordinate frame
88 | setTriangle( triangle2, 3 * bvh.resolveTriangleIndex( i ), thisIndex, thisPos );
89 | triangle2.needsUpdate = true;
90 | if ( tri.intersectsTriangle( triangle2 ) ) {
91 |
92 | return true;
93 |
94 | }
95 |
96 | }
97 |
98 | /* @else */
99 |
100 | for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
101 |
102 | // this triangle needs to be transformed into the current BVH coordinate frame
103 | setTriangle( triangle2, i, thisIndex, thisPos );
104 | triangle2.needsUpdate = true;
105 | if ( tri.intersectsTriangle( triangle2 ) ) {
106 |
107 | return true;
108 |
109 | }
110 |
111 | }
112 |
113 | /* @endif */
114 |
115 | return false;
116 |
117 | }
118 |
119 | } );
120 |
121 | return res;
122 |
123 | } else {
124 |
125 | // if we're just dealing with raw geometry
126 | /* @if INDIRECT */
127 |
128 | for ( let i = offset, l = count + offset; i < l; i ++ ) {
129 |
130 | // this triangle needs to be transformed into the current BVH coordinate frame
131 | const ti = bvh.resolveTriangleIndex( i );
132 | setTriangle( triangle, 3 * ti, thisIndex, thisPos );
133 |
134 | /* @else */
135 |
136 | for ( let i = offset * 3, l = ( count + offset ) * 3; i < l; i += 3 ) {
137 |
138 | // this triangle needs to be transformed into the current BVH coordinate frame
139 | setTriangle( triangle, i, thisIndex, thisPos );
140 |
141 | /* @endif */
142 |
143 | triangle.a.applyMatrix4( invertedMat );
144 | triangle.b.applyMatrix4( invertedMat );
145 | triangle.c.applyMatrix4( invertedMat );
146 | triangle.needsUpdate = true;
147 |
148 | for ( let i2 = 0, l2 = index.count; i2 < l2; i2 += 3 ) {
149 |
150 | setTriangle( triangle2, i2, index, pos );
151 | triangle2.needsUpdate = true;
152 |
153 | if ( triangle.intersectsTriangle( triangle2 ) ) {
154 |
155 | return true;
156 |
157 | }
158 |
159 | }
160 |
161 | /* @if INDIRECT */
162 |
163 | }
164 |
165 | /* @else */
166 |
167 | }
168 |
169 | /* @endif */
170 |
171 | }
172 |
173 | } else {
174 |
175 | const left = nodeIndex32 + 8;
176 | const right = uint32Array[ nodeIndex32 + 6 ];
177 |
178 | arrayToBox( BOUNDING_DATA_INDEX( left ), float32Array, boundingBox );
179 | const leftIntersection =
180 | cachedObb.intersectsBox( boundingBox ) &&
181 | _intersectsGeometry( left, bvh, otherGeometry, geometryToBvh, cachedObb );
182 |
183 | if ( leftIntersection ) return true;
184 |
185 | arrayToBox( BOUNDING_DATA_INDEX( right ), float32Array, boundingBox );
186 | const rightIntersection =
187 | cachedObb.intersectsBox( boundingBox ) &&
188 | _intersectsGeometry( right, bvh, otherGeometry, geometryToBvh, cachedObb );
189 |
190 | if ( rightIntersection ) return true;
191 |
192 | return false;
193 |
194 | }
195 |
196 | }
197 |
--------------------------------------------------------------------------------
/src/core/cast/raycast.template.js:
--------------------------------------------------------------------------------
1 | import { intersectRay } from '../utils/intersectUtils.js';
2 | import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF } from '../utils/nodeBufferUtils.js';
3 | import { BufferStack } from '../utils/BufferStack.js';
4 | import { intersectTris } from '../utils/iterationUtils.generated.js';
5 | import { intersectTris_indirect } from '../utils/iterationUtils_indirect.generated.js';
6 |
7 | export function raycast/* @echo INDIRECT_STRING */( bvh, root, side, ray, intersects, near, far ) {
8 |
9 | BufferStack.setBuffer( bvh._roots[ root ] );
10 | _raycast( 0, bvh, side, ray, intersects, near, far );
11 | BufferStack.clearBuffer();
12 |
13 | }
14 |
15 | function _raycast( nodeIndex32, bvh, side, ray, intersects, near, far ) {
16 |
17 | const { float32Array, uint16Array, uint32Array } = BufferStack;
18 | const nodeIndex16 = nodeIndex32 * 2;
19 | const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
20 | if ( isLeaf ) {
21 |
22 | const offset = OFFSET( nodeIndex32, uint32Array );
23 | const count = COUNT( nodeIndex16, uint16Array );
24 |
25 | /* @if INDIRECT */
26 |
27 | intersectTris_indirect( bvh, side, ray, offset, count, intersects, near, far );
28 |
29 | /* @else */
30 |
31 | intersectTris( bvh, side, ray, offset, count, intersects, near, far );
32 |
33 | /* @endif */
34 |
35 | } else {
36 |
37 | const leftIndex = LEFT_NODE( nodeIndex32 );
38 | if ( intersectRay( leftIndex, float32Array, ray, near, far ) ) {
39 |
40 | _raycast( leftIndex, bvh, side, ray, intersects, near, far );
41 |
42 | }
43 |
44 | const rightIndex = RIGHT_NODE( nodeIndex32, uint32Array );
45 | if ( intersectRay( rightIndex, float32Array, ray, near, far ) ) {
46 |
47 | _raycast( rightIndex, bvh, side, ray, intersects, near, far );
48 |
49 | }
50 |
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/src/core/cast/raycastFirst.template.js:
--------------------------------------------------------------------------------
1 | import { COUNT, OFFSET, LEFT_NODE, RIGHT_NODE, IS_LEAF, SPLIT_AXIS } from '../utils/nodeBufferUtils.js';
2 | import { BufferStack } from '../utils/BufferStack.js';
3 | import { intersectRay } from '../utils/intersectUtils.js';
4 | import { intersectClosestTri } from '../utils/iterationUtils.generated.js';
5 | import { intersectClosestTri_indirect } from '../utils/iterationUtils_indirect.generated.js';
6 |
7 | const _xyzFields = [ 'x', 'y', 'z' ];
8 |
9 | export function raycastFirst/* @echo INDIRECT_STRING */( bvh, root, side, ray, near, far ) {
10 |
11 | BufferStack.setBuffer( bvh._roots[ root ] );
12 | const result = _raycastFirst( 0, bvh, side, ray, near, far );
13 | BufferStack.clearBuffer();
14 |
15 | return result;
16 |
17 | }
18 |
19 | function _raycastFirst( nodeIndex32, bvh, side, ray, near, far ) {
20 |
21 | const { float32Array, uint16Array, uint32Array } = BufferStack;
22 | let nodeIndex16 = nodeIndex32 * 2;
23 |
24 | const isLeaf = IS_LEAF( nodeIndex16, uint16Array );
25 | if ( isLeaf ) {
26 |
27 | const offset = OFFSET( nodeIndex32, uint32Array );
28 | const count = COUNT( nodeIndex16, uint16Array );
29 |
30 | /* @if INDIRECT */
31 |
32 | return intersectClosestTri_indirect( bvh, side, ray, offset, count, near, far );
33 |
34 | /* @else */
35 |
36 | // eslint-disable-next-line no-unreachable
37 | return intersectClosestTri( bvh, side, ray, offset, count, near, far );
38 |
39 | /* @endif */
40 |
41 | } else {
42 |
43 | // consider the position of the split plane with respect to the oncoming ray; whichever direction
44 | // the ray is coming from, look for an intersection among that side of the tree first
45 | const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
46 | const xyzAxis = _xyzFields[ splitAxis ];
47 | const rayDir = ray.direction[ xyzAxis ];
48 | const leftToRight = rayDir >= 0;
49 |
50 | // c1 is the child to check first
51 | let c1, c2;
52 | if ( leftToRight ) {
53 |
54 | c1 = LEFT_NODE( nodeIndex32 );
55 | c2 = RIGHT_NODE( nodeIndex32, uint32Array );
56 |
57 | } else {
58 |
59 | c1 = RIGHT_NODE( nodeIndex32, uint32Array );
60 | c2 = LEFT_NODE( nodeIndex32 );
61 |
62 | }
63 |
64 | const c1Intersection = intersectRay( c1, float32Array, ray, near, far );
65 | const c1Result = c1Intersection ? _raycastFirst( c1, bvh, side, ray, near, far ) : null;
66 |
67 | // if we got an intersection in the first node and it's closer than the second node's bounding
68 | // box, we don't need to consider the second node because it couldn't possibly be a better result
69 | if ( c1Result ) {
70 |
71 | // check if the point is within the second bounds
72 | // "point" is in the local frame of the bvh
73 | const point = c1Result.point[ xyzAxis ];
74 | const isOutside = leftToRight ?
75 | point <= float32Array[ c2 + splitAxis ] : // min bounding data
76 | point >= float32Array[ c2 + splitAxis + 3 ]; // max bounding data
77 |
78 | if ( isOutside ) {
79 |
80 | return c1Result;
81 |
82 | }
83 |
84 | }
85 |
86 | // either there was no intersection in the first node, or there could still be a closer
87 | // intersection in the second, so check the second node and then take the better of the two
88 | const c2Intersection = intersectRay( c2, float32Array, ray, near, far );
89 | const c2Result = c2Intersection ? _raycastFirst( c2, bvh, side, ray, near, far ) : null;
90 |
91 | if ( c1Result && c2Result ) {
92 |
93 | return c1Result.distance <= c2Result.distance ? c1Result : c2Result;
94 |
95 | } else {
96 |
97 | return c1Result || c2Result || null;
98 |
99 | }
100 |
101 | }
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/src/core/cast/refit.template.js:
--------------------------------------------------------------------------------
1 | import { IS_LEAFNODE_FLAG } from '../Constants.js';
2 |
3 | export function refit/* @echo INDIRECT_STRING */( bvh, nodeIndices = null ) {
4 |
5 | if ( nodeIndices && Array.isArray( nodeIndices ) ) {
6 |
7 | nodeIndices = new Set( nodeIndices );
8 |
9 | }
10 |
11 | const geometry = bvh.geometry;
12 | const indexArr = geometry.index ? geometry.index.array : null;
13 | const posAttr = geometry.attributes.position;
14 |
15 | let buffer, uint32Array, uint16Array, float32Array;
16 | let byteOffset = 0;
17 | const roots = bvh._roots;
18 | for ( let i = 0, l = roots.length; i < l; i ++ ) {
19 |
20 | buffer = roots[ i ];
21 | uint32Array = new Uint32Array( buffer );
22 | uint16Array = new Uint16Array( buffer );
23 | float32Array = new Float32Array( buffer );
24 |
25 | _traverse( 0, byteOffset );
26 | byteOffset += buffer.byteLength;
27 |
28 | }
29 |
30 | function _traverse( node32Index, byteOffset, force = false ) {
31 |
32 | const node16Index = node32Index * 2;
33 | const isLeaf = uint16Array[ node16Index + 15 ] === IS_LEAFNODE_FLAG;
34 | if ( isLeaf ) {
35 |
36 | const offset = uint32Array[ node32Index + 6 ];
37 | const count = uint16Array[ node16Index + 14 ];
38 |
39 | let minx = Infinity;
40 | let miny = Infinity;
41 | let minz = Infinity;
42 | let maxx = - Infinity;
43 | let maxy = - Infinity;
44 | let maxz = - Infinity;
45 |
46 | /* @if INDIRECT */
47 |
48 | for ( let i = offset, l = offset + count; i < l; i ++ ) {
49 |
50 | const t = 3 * bvh.resolveTriangleIndex( i );
51 | for ( let j = 0; j < 3; j ++ ) {
52 |
53 | let index = t + j;
54 | index = indexArr ? indexArr[ index ] : index;
55 |
56 | const x = posAttr.getX( index );
57 | const y = posAttr.getY( index );
58 | const z = posAttr.getZ( index );
59 |
60 | if ( x < minx ) minx = x;
61 | if ( x > maxx ) maxx = x;
62 |
63 | if ( y < miny ) miny = y;
64 | if ( y > maxy ) maxy = y;
65 |
66 | if ( z < minz ) minz = z;
67 | if ( z > maxz ) maxz = z;
68 |
69 |
70 | }
71 |
72 | }
73 |
74 | /* @else */
75 |
76 | for ( let i = 3 * offset, l = 3 * ( offset + count ); i < l; i ++ ) {
77 |
78 | let index = indexArr[ i ];
79 | const x = posAttr.getX( index );
80 | const y = posAttr.getY( index );
81 | const z = posAttr.getZ( index );
82 |
83 | if ( x < minx ) minx = x;
84 | if ( x > maxx ) maxx = x;
85 |
86 | if ( y < miny ) miny = y;
87 | if ( y > maxy ) maxy = y;
88 |
89 | if ( z < minz ) minz = z;
90 | if ( z > maxz ) maxz = z;
91 |
92 | }
93 |
94 | /* @endif */
95 |
96 | if (
97 | float32Array[ node32Index + 0 ] !== minx ||
98 | float32Array[ node32Index + 1 ] !== miny ||
99 | float32Array[ node32Index + 2 ] !== minz ||
100 |
101 | float32Array[ node32Index + 3 ] !== maxx ||
102 | float32Array[ node32Index + 4 ] !== maxy ||
103 | float32Array[ node32Index + 5 ] !== maxz
104 | ) {
105 |
106 | float32Array[ node32Index + 0 ] = minx;
107 | float32Array[ node32Index + 1 ] = miny;
108 | float32Array[ node32Index + 2 ] = minz;
109 |
110 | float32Array[ node32Index + 3 ] = maxx;
111 | float32Array[ node32Index + 4 ] = maxy;
112 | float32Array[ node32Index + 5 ] = maxz;
113 |
114 | return true;
115 |
116 | } else {
117 |
118 | return false;
119 |
120 | }
121 |
122 | } else {
123 |
124 | const left = node32Index + 8;
125 | const right = uint32Array[ node32Index + 6 ];
126 |
127 | // the identifying node indices provided by the shapecast function include offsets of all
128 | // root buffers to guarantee they're unique between roots so offset left and right indices here.
129 | const offsetLeft = left + byteOffset;
130 | const offsetRight = right + byteOffset;
131 | let forceChildren = force;
132 | let includesLeft = false;
133 | let includesRight = false;
134 |
135 | if ( nodeIndices ) {
136 |
137 | // if we see that neither the left or right child are included in the set that need to be updated
138 | // then we assume that all children need to be updated.
139 | if ( ! forceChildren ) {
140 |
141 | includesLeft = nodeIndices.has( offsetLeft );
142 | includesRight = nodeIndices.has( offsetRight );
143 | forceChildren = ! includesLeft && ! includesRight;
144 |
145 | }
146 |
147 | } else {
148 |
149 | includesLeft = true;
150 | includesRight = true;
151 |
152 | }
153 |
154 | const traverseLeft = forceChildren || includesLeft;
155 | const traverseRight = forceChildren || includesRight;
156 |
157 | let leftChange = false;
158 | if ( traverseLeft ) {
159 |
160 | leftChange = _traverse( left, byteOffset, forceChildren );
161 |
162 | }
163 |
164 | let rightChange = false;
165 | if ( traverseRight ) {
166 |
167 | rightChange = _traverse( right, byteOffset, forceChildren );
168 |
169 | }
170 |
171 | const didChange = leftChange || rightChange;
172 | if ( didChange ) {
173 |
174 | for ( let i = 0; i < 3; i ++ ) {
175 |
176 | const lefti = left + i;
177 | const righti = right + i;
178 | const minLeftValue = float32Array[ lefti ];
179 | const maxLeftValue = float32Array[ lefti + 3 ];
180 | const minRightValue = float32Array[ righti ];
181 | const maxRightValue = float32Array[ righti + 3 ];
182 |
183 | float32Array[ node32Index + i ] = minLeftValue < minRightValue ? minLeftValue : minRightValue;
184 | float32Array[ node32Index + i + 3 ] = maxLeftValue > maxRightValue ? maxLeftValue : maxRightValue;
185 |
186 | }
187 |
188 | }
189 |
190 | return didChange;
191 |
192 | }
193 |
194 | }
195 |
196 | }
197 |
--------------------------------------------------------------------------------
/src/core/utils/BufferStack.js:
--------------------------------------------------------------------------------
1 | class _BufferStack {
2 |
3 | constructor() {
4 |
5 | this.float32Array = null;
6 | this.uint16Array = null;
7 | this.uint32Array = null;
8 |
9 | const stack = [];
10 | let prevBuffer = null;
11 | this.setBuffer = buffer => {
12 |
13 | if ( prevBuffer ) {
14 |
15 | stack.push( prevBuffer );
16 |
17 | }
18 |
19 | prevBuffer = buffer;
20 | this.float32Array = new Float32Array( buffer );
21 | this.uint16Array = new Uint16Array( buffer );
22 | this.uint32Array = new Uint32Array( buffer );
23 |
24 | };
25 |
26 | this.clearBuffer = () => {
27 |
28 | prevBuffer = null;
29 | this.float32Array = null;
30 | this.uint16Array = null;
31 | this.uint32Array = null;
32 |
33 | if ( stack.length !== 0 ) {
34 |
35 | this.setBuffer( stack.pop() );
36 |
37 | }
38 |
39 | };
40 |
41 | }
42 |
43 | }
44 |
45 | export const BufferStack = new _BufferStack();
46 |
--------------------------------------------------------------------------------
/src/core/utils/intersectUtils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This function performs intersection tests similar to Ray.intersectBox in three.js,
3 | * with the difference that the box values are read from an array to improve performance.
4 | */
5 | export function intersectRay( nodeIndex32, array, ray, near, far ) {
6 |
7 | let tmin, tmax, tymin, tymax, tzmin, tzmax;
8 |
9 | const invdirx = 1 / ray.direction.x,
10 | invdiry = 1 / ray.direction.y,
11 | invdirz = 1 / ray.direction.z;
12 |
13 | const ox = ray.origin.x;
14 | const oy = ray.origin.y;
15 | const oz = ray.origin.z;
16 |
17 | let minx = array[ nodeIndex32 ];
18 | let maxx = array[ nodeIndex32 + 3 ];
19 |
20 | let miny = array[ nodeIndex32 + 1 ];
21 | let maxy = array[ nodeIndex32 + 3 + 1 ];
22 |
23 | let minz = array[ nodeIndex32 + 2 ];
24 | let maxz = array[ nodeIndex32 + 3 + 2 ];
25 |
26 | if ( invdirx >= 0 ) {
27 |
28 | tmin = ( minx - ox ) * invdirx;
29 | tmax = ( maxx - ox ) * invdirx;
30 |
31 | } else {
32 |
33 | tmin = ( maxx - ox ) * invdirx;
34 | tmax = ( minx - ox ) * invdirx;
35 |
36 | }
37 |
38 | if ( invdiry >= 0 ) {
39 |
40 | tymin = ( miny - oy ) * invdiry;
41 | tymax = ( maxy - oy ) * invdiry;
42 |
43 | } else {
44 |
45 | tymin = ( maxy - oy ) * invdiry;
46 | tymax = ( miny - oy ) * invdiry;
47 |
48 | }
49 |
50 | if ( ( tmin > tymax ) || ( tymin > tmax ) ) return false;
51 |
52 | if ( tymin > tmin || isNaN( tmin ) ) tmin = tymin;
53 |
54 | if ( tymax < tmax || isNaN( tmax ) ) tmax = tymax;
55 |
56 | if ( invdirz >= 0 ) {
57 |
58 | tzmin = ( minz - oz ) * invdirz;
59 | tzmax = ( maxz - oz ) * invdirz;
60 |
61 | } else {
62 |
63 | tzmin = ( maxz - oz ) * invdirz;
64 | tzmax = ( minz - oz ) * invdirz;
65 |
66 | }
67 |
68 | if ( ( tmin > tzmax ) || ( tzmin > tmax ) ) return false;
69 |
70 | if ( tzmin > tmin || tmin !== tmin ) tmin = tzmin;
71 |
72 | if ( tzmax < tmax || tmax !== tmax ) tmax = tzmax;
73 |
74 | //return point closest to the ray (positive side)
75 |
76 | return tmin <= far && tmax >= near;
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/core/utils/iterationUtils.template.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable indent */
2 | import { intersectTri } from '../../utils/ThreeRayIntersectUtilities.js';
3 | import { setTriangle } from '../../utils/TriangleUtilities.js';
4 |
5 | export function intersectTris/* @echo INDIRECT_STRING */( bvh, side, ray, offset, count, intersections, near, far ) {
6 |
7 | const { geometry, _indirectBuffer } = bvh;
8 | for ( let i = offset, end = offset + count; i < end; i ++ ) {
9 |
10 | /* @if INDIRECT */
11 |
12 | let vi = _indirectBuffer ? _indirectBuffer[ i ] : i;
13 | intersectTri( geometry, side, ray, vi, intersections, near, far );
14 |
15 | /* @else */
16 |
17 | intersectTri( geometry, side, ray, i, intersections, near, far );
18 |
19 | /* @endif */
20 |
21 | }
22 |
23 | }
24 |
25 | export function intersectClosestTri/* @echo INDIRECT_STRING */( bvh, side, ray, offset, count, near, far ) {
26 |
27 | const { geometry, _indirectBuffer } = bvh;
28 | let dist = Infinity;
29 | let res = null;
30 | for ( let i = offset, end = offset + count; i < end; i ++ ) {
31 |
32 | let intersection;
33 | /* @if INDIRECT */
34 |
35 | intersection = intersectTri( geometry, side, ray, _indirectBuffer ? _indirectBuffer[ i ] : i, null, near, far );
36 |
37 | /* @else */
38 |
39 | intersection = intersectTri( geometry, side, ray, i, null, near, far );
40 |
41 | /* @endif */
42 |
43 | if ( intersection && intersection.distance < dist ) {
44 |
45 | res = intersection;
46 | dist = intersection.distance;
47 |
48 | }
49 |
50 | }
51 |
52 | return res;
53 |
54 | }
55 |
56 | export function iterateOverTriangles/* @echo INDIRECT_STRING */(
57 | offset,
58 | count,
59 | bvh,
60 | intersectsTriangleFunc,
61 | contained,
62 | depth,
63 | triangle
64 | ) {
65 |
66 | const { geometry } = bvh;
67 | const { index } = geometry;
68 | const pos = geometry.attributes.position;
69 | for ( let i = offset, l = count + offset; i < l; i ++ ) {
70 |
71 | let tri;
72 | /* @if INDIRECT */
73 |
74 | tri = bvh.resolveTriangleIndex( i );
75 |
76 | /* @else */
77 |
78 | tri = i;
79 |
80 | /* @endif */
81 | setTriangle( triangle, tri * 3, index, pos );
82 | triangle.needsUpdate = true;
83 |
84 | if ( intersectsTriangleFunc( triangle, tri, contained, depth ) ) {
85 |
86 | return true;
87 |
88 | }
89 |
90 | }
91 |
92 | return false;
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/src/core/utils/nodeBufferUtils.js:
--------------------------------------------------------------------------------
1 | export function IS_LEAF( n16, uint16Array ) {
2 |
3 | return uint16Array[ n16 + 15 ] === 0xFFFF;
4 |
5 | }
6 |
7 | export function OFFSET( n32, uint32Array ) {
8 |
9 | return uint32Array[ n32 + 6 ];
10 |
11 | }
12 |
13 | export function COUNT( n16, uint16Array ) {
14 |
15 | return uint16Array[ n16 + 14 ];
16 |
17 | }
18 |
19 | export function LEFT_NODE( n32 ) {
20 |
21 | return n32 + 8;
22 |
23 | }
24 |
25 | export function RIGHT_NODE( n32, uint32Array ) {
26 |
27 | return uint32Array[ n32 + 6 ];
28 |
29 | }
30 |
31 | export function SPLIT_AXIS( n32, uint32Array ) {
32 |
33 | return uint32Array[ n32 + 7 ];
34 |
35 | }
36 |
37 | export function BOUNDING_DATA_INDEX( n32 ) {
38 |
39 | return n32;
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/gpu/BVHShaderGLSL.js:
--------------------------------------------------------------------------------
1 | export * from './glsl/common_functions.glsl.js';
2 | export * from './glsl/bvh_distance_functions.glsl.js';
3 | export * from './glsl/bvh_ray_functions.glsl.js';
4 | export * from './glsl/bvh_struct_definitions.glsl.js';
5 |
--------------------------------------------------------------------------------
/src/gpu/MeshBVHUniformStruct.js:
--------------------------------------------------------------------------------
1 | import {
2 | DataTexture,
3 | FloatType,
4 | UnsignedIntType,
5 | RGBAFormat,
6 | RGIntegerFormat,
7 | NearestFilter,
8 | BufferAttribute,
9 | } from 'three';
10 | import {
11 | FloatVertexAttributeTexture,
12 | UIntVertexAttributeTexture,
13 | } from './VertexAttributeTexture.js';
14 | import { BYTES_PER_NODE } from '../core/Constants.js';
15 | import {
16 | BOUNDING_DATA_INDEX,
17 | COUNT,
18 | IS_LEAF,
19 | RIGHT_NODE,
20 | OFFSET,
21 | SPLIT_AXIS,
22 | } from '../core/utils/nodeBufferUtils.js';
23 | import { getIndexArray, getVertexCount } from '../core/build/geometryUtils.js';
24 |
25 | export class MeshBVHUniformStruct {
26 |
27 | constructor() {
28 |
29 | this.index = new UIntVertexAttributeTexture();
30 | this.position = new FloatVertexAttributeTexture();
31 | this.bvhBounds = new DataTexture();
32 | this.bvhContents = new DataTexture();
33 | this._cachedIndexAttr = null;
34 |
35 | this.index.overrideItemSize = 3;
36 |
37 | }
38 |
39 | updateFrom( bvh ) {
40 |
41 | const { geometry } = bvh;
42 | bvhToTextures( bvh, this.bvhBounds, this.bvhContents );
43 |
44 | this.position.updateFrom( geometry.attributes.position );
45 |
46 | // dereference a new index attribute if we're using indirect storage
47 | if ( bvh.indirect ) {
48 |
49 | const indirectBuffer = bvh._indirectBuffer;
50 | if (
51 | this._cachedIndexAttr === null ||
52 | this._cachedIndexAttr.count !== indirectBuffer.length
53 | ) {
54 |
55 | if ( geometry.index ) {
56 |
57 | this._cachedIndexAttr = geometry.index.clone();
58 |
59 | } else {
60 |
61 | const array = getIndexArray( getVertexCount( geometry ) );
62 | this._cachedIndexAttr = new BufferAttribute( array, 1, false );
63 |
64 | }
65 |
66 | }
67 |
68 | dereferenceIndex( geometry, indirectBuffer, this._cachedIndexAttr );
69 | this.index.updateFrom( this._cachedIndexAttr );
70 |
71 | } else {
72 |
73 | this.index.updateFrom( geometry.index );
74 |
75 | }
76 |
77 | }
78 |
79 | dispose() {
80 |
81 | const { index, position, bvhBounds, bvhContents } = this;
82 |
83 | if ( index ) index.dispose();
84 | if ( position ) position.dispose();
85 | if ( bvhBounds ) bvhBounds.dispose();
86 | if ( bvhContents ) bvhContents.dispose();
87 |
88 | }
89 |
90 | }
91 |
92 | function dereferenceIndex( geometry, indirectBuffer, target ) {
93 |
94 | const unpacked = target.array;
95 | const indexArray = geometry.index ? geometry.index.array : null;
96 | for ( let i = 0, l = indirectBuffer.length; i < l; i ++ ) {
97 |
98 | const i3 = 3 * i;
99 | const v3 = 3 * indirectBuffer[ i ];
100 | for ( let c = 0; c < 3; c ++ ) {
101 |
102 | unpacked[ i3 + c ] = indexArray ? indexArray[ v3 + c ] : v3 + c;
103 |
104 | }
105 |
106 | }
107 |
108 | }
109 |
110 | function bvhToTextures( bvh, boundsTexture, contentsTexture ) {
111 |
112 | const roots = bvh._roots;
113 |
114 | if ( roots.length !== 1 ) {
115 |
116 | throw new Error( 'MeshBVHUniformStruct: Multi-root BVHs not supported.' );
117 |
118 | }
119 |
120 | const root = roots[ 0 ];
121 | const uint16Array = new Uint16Array( root );
122 | const uint32Array = new Uint32Array( root );
123 | const float32Array = new Float32Array( root );
124 |
125 | // Both bounds need two elements per node so compute the height so it's twice as long as
126 | // the width so we can expand the row by two and still have a square texture
127 | const nodeCount = root.byteLength / BYTES_PER_NODE;
128 | const boundsDimension = 2 * Math.ceil( Math.sqrt( nodeCount / 2 ) );
129 | const boundsArray = new Float32Array( 4 * boundsDimension * boundsDimension );
130 |
131 | const contentsDimension = Math.ceil( Math.sqrt( nodeCount ) );
132 | const contentsArray = new Uint32Array( 2 * contentsDimension * contentsDimension );
133 |
134 | for ( let i = 0; i < nodeCount; i ++ ) {
135 |
136 | const nodeIndex32 = i * BYTES_PER_NODE / 4;
137 | const nodeIndex16 = nodeIndex32 * 2;
138 | const boundsIndex = BOUNDING_DATA_INDEX( nodeIndex32 );
139 | for ( let b = 0; b < 3; b ++ ) {
140 |
141 | boundsArray[ 8 * i + 0 + b ] = float32Array[ boundsIndex + 0 + b ];
142 | boundsArray[ 8 * i + 4 + b ] = float32Array[ boundsIndex + 3 + b ];
143 |
144 | }
145 |
146 | if ( IS_LEAF( nodeIndex16, uint16Array ) ) {
147 |
148 | const count = COUNT( nodeIndex16, uint16Array );
149 | const offset = OFFSET( nodeIndex32, uint32Array );
150 |
151 | const mergedLeafCount = 0xffff0000 | count;
152 | contentsArray[ i * 2 + 0 ] = mergedLeafCount;
153 | contentsArray[ i * 2 + 1 ] = offset;
154 |
155 | } else {
156 |
157 | const rightIndex = 4 * RIGHT_NODE( nodeIndex32, uint32Array ) / BYTES_PER_NODE;
158 | const splitAxis = SPLIT_AXIS( nodeIndex32, uint32Array );
159 |
160 | contentsArray[ i * 2 + 0 ] = splitAxis;
161 | contentsArray[ i * 2 + 1 ] = rightIndex;
162 |
163 | }
164 |
165 | }
166 |
167 | boundsTexture.image.data = boundsArray;
168 | boundsTexture.image.width = boundsDimension;
169 | boundsTexture.image.height = boundsDimension;
170 | boundsTexture.format = RGBAFormat;
171 | boundsTexture.type = FloatType;
172 | boundsTexture.internalFormat = 'RGBA32F';
173 | boundsTexture.minFilter = NearestFilter;
174 | boundsTexture.magFilter = NearestFilter;
175 | boundsTexture.generateMipmaps = false;
176 | boundsTexture.needsUpdate = true;
177 | boundsTexture.dispose();
178 |
179 | contentsTexture.image.data = contentsArray;
180 | contentsTexture.image.width = contentsDimension;
181 | contentsTexture.image.height = contentsDimension;
182 | contentsTexture.format = RGIntegerFormat;
183 | contentsTexture.type = UnsignedIntType;
184 | contentsTexture.internalFormat = 'RG32UI';
185 | contentsTexture.minFilter = NearestFilter;
186 | contentsTexture.magFilter = NearestFilter;
187 | contentsTexture.generateMipmaps = false;
188 | contentsTexture.needsUpdate = true;
189 | contentsTexture.dispose();
190 |
191 | }
192 |
--------------------------------------------------------------------------------
/src/gpu/glsl/bvh_distance_functions.glsl.js:
--------------------------------------------------------------------------------
1 | // Distance to Point
2 | export const bvh_distance_functions = /* glsl */`
3 |
4 | float dot2( vec3 v ) {
5 |
6 | return dot( v, v );
7 |
8 | }
9 |
10 | // https://www.shadertoy.com/view/ttfGWl
11 | vec3 closestPointToTriangle( vec3 p, vec3 v0, vec3 v1, vec3 v2, out vec3 barycoord ) {
12 |
13 | vec3 v10 = v1 - v0;
14 | vec3 v21 = v2 - v1;
15 | vec3 v02 = v0 - v2;
16 |
17 | vec3 p0 = p - v0;
18 | vec3 p1 = p - v1;
19 | vec3 p2 = p - v2;
20 |
21 | vec3 nor = cross( v10, v02 );
22 |
23 | // method 2, in barycentric space
24 | vec3 q = cross( nor, p0 );
25 | float d = 1.0 / dot2( nor );
26 | float u = d * dot( q, v02 );
27 | float v = d * dot( q, v10 );
28 | float w = 1.0 - u - v;
29 |
30 | if( u < 0.0 ) {
31 |
32 | w = clamp( dot( p2, v02 ) / dot2( v02 ), 0.0, 1.0 );
33 | u = 0.0;
34 | v = 1.0 - w;
35 |
36 | } else if( v < 0.0 ) {
37 |
38 | u = clamp( dot( p0, v10 ) / dot2( v10 ), 0.0, 1.0 );
39 | v = 0.0;
40 | w = 1.0 - u;
41 |
42 | } else if( w < 0.0 ) {
43 |
44 | v = clamp( dot( p1, v21 ) / dot2( v21 ), 0.0, 1.0 );
45 | w = 0.0;
46 | u = 1.0-v;
47 |
48 | }
49 |
50 | barycoord = vec3( u, v, w );
51 | return u * v1 + v * v2 + w * v0;
52 |
53 | }
54 |
55 | float distanceToTriangles(
56 | // geometry info and triangle range
57 | sampler2D positionAttr, usampler2D indexAttr, uint offset, uint count,
58 |
59 | // point and cut off range
60 | vec3 point, float closestDistanceSquared,
61 |
62 | // outputs
63 | inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord, inout float side, inout vec3 outPoint
64 | ) {
65 |
66 | bool found = false;
67 | vec3 localBarycoord;
68 | for ( uint i = offset, l = offset + count; i < l; i ++ ) {
69 |
70 | uvec3 indices = uTexelFetch1D( indexAttr, i ).xyz;
71 | vec3 a = texelFetch1D( positionAttr, indices.x ).rgb;
72 | vec3 b = texelFetch1D( positionAttr, indices.y ).rgb;
73 | vec3 c = texelFetch1D( positionAttr, indices.z ).rgb;
74 |
75 | // get the closest point and barycoord
76 | vec3 closestPoint = closestPointToTriangle( point, a, b, c, localBarycoord );
77 | vec3 delta = point - closestPoint;
78 | float sqDist = dot2( delta );
79 | if ( sqDist < closestDistanceSquared ) {
80 |
81 | // set the output results
82 | closestDistanceSquared = sqDist;
83 | faceIndices = uvec4( indices.xyz, i );
84 | faceNormal = normalize( cross( a - b, b - c ) );
85 | barycoord = localBarycoord;
86 | outPoint = closestPoint;
87 | side = sign( dot( faceNormal, delta ) );
88 |
89 | }
90 |
91 | }
92 |
93 | return closestDistanceSquared;
94 |
95 | }
96 |
97 | float distanceSqToBounds( vec3 point, vec3 boundsMin, vec3 boundsMax ) {
98 |
99 | vec3 clampedPoint = clamp( point, boundsMin, boundsMax );
100 | vec3 delta = point - clampedPoint;
101 | return dot( delta, delta );
102 |
103 | }
104 |
105 | float distanceSqToBVHNodeBoundsPoint( vec3 point, sampler2D bvhBounds, uint currNodeIndex ) {
106 |
107 | uint cni2 = currNodeIndex * 2u;
108 | vec3 boundsMin = texelFetch1D( bvhBounds, cni2 ).xyz;
109 | vec3 boundsMax = texelFetch1D( bvhBounds, cni2 + 1u ).xyz;
110 | return distanceSqToBounds( point, boundsMin, boundsMax );
111 |
112 | }
113 |
114 | // use a macro to hide the fact that we need to expand the struct into separate fields
115 | #define\
116 | bvhClosestPointToPoint(\
117 | bvh,\
118 | point, maxDistance, faceIndices, faceNormal, barycoord, side, outPoint\
119 | )\
120 | _bvhClosestPointToPoint(\
121 | bvh.position, bvh.index, bvh.bvhBounds, bvh.bvhContents,\
122 | point, maxDistance, faceIndices, faceNormal, barycoord, side, outPoint\
123 | )
124 |
125 | float _bvhClosestPointToPoint(
126 | // bvh info
127 | sampler2D bvh_position, usampler2D bvh_index, sampler2D bvh_bvhBounds, usampler2D bvh_bvhContents,
128 |
129 | // point to check
130 | vec3 point, float maxDistance,
131 |
132 | // output variables
133 | inout uvec4 faceIndices, inout vec3 faceNormal, inout vec3 barycoord,
134 | inout float side, inout vec3 outPoint
135 | ) {
136 |
137 | // stack needs to be twice as long as the deepest tree we expect because
138 | // we push both the left and right child onto the stack every traversal
139 | int ptr = 0;
140 | uint stack[ BVH_STACK_DEPTH ];
141 | stack[ 0 ] = 0u;
142 |
143 | float closestDistanceSquared = maxDistance * maxDistance;
144 | bool found = false;
145 | while ( ptr > - 1 && ptr < BVH_STACK_DEPTH ) {
146 |
147 | uint currNodeIndex = stack[ ptr ];
148 | ptr --;
149 |
150 | // check if we intersect the current bounds
151 | float boundsHitDistance = distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, currNodeIndex );
152 | if ( boundsHitDistance > closestDistanceSquared ) {
153 |
154 | continue;
155 |
156 | }
157 |
158 | uvec2 boundsInfo = uTexelFetch1D( bvh_bvhContents, currNodeIndex ).xy;
159 | bool isLeaf = bool( boundsInfo.x & 0xffff0000u );
160 | if ( isLeaf ) {
161 |
162 | uint count = boundsInfo.x & 0x0000ffffu;
163 | uint offset = boundsInfo.y;
164 | closestDistanceSquared = distanceToTriangles(
165 | bvh_position, bvh_index, offset, count, point, closestDistanceSquared,
166 |
167 | // outputs
168 | faceIndices, faceNormal, barycoord, side, outPoint
169 | );
170 |
171 | } else {
172 |
173 | uint leftIndex = currNodeIndex + 1u;
174 | uint splitAxis = boundsInfo.x & 0x0000ffffu;
175 | uint rightIndex = boundsInfo.y;
176 | bool leftToRight = distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, leftIndex ) < distanceSqToBVHNodeBoundsPoint( point, bvh_bvhBounds, rightIndex );//rayDirection[ splitAxis ] >= 0.0;
177 | uint c1 = leftToRight ? leftIndex : rightIndex;
178 | uint c2 = leftToRight ? rightIndex : leftIndex;
179 |
180 | // set c2 in the stack so we traverse it later. We need to keep track of a pointer in
181 | // the stack while we traverse. The second pointer added is the one that will be
182 | // traversed first
183 | ptr ++;
184 | stack[ ptr ] = c2;
185 | ptr ++;
186 | stack[ ptr ] = c1;
187 |
188 | }
189 |
190 | }
191 |
192 | return sqrt( closestDistanceSquared );
193 |
194 | }
195 | `;
196 |
--------------------------------------------------------------------------------
/src/gpu/glsl/bvh_struct_definitions.glsl.js:
--------------------------------------------------------------------------------
1 | // Note that a struct cannot be used for the hit record including faceIndices, faceNormal, barycoord,
2 | // side, and dist because on some mobile GPUS (such as Adreno) numbers are afforded less precision specifically
3 | // when in a struct leading to inaccurate hit results. See KhronosGroup/WebGL#3351 for more details.
4 | export const bvh_struct_definitions = /* glsl */`
5 | struct BVH {
6 |
7 | usampler2D index;
8 | sampler2D position;
9 |
10 | sampler2D bvhBounds;
11 | usampler2D bvhContents;
12 |
13 | };
14 | `;
15 |
--------------------------------------------------------------------------------
/src/gpu/glsl/common_functions.glsl.js:
--------------------------------------------------------------------------------
1 | export const common_functions = /* glsl */`
2 |
3 | // A stack of uint32 indices can can store the indices for
4 | // a perfectly balanced tree with a depth up to 31. Lower stack
5 | // depth gets higher performance.
6 | //
7 | // However not all trees are balanced. Best value to set this to
8 | // is the trees max depth.
9 | #ifndef BVH_STACK_DEPTH
10 | #define BVH_STACK_DEPTH 60
11 | #endif
12 |
13 | #ifndef INFINITY
14 | #define INFINITY 1e20
15 | #endif
16 |
17 | // Utilities
18 | uvec4 uTexelFetch1D( usampler2D tex, uint index ) {
19 |
20 | uint width = uint( textureSize( tex, 0 ).x );
21 | uvec2 uv;
22 | uv.x = index % width;
23 | uv.y = index / width;
24 |
25 | return texelFetch( tex, ivec2( uv ), 0 );
26 |
27 | }
28 |
29 | ivec4 iTexelFetch1D( isampler2D tex, uint index ) {
30 |
31 | uint width = uint( textureSize( tex, 0 ).x );
32 | uvec2 uv;
33 | uv.x = index % width;
34 | uv.y = index / width;
35 |
36 | return texelFetch( tex, ivec2( uv ), 0 );
37 |
38 | }
39 |
40 | vec4 texelFetch1D( sampler2D tex, uint index ) {
41 |
42 | uint width = uint( textureSize( tex, 0 ).x );
43 | uvec2 uv;
44 | uv.x = index % width;
45 | uv.y = index / width;
46 |
47 | return texelFetch( tex, ivec2( uv ), 0 );
48 |
49 | }
50 |
51 | vec4 textureSampleBarycoord( sampler2D tex, vec3 barycoord, uvec3 faceIndices ) {
52 |
53 | return
54 | barycoord.x * texelFetch1D( tex, faceIndices.x ) +
55 | barycoord.y * texelFetch1D( tex, faceIndices.y ) +
56 | barycoord.z * texelFetch1D( tex, faceIndices.z );
57 |
58 | }
59 |
60 | void ndcToCameraRay(
61 | vec2 coord, mat4 cameraWorld, mat4 invProjectionMatrix,
62 | out vec3 rayOrigin, out vec3 rayDirection
63 | ) {
64 |
65 | // get camera look direction and near plane for camera clipping
66 | vec4 lookDirection = cameraWorld * vec4( 0.0, 0.0, - 1.0, 0.0 );
67 | vec4 nearVector = invProjectionMatrix * vec4( 0.0, 0.0, - 1.0, 1.0 );
68 | float near = abs( nearVector.z / nearVector.w );
69 |
70 | // get the camera direction and position from camera matrices
71 | vec4 origin = cameraWorld * vec4( 0.0, 0.0, 0.0, 1.0 );
72 | vec4 direction = invProjectionMatrix * vec4( coord, 0.5, 1.0 );
73 | direction /= direction.w;
74 | direction = cameraWorld * direction - origin;
75 |
76 | // slide the origin along the ray until it sits at the near clip plane position
77 | origin.xyz += direction.xyz * near / dot( direction, lookDirection );
78 |
79 | rayOrigin = origin.xyz;
80 | rayDirection = direction.xyz;
81 |
82 | }
83 | `;
84 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | export { MeshBVH } from './core/MeshBVH.js';
2 | export { MeshBVHHelper } from './objects/MeshBVHHelper.js';
3 | export { CENTER, AVERAGE, SAH, NOT_INTERSECTED, INTERSECTED, CONTAINED } from './core/Constants.js';
4 | export { getBVHExtremes, estimateMemoryInBytes, getJSONStructure, validateBounds } from './debug/Debug.js';
5 | export * from './utils/ExtensionUtilities.js';
6 | export { getTriangleHitPointInfo } from './utils/TriangleUtilities.js';
7 | export * from './math/ExtendedTriangle.js';
8 | export * from './math/OrientedBox.js';
9 | export * from './gpu/MeshBVHUniformStruct.js';
10 | export * from './gpu/VertexAttributeTexture.js';
11 | export * from './utils/StaticGeometryGenerator.js';
12 | export * as BVHShaderGLSL from './gpu/BVHShaderGLSL.js';
13 |
14 | // backwards compatibility
15 | import * as BVHShaderGLSL from './gpu/BVHShaderGLSL.js';
16 | export const shaderStructs = BVHShaderGLSL.bvh_struct_definitions;
17 | export const shaderDistanceFunction = BVHShaderGLSL.bvh_distance_functions;
18 | export const shaderIntersectFunction = `
19 | ${ BVHShaderGLSL.common_functions }
20 | ${ BVHShaderGLSL.bvh_ray_functions }
21 | `;
22 |
--------------------------------------------------------------------------------
/src/math/MathUtilities.js:
--------------------------------------------------------------------------------
1 | import { Vector3, Vector2, Plane, Line3 } from 'three';
2 |
3 | export const closestPointLineToLine = ( function () {
4 |
5 | // https://github.com/juj/MathGeoLib/blob/master/src/Geometry/Line.cpp#L56
6 | const dir1 = new Vector3();
7 | const dir2 = new Vector3();
8 | const v02 = new Vector3();
9 | return function closestPointLineToLine( l1, l2, result ) {
10 |
11 | const v0 = l1.start;
12 | const v10 = dir1;
13 | const v2 = l2.start;
14 | const v32 = dir2;
15 |
16 | v02.subVectors( v0, v2 );
17 | dir1.subVectors( l1.end, l1.start );
18 | dir2.subVectors( l2.end, l2.start );
19 |
20 | // float d0232 = v02.Dot(v32);
21 | const d0232 = v02.dot( v32 );
22 |
23 | // float d3210 = v32.Dot(v10);
24 | const d3210 = v32.dot( v10 );
25 |
26 | // float d3232 = v32.Dot(v32);
27 | const d3232 = v32.dot( v32 );
28 |
29 | // float d0210 = v02.Dot(v10);
30 | const d0210 = v02.dot( v10 );
31 |
32 | // float d1010 = v10.Dot(v10);
33 | const d1010 = v10.dot( v10 );
34 |
35 | // float denom = d1010*d3232 - d3210*d3210;
36 | const denom = d1010 * d3232 - d3210 * d3210;
37 |
38 | let d, d2;
39 | if ( denom !== 0 ) {
40 |
41 | d = ( d0232 * d3210 - d0210 * d3232 ) / denom;
42 |
43 | } else {
44 |
45 | d = 0;
46 |
47 | }
48 |
49 | d2 = ( d0232 + d * d3210 ) / d3232;
50 |
51 | result.x = d;
52 | result.y = d2;
53 |
54 | };
55 |
56 | } )();
57 |
58 | export const closestPointsSegmentToSegment = ( function () {
59 |
60 | // https://github.com/juj/MathGeoLib/blob/master/src/Geometry/LineSegment.cpp#L187
61 | const paramResult = new Vector2();
62 | const temp1 = new Vector3();
63 | const temp2 = new Vector3();
64 | return function closestPointsSegmentToSegment( l1, l2, target1, target2 ) {
65 |
66 | closestPointLineToLine( l1, l2, paramResult );
67 |
68 | let d = paramResult.x;
69 | let d2 = paramResult.y;
70 | if ( d >= 0 && d <= 1 && d2 >= 0 && d2 <= 1 ) {
71 |
72 | l1.at( d, target1 );
73 | l2.at( d2, target2 );
74 |
75 | return;
76 |
77 | } else if ( d >= 0 && d <= 1 ) {
78 |
79 | // Only d2 is out of bounds.
80 | if ( d2 < 0 ) {
81 |
82 | l2.at( 0, target2 );
83 |
84 | } else {
85 |
86 | l2.at( 1, target2 );
87 |
88 | }
89 |
90 | l1.closestPointToPoint( target2, true, target1 );
91 | return;
92 |
93 | } else if ( d2 >= 0 && d2 <= 1 ) {
94 |
95 | // Only d is out of bounds.
96 | if ( d < 0 ) {
97 |
98 | l1.at( 0, target1 );
99 |
100 | } else {
101 |
102 | l1.at( 1, target1 );
103 |
104 | }
105 |
106 | l2.closestPointToPoint( target1, true, target2 );
107 | return;
108 |
109 | } else {
110 |
111 | // Both u and u2 are out of bounds.
112 | let p;
113 | if ( d < 0 ) {
114 |
115 | p = l1.start;
116 |
117 | } else {
118 |
119 | p = l1.end;
120 |
121 | }
122 |
123 | let p2;
124 | if ( d2 < 0 ) {
125 |
126 | p2 = l2.start;
127 |
128 | } else {
129 |
130 | p2 = l2.end;
131 |
132 | }
133 |
134 | const closestPoint = temp1;
135 | const closestPoint2 = temp2;
136 | l1.closestPointToPoint( p2, true, temp1 );
137 | l2.closestPointToPoint( p, true, temp2 );
138 |
139 | if ( closestPoint.distanceToSquared( p2 ) <= closestPoint2.distanceToSquared( p ) ) {
140 |
141 | target1.copy( closestPoint );
142 | target2.copy( p2 );
143 | return;
144 |
145 | } else {
146 |
147 | target1.copy( p );
148 | target2.copy( closestPoint2 );
149 | return;
150 |
151 | }
152 |
153 | }
154 |
155 | };
156 |
157 | } )();
158 |
159 |
160 | export const sphereIntersectTriangle = ( function () {
161 |
162 | // https://stackoverflow.com/questions/34043955/detect-collision-between-sphere-and-triangle-in-three-js
163 | const closestPointTemp = new Vector3();
164 | const projectedPointTemp = new Vector3();
165 | const planeTemp = new Plane();
166 | const lineTemp = new Line3();
167 | return function sphereIntersectTriangle( sphere, triangle ) {
168 |
169 | const { radius, center } = sphere;
170 | const { a, b, c } = triangle;
171 |
172 | // phase 1
173 | lineTemp.start = a;
174 | lineTemp.end = b;
175 | const closestPoint1 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
176 | if ( closestPoint1.distanceTo( center ) <= radius ) return true;
177 |
178 | lineTemp.start = a;
179 | lineTemp.end = c;
180 | const closestPoint2 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
181 | if ( closestPoint2.distanceTo( center ) <= radius ) return true;
182 |
183 | lineTemp.start = b;
184 | lineTemp.end = c;
185 | const closestPoint3 = lineTemp.closestPointToPoint( center, true, closestPointTemp );
186 | if ( closestPoint3.distanceTo( center ) <= radius ) return true;
187 |
188 | // phase 2
189 | const plane = triangle.getPlane( planeTemp );
190 | const dp = Math.abs( plane.distanceToPoint( center ) );
191 | if ( dp <= radius ) {
192 |
193 | const pp = plane.projectPoint( center, projectedPointTemp );
194 | const cp = triangle.containsPoint( pp );
195 | if ( cp ) return true;
196 |
197 | }
198 |
199 | return false;
200 |
201 | };
202 |
203 | } )();
204 |
--------------------------------------------------------------------------------
/src/math/SeparatingAxisBounds.js:
--------------------------------------------------------------------------------
1 | import { Vector3 } from 'three';
2 |
3 | export class SeparatingAxisBounds {
4 |
5 | constructor() {
6 |
7 | this.min = Infinity;
8 | this.max = - Infinity;
9 |
10 | }
11 |
12 | setFromPointsField( points, field ) {
13 |
14 | let min = Infinity;
15 | let max = - Infinity;
16 | for ( let i = 0, l = points.length; i < l; i ++ ) {
17 |
18 | const p = points[ i ];
19 | const val = p[ field ];
20 | min = val < min ? val : min;
21 | max = val > max ? val : max;
22 |
23 | }
24 |
25 | this.min = min;
26 | this.max = max;
27 |
28 | }
29 |
30 | setFromPoints( axis, points ) {
31 |
32 | let min = Infinity;
33 | let max = - Infinity;
34 | for ( let i = 0, l = points.length; i < l; i ++ ) {
35 |
36 | const p = points[ i ];
37 | const val = axis.dot( p );
38 | min = val < min ? val : min;
39 | max = val > max ? val : max;
40 |
41 | }
42 |
43 | this.min = min;
44 | this.max = max;
45 |
46 | }
47 |
48 | isSeparated( other ) {
49 |
50 | return this.min > other.max || other.min > this.max;
51 |
52 | }
53 |
54 | }
55 |
56 | SeparatingAxisBounds.prototype.setFromBox = ( function () {
57 |
58 | const p = new Vector3();
59 | return function setFromBox( axis, box ) {
60 |
61 | const boxMin = box.min;
62 | const boxMax = box.max;
63 | let min = Infinity;
64 | let max = - Infinity;
65 | for ( let x = 0; x <= 1; x ++ ) {
66 |
67 | for ( let y = 0; y <= 1; y ++ ) {
68 |
69 | for ( let z = 0; z <= 1; z ++ ) {
70 |
71 | p.x = boxMin.x * x + boxMax.x * ( 1 - x );
72 | p.y = boxMin.y * y + boxMax.y * ( 1 - y );
73 | p.z = boxMin.z * z + boxMax.z * ( 1 - z );
74 |
75 | const val = axis.dot( p );
76 | min = Math.min( val, min );
77 | max = Math.max( val, max );
78 |
79 | }
80 |
81 | }
82 |
83 | }
84 |
85 | this.min = min;
86 | this.max = max;
87 |
88 | };
89 |
90 | } )();
91 |
92 | export const areIntersecting = ( function () {
93 |
94 | const cacheSatBounds = new SeparatingAxisBounds();
95 | return function areIntersecting( shape1, shape2 ) {
96 |
97 | const points1 = shape1.points;
98 | const satAxes1 = shape1.satAxes;
99 | const satBounds1 = shape1.satBounds;
100 |
101 | const points2 = shape2.points;
102 | const satAxes2 = shape2.satAxes;
103 | const satBounds2 = shape2.satBounds;
104 |
105 | // check axes of the first shape
106 | for ( let i = 0; i < 3; i ++ ) {
107 |
108 | const sb = satBounds1[ i ];
109 | const sa = satAxes1[ i ];
110 | cacheSatBounds.setFromPoints( sa, points2 );
111 | if ( sb.isSeparated( cacheSatBounds ) ) return false;
112 |
113 | }
114 |
115 | // check axes of the second shape
116 | for ( let i = 0; i < 3; i ++ ) {
117 |
118 | const sb = satBounds2[ i ];
119 | const sa = satAxes2[ i ];
120 | cacheSatBounds.setFromPoints( sa, points1 );
121 | if ( sb.isSeparated( cacheSatBounds ) ) return false;
122 |
123 | }
124 |
125 | };
126 |
127 | } )();
128 |
--------------------------------------------------------------------------------
/src/utils/ArrayBoxUtilities.js:
--------------------------------------------------------------------------------
1 | export function arrayToBox( nodeIndex32, array, target ) {
2 |
3 | target.min.x = array[ nodeIndex32 ];
4 | target.min.y = array[ nodeIndex32 + 1 ];
5 | target.min.z = array[ nodeIndex32 + 2 ];
6 |
7 | target.max.x = array[ nodeIndex32 + 3 ];
8 | target.max.y = array[ nodeIndex32 + 4 ];
9 | target.max.z = array[ nodeIndex32 + 5 ];
10 |
11 | return target;
12 |
13 | }
14 |
15 | export function makeEmptyBounds( target ) {
16 |
17 | target[ 0 ] = target[ 1 ] = target[ 2 ] = Infinity;
18 | target[ 3 ] = target[ 4 ] = target[ 5 ] = - Infinity;
19 |
20 | }
21 |
22 | export function getLongestEdgeIndex( bounds ) {
23 |
24 | let splitDimIdx = - 1;
25 | let splitDist = - Infinity;
26 |
27 | for ( let i = 0; i < 3; i ++ ) {
28 |
29 | const dist = bounds[ i + 3 ] - bounds[ i ];
30 | if ( dist > splitDist ) {
31 |
32 | splitDist = dist;
33 | splitDimIdx = i;
34 |
35 | }
36 |
37 | }
38 |
39 | return splitDimIdx;
40 |
41 | }
42 |
43 | // copies bounds a into bounds b
44 | export function copyBounds( source, target ) {
45 |
46 | target.set( source );
47 |
48 | }
49 |
50 | // sets bounds target to the union of bounds a and b
51 | export function unionBounds( a, b, target ) {
52 |
53 | let aVal, bVal;
54 | for ( let d = 0; d < 3; d ++ ) {
55 |
56 | const d3 = d + 3;
57 |
58 | // set the minimum values
59 | aVal = a[ d ];
60 | bVal = b[ d ];
61 | target[ d ] = aVal < bVal ? aVal : bVal;
62 |
63 | // set the max values
64 | aVal = a[ d3 ];
65 | bVal = b[ d3 ];
66 | target[ d3 ] = aVal > bVal ? aVal : bVal;
67 |
68 | }
69 |
70 | }
71 |
72 | // expands the given bounds by the provided triangle bounds
73 | export function expandByTriangleBounds( startIndex, triangleBounds, bounds ) {
74 |
75 | for ( let d = 0; d < 3; d ++ ) {
76 |
77 | const tCenter = triangleBounds[ startIndex + 2 * d ];
78 | const tHalf = triangleBounds[ startIndex + 2 * d + 1 ];
79 |
80 | const tMin = tCenter - tHalf;
81 | const tMax = tCenter + tHalf;
82 |
83 | if ( tMin < bounds[ d ] ) {
84 |
85 | bounds[ d ] = tMin;
86 |
87 | }
88 |
89 | if ( tMax > bounds[ d + 3 ] ) {
90 |
91 | bounds[ d + 3 ] = tMax;
92 |
93 | }
94 |
95 | }
96 |
97 | }
98 |
99 | // compute bounds surface area
100 | export function computeSurfaceArea( bounds ) {
101 |
102 | const d0 = bounds[ 3 ] - bounds[ 0 ];
103 | const d1 = bounds[ 4 ] - bounds[ 1 ];
104 | const d2 = bounds[ 5 ] - bounds[ 2 ];
105 |
106 | return 2 * ( d0 * d1 + d1 * d2 + d2 * d0 );
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/utils/BufferUtils.js:
--------------------------------------------------------------------------------
1 | export function isSharedArrayBufferSupported() {
2 |
3 | return typeof SharedArrayBuffer !== 'undefined';
4 |
5 | }
6 |
7 | export function convertToBufferType( array, BufferConstructor ) {
8 |
9 | if ( array === null ) {
10 |
11 | return array;
12 |
13 | } else if ( array.buffer ) {
14 |
15 | const buffer = array.buffer;
16 | if ( buffer.constructor === BufferConstructor ) {
17 |
18 | return array;
19 |
20 | }
21 |
22 | const ArrayConstructor = array.constructor;
23 | const result = new ArrayConstructor( new BufferConstructor( buffer.byteLength ) );
24 | result.set( array );
25 | return result;
26 |
27 | } else {
28 |
29 | if ( array.constructor === BufferConstructor ) {
30 |
31 | return array;
32 |
33 | }
34 |
35 | const result = new BufferConstructor( array.byteLength );
36 | new Uint8Array( result ).set( new Uint8Array( array ) );
37 | return result;
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/src/utils/ExtendedTrianglePool.js:
--------------------------------------------------------------------------------
1 | import { ExtendedTriangle } from '../math/ExtendedTriangle.js';
2 | import { PrimitivePool } from './PrimitivePool.js';
3 |
4 | class ExtendedTrianglePoolBase extends PrimitivePool {
5 |
6 | constructor() {
7 |
8 | super( () => new ExtendedTriangle() );
9 |
10 | }
11 |
12 | }
13 |
14 | export const ExtendedTrianglePool = /* @__PURE__ */ new ExtendedTrianglePoolBase();
15 |
--------------------------------------------------------------------------------
/src/utils/GeometryRayIntersectUtilities.js:
--------------------------------------------------------------------------------
1 | // converts the given BVH raycast intersection to align with the three.js raycast
2 | // structure (include object, world space distance and point).
3 | export function convertRaycastIntersect( hit, object, raycaster ) {
4 |
5 | if ( hit === null ) {
6 |
7 | return null;
8 |
9 | }
10 |
11 | hit.point.applyMatrix4( object.matrixWorld );
12 | hit.distance = hit.point.distanceTo( raycaster.ray.origin );
13 | hit.object = object;
14 |
15 | return hit;
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/src/utils/PrimitivePool.js:
--------------------------------------------------------------------------------
1 | export class PrimitivePool {
2 |
3 | constructor( getNewPrimitive ) {
4 |
5 | this._getNewPrimitive = getNewPrimitive;
6 | this._primitives = [];
7 |
8 | }
9 |
10 | getPrimitive() {
11 |
12 | const primitives = this._primitives;
13 | if ( primitives.length === 0 ) {
14 |
15 | return this._getNewPrimitive();
16 |
17 | } else {
18 |
19 | return primitives.pop();
20 |
21 | }
22 |
23 | }
24 |
25 | releasePrimitive( primitive ) {
26 |
27 | this._primitives.push( primitive );
28 |
29 | }
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/ThreeRayIntersectUtilities.js:
--------------------------------------------------------------------------------
1 | import { Vector3, Vector2, Triangle, DoubleSide, BackSide, REVISION } from 'three';
2 |
3 | const IS_GT_REVISION_169 = parseInt( REVISION ) >= 169;
4 |
5 | // Ripped and modified From THREE.js Mesh raycast
6 | // https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L115
7 | const _vA = /* @__PURE__ */ new Vector3();
8 | const _vB = /* @__PURE__ */ new Vector3();
9 | const _vC = /* @__PURE__ */ new Vector3();
10 |
11 | const _uvA = /* @__PURE__ */ new Vector2();
12 | const _uvB = /* @__PURE__ */ new Vector2();
13 | const _uvC = /* @__PURE__ */ new Vector2();
14 |
15 | const _normalA = /* @__PURE__ */ new Vector3();
16 | const _normalB = /* @__PURE__ */ new Vector3();
17 | const _normalC = /* @__PURE__ */ new Vector3();
18 |
19 | const _intersectionPoint = /* @__PURE__ */ new Vector3();
20 | function checkIntersection( ray, pA, pB, pC, point, side, near, far ) {
21 |
22 | let intersect;
23 | if ( side === BackSide ) {
24 |
25 | intersect = ray.intersectTriangle( pC, pB, pA, true, point );
26 |
27 | } else {
28 |
29 | intersect = ray.intersectTriangle( pA, pB, pC, side !== DoubleSide, point );
30 |
31 | }
32 |
33 | if ( intersect === null ) return null;
34 |
35 | const distance = ray.origin.distanceTo( point );
36 |
37 | if ( distance < near || distance > far ) return null;
38 |
39 | return {
40 |
41 | distance: distance,
42 | point: point.clone(),
43 |
44 | };
45 |
46 | }
47 |
48 | function checkBufferGeometryIntersection( ray, position, normal, uv, uv1, a, b, c, side, near, far ) {
49 |
50 | _vA.fromBufferAttribute( position, a );
51 | _vB.fromBufferAttribute( position, b );
52 | _vC.fromBufferAttribute( position, c );
53 |
54 | const intersection = checkIntersection( ray, _vA, _vB, _vC, _intersectionPoint, side, near, far );
55 |
56 | if ( intersection ) {
57 |
58 | const barycoord = new Vector3();
59 | Triangle.getBarycoord( _intersectionPoint, _vA, _vB, _vC, barycoord );
60 |
61 | if ( uv ) {
62 |
63 | _uvA.fromBufferAttribute( uv, a );
64 | _uvB.fromBufferAttribute( uv, b );
65 | _uvC.fromBufferAttribute( uv, c );
66 |
67 | intersection.uv = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() );
68 |
69 | }
70 |
71 | if ( uv1 ) {
72 |
73 | _uvA.fromBufferAttribute( uv1, a );
74 | _uvB.fromBufferAttribute( uv1, b );
75 | _uvC.fromBufferAttribute( uv1, c );
76 |
77 | intersection.uv1 = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2() );
78 |
79 | }
80 |
81 | if ( normal ) {
82 |
83 | _normalA.fromBufferAttribute( normal, a );
84 | _normalB.fromBufferAttribute( normal, b );
85 | _normalC.fromBufferAttribute( normal, c );
86 |
87 | intersection.normal = Triangle.getInterpolation( _intersectionPoint, _vA, _vB, _vC, _normalA, _normalB, _normalC, new Vector3() );
88 | if ( intersection.normal.dot( ray.direction ) > 0 ) {
89 |
90 | intersection.normal.multiplyScalar( - 1 );
91 |
92 | }
93 |
94 | }
95 |
96 | const face = {
97 | a: a,
98 | b: b,
99 | c: c,
100 | normal: new Vector3(),
101 | materialIndex: 0
102 | };
103 |
104 | Triangle.getNormal( _vA, _vB, _vC, face.normal );
105 |
106 | intersection.face = face;
107 | intersection.faceIndex = a;
108 |
109 | if ( IS_GT_REVISION_169 ) {
110 |
111 | intersection.barycoord = barycoord;
112 |
113 | }
114 |
115 | }
116 |
117 | return intersection;
118 |
119 | }
120 |
121 | // https://github.com/mrdoob/three.js/blob/0aa87c999fe61e216c1133fba7a95772b503eddf/src/objects/Mesh.js#L258
122 | function intersectTri( geo, side, ray, tri, intersections, near, far ) {
123 |
124 | const triOffset = tri * 3;
125 | let a = triOffset + 0;
126 | let b = triOffset + 1;
127 | let c = triOffset + 2;
128 |
129 | const index = geo.index;
130 | if ( geo.index ) {
131 |
132 | a = index.getX( a );
133 | b = index.getX( b );
134 | c = index.getX( c );
135 |
136 | }
137 |
138 | const { position, normal, uv, uv1 } = geo.attributes;
139 | const intersection = checkBufferGeometryIntersection( ray, position, normal, uv, uv1, a, b, c, side, near, far );
140 |
141 | if ( intersection ) {
142 |
143 | intersection.faceIndex = tri;
144 | if ( intersections ) intersections.push( intersection );
145 | return intersection;
146 |
147 | }
148 |
149 | return null;
150 |
151 | }
152 |
153 | export { intersectTri };
154 |
--------------------------------------------------------------------------------
/src/utils/TriangleUtilities.js:
--------------------------------------------------------------------------------
1 |
2 | import { Vector2, Vector3, Triangle } from 'three';
3 |
4 | // sets the vertices of triangle `tri` with the 3 vertices after i
5 | export function setTriangle( tri, i, index, pos ) {
6 |
7 | const ta = tri.a;
8 | const tb = tri.b;
9 | const tc = tri.c;
10 |
11 | let i0 = i;
12 | let i1 = i + 1;
13 | let i2 = i + 2;
14 | if ( index ) {
15 |
16 | i0 = index.getX( i0 );
17 | i1 = index.getX( i1 );
18 | i2 = index.getX( i2 );
19 |
20 | }
21 |
22 | ta.x = pos.getX( i0 );
23 | ta.y = pos.getY( i0 );
24 | ta.z = pos.getZ( i0 );
25 |
26 | tb.x = pos.getX( i1 );
27 | tb.y = pos.getY( i1 );
28 | tb.z = pos.getZ( i1 );
29 |
30 | tc.x = pos.getX( i2 );
31 | tc.y = pos.getY( i2 );
32 | tc.z = pos.getZ( i2 );
33 |
34 | }
35 |
36 | const tempV1 = /* @__PURE__ */ new Vector3();
37 | const tempV2 = /* @__PURE__ */ new Vector3();
38 | const tempV3 = /* @__PURE__ */ new Vector3();
39 | const tempUV1 = /* @__PURE__ */ new Vector2();
40 | const tempUV2 = /* @__PURE__ */ new Vector2();
41 | const tempUV3 = /* @__PURE__ */ new Vector2();
42 |
43 | export function getTriangleHitPointInfo( point, geometry, triangleIndex, target ) {
44 |
45 | const indices = geometry.getIndex().array;
46 | const positions = geometry.getAttribute( 'position' );
47 | const uvs = geometry.getAttribute( 'uv' );
48 |
49 | const a = indices[ triangleIndex * 3 ];
50 | const b = indices[ triangleIndex * 3 + 1 ];
51 | const c = indices[ triangleIndex * 3 + 2 ];
52 |
53 | tempV1.fromBufferAttribute( positions, a );
54 | tempV2.fromBufferAttribute( positions, b );
55 | tempV3.fromBufferAttribute( positions, c );
56 |
57 | // find the associated material index
58 | let materialIndex = 0;
59 | const groups = geometry.groups;
60 | const firstVertexIndex = triangleIndex * 3;
61 | for ( let i = 0, l = groups.length; i < l; i ++ ) {
62 |
63 | const group = groups[ i ];
64 | const { start, count } = group;
65 | if ( firstVertexIndex >= start && firstVertexIndex < start + count ) {
66 |
67 | materialIndex = group.materialIndex;
68 | break;
69 |
70 | }
71 |
72 | }
73 |
74 | // extract barycoord
75 | const barycoord = target && target.barycoord ? target.barycoord : new Vector3();
76 | Triangle.getBarycoord( point, tempV1, tempV2, tempV3, barycoord );
77 |
78 | // extract uvs
79 | let uv = null;
80 | if ( uvs ) {
81 |
82 | tempUV1.fromBufferAttribute( uvs, a );
83 | tempUV2.fromBufferAttribute( uvs, b );
84 | tempUV3.fromBufferAttribute( uvs, c );
85 |
86 | if ( target && target.uv ) uv = target.uv;
87 | else uv = new Vector2();
88 |
89 | Triangle.getInterpolation( point, tempV1, tempV2, tempV3, tempUV1, tempUV2, tempUV3, uv );
90 |
91 | }
92 |
93 | // adjust the provided target or create a new one
94 | if ( target ) {
95 |
96 | if ( ! target.face ) target.face = { };
97 | target.face.a = a;
98 | target.face.b = b;
99 | target.face.c = c;
100 | target.face.materialIndex = materialIndex;
101 | if ( ! target.face.normal ) target.face.normal = new Vector3();
102 | Triangle.getNormal( tempV1, tempV2, tempV3, target.face.normal );
103 |
104 | if ( uv ) target.uv = uv;
105 | target.barycoord = barycoord;
106 |
107 | return target;
108 |
109 | } else {
110 |
111 | return {
112 | face: {
113 | a: a,
114 | b: b,
115 | c: c,
116 | materialIndex: materialIndex,
117 | normal: Triangle.getNormal( tempV1, tempV2, tempV3, new Vector3() )
118 | },
119 | uv: uv,
120 | barycoord: barycoord,
121 | };
122 |
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/src/workers/GenerateMeshBVHWorker.js:
--------------------------------------------------------------------------------
1 | import { Box3, BufferAttribute } from 'three';
2 | import { MeshBVH } from '../core/MeshBVH.js';
3 | import { WorkerBase } from './utils/WorkerBase.js';
4 |
5 | export class GenerateMeshBVHWorker extends WorkerBase {
6 |
7 | constructor() {
8 |
9 | const worker = new Worker( new URL( './generateMeshBVH.worker.js', import.meta.url ), { type: 'module' } );
10 | super( worker );
11 | this.name = 'GenerateMeshBVHWorker';
12 |
13 | }
14 |
15 | runTask( worker, geometry, options = {} ) {
16 |
17 | return new Promise( ( resolve, reject ) => {
18 |
19 | if (
20 | geometry.getAttribute( 'position' ).isInterleavedBufferAttribute ||
21 | geometry.index && geometry.index.isInterleavedBufferAttribute
22 | ) {
23 |
24 | throw new Error( 'GenerateMeshBVHWorker: InterleavedBufferAttribute are not supported for the geometry attributes.' );
25 |
26 | }
27 |
28 | worker.onerror = e => {
29 |
30 | reject( new Error( `GenerateMeshBVHWorker: ${ e.message }` ) );
31 |
32 | };
33 |
34 | worker.onmessage = e => {
35 |
36 | const { data } = e;
37 |
38 | if ( data.error ) {
39 |
40 | reject( new Error( data.error ) );
41 | worker.onmessage = null;
42 |
43 | } else if ( data.serialized ) {
44 |
45 | const { serialized, position } = data;
46 | const bvh = MeshBVH.deserialize( serialized, geometry, { setIndex: false } );
47 | const boundsOptions = Object.assign( {
48 |
49 | setBoundingBox: true,
50 |
51 | }, options );
52 |
53 | // we need to replace the arrays because they're neutered entirely by the
54 | // webworker transfer.
55 | geometry.attributes.position.array = position;
56 | if ( serialized.index ) {
57 |
58 | if ( geometry.index ) {
59 |
60 | geometry.index.array = serialized.index;
61 |
62 | } else {
63 |
64 | const newIndex = new BufferAttribute( serialized.index, 1, false );
65 | geometry.setIndex( newIndex );
66 |
67 | }
68 |
69 | }
70 |
71 | if ( boundsOptions.setBoundingBox ) {
72 |
73 | geometry.boundingBox = bvh.getBoundingBox( new Box3() );
74 |
75 | }
76 |
77 | if ( options.onProgress ) {
78 |
79 | options.onProgress( data.progress );
80 |
81 | }
82 |
83 | resolve( bvh );
84 | worker.onmessage = null;
85 |
86 | } else if ( options.onProgress ) {
87 |
88 | options.onProgress( data.progress );
89 |
90 | }
91 |
92 | };
93 |
94 | const index = geometry.index ? geometry.index.array : null;
95 | const position = geometry.attributes.position.array;
96 | const transferable = [ position ];
97 | if ( index ) {
98 |
99 | transferable.push( index );
100 |
101 | }
102 |
103 | worker.postMessage( {
104 |
105 | index,
106 | position,
107 | options: {
108 | ...options,
109 | onProgress: null,
110 | includedProgressCallback: Boolean( options.onProgress ),
111 | groups: [ ... geometry.groups ],
112 | },
113 |
114 | }, transferable.map( arr => arr.buffer ).filter( v => ( typeof SharedArrayBuffer === 'undefined' ) || ! ( v instanceof SharedArrayBuffer ) ) );
115 |
116 | } );
117 |
118 | }
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/workers/ParallelMeshBVHWorker.js:
--------------------------------------------------------------------------------
1 | import { Box3, BufferAttribute } from 'three';
2 | import { MeshBVH } from '../core/MeshBVH.js';
3 | import { WorkerBase } from './utils/WorkerBase.js';
4 | import { convertToBufferType, isSharedArrayBufferSupported } from '../utils/BufferUtils.js';
5 | import { GenerateMeshBVHWorker } from './GenerateMeshBVHWorker.js';
6 | import { ensureIndex } from '../core/build/geometryUtils.js';
7 |
8 | const DEFAULT_WORKER_COUNT = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency : 4;
9 | class _ParallelMeshBVHWorker extends WorkerBase {
10 |
11 | constructor() {
12 |
13 | const worker = new Worker( new URL( './parallelMeshBVH.worker.js', import.meta.url ), { type: 'module' } );
14 | super( worker );
15 |
16 | this.name = 'ParallelMeshBVHWorker';
17 | this.maxWorkerCount = Math.max( DEFAULT_WORKER_COUNT, 4 );
18 |
19 | if ( ! isSharedArrayBufferSupported() ) {
20 |
21 | throw new Error( 'ParallelMeshBVHWorker: Shared Array Buffers are not supported.' );
22 |
23 | }
24 |
25 | }
26 |
27 | runTask( worker, geometry, options = {} ) {
28 |
29 | return new Promise( ( resolve, reject ) => {
30 |
31 | if ( ! geometry.index && ! options.indirect ) {
32 |
33 | ensureIndex( geometry, options );
34 |
35 | }
36 |
37 | if (
38 | geometry.getAttribute( 'position' ).isInterleavedBufferAttribute ||
39 | geometry.index && geometry.index.isInterleavedBufferAttribute
40 | ) {
41 |
42 | throw new Error( 'ParallelMeshBVHWorker: InterleavedBufferAttribute are not supported for the geometry attributes.' );
43 |
44 | }
45 |
46 | worker.onerror = e => {
47 |
48 | reject( new Error( `ParallelMeshBVHWorker: ${ e.message }` ) );
49 |
50 | };
51 |
52 | worker.onmessage = e => {
53 |
54 | const { data } = e;
55 |
56 | if ( data.error ) {
57 |
58 | reject( new Error( data.error ) );
59 | worker.onmessage = null;
60 |
61 | } else if ( data.serialized ) {
62 |
63 | const { serialized, position } = data;
64 | const bvh = MeshBVH.deserialize( serialized, geometry, { setIndex: false } );
65 | const boundsOptions = {
66 | setBoundingBox: true,
67 | ...options,
68 | };
69 |
70 | // we need to replace the arrays because they're neutered entirely by the
71 | // webworker transfer.
72 | geometry.attributes.position.array = position;
73 | if ( serialized.index ) {
74 |
75 | if ( geometry.index ) {
76 |
77 | geometry.index.array = serialized.index;
78 |
79 | } else {
80 |
81 | const newIndex = new BufferAttribute( serialized.index, 1, false );
82 | geometry.setIndex( newIndex );
83 |
84 | }
85 |
86 | }
87 |
88 | if ( boundsOptions.setBoundingBox ) {
89 |
90 | geometry.boundingBox = bvh.getBoundingBox( new Box3() );
91 |
92 | }
93 |
94 | if ( options.onProgress ) {
95 |
96 | options.onProgress( data.progress );
97 |
98 | }
99 |
100 | resolve( bvh );
101 | worker.onmessage = null;
102 |
103 | } else if ( options.onProgress ) {
104 |
105 | options.onProgress( data.progress );
106 |
107 | }
108 |
109 | };
110 |
111 | const index = geometry.index ? geometry.index.array : null;
112 | const position = geometry.attributes.position.array;
113 | worker.postMessage( {
114 |
115 | operation: 'BUILD_BVH',
116 | maxWorkerCount: this.maxWorkerCount,
117 | index: convertToBufferType( index, SharedArrayBuffer ),
118 | position: convertToBufferType( position, SharedArrayBuffer ),
119 | options: {
120 | ...options,
121 | onProgress: null,
122 | includedProgressCallback: Boolean( options.onProgress ),
123 | groups: [ ... geometry.groups ],
124 | },
125 |
126 | } );
127 |
128 | } );
129 |
130 | }
131 |
132 | }
133 |
134 | export class ParallelMeshBVHWorker {
135 |
136 | constructor() {
137 |
138 | if ( isSharedArrayBufferSupported() ) {
139 |
140 | return new _ParallelMeshBVHWorker();
141 |
142 | } else {
143 |
144 | console.warn( 'ParallelMeshBVHWorker: SharedArrayBuffers not supported. Falling back to single-threaded GenerateMeshBVHWorker.' );
145 |
146 | const object = new GenerateMeshBVHWorker();
147 | object.maxWorkerCount = DEFAULT_WORKER_COUNT;
148 | return object;
149 |
150 | }
151 |
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/src/workers/generateMeshBVH.worker.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferGeometry,
3 | BufferAttribute,
4 | } from 'three';
5 | import { MeshBVH } from '../core/MeshBVH.js';
6 |
7 | onmessage = ( { data } ) => {
8 |
9 | let prevTime = performance.now();
10 | function onProgressCallback( progress ) {
11 |
12 | // account for error
13 | progress = Math.min( progress, 1 );
14 |
15 | const currTime = performance.now();
16 | if ( currTime - prevTime >= 10 && progress !== 1.0 ) {
17 |
18 | postMessage( {
19 |
20 | error: null,
21 | serialized: null,
22 | position: null,
23 | progress,
24 |
25 | } );
26 | prevTime = currTime;
27 |
28 | }
29 |
30 | }
31 |
32 | const { index, position, options } = data;
33 | try {
34 |
35 | const geometry = new BufferGeometry();
36 | geometry.setAttribute( 'position', new BufferAttribute( position, 3, false ) );
37 | if ( index ) {
38 |
39 | geometry.setIndex( new BufferAttribute( index, 1, false ) );
40 |
41 | }
42 |
43 | if ( options.includedProgressCallback ) {
44 |
45 | options.onProgress = onProgressCallback;
46 |
47 | }
48 |
49 | if ( options.groups ) {
50 |
51 | const groups = options.groups;
52 | for ( const i in groups ) {
53 |
54 | const group = groups[ i ];
55 | geometry.addGroup( group.start, group.count, group.materialIndex );
56 |
57 | }
58 |
59 | }
60 |
61 | const bvh = new MeshBVH( geometry, options );
62 | const serialized = MeshBVH.serialize( bvh, { copyIndexBuffer: false } );
63 | let toTransfer = [ position.buffer, ...serialized.roots ];
64 | if ( serialized.index ) {
65 |
66 | toTransfer.push( serialized.index.buffer );
67 |
68 | }
69 |
70 | toTransfer = toTransfer.filter( v => ( typeof SharedArrayBuffer === 'undefined' ) || ! ( v instanceof SharedArrayBuffer ) );
71 |
72 | if ( bvh._indirectBuffer ) {
73 |
74 | toTransfer.push( serialized.indirectBuffer.buffer );
75 |
76 | }
77 |
78 | postMessage( {
79 |
80 | error: null,
81 | serialized,
82 | position,
83 | progress: 1,
84 |
85 | }, toTransfer );
86 |
87 | } catch ( error ) {
88 |
89 | postMessage( {
90 |
91 | error,
92 | serialized: null,
93 | position: null,
94 | progress: 1,
95 |
96 | } );
97 |
98 | }
99 |
100 | };
101 |
--------------------------------------------------------------------------------
/src/workers/utils/WorkerBase.js:
--------------------------------------------------------------------------------
1 | export class WorkerBase {
2 |
3 | constructor( worker ) {
4 |
5 | this.name = 'WorkerBase';
6 | this.running = false;
7 | this.worker = worker;
8 | this.worker.onerror = e => {
9 |
10 | if ( e.message ) {
11 |
12 | throw new Error( `${ this.name }: Could not create Web Worker with error "${ e.message }"` );
13 |
14 | } else {
15 |
16 | throw new Error( `${ this.name }: Could not create Web Worker.` );
17 |
18 | }
19 |
20 | };
21 |
22 | }
23 |
24 | runTask() {}
25 |
26 | generate( ...args ) {
27 |
28 | if ( this.running ) {
29 |
30 | throw new Error( 'GenerateMeshBVHWorker: Already running job.' );
31 |
32 | }
33 |
34 | if ( this.worker === null ) {
35 |
36 | throw new Error( 'GenerateMeshBVHWorker: Worker has been disposed.' );
37 |
38 | }
39 |
40 | this.running = true;
41 |
42 | const promise = this.runTask( this.worker, ...args );
43 | promise.finally( () => {
44 |
45 | this.running = false;
46 |
47 | } );
48 |
49 | return promise;
50 |
51 | }
52 |
53 | dispose() {
54 |
55 | this.worker.terminate();
56 | this.worker = null;
57 |
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/workers/utils/WorkerPool.js:
--------------------------------------------------------------------------------
1 | export class WorkerPool {
2 |
3 | get workerCount() {
4 |
5 | return this.workers.length;
6 |
7 | }
8 |
9 | constructor( getWorkerCallback ) {
10 |
11 | this.workers = [];
12 | this._getWorker = getWorkerCallback;
13 |
14 | }
15 |
16 | setWorkerCount( count ) {
17 |
18 | const workers = this.workers;
19 | while ( workers.length < count ) {
20 |
21 | workers.push( this._getWorker() );
22 |
23 | }
24 |
25 | while ( workers.length > count ) {
26 |
27 | workers.pop().terminate();
28 |
29 | }
30 |
31 | }
32 |
33 | runSubTask( i, msg, onProgress ) {
34 |
35 | return new Promise( ( resolve, reject ) => {
36 |
37 | const worker = this.workers[ i ];
38 | if ( worker.isRunning ) {
39 |
40 | throw new Error( `${ this.name }: Worker ${ i } is already running.` );
41 |
42 | }
43 |
44 | worker.isRunning = true;
45 | worker.postMessage( msg );
46 | worker.onerror = e => {
47 |
48 | worker.isRunning = false;
49 | reject( e );
50 |
51 | };
52 |
53 | worker.onmessage = e => {
54 |
55 | if ( e.data.type === 'progress' ) {
56 |
57 | if ( onProgress ) {
58 |
59 | onProgress( e.data.progress );
60 |
61 | }
62 |
63 | } else {
64 |
65 | if ( onProgress ) {
66 |
67 | onProgress( 1 );
68 |
69 | }
70 |
71 | worker.isRunning = false;
72 | resolve( e.data );
73 |
74 | }
75 |
76 | };
77 |
78 | } );
79 |
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/test/Math.OBB.test.js:
--------------------------------------------------------------------------------
1 | import { Vector3, Triangle, Plane } from 'three';
2 | import { OrientedBox } from '../src/math/OrientedBox.js';
3 | import { setRandomVector, getRandomOrientation } from './utils.js';
4 |
5 | describe( 'OBB Intersections', () => {
6 |
7 | let box, center;
8 | beforeEach( () => {
9 |
10 | box = new OrientedBox();
11 | box.min.set( - 1, - 1, - 1 );
12 | box.max.set( 1, 1, 1 );
13 | getRandomOrientation( box.matrix, 10 );
14 | box.needsUpdate = true;
15 |
16 | center = new Vector3();
17 | center.setFromMatrixPosition( box.matrix );
18 |
19 | } );
20 |
21 | it( 'should intersect triangles with a vertex inside', () => {
22 |
23 | const triangle = new Triangle();
24 | for ( let i = 0; i < 100; i ++ ) {
25 |
26 | const fields = [ 'a', 'b', 'c' ];
27 | const i0 = i % 3;
28 | const i1 = ( i + 1 ) % 3;
29 | const i2 = ( i + 2 ) % 3;
30 |
31 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
32 | .add( center );
33 |
34 | setRandomVector( triangle[ fields[ i1 ] ], 3 + 0.0001 + Math.random() )
35 | .add( center );
36 |
37 | setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
38 | .add( center );
39 |
40 | expect( box.intersectsTriangle( triangle ) ).toBe( true );
41 |
42 | }
43 |
44 | } );
45 |
46 | it( 'should intersect triangles with two vertices inside', () => {
47 |
48 | const triangle = new Triangle();
49 | for ( let i = 0; i < 100; i ++ ) {
50 |
51 | const fields = [ 'a', 'b', 'c' ];
52 | const i0 = i % 3;
53 | const i1 = ( i + 1 ) % 3;
54 | const i2 = ( i + 2 ) % 3;
55 |
56 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
57 | .add( center );
58 |
59 | setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
60 | .add( center );
61 |
62 | setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
63 | .add( center );
64 |
65 | expect( box.intersectsTriangle( triangle ) ).toBe( true );
66 |
67 | }
68 |
69 | } );
70 |
71 | it( 'should intersect triangles with all vertices inside', () => {
72 |
73 | const triangle = new Triangle();
74 | for ( let i = 0; i < 100; i ++ ) {
75 |
76 | const fields = [ 'a', 'b', 'c' ];
77 | const i0 = i % 3;
78 | const i1 = ( i + 1 ) % 3;
79 | const i2 = ( i + 2 ) % 3;
80 |
81 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
82 | .add( center );
83 |
84 | setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
85 | .add( center );
86 |
87 | setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
88 | .add( center );
89 |
90 | expect( box.intersectsTriangle( triangle ) ).toBe( true );
91 |
92 | }
93 |
94 | } );
95 |
96 |
97 | it( 'should intersect triangles that cut across', () => {
98 |
99 | const triangle = new Triangle();
100 | for ( let i = 0; i < 100; i ++ ) {
101 |
102 | const fields = [ 'a', 'b', 'c' ];
103 | const i0 = i % 3;
104 | const i1 = ( i + 1 ) % 3;
105 | const i2 = ( i + 2 ) % 3;
106 |
107 | setRandomVector( triangle[ fields[ i0 ] ], 3 + 0.0001 + Math.random() );
108 |
109 | triangle[ fields[ i1 ] ]
110 | .copy( triangle[ fields[ i0 ] ] )
111 | .multiplyScalar( - 1 )
112 | .add( center );
113 |
114 | triangle[ fields[ i0 ] ]
115 | .add( center );
116 |
117 | setRandomVector( triangle[ fields[ i2 ] ], 3 + 0.0001 + Math.random() )
118 | .add( center );
119 |
120 | expect( box.intersectsTriangle( triangle ) ).toBe( true );
121 |
122 | }
123 |
124 | } );
125 |
126 | it( 'should not intersect triangles outside sphere', () => {
127 |
128 | const plane = new Plane();
129 | const vec = new Vector3();
130 |
131 | const triangle = new Triangle();
132 | for ( let i = 0; i < 100; i ++ ) {
133 |
134 | // project the triangle out onto a plane
135 | setRandomVector( plane.normal, 1 );
136 | plane.setFromNormalAndCoplanarPoint( plane.normal, center );
137 | plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 5.001;
138 |
139 | const fields = [ 'a', 'b', 'c' ];
140 | const i0 = i % 3;
141 | const i1 = ( i + 1 ) % 3;
142 | const i2 = ( i + 2 ) % 3;
143 |
144 | setRandomVector( vec, 10 * Math.random() )
145 | .add( center );
146 | plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
147 |
148 | setRandomVector( vec, 10 * Math.random() )
149 | .add( center );
150 | plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
151 |
152 | setRandomVector( vec, 10 * Math.random() )
153 | .add( center );
154 | plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
155 |
156 | expect( box.intersectsTriangle( triangle ) ).toBe( false );
157 |
158 | }
159 |
160 | } );
161 |
162 | } );
163 |
--------------------------------------------------------------------------------
/test/Math.SphereIntersections.test.js:
--------------------------------------------------------------------------------
1 | import { Vector3, Triangle, Sphere, Plane } from 'three';
2 | import { sphereIntersectTriangle } from '../src/math/MathUtilities.js';
3 | import { setRandomVector } from './utils.js';
4 |
5 | describe( 'Sphere Intersections', () => {
6 |
7 | it( 'should intersect triangles with a vertex inside', () => {
8 |
9 | const sphere = new Sphere();
10 | sphere.radius = 1;
11 | setRandomVector( sphere.center, 10 );
12 |
13 | const triangle = new Triangle();
14 | for ( let i = 0; i < 100; i ++ ) {
15 |
16 | const fields = [ 'a', 'b', 'c' ];
17 | const i0 = i % 3;
18 | const i1 = ( i + 1 ) % 3;
19 | const i2 = ( i + 2 ) % 3;
20 |
21 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
22 | .add( sphere.center );
23 |
24 | setRandomVector( triangle[ fields[ i1 ] ], 1 + 0.0001 + Math.random() )
25 | .add( sphere.center );
26 |
27 | setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
28 | .add( sphere.center );
29 |
30 | expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
31 |
32 | }
33 |
34 | } );
35 |
36 | it( 'should intersect triangles with two vertices inside', () => {
37 |
38 | const sphere = new Sphere();
39 | sphere.radius = 1;
40 | setRandomVector( sphere.center, 10 );
41 |
42 | const triangle = new Triangle();
43 | for ( let i = 0; i < 100; i ++ ) {
44 |
45 | const fields = [ 'a', 'b', 'c' ];
46 | const i0 = i % 3;
47 | const i1 = ( i + 1 ) % 3;
48 | const i2 = ( i + 2 ) % 3;
49 |
50 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
51 | .add( sphere.center );
52 |
53 | setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
54 | .add( sphere.center );
55 |
56 | setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
57 | .add( sphere.center );
58 |
59 | expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
60 |
61 | }
62 |
63 | } );
64 |
65 | it( 'should intersect triangles with all vertices inside', () => {
66 |
67 | const sphere = new Sphere();
68 | sphere.radius = 1;
69 | setRandomVector( sphere.center, 10 );
70 |
71 | const triangle = new Triangle();
72 | for ( let i = 0; i < 100; i ++ ) {
73 |
74 | const fields = [ 'a', 'b', 'c' ];
75 | const i0 = i % 3;
76 | const i1 = ( i + 1 ) % 3;
77 | const i2 = ( i + 2 ) % 3;
78 |
79 | setRandomVector( triangle[ fields[ i0 ] ], Math.random() - 0.0001 )
80 | .add( sphere.center );
81 |
82 | setRandomVector( triangle[ fields[ i1 ] ], Math.random() - 0.0001 )
83 | .add( sphere.center );
84 |
85 | setRandomVector( triangle[ fields[ i2 ] ], Math.random() - 0.0001 )
86 | .add( sphere.center );
87 |
88 | expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
89 |
90 | }
91 |
92 | } );
93 |
94 | it( 'should intersect triangles that only intersect the middle', () => {
95 |
96 | const sphere = new Sphere();
97 | sphere.radius = 1;
98 | setRandomVector( sphere.center, 10 );
99 |
100 | const triangle = new Triangle();
101 | for ( let i = 0; i < 100; i ++ ) {
102 |
103 | const fields = [ 'a', 'b', 'c' ];
104 | const i0 = i % 3;
105 | const i1 = ( i + 1 ) % 3;
106 | const i2 = ( i + 2 ) % 3;
107 |
108 | setRandomVector( triangle[ fields[ i0 ] ], 1 + 0.0001 + Math.random() );
109 |
110 | triangle[ fields[ i1 ] ]
111 | .copy( triangle[ fields[ i0 ] ] )
112 | .multiplyScalar( - 1 )
113 | .add( sphere.center );
114 |
115 | triangle[ fields[ i0 ] ]
116 | .add( sphere.center );
117 |
118 | setRandomVector( triangle[ fields[ i2 ] ], 1 + 0.0001 + Math.random() )
119 | .add( sphere.center );
120 |
121 | expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( true );
122 |
123 | }
124 |
125 | } );
126 |
127 | it( 'should not intersect triangles outside sphere', () => {
128 |
129 | const sphere = new Sphere();
130 | sphere.radius = 1;
131 | setRandomVector( sphere.center, 10 );
132 |
133 | const plane = new Plane();
134 | const vec = new Vector3();
135 |
136 | const triangle = new Triangle();
137 | for ( let i = 0; i < 100; i ++ ) {
138 |
139 | // project the triangle out onto a plane
140 | setRandomVector( plane.normal, 1 );
141 | plane.setFromNormalAndCoplanarPoint( plane.normal, sphere.center );
142 | plane.constant += ( Math.sign( Math.random() - 0.5 ) || 1 ) * 1.001;
143 |
144 | const fields = [ 'a', 'b', 'c' ];
145 | const i0 = i % 3;
146 | const i1 = ( i + 1 ) % 3;
147 | const i2 = ( i + 2 ) % 3;
148 |
149 | setRandomVector( vec, 10 * Math.random() )
150 | .add( sphere.center );
151 | plane.projectPoint( vec, triangle[ fields[ i0 ] ] );
152 |
153 | setRandomVector( vec, 10 * Math.random() )
154 | .add( sphere.center );
155 | plane.projectPoint( vec, triangle[ fields[ i1 ] ] );
156 |
157 | setRandomVector( vec, 10 * Math.random() )
158 | .add( sphere.center );
159 | plane.projectPoint( vec, triangle[ fields[ i2 ] ] );
160 |
161 | expect( sphereIntersectTriangle( sphere, triangle ) ).toBe( false );
162 |
163 | }
164 |
165 | } );
166 |
167 | } );
168 |
--------------------------------------------------------------------------------
/test/MeshBVH.serialize.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | BufferGeometry,
3 | SphereGeometry,
4 | BufferAttribute,
5 | } from 'three';
6 | import {
7 | MeshBVH,
8 | } from '../src/index.js';
9 |
10 | describe( 'Serialization', () => {
11 |
12 | let geometry;
13 | beforeEach( () => {
14 |
15 | geometry = new SphereGeometry( 1, 10, 10 );
16 |
17 | } );
18 |
19 | it( 'should serialize then deserialize to the same structure.', () => {
20 |
21 | const bvh = new MeshBVH( geometry );
22 | const serialized = MeshBVH.serialize( bvh );
23 | const deserializedBVH = MeshBVH.deserialize( serialized, geometry );
24 |
25 | // use a custom object since anonymous functions cause the
26 | // test function to fail
27 | const testObj = { ...bvh };
28 | delete testObj.resolveTriangleIndex;
29 | expect( deserializedBVH ).toMatchObject( testObj );
30 |
31 | } );
32 |
33 | it( 'should serialize then deserialize to the same structure with indirect = true.', () => {
34 |
35 | const bvh = new MeshBVH( geometry, { indirect: true } );
36 | const serialized = MeshBVH.serialize( bvh );
37 | const deserializedBVH = MeshBVH.deserialize( serialized, geometry );
38 |
39 | // use a custom object since anonymous functions cause the
40 | // test function to fail
41 | const testObj = { ...bvh };
42 | delete testObj.resolveTriangleIndex;
43 | expect( deserializedBVH ).toMatchObject( testObj );
44 | expect( bvh.resolveTriangleIndex( 0 ) ).toEqual( deserializedBVH.resolveTriangleIndex( 0 ) );
45 |
46 | } );
47 |
48 | it( 'should create a new index if one does not exist when deserializing.', () => {
49 |
50 | const bvh = new MeshBVH( geometry );
51 | const serialized = MeshBVH.serialize( bvh );
52 |
53 | geometry.setIndex( null );
54 | MeshBVH.deserialize( serialized, geometry );
55 |
56 | expect( geometry.index ).toBeTruthy();
57 |
58 | } );
59 |
60 | it( 'should create an index buffer with Uint16Array if the geometry is small enough.', () => {
61 |
62 | const geometry = new BufferGeometry();
63 | geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( 60000 * 3 ), 3, false ) );
64 |
65 | const bvh = new MeshBVH( geometry );
66 | expect( geometry.index.array instanceof Uint16Array ).toBe( true );
67 | expect( bvh ).toBeTruthy();
68 |
69 | } );
70 |
71 | it( 'should create an index buffer with Uint32Array if the geometry is large enough.', () => {
72 |
73 | const geometry = new BufferGeometry();
74 | geometry.setAttribute( 'position', new BufferAttribute( new Float32Array( 70000 * 3 ), 3, false ) );
75 |
76 | const bvh = new MeshBVH( geometry );
77 | expect( geometry.index.array instanceof Uint32Array ).toBe( true );
78 | expect( bvh ).toBeTruthy();
79 |
80 | } );
81 |
82 | describe( 'cloneBuffers', () => {
83 |
84 | it( 'should clone the index buffer from the target geometry when true.', () => {
85 |
86 | const bvh = new MeshBVH( geometry );
87 | const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true, indirect: true } );
88 | expect( geometry.index.array ).not.toBe( serialized.index );
89 | expect( bvh._roots ).not.toBe( serialized.roots );
90 | expect( bvh._roots[ 0 ] ).not.toBe( serialized.roots[ 0 ] );
91 | expect( bvh._roots ).toEqual( serialized.roots );
92 | expect( bvh._indirectBuffer ).toBe( serialized.indirectBuffer );
93 |
94 | } );
95 |
96 | it( 'should clone the index buffer from the target geometry when false.', () => {
97 |
98 | const bvh = new MeshBVH( geometry );
99 | const serialized = MeshBVH.serialize( bvh, { cloneBuffers: false, indirect: true } );
100 | expect( geometry.index.array ).toBe( serialized.index );
101 | expect( bvh._roots ).toBe( serialized.roots );
102 | expect( bvh._roots[ 0 ] ).toBe( serialized.roots[ 0 ] );
103 | expect( bvh._roots ).toEqual( serialized.roots );
104 | expect( bvh._indirectBuffer ).toBe( serialized.indirectBuffer );
105 |
106 | } );
107 |
108 | } );
109 |
110 | describe( 'setIndex', () => {
111 |
112 | it( 'should not copy the index buffer onto the target geometry if setIndex is false.', () => {
113 |
114 | const cloned = geometry.clone();
115 | const bvh = new MeshBVH( geometry );
116 | const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true } );
117 |
118 | expect( cloned.index.array ).not.toBe( serialized.index );
119 | expect( cloned.index.array ).not.toEqual( serialized.index );
120 |
121 | MeshBVH.deserialize( serialized, cloned, { setIndex: false } );
122 | expect( cloned.index.array ).not.toBe( serialized.index );
123 | expect( cloned.index.array ).not.toEqual( serialized.index );
124 |
125 | } );
126 |
127 | it( 'should copy the index buffer onto the target geometry if setIndex is true.', () => {
128 |
129 | const cloned = geometry.clone();
130 | const bvh = new MeshBVH( geometry );
131 | const serialized = MeshBVH.serialize( bvh, { cloneBuffers: true } );
132 |
133 | expect( cloned.index.array ).not.toBe( serialized.index );
134 | expect( cloned.index.array ).not.toEqual( serialized.index );
135 |
136 | MeshBVH.deserialize( serialized, cloned, { setIndex: true } );
137 | expect( cloned.index.array ).not.toBe( serialized.index );
138 | expect( cloned.index.array ).toEqual( serialized.index );
139 |
140 | } );
141 |
142 | } );
143 |
144 | describe( 'indirect', () => {
145 |
146 | it( 'should correctly deserialize the bvh.', () => {
147 |
148 | const cloned = geometry.clone();
149 | const bvh = new MeshBVH( geometry, { indirect: true } );
150 | const serialized = MeshBVH.serialize( bvh );
151 |
152 | const deserialized = MeshBVH.deserialize( serialized, cloned );
153 | expect( deserialized.indirect ).toBe( true );
154 | expect( () => {
155 |
156 | deserialized.resolveTriangleIndex( 0 );
157 |
158 | } ).not.toThrow();
159 |
160 | } );
161 |
162 | } );
163 |
164 | } );
165 |
--------------------------------------------------------------------------------
/test/MeshBVHUniformStruct.test.js:
--------------------------------------------------------------------------------
1 | import { SphereGeometry } from 'three';
2 | import { MeshBVH, MeshBVHUniformStruct, getBVHExtremes } from '../src';
3 |
4 | describe( 'MeshBVHUniformStruct', () => {
5 |
6 | let struct, geometry;
7 | beforeEach( () => {
8 |
9 | struct = new MeshBVHUniformStruct();
10 | geometry = new SphereGeometry( 1, 30, 30 );
11 |
12 | } );
13 |
14 | it( 'should fail if more than one group is present.', () => {
15 |
16 | geometry.addGroup( 0, 300, 0 );
17 | geometry.addGroup( 300, 600, 1 );
18 |
19 | const bvh = new MeshBVH( geometry );
20 | let error;
21 |
22 | try {
23 |
24 | struct.updateFrom( bvh );
25 |
26 | } catch ( e ) {
27 |
28 | error = e;
29 |
30 | }
31 |
32 | expect( error ).toBeTruthy();
33 |
34 | } );
35 |
36 | it( 'should create textures allocated for each element.', () => {
37 |
38 | const bvh = new MeshBVH( geometry );
39 | struct.updateFrom( bvh );
40 |
41 | const posAttr = geometry.attributes.position;
42 | const indexAttr = geometry.index;
43 | const bvhData = getBVHExtremes( bvh )[ 0 ];
44 |
45 | expect( posAttr.count ).toBeLessThanOrEqual( struct.position.image.width * struct.position.image.height );
46 | expect( indexAttr.count / 3 ).toBeLessThan( struct.index.image.width * struct.index.image.height );
47 | expect( bvhData.nodeCount ).toBeLessThan( struct.bvhBounds.image.width * struct.bvhBounds.image.height / 2 );
48 | expect( bvhData.nodeCount ).toBeLessThan( struct.bvhContents.image.width * struct.bvhContents.image.height );
49 |
50 | } );
51 |
52 | it( 'should create textures allocated for each element with indirect enabled.', () => {
53 |
54 | const bvh = new MeshBVH( geometry, { indirect: true } );
55 | struct.updateFrom( bvh );
56 |
57 | const posAttr = geometry.attributes.position;
58 | const indexAttr = geometry.index;
59 | const bvhData = getBVHExtremes( bvh )[ 0 ];
60 |
61 | expect( posAttr.count ).toBeLessThanOrEqual( struct.position.image.width * struct.position.image.height );
62 | expect( indexAttr.count / 3 ).toBeLessThan( struct.index.image.width * struct.index.image.height );
63 | expect( bvhData.nodeCount ).toBeLessThan( struct.bvhBounds.image.width * struct.bvhBounds.image.height / 2 );
64 | expect( bvhData.nodeCount ).toBeLessThan( struct.bvhContents.image.width * struct.bvhContents.image.height );
65 |
66 | } );
67 |
68 | it( 'should produce the same textures with indirect enabled.', () => {
69 |
70 | const bvh = new MeshBVH( geometry.clone(), { indirect: false } );
71 | const struct = new MeshBVHUniformStruct();
72 | struct.updateFrom( bvh );
73 |
74 | const bvhIndirect = new MeshBVH( geometry.clone(), { indirect: true } );
75 | const structIndirect = new MeshBVHUniformStruct();
76 | structIndirect.updateFrom( bvhIndirect );
77 |
78 | expect( struct.position.image ).toEqual( structIndirect.position.image );
79 | expect( struct.index.image ).toEqual( structIndirect.index.image );
80 | expect( struct.bvhBounds.image ).toEqual( structIndirect.bvhBounds.image );
81 | expect( struct.bvhContents.image ).toEqual( structIndirect.bvhContents.image );
82 |
83 | } );
84 |
85 | it( 'should produce the same textures even with indirect enabled and no index.', () => {
86 |
87 | const bvh = new MeshBVH( geometry.toNonIndexed(), { indirect: false } );
88 | const struct = new MeshBVHUniformStruct();
89 | struct.updateFrom( bvh );
90 |
91 | const bvhIndirect = new MeshBVH( geometry.toNonIndexed(), { indirect: true } );
92 | const structIndirect = new MeshBVHUniformStruct();
93 | structIndirect.updateFrom( bvhIndirect );
94 |
95 | expect( struct.position.image ).toEqual( structIndirect.position.image );
96 | expect( struct.index.image ).toEqual( structIndirect.index.image );
97 | expect( struct.bvhBounds.image ).toEqual( structIndirect.bvhBounds.image );
98 | expect( struct.bvhContents.image ).toEqual( structIndirect.bvhContents.image );
99 |
100 | } );
101 |
102 | } );
103 |
--------------------------------------------------------------------------------
/test/TypescriptImportTest.ts:
--------------------------------------------------------------------------------
1 | import { BufferGeometry, Mesh, Raycaster, BatchedMesh } from 'three';
2 | import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, computeBatchedBoundsTree, disposeBatchedBoundsTree } from '../src/index';
3 |
4 | Mesh.prototype.raycast = acceleratedRaycast;
5 | BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
6 | BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
7 |
8 | BatchedMesh.prototype.raycast = acceleratedRaycast;
9 | BatchedMesh.prototype.computeBoundsTree = computeBatchedBoundsTree;
10 | BatchedMesh.prototype.disposeBoundsTree = disposeBatchedBoundsTree;
11 |
12 | const mesh = new Mesh();
13 | mesh.geometry.computeBoundsTree();
14 | mesh.geometry.disposeBoundsTree();
15 |
16 | const batchedMesh = new BatchedMesh( 1, 1, 1 );
17 | batchedMesh.computeBoundsTree();
18 | batchedMesh.disposeBoundsTree();
19 |
20 | const raycaster = new Raycaster();
21 | raycaster.firstHitOnly = true;
22 |
23 | mesh.raycast( raycaster, [] );
24 |
--------------------------------------------------------------------------------
/test/Utils.geometryUtils.test.js:
--------------------------------------------------------------------------------
1 | import { SphereGeometry, BoxGeometry } from 'three';
2 | import { getVertexCount, hasGroupGaps } from '../src/core/build/geometryUtils.js';
3 |
4 | describe( 'hasGroupGaps', () => {
5 |
6 | it( 'should not report a geometry with no groups.', () => {
7 |
8 | const geometry = new SphereGeometry();
9 | expect( hasGroupGaps( geometry ) ).toBe( false );
10 |
11 | } );
12 |
13 | it( 'should not report a geometry with properly formed groups.', () => {
14 |
15 | const geometry = new BoxGeometry();
16 | expect( hasGroupGaps( geometry ) ).toBe( false );
17 |
18 | } );
19 |
20 | it( 'should not report a geometry with final "infinite" group.', () => {
21 |
22 | const geometry = new SphereGeometry();
23 | geometry.addGroup( 0, Infinity, 0 );
24 | expect( hasGroupGaps( geometry ) ).toBe( false );
25 |
26 | } );
27 |
28 | it( 'should not report if range is "infinite".', () => {
29 |
30 | const geometry = new SphereGeometry();
31 | const range = { start: 0, count: Infinity };
32 | expect( hasGroupGaps( geometry, range ) ).toBe( false );
33 |
34 | } );
35 |
36 | it( 'should not report when range spans the entire vertex buffer while geometry.drawRange does not.', () => {
37 |
38 | const geometry = new SphereGeometry();
39 | geometry.setDrawRange( 10, getVertexCount( geometry ) - 11 );
40 | const range = { start: 0, count: getVertexCount( geometry ) };
41 | expect( hasGroupGaps( geometry, range ) ).toBe( false );
42 |
43 | } );
44 |
45 | it( 'should report when a geometry.drawRange does not span the whole vertex buffer.', () => {
46 |
47 | const geometry = new SphereGeometry();
48 | geometry.setDrawRange( 0, getVertexCount( geometry ) - 1, );
49 | expect( hasGroupGaps( geometry ) ).toBe( true );
50 |
51 | } );
52 |
53 | it( 'should report when a geometry has a group that does not span the whole vertex buffer.', () => {
54 |
55 | const geometry = new SphereGeometry();
56 | geometry.addGroup( 0, getVertexCount( geometry ) - 1, 0 );
57 | expect( hasGroupGaps( geometry ) ).toBe( true );
58 |
59 | } );
60 |
61 | it( 'should report when a geometry has two group that are not up against each other.', () => {
62 |
63 | const geometry = new SphereGeometry();
64 | geometry.addGroup( 0, 10, 0 );
65 | geometry.addGroup( 10, getVertexCount( geometry ) - 11, 0 );
66 | expect( hasGroupGaps( geometry ) ).toBe( true );
67 |
68 | } );
69 |
70 | it( 'should report when range does not span the whole vertex buffer.', () => {
71 |
72 | const geometry = new SphereGeometry();
73 | const range = { start: 0, count: getVertexCount( geometry ) - 1 };
74 | expect( hasGroupGaps( geometry, range ) ).toBe( true );
75 |
76 | } );
77 |
78 | it( 'should report when range does not span the whole vertex buffer while geometry groups do.', () => {
79 |
80 | const geometry = new BoxGeometry();
81 | const range = { start: 0, count: getVertexCount( geometry ) - 1 };
82 | expect( hasGroupGaps( geometry, range ) ).toBe( true );
83 |
84 | } );
85 |
86 | it( 'should report when a geometry has a group that does not span the whole vertex buffer while range does.', () => {
87 |
88 | const geometry = new SphereGeometry();
89 | geometry.addGroup( 0, getVertexCount( geometry ) - 1, 0 );
90 | const range = { start: 0, count: Infinity };
91 | expect( hasGroupGaps( geometry, range ) ).toBe( true );
92 |
93 | } );
94 |
95 | } );
96 |
--------------------------------------------------------------------------------
/test/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [ [ "@babel/preset-env", {
3 | "targets": {
4 | "node": "current"
5 | }
6 | } ] ]
7 | }
8 |
--------------------------------------------------------------------------------
/test/data/points.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gkjohnson/three-mesh-bvh/169bcdde55bf948e9ae412fa21d5bc2f2bc8f08d/test/data/points.bin
--------------------------------------------------------------------------------
/test/repro/FloatingPointBoundsError.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | Mesh,
3 | BufferGeometry,
4 | BufferAttribute,
5 | Vector3,
6 | Raycaster,
7 | } from 'three';
8 | import {
9 | MeshBVH,
10 | acceleratedRaycast,
11 | computeBoundsTree,
12 | disposeBoundsTree,
13 | CENTER,
14 | SAH,
15 | AVERAGE,
16 | } from '../../src/index.js';
17 | import fs from 'fs';
18 | import path from 'path';
19 |
20 | Mesh.prototype.raycast = acceleratedRaycast;
21 | BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
22 | BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
23 |
24 | // TODO: clean up test
25 | describe( 'AVERAGE Points Raycast', () => {
26 |
27 | let geometry, mesh, raycaster;
28 | beforeEach( () => {
29 |
30 | const dataPath = path.resolve( __dirname, '../data/points.bin' );
31 | const buffer = fs.readFileSync( dataPath );
32 | const points = new Float32Array( buffer.buffer, buffer.byteOffset, buffer.byteLength / 4 );
33 |
34 | geometry = new BufferGeometry();
35 | geometry.setAttribute( 'position', new BufferAttribute( points.slice(), 3 ) );
36 | geometry.computeVertexNormals();
37 |
38 | mesh = new Mesh( geometry );
39 |
40 | // NOTE: If the geometry is not centered then the AVERAGE split strategy
41 | // case fails.
42 | geometry.computeBoundingBox();
43 | const center = new Vector3();
44 | geometry.boundingBox.getCenter( center );
45 |
46 | const x = 101086.2438562272 - center.x;
47 | const y = 99421.40510391879 - center.y;
48 |
49 | geometry.center();
50 |
51 | raycaster = new Raycaster();
52 | raycaster.firstHitOnly = true;
53 | raycaster.set( new Vector3( x, y, - 1000 ), new Vector3( 0, 0, 1 ) );
54 |
55 | } );
56 |
57 | it( 'should collide against the geometry with CENTER split', () => {
58 |
59 | geometry.boundsTree = new MeshBVH( geometry, {
60 | strategy: CENTER,
61 | maxDepth: 64,
62 | maxLeafTris: 16
63 | } );
64 |
65 | const res1 = raycaster.intersectObject( mesh );
66 |
67 | geometry.boundsTree = null;
68 | const res2 = raycaster.intersectObject( mesh );
69 |
70 | expect( res1 ).toEqual( res2 );
71 |
72 | } );
73 |
74 | it( 'should collide against the geometry with SAH split', () => {
75 |
76 | geometry.boundsTree = new MeshBVH( geometry, {
77 | strategy: SAH,
78 | maxDepth: 64,
79 | maxLeafTris: 16
80 | } );
81 |
82 | const res1 = raycaster.intersectObject( mesh );
83 |
84 | geometry.boundsTree = null;
85 | const res2 = raycaster.intersectObject( mesh );
86 |
87 | expect( res1 ).toEqual( res2 );
88 |
89 | } );
90 |
91 | it( 'should collide against the geometry with AVERAGE split', () => {
92 |
93 | geometry.boundsTree = new MeshBVH( geometry, {
94 | strategy: AVERAGE,
95 | maxDepth: 64,
96 | maxLeafTris: 16
97 | } );
98 |
99 | const res1 = raycaster.intersectObject( mesh );
100 |
101 | geometry.boundsTree = null;
102 | const res2 = raycaster.intersectObject( mesh );
103 |
104 | res1.length && delete res1[ 0 ].object;
105 | res2.length && delete res2[ 0 ].object;
106 |
107 | expect( res1 ).toEqual( res2 );
108 |
109 | } );
110 |
111 | } );
112 |
--------------------------------------------------------------------------------
/test/repro/RaycastsRepro.test.js:
--------------------------------------------------------------------------------
1 | // Test cases specifically for issue #180
2 | import { Mesh, BufferGeometry, TorusGeometry, Scene, Raycaster, MeshBasicMaterial } from 'three';
3 | import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, AVERAGE } from '../../src/index.js';
4 | import { random, setSeed } from '../utils.js';
5 |
6 | Mesh.prototype.raycast = acceleratedRaycast;
7 | BufferGeometry.prototype.computeBoundsTree = computeBoundsTree;
8 | BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
9 |
10 | runRandomTest( { strategy: AVERAGE }, 7830035629, 4697211981 );
11 | runRandomTest( { strategy: AVERAGE }, 8294928772, 1592666709 );
12 | runRandomTest( { strategy: SAH }, 81992501, 8903271423 );
13 |
14 | runRandomTest( { strategy: AVERAGE, indirect: true }, 7830035629, 4697211981 );
15 | runRandomTest( { strategy: AVERAGE, indirect: true }, 8294928772, 1592666709 );
16 | runRandomTest( { strategy: SAH, indirect: true }, 81992501, 8903271423 );
17 |
18 | function runRandomTest( options, transformSeed, raySeed ) {
19 |
20 | let scene = null;
21 | let raycaster = null;
22 | let ungroupedGeometry = null;
23 | let ungroupedBvh = null;
24 | let groupedGeometry = null;
25 | let groupedBvh = null;
26 |
27 | describe( `Transform Seed : ${ transformSeed }`, () => {
28 |
29 | beforeAll( () => {
30 |
31 | ungroupedGeometry = new TorusGeometry( 1, 1, 40, 10 );
32 | groupedGeometry = new TorusGeometry( 1, 1, 40, 10 );
33 |
34 | const groupCount = 10;
35 | const groupSize = groupedGeometry.index.array.length / groupCount;
36 |
37 | for ( let g = 0; g < groupCount; g ++ ) {
38 |
39 | const groupStart = g * groupSize;
40 | groupedGeometry.addGroup( groupStart, groupSize, 0 );
41 |
42 | }
43 |
44 | groupedGeometry.computeBoundsTree( options );
45 | ungroupedGeometry.computeBoundsTree( options );
46 |
47 | ungroupedBvh = ungroupedGeometry.boundsTree;
48 | groupedBvh = groupedGeometry.boundsTree;
49 |
50 | scene = new Scene();
51 | raycaster = new Raycaster();
52 |
53 | setSeed( transformSeed );
54 | random(); // call random() to seed with a larger value
55 |
56 | for ( var i = 0; i < 10; i ++ ) {
57 |
58 | let geo = i % 2 ? groupedGeometry : ungroupedGeometry;
59 | let mesh = new Mesh( geo, new MeshBasicMaterial() );
60 | mesh.rotation.x = random() * 10;
61 | mesh.rotation.y = random() * 10;
62 | mesh.rotation.z = random() * 10;
63 |
64 | mesh.position.x = random();
65 | mesh.position.y = random();
66 | mesh.position.z = random();
67 |
68 | scene.add( mesh );
69 | mesh.updateMatrix( true );
70 | mesh.updateMatrixWorld( true );
71 |
72 | }
73 |
74 | } );
75 |
76 | it( `Cast Seed : ${ raySeed }`, () => {
77 |
78 | setSeed( raySeed );
79 | random(); // call random() to seed with a larger value
80 |
81 | raycaster.firstHitOnly = false;
82 | raycaster.ray.origin.set( random() * 10, random() * 10, random() * 10 );
83 | raycaster.ray.direction.copy( raycaster.ray.origin ).multiplyScalar( - 1 ).normalize();
84 |
85 | ungroupedGeometry.boundsTree = ungroupedBvh;
86 | groupedGeometry.boundsTree = groupedBvh;
87 | const bvhHits = raycaster.intersectObject( scene, true );
88 |
89 | raycaster.firstHitOnly = true;
90 | const firstHit = raycaster.intersectObject( scene, true );
91 |
92 | ungroupedGeometry.boundsTree = null;
93 | groupedGeometry.boundsTree = null;
94 | const ogHits = raycaster.intersectObject( scene, true );
95 |
96 | expect( ogHits ).toEqual( bvhHits );
97 | expect( firstHit[ 0 ] ).toEqual( ogHits[ 0 ] );
98 |
99 | } );
100 |
101 | } );
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | import { getBVHExtremes } from '../src';
2 | import { Vector3, Quaternion, Euler } from 'three';
3 |
4 | // https://stackoverflow.com/questions/3062746/special-simple-random-number-generator
5 | let _seed = null;
6 | export function setSeed( seed ) {
7 |
8 | _seed = seed;
9 |
10 | }
11 |
12 | export function random() {
13 |
14 | if ( _seed === null ) throw new Error();
15 |
16 | const a = 1103515245;
17 | const c = 12345;
18 | const m = 2e31;
19 |
20 | _seed = ( a * _seed + c ) % m;
21 | return _seed / m;
22 |
23 | }
24 |
25 | // Returns the max tree depth of the BVH
26 | export function getMaxDepth( bvh ) {
27 |
28 | return getBVHExtremes( bvh )[ 0 ].depth.max;
29 |
30 | }
31 |
32 | export function setRandomVector( vector, length ) {
33 |
34 | vector
35 | .set(
36 | Math.random() - 0.5,
37 | Math.random() - 0.5,
38 | Math.random() - 0.5
39 | )
40 | .normalize()
41 | .multiplyScalar( length );
42 |
43 | return vector;
44 |
45 | }
46 |
47 | export function getRandomOrientation( matrix, range ) {
48 |
49 | const pos = new Vector3();
50 | const quat = new Quaternion();
51 | const sca = new Vector3( 1, 1, 1 );
52 |
53 | setRandomVector( pos, range );
54 | quat.setFromEuler( new Euler( Math.random() * 180, Math.random() * 180, Math.random() * 180 ) );
55 | matrix.compose( pos, quat, sca );
56 | return matrix;
57 |
58 | }
59 |
60 | export function runOptionsMatrix( options, cb ) {
61 |
62 | traverse( Object.keys( options ) );
63 |
64 | function traverse( remainingKeys, state = {} ) {
65 |
66 | if ( remainingKeys.length === 0 ) {
67 |
68 | cb( { ...state } );
69 | return;
70 |
71 | }
72 |
73 | let values;
74 | const key = remainingKeys.pop();
75 | if ( Array.isArray( options[ key ] ) ) {
76 |
77 | values = options[ key ];
78 |
79 | } else {
80 |
81 | values = [ options[ key ] ];
82 |
83 | }
84 |
85 | for ( let i = 0, l = values.length; i < l; i ++ ) {
86 |
87 | const value = values[ i ];
88 | const newState = { ...state, [ key ]: value };
89 | traverse( [ ...remainingKeys ], newState );
90 |
91 | }
92 |
93 | }
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "noEmit": true,
5 | "strict": true,
6 | "noUnusedParameters": true,
7 | "strictNullChecks":true,
8 | },
9 | "include": [
10 | "src",
11 | "test"
12 | ],
13 | "exclude": [
14 | "node_modules"
15 | ]
16 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { searchForWorkspaceRoot } from 'vite';
2 | import fs from 'fs';
3 |
4 | export default {
5 |
6 | root: './example/',
7 | base: '',
8 | build: {
9 | outDir: './bundle/',
10 | rollupOptions: {
11 | input: fs
12 | .readdirSync( './example/' )
13 | .filter( p => /\.html$/.test( p ) )
14 | .map( p => `./example/${ p }` ),
15 | },
16 | },
17 | server: {
18 | fs: {
19 | allow: [
20 | // search up for workspace root
21 | searchForWorkspaceRoot( process.cwd() ),
22 | ],
23 | },
24 | }
25 |
26 | };
27 |
--------------------------------------------------------------------------------