|
| 1 | +import { |
| 2 | + ChangeDetectionStrategy, |
| 3 | + Component, |
| 4 | + CUSTOM_ELEMENTS_SCHEMA, |
| 5 | + effect, |
| 6 | + ElementRef, |
| 7 | + input, |
| 8 | + untracked, |
| 9 | + viewChild, |
| 10 | +} from '@angular/core'; |
| 11 | +import { extend, injectStore, is, NgtThreeElements, omit, pick } from 'angular-three'; |
| 12 | +import { mergeInputs } from 'ngxtension/inject-inputs'; |
| 13 | +import * as THREE from 'three'; |
| 14 | +import { Group } from 'three'; |
| 15 | +import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree, SAH, SplitStrategy } from 'three-mesh-bvh'; |
| 16 | + |
| 17 | +export type NgtsBvhOptions = Partial<NgtThreeElements['ngt-group']> & { |
| 18 | + /**Enabled, default: true */ |
| 19 | + enabled: boolean; |
| 20 | + /** Use .raycastFirst to retrieve hits which is generally faster, default: false */ |
| 21 | + firstHitOnly: boolean; |
| 22 | + /** Split strategy, default: SAH (slowest to construct, fastest runtime, least memory) */ |
| 23 | + strategy: SplitStrategy; |
| 24 | + /** Print out warnings encountered during tree construction, default: false */ |
| 25 | + verbose: boolean; |
| 26 | + /** If true then the bounding box for the geometry is set once the BVH has been constructed, default: true */ |
| 27 | + setBoundingBox: boolean; |
| 28 | + /** The maximum depth to allow the tree to build to, default: 40 */ |
| 29 | + maxDepth: number; |
| 30 | + /** The number of triangles to aim for in a leaf node, default: 10 */ |
| 31 | + maxLeafTris: number; |
| 32 | + |
| 33 | + /** If false then an index buffer is created if it does not exist and is rearranged */ |
| 34 | + /** to hold the bvh structure. If false then a separate buffer is created to store the */ |
| 35 | + /** structure and the index buffer (or lack thereof) is retained. This can be used */ |
| 36 | + /** when the existing index layout is important or groups are being used so a */ |
| 37 | + /** single BVH hierarchy can be created to improve performance. */ |
| 38 | + /** default: false */ |
| 39 | + /** Note: This setting is experimental */ |
| 40 | + indirect?: boolean; |
| 41 | +}; |
| 42 | + |
| 43 | +const defaultOptions: NgtsBvhOptions = { |
| 44 | + enabled: true, |
| 45 | + firstHitOnly: false, |
| 46 | + strategy: SAH, |
| 47 | + verbose: false, |
| 48 | + setBoundingBox: true, |
| 49 | + maxDepth: 40, |
| 50 | + maxLeafTris: 10, |
| 51 | + indirect: false, |
| 52 | +}; |
| 53 | + |
| 54 | +@Component({ |
| 55 | + selector: 'ngts-bvh', |
| 56 | + template: ` |
| 57 | + <ngt-group #group [parameters]="parameters()"> |
| 58 | + <ng-content /> |
| 59 | + </ngt-group> |
| 60 | + `, |
| 61 | + schemas: [CUSTOM_ELEMENTS_SCHEMA], |
| 62 | + changeDetection: ChangeDetectionStrategy.OnPush, |
| 63 | +}) |
| 64 | +export class NgtsBvh { |
| 65 | + options = input(defaultOptions, { transform: mergeInputs(defaultOptions) }); |
| 66 | + protected parameters = omit(this.options, [ |
| 67 | + 'enabled', |
| 68 | + 'firstHitOnly', |
| 69 | + 'strategy', |
| 70 | + 'verbose', |
| 71 | + 'setBoundingBox', |
| 72 | + 'maxDepth', |
| 73 | + 'maxLeafTris', |
| 74 | + 'indirect', |
| 75 | + ]); |
| 76 | + |
| 77 | + groupRef = viewChild.required<ElementRef<THREE.Group>>('group'); |
| 78 | + |
| 79 | + private store = injectStore(); |
| 80 | + |
| 81 | + private enabled = pick(this.options, 'enabled'); |
| 82 | + private firstHitOnly = pick(this.options, 'firstHitOnly'); |
| 83 | + private strategy = pick(this.options, 'strategy'); |
| 84 | + private verbose = pick(this.options, 'verbose'); |
| 85 | + private setBoundingBox = pick(this.options, 'setBoundingBox'); |
| 86 | + private maxDepth = pick(this.options, 'maxDepth'); |
| 87 | + private maxLeafTris = pick(this.options, 'maxLeafTris'); |
| 88 | + private indirect = pick(this.options, 'indirect'); |
| 89 | + |
| 90 | + constructor() { |
| 91 | + extend({ Group }); |
| 92 | + |
| 93 | + effect((onCleanup) => { |
| 94 | + const enabled = this.enabled(); |
| 95 | + if (!enabled) return; |
| 96 | + |
| 97 | + // This can only safely work if the component is used once, but there is no alternative. |
| 98 | + // Hijacking the raycast method to do it for individual meshes is not an option as it would |
| 99 | + // cost too much memory ... |
| 100 | + const [firstHitOnly, strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect, group, raycaster] = |
| 101 | + [ |
| 102 | + untracked(this.firstHitOnly), |
| 103 | + untracked(this.strategy), |
| 104 | + untracked(this.verbose), |
| 105 | + untracked(this.setBoundingBox), |
| 106 | + untracked(this.maxDepth), |
| 107 | + untracked(this.maxLeafTris), |
| 108 | + untracked(this.indirect), |
| 109 | + this.groupRef().nativeElement, |
| 110 | + this.store.snapshot.raycaster, |
| 111 | + ]; |
| 112 | + |
| 113 | + const options = { strategy, verbose, setBoundingBox, maxDepth, maxLeafTris, indirect }; |
| 114 | + raycaster.firstHitOnly = firstHitOnly; |
| 115 | + |
| 116 | + group.traverse((child) => { |
| 117 | + if ( |
| 118 | + is.three<THREE.Mesh>(child, 'isMesh') && |
| 119 | + !child.geometry.boundsTree && |
| 120 | + child.raycast === THREE.Mesh.prototype.raycast |
| 121 | + ) { |
| 122 | + child.raycast = acceleratedRaycast; |
| 123 | + child.geometry.computeBoundsTree = computeBoundsTree; |
| 124 | + child.geometry.disposeBoundsTree = disposeBoundsTree; |
| 125 | + child.geometry.computeBoundsTree(options); |
| 126 | + } |
| 127 | + }); |
| 128 | + |
| 129 | + onCleanup(() => { |
| 130 | + delete raycaster.firstHitOnly; |
| 131 | + group.traverse((child) => { |
| 132 | + if (is.three<THREE.Mesh>(child, 'isMesh') && child.geometry.boundsTree) { |
| 133 | + child.geometry.disposeBoundsTree(); |
| 134 | + child.raycast = THREE.Mesh.prototype.raycast; |
| 135 | + } |
| 136 | + }); |
| 137 | + }); |
| 138 | + }); |
| 139 | + } |
| 140 | +} |
0 commit comments