three-zoo 0.12.0 → 0.13.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 +147 -89
- package/dist/ik/AimChainIK.d.ts +58 -0
- package/dist/ik/AimChainIK.js +130 -0
- package/dist/{miscellaneous → ik}/TwoBoneIK.js +5 -7
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/instancedMeshPool/InstancedMeshInstance.d.ts +2 -1
- package/dist/instancedMeshPool/InstancedMeshInstance.js +11 -0
- package/dist/miscellaneous/SkinnedMeshBaker.d.ts +1 -2
- package/dist/miscellaneous/SkinnedMeshBaker.js +1 -2
- package/package.json +10 -3
- /package/dist/{miscellaneous → ik}/TwoBoneIK.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,152 +1,210 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<h1 align="center">🦁 three-zoo</h1>
|
|
3
|
-
<p align="center">
|
|
4
|
-
A small collection of Three.js utilities I use in my daily work with 3D development.
|
|
5
|
-
</p>
|
|
2
|
+
<h1 align="center">🦁 🐘 🦊 three-zoo</h1>
|
|
3
|
+
<p align="center">Reusable Three.js utilities.</p>
|
|
6
4
|
</p>
|
|
7
5
|
|
|
8
6
|
<p align="center">
|
|
9
7
|
<a href="https://www.npmjs.com/package/three-zoo"><img src="https://img.shields.io/npm/v/three-zoo.svg" alt="npm version"></a>
|
|
10
|
-
<a href="https://bundlephobia.com/package/three-zoo"><img src="https://badgen.net/bundlephobia/min/three-zoo" alt="bundle size (min)"></a>
|
|
11
8
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
12
9
|
<a href="https://www.typescriptlang.org/"><img src="https://img.shields.io/badge/TypeScript-%5E5.8.0-blue" alt="TypeScript"></a>
|
|
13
10
|
<a href="https://threejs.org/"><img src="https://img.shields.io/badge/Three.js-%5E0.175.0-green" alt="Three.js"></a>
|
|
14
11
|
</p>
|
|
15
12
|
|
|
16
|
-
## What's included
|
|
17
|
-
|
|
18
|
-
- 📷 **DualFovCamera** - Camera with separate horizontal and vertical FOV controls
|
|
19
|
-
- ☀️ **Sun** - Directional light with spherical positioning
|
|
20
|
-
- 🔍 **SceneTraversal** - Helper functions for finding objects and materials in scenes
|
|
21
|
-
- 🎭 **SkinnedMeshBaker** - Converts animated meshes to static geometry
|
|
22
|
-
- 🎨 **StandardToLambertConverter** - Converts PBR materials to Lambert materials
|
|
23
|
-
- ✨ **StandardToBasicConverter** - Converts PBR materials to Basic materials
|
|
24
|
-
|
|
25
13
|
## Installation
|
|
26
14
|
|
|
27
15
|
```bash
|
|
28
16
|
npm install three-zoo
|
|
29
17
|
```
|
|
30
18
|
|
|
31
|
-
##
|
|
19
|
+
## Contents
|
|
32
20
|
|
|
33
|
-
|
|
21
|
+
- **IK** - `TwoBoneIK`, `AimChainIK`
|
|
22
|
+
- **Instanced Mesh Pool** - `InstancedMeshPool`, `InstancedMeshInstance`, `InstancedMeshGroup`
|
|
23
|
+
- **Lighting** - `Sun`, `SkyLight`
|
|
24
|
+
- **Material Converters** - Standard to Basic / Lambert / Phong / Toon / Physical, Basic to Physical
|
|
25
|
+
- **Miscellaneous** - `DualFovCamera`, `SceneTraversal`, `SceneSorter`, `SkinnedMeshBaker`
|
|
34
26
|
|
|
35
|
-
|
|
36
|
-
const camera = new DualFovCamera(90, 60);
|
|
27
|
+
---
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
camera.horizontalFov = 100;
|
|
40
|
-
camera.verticalFov = 70;
|
|
29
|
+
## IK
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
camera.fitVerticalFovToPoints(vertices);
|
|
44
|
-
camera.fitVerticalFovToBox(boundingBox);
|
|
45
|
-
camera.fitVerticalFovToMesh(skinnedMesh);
|
|
31
|
+
### TwoBoneIK
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
33
|
+
Analytical two-bone IK solver. Chain: `root -> middle -> end`. Pole controls bend direction. Writes local quaternions to `root` and `middle`.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const twoBoneIK = new TwoBoneIK(upperArm, foreArm, hand, poleObject, targetObject);
|
|
37
|
+
|
|
38
|
+
// call after AnimationMixer.update() each frame
|
|
39
|
+
twoBoneIK.solve();
|
|
40
|
+
|
|
41
|
+
// tune pole twist per bone
|
|
42
|
+
twoBoneIK.rootPoleAxis.set(0, 1, 0);
|
|
43
|
+
twoBoneIK.middlePoleTwist = false;
|
|
49
44
|
```
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
### AimChainIK
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
Distributes aim rotation across a bone chain according to per-bone weights.
|
|
54
49
|
|
|
55
50
|
```typescript
|
|
56
|
-
const
|
|
51
|
+
const aimChainIK = new AimChainIK([spine1, spine2, spine3, head]);
|
|
57
52
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
sun.azimuth = Math.PI / 2; // 90° rotation
|
|
61
|
-
sun.distance = 100; // Distance from origin
|
|
53
|
+
aimChainIK.curve = [0.2, 0.5, 0.8, 1.0]; // root gets least, tip gets most
|
|
54
|
+
aimChainIK.weight = 0.8; // global blend
|
|
62
55
|
|
|
63
|
-
//
|
|
64
|
-
|
|
56
|
+
// sample directions before calling - mutates bone quaternions
|
|
57
|
+
aimChainIK.solve(currentForward, targetDirection);
|
|
58
|
+
```
|
|
65
59
|
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Instanced Mesh Pool
|
|
63
|
+
|
|
64
|
+
Manages `InstancedMesh` instances keyed by geometry+material. Grows capacity automatically.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const pool = new InstancedMeshPool(scene, { initialCapacity: 32, capacityStep: 16 });
|
|
68
|
+
|
|
69
|
+
// allocate / update / release individual instances
|
|
70
|
+
const instance = new InstancedMeshInstance(pool, geometry, material);
|
|
71
|
+
instance.setPosition3f(1, 0, 0).setScale3f(2, 2, 2).flushTransform();
|
|
72
|
+
instance.destroy();
|
|
73
|
+
|
|
74
|
+
// group multiple instances under a shared Object3D transform
|
|
75
|
+
const group = new InstancedMeshGroup([instanceA, instanceB]);
|
|
76
|
+
scene.add(group);
|
|
77
|
+
group.position.set(10, 0, 0);
|
|
78
|
+
group.flushTransform(); // propagates group world matrix to all instances
|
|
79
|
+
group.destroy();
|
|
68
80
|
```
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Lighting
|
|
71
85
|
|
|
72
|
-
|
|
86
|
+
### Sun
|
|
87
|
+
|
|
88
|
+
`DirectionalLight` with spherical positioning and shadow auto-configuration.
|
|
73
89
|
|
|
74
90
|
```typescript
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
const sun = new Sun();
|
|
92
|
+
sun.elevation = Math.PI / 4;
|
|
93
|
+
sun.azimuth = Math.PI / 2;
|
|
94
|
+
sun.distance = 100;
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
96
|
+
sun.configureShadowsForBoundingBox(sceneBounds);
|
|
97
|
+
sun.setDirectionFromHDRTexture(hdrTexture, 50);
|
|
98
|
+
```
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
const glassObjects = SceneTraversal.findMaterialUsers(scene, /glass/i);
|
|
100
|
+
### SkyLight
|
|
85
101
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
102
|
+
`HemisphereLight` that extracts sky and ground colors from an HDR environment map.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const skyLight = new SkyLight();
|
|
106
|
+
skyLight.setColorsFromHDRTexture(hdrTexture, {
|
|
107
|
+
skySampleCount: 100,
|
|
108
|
+
groundSampleCount: 100,
|
|
89
109
|
});
|
|
90
110
|
```
|
|
91
111
|
|
|
92
|
-
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Material Converters
|
|
115
|
+
|
|
116
|
+
All converters expose a single static `convert(material, options?)`. Common options:
|
|
93
117
|
|
|
94
|
-
|
|
118
|
+
| Option | Default | Description |
|
|
119
|
+
|------------------|---------|--------------------------------------|
|
|
120
|
+
| `preserveName` | `true` | Copy `.name` to the new material |
|
|
121
|
+
| `copyUserData` | `true` | Copy `.userData` |
|
|
122
|
+
| `disposeOriginal`| `false` | Dispose source material after conversion |
|
|
95
123
|
|
|
96
124
|
```typescript
|
|
97
|
-
//
|
|
98
|
-
const
|
|
125
|
+
// Standard -> unlit
|
|
126
|
+
const basicMaterial = StandardToBasicConverter.convert(standardMaterial, { brightnessFactor: 1.3, combineEmissive: true });
|
|
127
|
+
|
|
128
|
+
// Standard -> diffuse-only lit
|
|
129
|
+
const lambertMaterial = StandardToLambertConverter.convert(standardMaterial);
|
|
130
|
+
const phongMaterial = StandardToPhongConverter.convert(standardMaterial);
|
|
131
|
+
const toonMaterial = StandardToToonConverter.convert(standardMaterial);
|
|
99
132
|
|
|
100
|
-
//
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
skinnedMesh,
|
|
104
|
-
1.5, // Time in seconds
|
|
105
|
-
animationClip
|
|
106
|
-
);
|
|
133
|
+
// Standard <-> Physical
|
|
134
|
+
const physicalMaterial = StandardToPhysicalConverter.convert(standardMaterial);
|
|
135
|
+
const standardMaterial2 = BasicToPhysicalConverter.convert(basicMaterial);
|
|
107
136
|
```
|
|
108
137
|
|
|
109
|
-
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Miscellaneous
|
|
110
141
|
|
|
111
|
-
|
|
142
|
+
### DualFovCamera
|
|
112
143
|
|
|
113
|
-
|
|
144
|
+
`PerspectiveCamera` with independent horizontal and vertical FOV.
|
|
114
145
|
|
|
115
146
|
```typescript
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
147
|
+
const camera = new DualFovCamera(90, 60);
|
|
148
|
+
camera.horizontalFov = 100;
|
|
149
|
+
camera.verticalFov = 70;
|
|
150
|
+
|
|
151
|
+
camera.fitVerticalFovToPoints(vertices);
|
|
152
|
+
camera.fitVerticalFovToBox(boundingBox);
|
|
153
|
+
camera.fitVerticalFovToMesh(skinnedMesh);
|
|
154
|
+
camera.lookAtMeshCenterOfMass(skinnedMesh);
|
|
125
155
|
```
|
|
126
156
|
|
|
127
|
-
###
|
|
157
|
+
### SceneTraversal
|
|
158
|
+
|
|
159
|
+
Static helpers for depth-first scene graph traversal.
|
|
128
160
|
|
|
129
161
|
```typescript
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
// find by name
|
|
163
|
+
const playerObject = SceneTraversal.getObjectByName(scene, 'Player');
|
|
164
|
+
const metalMaterial = SceneTraversal.getMaterialByName(scene, 'Metal');
|
|
165
|
+
|
|
166
|
+
// filter by regex or predicate
|
|
167
|
+
const enemies = SceneTraversal.filterObjects(scene, /^enemy_/);
|
|
168
|
+
const glassMaterials = SceneTraversal.filterMaterials(scene, /glass/i);
|
|
169
|
+
|
|
170
|
+
// enumerate with callback
|
|
171
|
+
SceneTraversal.enumerateMaterials(scene, (material) => {
|
|
172
|
+
material.needsUpdate = true;
|
|
139
173
|
});
|
|
174
|
+
|
|
175
|
+
// find meshes that use specific materials
|
|
176
|
+
const glassMeshes = SceneTraversal.findMaterialUsers(scene, glassMaterials);
|
|
140
177
|
```
|
|
141
178
|
|
|
142
|
-
|
|
179
|
+
### SceneSorter
|
|
180
|
+
|
|
181
|
+
Assigns sequential `renderOrder` values sorted by distance to a point. Useful for transparent meshes.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
// front-to-back
|
|
185
|
+
SceneSorter.sortByDistanceToPoint(object, cameraPosition, 0);
|
|
186
|
+
|
|
187
|
+
// back-to-front (transparent objects)
|
|
188
|
+
SceneSorter.sortByDistanceToPoint(object, cameraPosition, 0, true);
|
|
189
|
+
```
|
|
143
190
|
|
|
144
|
-
|
|
145
|
-
- TypeScript support included
|
|
191
|
+
### SkinnedMeshBaker
|
|
146
192
|
|
|
147
|
-
|
|
193
|
+
Bakes a `SkinnedMesh` to static geometry.
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// current pose
|
|
197
|
+
const staticMesh = SkinnedMeshBaker.bakePose(skinnedMesh);
|
|
198
|
+
|
|
199
|
+
// specific animation frame
|
|
200
|
+
const frameMesh = SkinnedMeshBaker.bakeAnimationFrame(armature, skinnedMesh, 1.5, animationClip);
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Requirements
|
|
148
206
|
|
|
149
|
-
|
|
207
|
+
- `three` >=0.157.0 <0.180.0 (peer dependency)
|
|
150
208
|
|
|
151
209
|
## License
|
|
152
210
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Object3D } from "three";
|
|
2
|
+
import { Vector3 } from "three";
|
|
3
|
+
/**
|
|
4
|
+
* Distributes aim rotation across a chain of bones.
|
|
5
|
+
*
|
|
6
|
+
* Computes the swing rotation from `currentDirection` to `targetDirection`
|
|
7
|
+
* and splits it across bones according to `curve` weights.
|
|
8
|
+
*
|
|
9
|
+
* Mutates `bone.quaternion` directly. Call after AnimationMixer update.
|
|
10
|
+
*/
|
|
11
|
+
export declare class AimChainIK {
|
|
12
|
+
readonly bones: readonly Object3D[];
|
|
13
|
+
/** Numerical stability threshold. */
|
|
14
|
+
epsilon: number;
|
|
15
|
+
/**
|
|
16
|
+
* Global blend weight. 0 = solver has no effect, 1 = full effect.
|
|
17
|
+
* Clamped to 0–1 internally.
|
|
18
|
+
*/
|
|
19
|
+
weight: number;
|
|
20
|
+
/**
|
|
21
|
+
* Per-bone rotation weights. Normalized internally so their sum equals 1.
|
|
22
|
+
*
|
|
23
|
+
* `[1, 1, 1, 1]` = uniform. `[0.2, 0.5, 0.8, 1.0]` = root gets least, tip gets most.
|
|
24
|
+
*
|
|
25
|
+
* Compared by reference — mutating values in-place won't trigger
|
|
26
|
+
* renormalization. Assign a new array to update.
|
|
27
|
+
*
|
|
28
|
+
* Length must match bone count; mismatches throw on `solve()`.
|
|
29
|
+
*/
|
|
30
|
+
curve: readonly number[];
|
|
31
|
+
private readonly swingAxis;
|
|
32
|
+
private readonly deltaRotation;
|
|
33
|
+
private readonly boneWorldQuaternion;
|
|
34
|
+
private readonly parentWorldQuaternion;
|
|
35
|
+
private normalizedCurve;
|
|
36
|
+
private lastCurveReference;
|
|
37
|
+
/**
|
|
38
|
+
* @param bones - Ordered from root to tip. Must contain at least one bone.
|
|
39
|
+
*/
|
|
40
|
+
constructor(bones: readonly Object3D[]);
|
|
41
|
+
/**
|
|
42
|
+
* Rotate the chain so that `currentDirection` aligns with `targetDirection`.
|
|
43
|
+
*
|
|
44
|
+
* Both vectors are in world space and are not mutated.
|
|
45
|
+
*
|
|
46
|
+
* Sample directions **before** calling — this method mutates bone quaternions,
|
|
47
|
+
* so any direction derived from the chain will be stale after the call.
|
|
48
|
+
*
|
|
49
|
+
* @param currentDirection - Where the chain currently aims.
|
|
50
|
+
* @param targetDirection - Where it should aim.
|
|
51
|
+
*/
|
|
52
|
+
solve(currentDirection: Vector3, targetDirection: Vector3): void;
|
|
53
|
+
/**
|
|
54
|
+
* Returns normalized curve (sums to 1).
|
|
55
|
+
* Rebuilds only when the `curve` reference changes.
|
|
56
|
+
*/
|
|
57
|
+
private getNormalizedCurve;
|
|
58
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Vector3, Quaternion, MathUtils } from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Distributes aim rotation across a chain of bones.
|
|
5
|
+
*
|
|
6
|
+
* Computes the swing rotation from `currentDirection` to `targetDirection`
|
|
7
|
+
* and splits it across bones according to `curve` weights.
|
|
8
|
+
*
|
|
9
|
+
* Mutates `bone.quaternion` directly. Call after AnimationMixer update.
|
|
10
|
+
*/
|
|
11
|
+
class AimChainIK {
|
|
12
|
+
/**
|
|
13
|
+
* @param bones - Ordered from root to tip. Must contain at least one bone.
|
|
14
|
+
*/
|
|
15
|
+
constructor(bones) {
|
|
16
|
+
this.bones = bones;
|
|
17
|
+
/** Numerical stability threshold. */
|
|
18
|
+
this.epsilon = 1e-5;
|
|
19
|
+
/**
|
|
20
|
+
* Global blend weight. 0 = solver has no effect, 1 = full effect.
|
|
21
|
+
* Clamped to 0–1 internally.
|
|
22
|
+
*/
|
|
23
|
+
this.weight = 1;
|
|
24
|
+
this._private_swingAxis = new Vector3();
|
|
25
|
+
this._private_deltaRotation = new Quaternion();
|
|
26
|
+
this._private_boneWorldQuaternion = new Quaternion();
|
|
27
|
+
this._private_parentWorldQuaternion = new Quaternion();
|
|
28
|
+
this._private_normalizedCurve = [];
|
|
29
|
+
this._private_lastCurveReference = undefined;
|
|
30
|
+
if (bones.length === 0) {
|
|
31
|
+
throw new Error("AimChainIK requires at least one bone.");
|
|
32
|
+
}
|
|
33
|
+
const uniform = Array(bones.length).fill(1);
|
|
34
|
+
this.curve = uniform;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Rotate the chain so that `currentDirection` aligns with `targetDirection`.
|
|
38
|
+
*
|
|
39
|
+
* Both vectors are in world space and are not mutated.
|
|
40
|
+
*
|
|
41
|
+
* Sample directions **before** calling — this method mutates bone quaternions,
|
|
42
|
+
* so any direction derived from the chain will be stale after the call.
|
|
43
|
+
*
|
|
44
|
+
* @param currentDirection - Where the chain currently aims.
|
|
45
|
+
* @param targetDirection - Where it should aim.
|
|
46
|
+
*/
|
|
47
|
+
solve(currentDirection, targetDirection) {
|
|
48
|
+
const effectiveWeight = MathUtils.clamp(this.weight, 0, 1);
|
|
49
|
+
if (effectiveWeight < this.epsilon) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const currentLength = currentDirection.length();
|
|
53
|
+
const targetLength = targetDirection.length();
|
|
54
|
+
if (currentLength < this.epsilon || targetLength < this.epsilon) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const dotProduct = MathUtils.clamp(currentDirection.dot(targetDirection) / (currentLength * targetLength), -1, 1);
|
|
58
|
+
const totalAngle = Math.acos(dotProduct);
|
|
59
|
+
if (totalAngle < this.epsilon) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this._private_swingAxis.copy(currentDirection).cross(targetDirection).normalize();
|
|
63
|
+
// Near-opposite directions: pick an arbitrary perpendicular axis.
|
|
64
|
+
if (this._private_swingAxis.lengthSq() < this.epsilon) {
|
|
65
|
+
this._private_swingAxis.set(0, 1, 0);
|
|
66
|
+
this._private_swingAxis.addScaledVector(currentDirection, -this._private_swingAxis.dot(currentDirection) / (currentLength * currentLength));
|
|
67
|
+
if (this._private_swingAxis.lengthSq() < this.epsilon) {
|
|
68
|
+
this._private_swingAxis.set(1, 0, 0);
|
|
69
|
+
}
|
|
70
|
+
this._private_swingAxis.normalize();
|
|
71
|
+
}
|
|
72
|
+
const curve = this._private_getNormalizedCurve();
|
|
73
|
+
const weightedAngle = totalAngle * effectiveWeight;
|
|
74
|
+
for (let index = 0; index < this.bones.length; index++) {
|
|
75
|
+
const bone = this.bones[index];
|
|
76
|
+
const boneAngle = weightedAngle * curve[index];
|
|
77
|
+
if (boneAngle < this.epsilon) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
this._private_deltaRotation.setFromAxisAngle(this._private_swingAxis, boneAngle);
|
|
81
|
+
if (bone.parent) {
|
|
82
|
+
bone.parent.getWorldQuaternion(this._private_parentWorldQuaternion);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
this._private_parentWorldQuaternion.identity();
|
|
86
|
+
}
|
|
87
|
+
// localDelta = parentInverse * worldDelta * parentWorld
|
|
88
|
+
this._private_boneWorldQuaternion.copy(this._private_parentWorldQuaternion)
|
|
89
|
+
.invert()
|
|
90
|
+
.multiply(this._private_deltaRotation)
|
|
91
|
+
.multiply(this._private_parentWorldQuaternion);
|
|
92
|
+
bone.quaternion.premultiply(this._private_boneWorldQuaternion);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Returns normalized curve (sums to 1).
|
|
97
|
+
* Rebuilds only when the `curve` reference changes.
|
|
98
|
+
*/
|
|
99
|
+
_private_getNormalizedCurve() {
|
|
100
|
+
if (this._private_lastCurveReference === this.curve) {
|
|
101
|
+
return this._private_normalizedCurve;
|
|
102
|
+
}
|
|
103
|
+
if (this.curve.length !== this.bones.length) {
|
|
104
|
+
throw new Error(`AimChainIK: curve length (${this.curve.length}) must match bone count (${this.bones.length}).`);
|
|
105
|
+
}
|
|
106
|
+
let sum = 0;
|
|
107
|
+
for (const value of this.curve) {
|
|
108
|
+
sum += value;
|
|
109
|
+
}
|
|
110
|
+
if (this._private_normalizedCurve.length !== this.curve.length) {
|
|
111
|
+
this._private_normalizedCurve = new Array(this.curve.length);
|
|
112
|
+
}
|
|
113
|
+
if (sum < this.epsilon) {
|
|
114
|
+
const uniform = 1 / this.bones.length;
|
|
115
|
+
for (let index = 0; index < this._private_normalizedCurve.length; index++) {
|
|
116
|
+
this._private_normalizedCurve[index] = uniform;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const inverseSum = 1 / sum;
|
|
121
|
+
for (let index = 0; index < this.curve.length; index++) {
|
|
122
|
+
this._private_normalizedCurve[index] = this.curve[index] * inverseSum;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
this._private_lastCurveReference = this.curve;
|
|
126
|
+
return this._private_normalizedCurve;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { AimChainIK };
|
|
@@ -91,8 +91,7 @@ class TwoBoneIK {
|
|
|
91
91
|
const middlePerpendicular = upperLength * Math.sin(angleRoot);
|
|
92
92
|
this._private_perpendicularDirection.subVectors(this._private_polePosition, this._private_rootPosition);
|
|
93
93
|
this._private_perpendicularDirection.addScaledVector(this._private_chainDirection, -this._private_perpendicularDirection.dot(this._private_chainDirection));
|
|
94
|
-
//
|
|
95
|
-
// Pick an arbitrary axis perpendicular to chain.
|
|
94
|
+
// If the pole is collinear with the chain then we must choose an arbitrary axis perpendicular to the chain.
|
|
96
95
|
if (this._private_perpendicularDirection.lengthSq() < this.epsilon) {
|
|
97
96
|
this._private_perpendicularDirection.set(0, 1, 0);
|
|
98
97
|
this._private_perpendicularDirection.addScaledVector(this._private_chainDirection, -this._private_perpendicularDirection.dot(this._private_chainDirection));
|
|
@@ -121,11 +120,10 @@ class TwoBoneIK {
|
|
|
121
120
|
* which constrains aim direction but not roll.
|
|
122
121
|
*/
|
|
123
122
|
_private_twistBoneTowardPole(bone, child, poleAxis) {
|
|
124
|
-
const epsilon = this.epsilon;
|
|
125
123
|
bone.getWorldPosition(this._private_bonePosition);
|
|
126
124
|
child.getWorldPosition(this._private_childPosition);
|
|
127
125
|
this._private_aimAxis.subVectors(this._private_childPosition, this._private_bonePosition);
|
|
128
|
-
if (this._private_aimAxis.lengthSq() < epsilon) {
|
|
126
|
+
if (this._private_aimAxis.lengthSq() < this.epsilon) {
|
|
129
127
|
return;
|
|
130
128
|
}
|
|
131
129
|
this._private_aimAxis.normalize();
|
|
@@ -133,12 +131,12 @@ class TwoBoneIK {
|
|
|
133
131
|
this._private_currentUp.copy(poleAxis).applyQuaternion(this._private_boneWorldQuaternion);
|
|
134
132
|
this._private_desiredUp.subVectors(this._private_polePosition, this._private_bonePosition);
|
|
135
133
|
this._private_desiredUp.addScaledVector(this._private_aimAxis, -this._private_desiredUp.dot(this._private_aimAxis));
|
|
136
|
-
if (this._private_desiredUp.lengthSq() < epsilon) {
|
|
134
|
+
if (this._private_desiredUp.lengthSq() < this.epsilon) {
|
|
137
135
|
return;
|
|
138
136
|
}
|
|
139
137
|
this._private_desiredUp.normalize();
|
|
140
138
|
this._private_currentUp.addScaledVector(this._private_aimAxis, -this._private_currentUp.dot(this._private_aimAxis));
|
|
141
|
-
if (this._private_currentUp.lengthSq() < epsilon) {
|
|
139
|
+
if (this._private_currentUp.lengthSq() < this.epsilon) {
|
|
142
140
|
console.warn(`TwoBoneIK: poleAxis is parallel to bone's aim axis - twist is undefined.`);
|
|
143
141
|
return;
|
|
144
142
|
}
|
|
@@ -148,7 +146,7 @@ class TwoBoneIK {
|
|
|
148
146
|
if (this._private_currentDirection.dot(this._private_aimAxis) < 0) {
|
|
149
147
|
angle = -angle;
|
|
150
148
|
}
|
|
151
|
-
if (Math.abs(angle) < epsilon) {
|
|
149
|
+
if (Math.abs(angle) < this.epsilon) {
|
|
152
150
|
return;
|
|
153
151
|
}
|
|
154
152
|
this._private_twistRotation.setFromAxisAngle(this._private_aimAxis, angle);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { AimChainIK } from "./ik/AimChainIK";
|
|
2
|
+
export { TwoBoneIK } from "./ik/TwoBoneIK";
|
|
1
3
|
export { InstancedMeshGroup } from "./instancedMeshPool/InstancedMeshGroup";
|
|
2
4
|
export { InstancedMeshInstance } from "./instancedMeshPool/InstancedMeshInstance";
|
|
3
5
|
export { InstancedMeshPool } from "./instancedMeshPool/InstancedMeshPool";
|
|
@@ -13,4 +15,3 @@ export { DualFovCamera } from "./miscellaneous/DualFovCamera";
|
|
|
13
15
|
export { SceneSorter } from "./miscellaneous/SceneSorter";
|
|
14
16
|
export { SceneTraversal } from "./miscellaneous/SceneTraversal";
|
|
15
17
|
export { SkinnedMeshBaker } from "./miscellaneous/SkinnedMeshBaker";
|
|
16
|
-
export { TwoBoneIK } from "./miscellaneous/TwoBoneIK";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { AimChainIK } from './ik/AimChainIK.js';
|
|
2
|
+
export { TwoBoneIK } from './ik/TwoBoneIK.js';
|
|
1
3
|
export { InstancedMeshGroup } from './instancedMeshPool/InstancedMeshGroup.js';
|
|
2
4
|
export { InstancedMeshInstance } from './instancedMeshPool/InstancedMeshInstance.js';
|
|
3
5
|
export { InstancedMeshPool } from './instancedMeshPool/InstancedMeshPool.js';
|
|
@@ -13,4 +15,3 @@ export { DualFovCamera } from './miscellaneous/DualFovCamera.js';
|
|
|
13
15
|
export { SceneSorter } from './miscellaneous/SceneSorter.js';
|
|
14
16
|
export { SceneTraversal } from './miscellaneous/SceneTraversal.js';
|
|
15
17
|
export { SkinnedMeshBaker } from './miscellaneous/SkinnedMeshBaker.js';
|
|
16
|
-
export { TwoBoneIK } from './miscellaneous/TwoBoneIK.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BufferGeometry, Material } from "three";
|
|
1
|
+
import type { BufferGeometry, InstancedMesh, Material } from "three";
|
|
2
2
|
import { Matrix4, Quaternion, Vector3 } from "three";
|
|
3
3
|
import type { InstancedMeshPool } from "./InstancedMeshPool";
|
|
4
4
|
export declare class InstancedMeshInstance {
|
|
@@ -12,6 +12,7 @@ export declare class InstancedMeshInstance {
|
|
|
12
12
|
private needsUpdateInstancedMatrixFromLocalMatrix;
|
|
13
13
|
private handler;
|
|
14
14
|
constructor(pool: InstancedMeshPool, geometry: BufferGeometry, material: Material, tag?: string);
|
|
15
|
+
static fromInstancedMesh(pool: InstancedMeshPool, mesh: InstancedMesh<BufferGeometry, Material>, tag?: string): InstancedMeshInstance[];
|
|
15
16
|
destroy(): void;
|
|
16
17
|
isDestroyed(): boolean;
|
|
17
18
|
setPosition(source: Vector3, flushTransform?: boolean): this;
|
|
@@ -12,6 +12,17 @@ class InstancedMeshInstance {
|
|
|
12
12
|
this._private_needsUpdateInstancedMatrixFromLocalMatrix = false;
|
|
13
13
|
this._private_handler = this._private_pool.allocate(geometry, material, tag);
|
|
14
14
|
}
|
|
15
|
+
static fromInstancedMesh(pool, mesh, tag = "") {
|
|
16
|
+
const matrix = new Matrix4();
|
|
17
|
+
const instances = [];
|
|
18
|
+
for (let i = 0; i < mesh.count; i++) {
|
|
19
|
+
const instance = new InstancedMeshInstance(pool, mesh.geometry, mesh.material, tag);
|
|
20
|
+
mesh.getMatrixAt(i, matrix);
|
|
21
|
+
instance.setTransform(matrix, true);
|
|
22
|
+
instances.push(instance);
|
|
23
|
+
}
|
|
24
|
+
return instances;
|
|
25
|
+
}
|
|
15
26
|
destroy() {
|
|
16
27
|
if (this._private_handler >= 0) {
|
|
17
28
|
this._private_pool.deallocate(this._private_handler);
|
|
@@ -2,8 +2,7 @@ import type { AnimationClip, Object3D, SkinnedMesh } from "three";
|
|
|
2
2
|
import { Mesh } from "three";
|
|
3
3
|
export declare class SkinnedMeshBaker {
|
|
4
4
|
/**
|
|
5
|
-
* Does not call
|
|
6
|
-
* The returned mesh shares the original material (not cloned).
|
|
5
|
+
* Does not call the skeleton update, assuming it is already in a state ready for baking.
|
|
7
6
|
*/
|
|
8
7
|
static bakePose(skinnedMesh: SkinnedMesh): Mesh;
|
|
9
8
|
/**
|
|
@@ -3,8 +3,7 @@ import { Vector3, BufferAttribute, Mesh, AnimationMixer } from 'three';
|
|
|
3
3
|
const COMPONENT_COUNT = 3;
|
|
4
4
|
class SkinnedMeshBaker {
|
|
5
5
|
/**
|
|
6
|
-
* Does not call
|
|
7
|
-
* The returned mesh shares the original material (not cloned).
|
|
6
|
+
* Does not call the skeleton update, assuming it is already in a state ready for baking.
|
|
8
7
|
*/
|
|
9
8
|
static bakePose(skinnedMesh) {
|
|
10
9
|
const bakedGeometry = skinnedMesh.geometry.clone();
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-zoo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Some reusable bits for building things with Three.js
|
|
3
|
+
"version": "0.13.0",
|
|
4
|
+
"description": "Some reusable bits for building things with Three.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"three.js",
|
|
7
7
|
"typescript",
|
|
8
|
-
"3d"
|
|
8
|
+
"3d",
|
|
9
|
+
"webgl",
|
|
10
|
+
"ik",
|
|
11
|
+
"inverse-kinematics",
|
|
12
|
+
"instanced-mesh",
|
|
13
|
+
"material",
|
|
14
|
+
"scene",
|
|
15
|
+
"lighting"
|
|
9
16
|
],
|
|
10
17
|
"author": "jango",
|
|
11
18
|
"license": "MIT",
|
|
File without changes
|