three-instance-batch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +177 -0
- package/dist/three-instance-batch.js +1 -0
- package/dist/types/core/Instance.d.ts +50 -0
- package/dist/types/core/InstanceBatcher.d.ts +40 -0
- package/dist/types/core/groupKey.d.ts +3 -0
- package/dist/types/core/index.d.ts +2 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/types.d.ts +28 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# three-instance-batch
|
|
2
|
+
|
|
3
|
+
`InstancedMesh` management layer for Three.js. Handles dirty tracking, group batching, and dynamic add/remove — so you don't have to.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install three-instance-batch
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
import { Instance, InstanceBatcher } from 'three-instance-batch'
|
|
16
|
+
|
|
17
|
+
const batcher = new InstanceBatcher()
|
|
18
|
+
scene.add(batcher)
|
|
19
|
+
|
|
20
|
+
const geo = new THREE.BoxGeometry(1, 1, 1)
|
|
21
|
+
const mat = new THREE.MeshStandardMaterial({ color: '#ff6b6b' })
|
|
22
|
+
|
|
23
|
+
const inst = new Instance(geo, mat)
|
|
24
|
+
inst.position.set(3, 0, 0)
|
|
25
|
+
inst.color.set('#00ffcc')
|
|
26
|
+
inst.castShadow = true
|
|
27
|
+
|
|
28
|
+
batcher.addInstance(inst)
|
|
29
|
+
|
|
30
|
+
function animate() {
|
|
31
|
+
inst.position.x = Math.sin(Date.now() * 0.001) * 5
|
|
32
|
+
batcher.update(camera)
|
|
33
|
+
renderer.render(scene, camera)
|
|
34
|
+
requestAnimationFrame(animate)
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No `setMatrixAt`. No `needsUpdate`. Just change properties and call `update()`.
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
`Instance` wraps `Vector3`, `Quaternion`, and `Color` setter methods at construction time. Any mutation — `position.set()`, `quaternion.setFromEuler()`, `color.setHSL()`, etc. — automatically marks that instance dirty. On `batcher.update()`, only the changed matrix rows and color rows are written to the GPU buffer.
|
|
43
|
+
|
|
44
|
+
Instances are grouped into `InstancedMesh` objects by a **group key** derived from geometry UUID, material properties, and shadow flags. Instances sharing the same key share one draw call.
|
|
45
|
+
|
|
46
|
+
## Multiple Geometries and Materials
|
|
47
|
+
|
|
48
|
+
The batcher handles mixed types automatically:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
const meshes = [
|
|
52
|
+
new Instance(boxGeo, redMat),
|
|
53
|
+
new Instance(sphereGeo, blueMat),
|
|
54
|
+
new Instance(boxGeo, redMat), // same key as first → same InstancedMesh
|
|
55
|
+
]
|
|
56
|
+
batcher.addInstance(meshes)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Reusing the same geometry and material object is enough to share a batch.
|
|
60
|
+
|
|
61
|
+
## Shadow Changes Trigger Remigration
|
|
62
|
+
|
|
63
|
+
`castShadow` and `receiveShadow` are part of the group key. Changing them at runtime causes the instance to be moved to the correct group automatically on the next `update()`.
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
inst.castShadow = true // migrates to the shadow-casting group on next update()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Parent-Child Hierarchy
|
|
70
|
+
|
|
71
|
+
Instances support parent-child nesting. A child's world matrix is computed from its parent's transform chain.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const parent = new Instance(geo, mat)
|
|
75
|
+
const child = new Instance(geo, mat)
|
|
76
|
+
child.parent = parent // child follows parent's transform
|
|
77
|
+
|
|
78
|
+
parent.position.set(0, 5, 0)
|
|
79
|
+
batcher.update() // child world matrix recalculated automatically
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Circular references are detected and rejected at runtime. Disposing a parent detaches all children but does not dispose them.
|
|
83
|
+
|
|
84
|
+
## Raycasting
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
const raycaster = new THREE.Raycaster()
|
|
88
|
+
raycaster.setFromCamera(mouse, camera)
|
|
89
|
+
const hits = raycaster.intersectObjects(batcher.children)
|
|
90
|
+
|
|
91
|
+
for (const hit of hits) {
|
|
92
|
+
const inst = batcher.getInstanceFromIntersect(hit)
|
|
93
|
+
if (inst) console.log('hit', inst.id)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API
|
|
98
|
+
|
|
99
|
+
### `Instance`
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
new Instance(geometry: BufferGeometry, material: Material)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Member | Type | Notes |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `id` | `number` | Auto-incremented, readonly |
|
|
108
|
+
| `geometry` | `BufferGeometry` | Readonly |
|
|
109
|
+
| `material` | `Material` | Readonly |
|
|
110
|
+
| `position` | `Vector3` | Auto-tracked |
|
|
111
|
+
| `scale` | `Vector3` | Default `(1,1,1)`, auto-tracked |
|
|
112
|
+
| `quaternion` | `Quaternion` | Auto-tracked |
|
|
113
|
+
| `color` | `Color` | Default white, auto-tracked |
|
|
114
|
+
| `visible` | `boolean` | Hidden instances write a zero matrix |
|
|
115
|
+
| `castShadow` | `boolean` | Change triggers group remigration |
|
|
116
|
+
| `receiveShadow` | `boolean` | Change triggers group remigration |
|
|
117
|
+
| `parent` | `Instance \| null` | Setter validates against cycles |
|
|
118
|
+
| `children` | `Instance[]` | Readonly array |
|
|
119
|
+
| `disposed` | `boolean` | Readonly |
|
|
120
|
+
| `groupKey` | `string` | Derived from geometry, material, shadow flags |
|
|
121
|
+
| `localMatrix` | `Matrix4` | Readonly, computed from transform components |
|
|
122
|
+
| `worldMatrix` | `Matrix4` | Readonly, local × ancestor chain |
|
|
123
|
+
| `isAncestorOf(other)` | `boolean` | |
|
|
124
|
+
| `dispose()` | `void` | Detaches from parent, detaches children, unsubscribes from all batchers |
|
|
125
|
+
|
|
126
|
+
> **Note:** `rotation` (Euler) mutates are tracked and auto-sync to `quaternion`.
|
|
127
|
+
|
|
128
|
+
### `InstanceBatcher`
|
|
129
|
+
|
|
130
|
+
Extends `THREE.Group`. Add it to the scene and call `update()` every frame.
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
new InstanceBatcher(options?: BatcherOptions)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Member | Type | Notes |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `count` | `number` | Total managed instances |
|
|
139
|
+
| `groupCount` | `number` | Number of internal `InstancedMesh` groups |
|
|
140
|
+
| `hasDirty` | `boolean` | Whether any data needs flushing to GPU |
|
|
141
|
+
| `addInstance(inst \| inst[])` | `this` | Duplicates are ignored |
|
|
142
|
+
| `removeInstance(inst \| inst[])` | `this` | Missing instances are ignored |
|
|
143
|
+
| `has(inst)` | `boolean` | |
|
|
144
|
+
| `getMatrixAt(inst, target)` | `Matrix4` | Reads back from GPU buffer |
|
|
145
|
+
| `getColorAt(inst, target)` | `Color` | Reads back from GPU buffer |
|
|
146
|
+
| `getInstanceFromIntersect(hit)` | `Instance \| null` | Resolves a raycaster hit to an instance |
|
|
147
|
+
| `update(camera?)` | `void` | Flushes dirty matrices and colors; pass camera to enable frustum culling |
|
|
148
|
+
| `disposeBatcher()` | `void` | Disposes all internal meshes and clears all subscriptions |
|
|
149
|
+
|
|
150
|
+
### `BatcherOptions`
|
|
151
|
+
|
|
152
|
+
| Option | Type | Default | Description |
|
|
153
|
+
|---|---|---|---|
|
|
154
|
+
| `initialCapacity` | `number` | `16` | Initial slot count per `InstancedMesh` group |
|
|
155
|
+
| `overAllocation` | `number` | `0.2` | Extra capacity fraction when a group grows (0.2 = 20%) |
|
|
156
|
+
| `frustumCulling` | `boolean` | `false` | Skip instances outside the camera frustum |
|
|
157
|
+
| `customDepthMaterial` | `Material` | — | Custom depth material for shadow passes |
|
|
158
|
+
| `customDistanceMaterial` | `Material` | — | Custom distance material for shadow passes |
|
|
159
|
+
|
|
160
|
+
## Memory Overhead
|
|
161
|
+
|
|
162
|
+
| Overhead | Per 1,000 instances |
|
|
163
|
+
|---|---|
|
|
164
|
+
| 35 wrapped setter closures | ~1.1 MB |
|
|
165
|
+
| Three.js transform objects | ~0.8 MB |
|
|
166
|
+
| GPU buffer (matrix 64B + color 12B) | ~76 KB |
|
|
167
|
+
| **Total** | **~2.0 MB / 1k** |
|
|
168
|
+
|
|
169
|
+
The real bottleneck at scale is GPU draw calls and vertex throughput, not JS-side dirty tracking.
|
|
170
|
+
|
|
171
|
+
## Known Limitations
|
|
172
|
+
|
|
173
|
+
- Frustum culling is JS-side only; it does not write zero matrices for culled instances.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import*as n from"three";const _=["type","map","alphaMap","normalMap","roughnessMap","metalnessMap","transparent","alphaTest","side","depthWrite","depthTest","wireframe","flatShading","blending","blendSrc","blendDst"];function m(a){const t=[];for(const s of _){const e=a[s];e==null?t.push(`${s}=_`):e instanceof n.Texture?t.push(`${s}=tex:${e.uuid}`):t.push(`${s}=${String(e)}`)}return t.join(";")}function y(a,t,s,e){const i=a.uuid??String(a.id),r=m(t);return`${i}|${r}|${`${s?1:0}${e?1:0}`}`}let f=0,u=0;function g(){return++u}function M(){return u}function c(a,t,s){const e=a[t];typeof e=="function"&&(a[t]=function(...i){const r=e.apply(this,i);return s(),r})}const d=["set","copy","setScalar","setX","setY","setZ","setComponent"],w=["set","setX","setY","setZ","copy","setFromVector3","setFromQuaternion"],S=["set","copy","identity","setFromEuler","setFromAxisAngle","setFromRotationMatrix","multiply","premultiply"],x=["set","setScalar","setHex","setRGB","setHSL","copy"];class b{constructor(t,s){this._visible=!0,this._castShadow=!1,this._receiveShadow=!1,this._parent=null,this.children=[],this._localDirty=!0,this._worldDirty=!0,this._dirtyColor=!0,this._dirtyShadow=!1,this._frameVersion=0,this._disposed=!1,this._subscribers=new Set,this.id=++f,this.geometry=t,this.material=s,this.position=new n.Vector3,this.quaternion=new n.Quaternion,this.rotation=new n.Euler,this.scale=new n.Vector3(1,1,1),this.color=new n.Color(1,1,1),this.localMatrix=new n.Matrix4,this.worldMatrix=new n.Matrix4;const e=()=>{this._localDirty=!0,this._markWorldDirty()},i=()=>{this.quaternion.setFromEuler(this.rotation),e()},r=()=>{this._dirtyColor=!0,this._notifyDirty("color")};for(const o of d)c(this.position,o,e);for(const o of d)c(this.scale,o,e);for(const o of w)c(this.rotation,o,i);for(const o of S)c(this.quaternion,o,e);for(const o of x)c(this.color,o,r)}get visible(){return this._visible}set visible(t){this._visible!==t&&(this._visible=t,this._notifyDirty("matrix"))}get castShadow(){return this._castShadow}set castShadow(t){this._castShadow!==t&&(this._castShadow=t,this._dirtyShadow=!0,this._notifyShadowChange())}get receiveShadow(){return this._receiveShadow}set receiveShadow(t){this._receiveShadow!==t&&(this._receiveShadow=t,this._dirtyShadow=!0,this._notifyShadowChange())}get groupKey(){return y(this.geometry,this.material,this._castShadow,this._receiveShadow)}get parent(){return this._parent}set parent(t){if(t!==this._parent){if(t&&this._wouldCreateCycle(t))throw new Error("Instance: setting parent would create a cycle");if(this._parent){const s=this._parent.children.indexOf(this);s!==-1&&this._parent.children.splice(s,1)}this._parent=t,t&&t.children.push(this),this._markWorldDirty()}}isAncestorOf(t){let s=t.parent;for(;s;){if(s===this)return!0;s=s.parent}return!1}get disposed(){return this._disposed}dispose(){if(this._disposed)return;if(this._disposed=!0,this._parent){const s=this._parent.children.indexOf(this);s!==-1&&this._parent.children.splice(s,1),this._parent=null}for(const s of this.children)s._parent=null,s._markWorldDirty();this.children.length=0;const t=[...this._subscribers];this._subscribers.clear();for(const s of t)s.removeInstance(this)}recomputeLocalMatrix(){this._localDirty&&(this.localMatrix.compose(this.position,this.quaternion,this.scale),this._localDirty=!1,this._markWorldDirty())}recomputeWorldMatrix(){const t=M();this._frameVersion!==t&&(this._frameVersion=t,this._parent?(this._parent.recomputeWorldMatrix(),this.recomputeLocalMatrix(),this.worldMatrix.multiplyMatrices(this._parent.worldMatrix,this.localMatrix)):(this.recomputeLocalMatrix(),this.worldMatrix.copy(this.localMatrix)),this._worldDirty=!1)}_subscribe(t){this._subscribers.add(t)}_unsubscribe(t){this._subscribers.delete(t)}_markWorldDirty(){if(!this._worldDirty){this._worldDirty=!0,this._notifyDirty("matrix");for(const t of this.children)t._markWorldDirty()}}_markDirtyForBatcher(t){t.markDirty(this,"matrix"),t.markDirty(this,"color")}_notifyDirty(t){for(const s of this._subscribers)s.markDirty(this,t)}_notifyShadowChange(){for(const t of this._subscribers)t.markShadowChange(this)}_wouldCreateCycle(t){return this.isAncestorOf(t)||t===this}}function p(a,t,s,e,i){const r=new n.InstancedMesh(a,t,s);r.castShadow=!1,r.receiveShadow=!1,r.frustumCulled=!1,r.count=0,e&&(r.customDepthMaterial=e),i&&(r.customDistanceMaterial=i),r.instanceColor=new n.InstancedBufferAttribute(new Float32Array(s*3),3);const o=r.instanceMatrix.array;for(let h=0;h<s;h++){const l=h*16;o[l]=1,o[l+5]=1,o[l+10]=1,o[l+15]=1}return r}const D=new n.Matrix4().identity().multiplyScalar(0);class C extends n.Group{constructor(t={}){super(),this._groups=new Map,this._meshToGroup=new Map,this._allInstances=new Set,this._instanceMeta=new Map,this._needsGroupRebuild=new Set,this._hasDirty=!0,this._frustum=new n.Frustum,this._projScreenMatrix=new n.Matrix4,this._tempVec=new n.Vector3,this._tempSphere=new n.Sphere,this._opts={overAllocation:t.overAllocation??.2,initialCapacity:t.initialCapacity??16,frustumCulling:t.frustumCulling??!1,customDepthMaterial:t.customDepthMaterial,customDistanceMaterial:t.customDistanceMaterial}}get count(){return this._allInstances.size}get groupCount(){return this._groups.size}get hasDirty(){return this._hasDirty||this._needsGroupRebuild.size>0}addInstance(t){const s=Array.isArray(t)?t:[t];for(const e of s)this._allInstances.has(e)||(this._addToGroup(e),e._subscribe(this),this._allInstances.add(e));return this}removeInstance(t){const s=Array.isArray(t)?t:[t];for(const e of s)this._allInstances.has(e)&&(this._removeFromGroup(e),e._unsubscribe(this),this._allInstances.delete(e),this._needsGroupRebuild.delete(e));return this}has(t){return this._allInstances.has(t)}getInstanceFromIntersect(t){if(t.instanceId===void 0)return null;const s=this._meshToGroup.get(t.object);return s&&t.instanceId<s.instances.length?s.instances[t.instanceId]:null}getMatrixAt(t,s){const e=this._instanceMeta.get(t);if(!e)return s.identity();const i=this._groups.get(e.groupKey);return i?(i.mesh.getMatrixAt(e.idx,s),s):s.identity()}getColorAt(t,s){const e=this._instanceMeta.get(t);if(!e)return s.set(1,1,1);const i=this._groups.get(e.groupKey);return!i||!i.mesh.instanceColor?s.set(1,1,1):(i.mesh.getColorAt(e.idx,s),s)}update(t){if(!(!this._hasDirty&&this._needsGroupRebuild.size===0)){if(g(),this._needsGroupRebuild.size>0){for(const s of this._needsGroupRebuild)this._removeFromGroup(s),this._addToGroup(s);this._needsGroupRebuild.clear()}this._opts.frustumCulling&&t&&this._updateFrustumCulling(t);for(const s of this._groups.values())s.hasDirty&&this._processGroup(s);this._hasDirty=!1}}disposeBatcher(){for(const t of this._allInstances)t._unsubscribe(this);for(const t of this._groups.values())t.mesh.dispose();this._groups.clear(),this._meshToGroup.clear(),this._allInstances.clear(),this._instanceMeta.clear(),this._needsGroupRebuild.clear(),this._hasDirty=!1,this.clear()}markDirty(t,s){const e=this._instanceMeta.get(t);if(!e)return;const i=this._groups.get(e.groupKey);i&&(this._hasDirty=!0,i.hasDirty=!0,s==="matrix"&&i.dirtyIndices.add(e.idx),s==="color"&&i.dirtyColors.add(e.idx))}markShadowChange(t){this._allInstances.has(t)&&(this._hasDirty=!0,this._needsGroupRebuild.add(t))}_addMeshToScene(t){super.add(t)}_removeMeshFromScene(t){super.remove(t)}_addToGroup(t){const s=t.groupKey;let e=this._groups.get(s);if(!e){const r=this._opts.initialCapacity;e={key:s,mesh:null,instances:new Array(r).fill(null),indexMap:new Map,dirtyIndices:new Set,dirtyColors:new Set,emptySlots:[],frustumVisible:new Set,hasDirty:!0,capacity:r,count:0};const o=p(t.geometry,t.material,r,this._opts.customDepthMaterial,this._opts.customDistanceMaterial);o.castShadow=t.castShadow,o.receiveShadow=t.receiveShadow,e.mesh=o,this._addMeshToScene(o),this._groups.set(s,e),this._meshToGroup.set(o,e)}e.count>=e.capacity&&this._growGroup(e);let i;e.emptySlots.length>0?i=e.emptySlots.pop():i=e.count,e.instances[i]=t,e.indexMap.set(t,i),this._instanceMeta.set(t,{groupKey:s,idx:i}),e.count++,e.mesh.count=e.count,this._hasDirty=!0,e.hasDirty=!0,e.dirtyIndices.add(i),e.dirtyColors.add(i)}_removeFromGroup(t){const s=t.groupKey,e=this._groups.get(s);if(!e)return;const i=e.indexMap.get(t);if(i===void 0)return;const r=e.count-1;if(i===r)e.instances[i]=null,e.count--;else{const o=e.instances[r];e.instances[i]=o,e.instances[r]=null,e.indexMap.set(o,i),this._instanceMeta.set(o,{groupKey:s,idx:i}),e.dirtyIndices.add(i),e.count--}e.indexMap.delete(t),this._instanceMeta.delete(t),e.emptySlots.push(r),e.dirtyIndices.delete(i),e.dirtyColors.delete(i),e.mesh.count=e.count,e.count===0&&(this._meshToGroup.delete(e.mesh),e.mesh.dispose(),this._removeMeshFromScene(e.mesh),this._groups.delete(s))}_processGroup(t){const s=t.mesh,e=s.instanceMatrix.array;for(const i of t.dirtyIndices){const r=t.instances[i];r&&(r.recomputeWorldMatrix(),r.visible?r.worldMatrix.toArray(e,i*16):D.toArray(e,i*16))}if(t.dirtyIndices.size>0&&(s.instanceMatrix.needsUpdate=!0,t.dirtyIndices.clear()),t.hasDirty=!1,t.dirtyColors.size>0&&s.instanceColor){const i=s.instanceColor.array;for(const r of t.dirtyColors){const o=t.instances[r];if(o){const h=o.color;i[r*3]=h.r,i[r*3+1]=h.g,i[r*3+2]=h.b}}s.instanceColor.needsUpdate=!0,t.dirtyColors.clear()}s.computeBoundingSphere(),s.computeBoundingBox()}_updateFrustumCulling(t){this._projScreenMatrix.multiplyMatrices(t.projectionMatrix,t.matrixWorldInverse),this._frustum.setFromProjectionMatrix(this._projScreenMatrix);for(const s of this._groups.values()){const e=s.mesh.geometry;e.boundingSphere||e.computeBoundingSphere();const i=e.boundingSphere;s.frustumVisible.clear();for(let r=0;r<s.count;r++){const o=s.instances[r];if(!o)continue;o.recomputeWorldMatrix();const h=o.scale;this._tempVec.copy(i.center).applyMatrix4(o.worldMatrix),this._tempSphere.set(this._tempVec,i.radius*Math.max(h.x,h.y,h.z)),this._frustum.intersectsSphere(this._tempSphere)&&s.frustumVisible.add(r)}}}_growGroup(t){const s=t.capacity+Math.max(1,Math.ceil(t.capacity*this._opts.overAllocation)),e=t.mesh,i=p(e.geometry,e.material,s,this._opts.customDepthMaterial,this._opts.customDistanceMaterial);i.castShadow=e.castShadow,i.receiveShadow=e.receiveShadow;const r=e.instanceMatrix.array;if(i.instanceMatrix.array.set(r.subarray(0,t.count*16)),e.instanceColor&&i.instanceColor){const o=e.instanceColor.array;i.instanceColor.array.set(o.subarray(0,t.count*3))}i.count=t.count,this._meshToGroup.delete(e),this._removeMeshFromScene(e),e.dispose(),this._addMeshToScene(i),this._meshToGroup.set(i,t),t.mesh=i,t.capacity=s,t.instances.length=s}}export{b as Instance,C as InstanceBatcher};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { BatcherSubscriber } from '../types';
|
|
3
|
+
export declare function bumpFrameVersion(): number;
|
|
4
|
+
export declare function getFrameVersion(): number;
|
|
5
|
+
export declare class Instance {
|
|
6
|
+
readonly id: number;
|
|
7
|
+
readonly geometry: THREE.BufferGeometry;
|
|
8
|
+
readonly material: THREE.Material;
|
|
9
|
+
readonly position: THREE.Vector3;
|
|
10
|
+
readonly scale: THREE.Vector3;
|
|
11
|
+
readonly quaternion: THREE.Quaternion;
|
|
12
|
+
readonly rotation: THREE.Euler;
|
|
13
|
+
readonly color: THREE.Color;
|
|
14
|
+
readonly localMatrix: THREE.Matrix4;
|
|
15
|
+
readonly worldMatrix: THREE.Matrix4;
|
|
16
|
+
private _visible;
|
|
17
|
+
private _castShadow;
|
|
18
|
+
private _receiveShadow;
|
|
19
|
+
private _parent;
|
|
20
|
+
readonly children: Instance[];
|
|
21
|
+
_localDirty: boolean;
|
|
22
|
+
_worldDirty: boolean;
|
|
23
|
+
_dirtyColor: boolean;
|
|
24
|
+
_dirtyShadow: boolean;
|
|
25
|
+
_frameVersion: number;
|
|
26
|
+
private _disposed;
|
|
27
|
+
readonly _subscribers: Set<BatcherSubscriber>;
|
|
28
|
+
constructor(geometry: THREE.BufferGeometry, material: THREE.Material);
|
|
29
|
+
get visible(): boolean;
|
|
30
|
+
set visible(v: boolean);
|
|
31
|
+
get castShadow(): boolean;
|
|
32
|
+
set castShadow(v: boolean);
|
|
33
|
+
get receiveShadow(): boolean;
|
|
34
|
+
set receiveShadow(v: boolean);
|
|
35
|
+
get groupKey(): string;
|
|
36
|
+
get parent(): Instance | null;
|
|
37
|
+
set parent(p: Instance | null);
|
|
38
|
+
isAncestorOf(other: Instance): boolean;
|
|
39
|
+
get disposed(): boolean;
|
|
40
|
+
dispose(): void;
|
|
41
|
+
recomputeLocalMatrix(): void;
|
|
42
|
+
recomputeWorldMatrix(): void;
|
|
43
|
+
_subscribe(sub: BatcherSubscriber): void;
|
|
44
|
+
_unsubscribe(sub: BatcherSubscriber): void;
|
|
45
|
+
_markWorldDirty(): void;
|
|
46
|
+
_markDirtyForBatcher(batcher: BatcherSubscriber): void;
|
|
47
|
+
private _notifyDirty;
|
|
48
|
+
private _notifyShadowChange;
|
|
49
|
+
private _wouldCreateCycle;
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import { Instance } from './Instance';
|
|
3
|
+
import type { BatcherSubscriber, DirtyType, BatcherOptions } from '../types';
|
|
4
|
+
export declare class InstanceBatcher extends THREE.Group implements BatcherSubscriber {
|
|
5
|
+
private _groups;
|
|
6
|
+
private _meshToGroup;
|
|
7
|
+
private _allInstances;
|
|
8
|
+
private _instanceMeta;
|
|
9
|
+
private _needsGroupRebuild;
|
|
10
|
+
private _hasDirty;
|
|
11
|
+
private _opts;
|
|
12
|
+
private _frustum;
|
|
13
|
+
private _projScreenMatrix;
|
|
14
|
+
constructor(options?: BatcherOptions);
|
|
15
|
+
get count(): number;
|
|
16
|
+
get groupCount(): number;
|
|
17
|
+
get hasDirty(): boolean;
|
|
18
|
+
addInstance(instance: Instance | Instance[]): this;
|
|
19
|
+
removeInstance(instance: Instance | Instance[]): this;
|
|
20
|
+
has(instance: Instance): boolean;
|
|
21
|
+
getInstanceFromIntersect(hit: {
|
|
22
|
+
instanceId?: number;
|
|
23
|
+
object: THREE.Object3D;
|
|
24
|
+
}): Instance | null;
|
|
25
|
+
getMatrixAt(instance: Instance, target: THREE.Matrix4): THREE.Matrix4;
|
|
26
|
+
getColorAt(instance: Instance, target: THREE.Color): THREE.Color;
|
|
27
|
+
update(camera?: THREE.Camera): void;
|
|
28
|
+
disposeBatcher(): void;
|
|
29
|
+
markDirty(instance: Instance, type: DirtyType): void;
|
|
30
|
+
markShadowChange(instance: Instance): void;
|
|
31
|
+
private _addMeshToScene;
|
|
32
|
+
private _removeMeshFromScene;
|
|
33
|
+
private _addToGroup;
|
|
34
|
+
private _removeFromGroup;
|
|
35
|
+
private _processGroup;
|
|
36
|
+
private _tempVec;
|
|
37
|
+
private _tempSphere;
|
|
38
|
+
private _updateFrustumCulling;
|
|
39
|
+
private _growGroup;
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type * as THREE from 'three';
|
|
2
|
+
import type { Instance } from './core/Instance';
|
|
3
|
+
export type DirtyType = 'matrix' | 'color';
|
|
4
|
+
export interface BatcherSubscriber {
|
|
5
|
+
markDirty(instance: Instance, type: DirtyType): void;
|
|
6
|
+
markShadowChange(instance: Instance): void;
|
|
7
|
+
removeInstance(instance: Instance): void;
|
|
8
|
+
}
|
|
9
|
+
export interface BatcherOptions {
|
|
10
|
+
overAllocation?: number;
|
|
11
|
+
initialCapacity?: number;
|
|
12
|
+
frustumCulling?: boolean;
|
|
13
|
+
customDepthMaterial?: THREE.Material;
|
|
14
|
+
customDistanceMaterial?: THREE.Material;
|
|
15
|
+
}
|
|
16
|
+
export interface BatchGroup {
|
|
17
|
+
key: string;
|
|
18
|
+
mesh: THREE.InstancedMesh;
|
|
19
|
+
instances: (Instance | null)[];
|
|
20
|
+
indexMap: Map<Instance, number>;
|
|
21
|
+
dirtyIndices: Set<number>;
|
|
22
|
+
dirtyColors: Set<number>;
|
|
23
|
+
emptySlots: number[];
|
|
24
|
+
frustumVisible: Set<number>;
|
|
25
|
+
hasDirty: boolean;
|
|
26
|
+
capacity: number;
|
|
27
|
+
count: number;
|
|
28
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "three-instance-batch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Declarative instanced rendering for Three.js — Instance describes what, InstanceBatcher decides how to draw",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/three-instance-batch.js",
|
|
7
|
+
"module": "./dist/three-instance-batch.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/three-instance-batch.js",
|
|
12
|
+
"types": "./dist/types/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "vite",
|
|
20
|
+
"build": "vite build && esbuild dist/three-instance-batch.js --minify --format=esm --outfile=dist/three-instance-batch.js --allow-overwrite && tsc --emitDeclarationOnly",
|
|
21
|
+
"prepublishOnly": "npm run test:run && npm run build",
|
|
22
|
+
"preview": "vite preview",
|
|
23
|
+
"test": "vitest --config vitest.config.ts",
|
|
24
|
+
"test:run": "vitest run --config vitest.config.ts"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/xier123456/three-instance-batch"
|
|
29
|
+
},
|
|
30
|
+
"author": "xier123456",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/xier123456/three-instance-batch/issues"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"three": ">=0.152.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/three": "^0.183.0",
|
|
39
|
+
"esbuild": "^0.28.0",
|
|
40
|
+
"three": "^0.183.0",
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"vite": "^5.0.0",
|
|
43
|
+
"vitest": "^1.0.0"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"three.js",
|
|
47
|
+
"instanced-mesh",
|
|
48
|
+
"batching",
|
|
49
|
+
"rendering"
|
|
50
|
+
],
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|