├── .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 |
56 |
100%
57 |
58 |
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 |
50 | CPU Path Tracer demo based on a smattering of online resources. Engine by T-FLEX CAD on Sketchfab. 51 |

52 | Checkout the Raytracing in One Weekend series and PBR Book for some great introductions to path tracing! 53 |
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 |
58 |
59 |
60 |
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 |
36 | Inspired by SculptGL. 37 |
38 |
39 | Matcap textures from nidorx/matcaps repo. 40 |
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 |
50 |
51 |
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 | --------------------------------------------------------------------------------