threejs-cannones-rigger 1.0.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/LICENSE +21 -0
- package/dist/threejs-cannones-rigger.cjs +1 -0
- package/dist/threejs-cannones-rigger.d.ts +212 -0
- package/dist/threejs-cannones-rigger.js +365 -0
- package/package.json +44 -0
- package/readme.md +186 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Pablo Bandinopla (https://x.com/bandinopla)
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("cannon-es"),i=require("three"),B=require("threejs-cannones-tube");function b(h){return h.reduce((n,t,e)=>n|t<<e,0)}const w=new i.Vector3,y=new i.Vector3,Q=new i.Quaternion,j=new i.Quaternion;class D{constructor(n,t){this.world=n,t&&this.rigScene(t)}obj2bod=new Map;constraints=[];customId2ConstraintFactory=new Map;customConstraints=new Map;registerCustomConstraint(n,t){this.customId2ConstraintFactory.set(n,t)}getConstraintByName(n){return this.constraints.find(t=>t.name==n)}getCustomConstraint(n){return this.customConstraints.get(n)}getCableConstraint(n){return this.getConstraintByName(n)}getSyncConstraint(n){return this.getConstraintByName(n)}getLockConstraint(n){return this.getConstraintByName(n)?.cannonConstraint}getHingeConstraint(n){return this.getConstraintByName(n)?.cannonConstraint}getPointConstraint(n){return this.getConstraintByName(n)?.cannonConstraint}getDistanceConstraint(n){return this.getConstraintByName(n)?.cannonConstraint}getBodyByName(n){for(const[t,e]of this.obj2bod.entries())if(t.userData.name===n)return e}rigScene(n){n.traverse(t=>{let e;t.userData.threejscannones_cgroup&&(t.userData.threejscannones_cgroup=b(t.userData.threejscannones_cgroup)),t.userData.threejscannones_cwith&&(t.userData.threejscannones_cwith=b(t.userData.threejscannones_cwith)),t.userData.threejscannones_type==1?e=this.createCollider(new c.Box(new c.Vec3(t.scale.x,t.scale.y,t.scale.z)),t):t.userData.threejscannones_type==2?e=this.createCollider(new c.Sphere(t.scale.x),t):t.userData.threejscannones_type==3&&(e=this.createCollider(void 0,t),this.addCompoundShapes(e,t))}),n.traverse(t=>{const e=this.getBodyByName(t.userData.threejscannones_A?.name),o=this.getBodyByName(t.userData.threejscannones_B?.name);let s;switch(t.userData.threejscannones_type){case 7:s=this.createDistanceConstraint(e,o);break;case 6:s=this.createPointConstraint(e,o,t);break;case 5:s=this.createHingeConstraint(e,o,t);break;case 4:s=this.createLockConstraint(e,o);break;case 8:const a=this.getBodyByName(t.userData.threejscannones_syncSource?.name);this.createSyncBetween(t,a);break;case 9:this.createCable(t,e,o);break;case 10:this.createCustomConstraint(t,e,o)}s&&this.constraints.push(new C(t,s))})}clear(){for(w.set(0,0,0),y.set(0,0,0),Q.set(0,0,0,0),j.set(0,0,0,0);this.constraints.length;)this.constraints.pop().removeFrom(this.world);for(const[n,t]of this.obj2bod.entries())this.world.removeBody(t);this.obj2bod.clear()}addCompoundShapes(n,t){const e=t.children;t.updateMatrixWorld();const o=t.getWorldPosition(new i.Vector3),s=t.getWorldQuaternion(new i.Quaternion),a=s.clone().invert();for(const r of e){r.updateMatrixWorld();const u=r.getWorldPosition(new i.Vector3),m=r.getWorldQuaternion(new i.Quaternion),d=r.getWorldScale(new i.Vector3),p=u.clone().sub(o).applyQuaternion(a),l=a.clone().multiply(m),T=new c.Vec3(p.x,p.y,p.z),k=new c.Quaternion(l.x,l.y,l.z,l.w),v=new c.Vec3(d.x,d.y,d.z),x=new c.Box(v);n.addShape(x,T,k)}return n.position.copy(new c.Vec3(o.x,o.y,o.z)),n.quaternion.copy(new c.Quaternion(s.x,s.y,s.z,s.w)),n}createCustomConstraint(n,t,e){let o=n.userData.threejscannones_cgroup??1,s=n.userData.threejscannones_cwith??1;const a=n.userData.threejscannones_customId;if(!a)throw new Error("A custom constraint MUST have an id...");const r=this.customId2ConstraintFactory.get(a);if(!r)throw new Error(`Custom constraint with id ${a} not found. Dis you forgot to call registerCustomConstraint?`);const u=r({obj:n,collisionGroup:o,collisionMask:s,A:t,B:e});this.customConstraints.set(n.userData.name,u)}createCable(n,t,e){let o=1,s=!1,a=new i.Vector3;n.children.length>1&&(n.children[0].getWorldPosition(w),n.children[1].localToWorld(y),o=w.distanceTo(y),s=!0,a.copy(y));const r=new B.CannonTubeRig(o,20,.1,8);r.material=n instanceof i.Mesh?n.material:new i.MeshNormalMaterial,n.parent?.add(r),s&&(r.position.copy(w),r.lookAt(y)),r.addToPhysicalWorld(this.world),r.syncRig(),this.constraints.push(new g(this.world,n,r,t,e))}createSyncBetween(n,t){if(!t){console.warn(`Object ${n.name} points to a non existent collider for it's sync constrain.`);return}const e=new f(n,t);this.constraints.push(e)}createLockConstraint(n,t){const e=new c.LockConstraint(n,t);return e.collideConnected=!1,this.world.addConstraint(e),e}createHingeConstraint(n,t,e){const o=e.getWorldPosition(new i.Vector3),s=new i.Vector3(0,1,0).applyQuaternion(e.getWorldQuaternion(new i.Quaternion)).normalize(),a=new c.Vec3(o.x,o.y,o.z),r=new c.Vec3(s.x,s.y,s.z),u=n.pointToLocalFrame(a),m=t.pointToLocalFrame(a),d=n.vectorToLocalFrame(r),p=t.vectorToLocalFrame(r),l=new c.HingeConstraint(n,t,{pivotA:u,pivotB:m,axisA:d,axisB:p,collideConnected:!1});return this.world.addConstraint(l),l}createPointConstraint(n,t,e){const o=e.getWorldPosition(new i.Vector3),s=n.pointToLocalFrame(new c.Vec3(o.x,o.y,o.z)),a=t.pointToLocalFrame(new c.Vec3(o.x,o.y,o.z)),r=new c.PointToPointConstraint(n,s,t,a);return this.world.addConstraint(r),r}createDistanceConstraint(n,t){const e=new c.DistanceConstraint(n,t);return this.world.addConstraint(e),e}createCollider(n,t){t.visible=!1;let e=t.userData.threejscannones_cgroup??1,o=t.userData.threejscannones_cwith??1;const s=new c.Body({shape:n,mass:t.userData.threejscannones_mass??0,collisionFilterMask:o,collisionFilterGroup:e}),a=t.getWorldPosition(new i.Vector3);s.position.set(a.x,a.y,a.z);const r=t.getWorldQuaternion(new i.Quaternion);return s.quaternion.set(r.x,r.y,r.z,r.w),this.world.addBody(s),this.obj2bod.set(t,s),s}update(n){for(let t=0;t<this.constraints.length;t++)this.constraints[t].update(n)}}class C{constructor(n,t){this.obj=n,this.cannonConstraint=t}enable(){this.cannonConstraint?.enable()}disable(){this.cannonConstraint?.disable()}update(n){}get name(){return this.obj.userData.name}removeFrom(n){this.cannonConstraint&&n.removeConstraint(this.cannonConstraint)}}class f extends C{constructor(n,t){super(n),this.body=t}offsetPos=new i.Vector3;offsetQuat=new i.Quaternion;hasInit=!1;initOffsets(){const n=new i.Vector3(this.body.position.x,this.body.position.y,this.body.position.z),t=new i.Quaternion(this.body.quaternion.x,this.body.quaternion.y,this.body.quaternion.z,this.body.quaternion.w),e=new i.Vector3,o=new i.Quaternion;this.obj.getWorldPosition(e),this.obj.getWorldQuaternion(o),this.offsetPos.copy(e).sub(n).applyQuaternion(t.clone().invert()),this.offsetQuat.copy(t.clone().invert().multiply(o)),this.hasInit=!0}update(){this.hasInit||this.initOffsets();const n=new i.Vector3(this.body.position.x,this.body.position.y,this.body.position.z),t=new i.Quaternion(this.body.quaternion.x,this.body.quaternion.y,this.body.quaternion.z,this.body.quaternion.w),e=t.clone().multiply(this.offsetQuat),o=this.offsetPos.clone().applyQuaternion(t).add(n);if(this.obj.parent){this.obj.parent.updateMatrixWorld(!0);const s=new i.Matrix4().compose(o,e,new i.Vector3(1,1,1)),a=new i.Matrix4().copy(this.obj.parent.matrixWorld).invert();s.premultiply(a),s.decompose(this.obj.position,this.obj.quaternion,new i.Vector3)}else this.obj.position.copy(o),this.obj.quaternion.copy(e)}}class g extends C{constructor(n,t,e,o,s){super(t),this.world=n,this.cable=e,o&&this.lockHeadTo(o),s&&this.lockTailTo(s)}lockToA;lockToB;lockXTo(n,t,e){let o;return n&&this.world.removeConstraint(n),e&&(o=new c.PointToPointConstraint(t,new c.Vec3,e,e.pointToLocalFrame(t.position)),o.collideConnected=!1,this.world.addConstraint(o)),o}lockHeadTo(n){this.lockToA=this.lockXTo(this.lockToA,this.cable.head,n)}lockTailTo(n){this.lockToB=this.lockXTo(this.lockToB,this.cable.tail,n)}enable(){this.lockToA?.enable(),this.lockToB?.enable(),this.cable.constraints.forEach(n=>n.enable())}disable(){this.lockToA?.disable(),this.lockToB?.disable(),this.cable.constraints.forEach(n=>n.disable())}update(){this.cable.syncRig()}removeFrom(n){this.lockToA&&(n.removeConstraint(this.lockToA),this.lockToA=void 0),this.lockToB&&(n.removeConstraint(this.lockToB),this.lockToA=void 0),this.cable.removeFromPhysicalWorld(n),super.removeFrom(n)}}exports.CableConstraint=g;exports.SyncConstraint=f;exports.ThreeJsCannonEsConstraint=C;exports.ThreeJsCannonEsSceneRigger=D;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { Body, Constraint, DistanceConstraint, HingeConstraint, LockConstraint, PointToPointConstraint, World } from 'cannon-es';
|
|
2
|
+
import { Object3D } from 'three';
|
|
3
|
+
import { CannonTubeRig } from 'threejs-cannones-tube';
|
|
4
|
+
export type CustomContraintConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* The object that defined this custom constraint
|
|
7
|
+
*/
|
|
8
|
+
obj: Object3D;
|
|
9
|
+
/**
|
|
10
|
+
* If set or useful, this is the group mask assosiated with this obj...
|
|
11
|
+
*/
|
|
12
|
+
collisionGroup?: number;
|
|
13
|
+
/**
|
|
14
|
+
* If set or useful, this is the collision mask assosiated with this obj...
|
|
15
|
+
*/
|
|
16
|
+
collisionMask?: number;
|
|
17
|
+
/**
|
|
18
|
+
* If set or useful, this is referencing a Body
|
|
19
|
+
*/
|
|
20
|
+
A?: Body;
|
|
21
|
+
/**
|
|
22
|
+
* If set or useful, this is referencing a Body
|
|
23
|
+
*/
|
|
24
|
+
B?: Body;
|
|
25
|
+
};
|
|
26
|
+
export type ConstraintFactory = (config: CustomContraintConfig) => IConstraint;
|
|
27
|
+
export declare class ThreeJsCannonEsSceneRigger {
|
|
28
|
+
readonly world: World;
|
|
29
|
+
/**
|
|
30
|
+
* Keeps a relation between the object3D that defined the collider and the collider itself.
|
|
31
|
+
*/
|
|
32
|
+
private obj2bod;
|
|
33
|
+
private constraints;
|
|
34
|
+
private customId2ConstraintFactory;
|
|
35
|
+
private customConstraints;
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new ThreeJsCannonEsSceneRigger.
|
|
38
|
+
* @param world The Cannon-es physics world.
|
|
39
|
+
* @param scene (Optional) The Three.js scene or root object to rig immediately.
|
|
40
|
+
*/
|
|
41
|
+
constructor(world: World, scene?: Object3D);
|
|
42
|
+
/**
|
|
43
|
+
* You can create a custom constrain using this method. So when you select "Custom Constrain" in blender, this function
|
|
44
|
+
* will be called when parsing that constraint allowing you to extend the functionality to suit your needs.
|
|
45
|
+
*
|
|
46
|
+
* @param id what you type in "custom ID" on blender side...
|
|
47
|
+
* @param creator the function that creates your custom constraint
|
|
48
|
+
*/
|
|
49
|
+
registerCustomConstraint(id: string, creator: ConstraintFactory): void;
|
|
50
|
+
/**
|
|
51
|
+
* Returns a constraint by its name.
|
|
52
|
+
* @param name The name of the constraint.
|
|
53
|
+
* @returns The constraint instance, or undefined if not found.
|
|
54
|
+
*/
|
|
55
|
+
private getConstraintByName;
|
|
56
|
+
getCustomConstraint(name: string): IConstraint | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Returns a CableConstraint by name.
|
|
59
|
+
* @param name The name of the constraint.
|
|
60
|
+
*/
|
|
61
|
+
getCableConstraint(name: string): CableConstraint | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Returns a SyncConstraint by name.
|
|
64
|
+
* @param name The name of the constraint.
|
|
65
|
+
*/
|
|
66
|
+
getSyncConstraint(name: string): SyncConstraint | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Returns a LockConstraint (Cannon-es) by name.
|
|
69
|
+
* @param name The name of the constraint.
|
|
70
|
+
*/
|
|
71
|
+
getLockConstraint(name: string): LockConstraint | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Returns a HingeConstraint (Cannon-es) by name.
|
|
74
|
+
* @param name The name of the constraint.
|
|
75
|
+
*/
|
|
76
|
+
getHingeConstraint(name: string): HingeConstraint | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* Returns a PointToPointConstraint (Cannon-es) by name.
|
|
79
|
+
* @param name The name of the constraint.
|
|
80
|
+
*/
|
|
81
|
+
getPointConstraint(name: string): PointToPointConstraint | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Returns a DistanceConstraint (Cannon-es) by name.
|
|
84
|
+
* @param name The name of the constraint.
|
|
85
|
+
*/
|
|
86
|
+
getDistanceConstraint(name: string): DistanceConstraint | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Returns a Cannon body by the name of its associated Three.js object.
|
|
89
|
+
* @param name The name to search for (from userData.name).
|
|
90
|
+
* @returns The Cannon body, or undefined if not found.
|
|
91
|
+
*/
|
|
92
|
+
getBodyByName(name: string): Body | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* Scans the scene and creates physics bodies and constraints based on object userData.
|
|
95
|
+
* @param scene The Three.js scene or root object to rig.
|
|
96
|
+
*/
|
|
97
|
+
rigScene(scene: Object3D): void;
|
|
98
|
+
/**
|
|
99
|
+
* Removes all (known) created bodies and constraints from the world and clears internal state.
|
|
100
|
+
*/
|
|
101
|
+
clear(): void;
|
|
102
|
+
private addCompoundShapes;
|
|
103
|
+
private createCustomConstraint;
|
|
104
|
+
/**
|
|
105
|
+
* Creates a cable constraint between objects.
|
|
106
|
+
* @param obj The Three.js object representing the cable.
|
|
107
|
+
* @param stickToA (Optional) The body to attach the cable head to.
|
|
108
|
+
* @param stickToB (Optional) The body to attach the cable tail to.
|
|
109
|
+
*/
|
|
110
|
+
private createCable;
|
|
111
|
+
/**
|
|
112
|
+
* Synchronizes a Three.js object with a Cannon body.
|
|
113
|
+
* @param obj The Three.js object to sync.
|
|
114
|
+
* @param body The Cannon body to sync with.
|
|
115
|
+
*/
|
|
116
|
+
private createSyncBetween;
|
|
117
|
+
/**
|
|
118
|
+
* Creates a lock constraint between two bodies.
|
|
119
|
+
* @param a The first body.
|
|
120
|
+
* @param b The second body.
|
|
121
|
+
* @returns The created LockConstraint.
|
|
122
|
+
*/
|
|
123
|
+
private createLockConstraint;
|
|
124
|
+
/**
|
|
125
|
+
* Creates a hinge constraint between two bodies.
|
|
126
|
+
* @param a The first body.
|
|
127
|
+
* @param b The second body.
|
|
128
|
+
* @param o The Three.js object representing the hinge.
|
|
129
|
+
* @returns The created HingeConstraint.
|
|
130
|
+
*/
|
|
131
|
+
private createHingeConstraint;
|
|
132
|
+
/**
|
|
133
|
+
* Creates a point-to-point constraint between two bodies.
|
|
134
|
+
* @param a The first body.
|
|
135
|
+
* @param b The second body.
|
|
136
|
+
* @param o The Three.js object representing the constraint.
|
|
137
|
+
* @returns The created PointToPointConstraint.
|
|
138
|
+
*/
|
|
139
|
+
private createPointConstraint;
|
|
140
|
+
/**
|
|
141
|
+
* Creates a distance constraint between two bodies.
|
|
142
|
+
* @param a The first body.
|
|
143
|
+
* @param b The second body.
|
|
144
|
+
* @returns The created DistanceConstraint.
|
|
145
|
+
*/
|
|
146
|
+
private createDistanceConstraint;
|
|
147
|
+
/**
|
|
148
|
+
* Creates a Cannon body for a given Three.js object and shape.
|
|
149
|
+
* @param shape The Cannon-es shape.
|
|
150
|
+
* @param o The Three.js object.
|
|
151
|
+
* @returns The created Cannon body.
|
|
152
|
+
*/
|
|
153
|
+
private createCollider;
|
|
154
|
+
/**
|
|
155
|
+
* Updates all constraints. Should be called every frame.
|
|
156
|
+
* @param delta The time step.
|
|
157
|
+
*/
|
|
158
|
+
update(delta: number): void;
|
|
159
|
+
}
|
|
160
|
+
export interface IConstraint {
|
|
161
|
+
enable: VoidFunction;
|
|
162
|
+
disable: VoidFunction;
|
|
163
|
+
update: (delta: number) => void;
|
|
164
|
+
removeFrom: (world: World) => void;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Base class for all ThreeJsCannonEs constraints.
|
|
168
|
+
*/
|
|
169
|
+
export declare class ThreeJsCannonEsConstraint implements IConstraint {
|
|
170
|
+
readonly obj: Object3D;
|
|
171
|
+
readonly cannonConstraint?: Constraint | undefined;
|
|
172
|
+
enable(): void;
|
|
173
|
+
disable(): void;
|
|
174
|
+
update(_delta: number): void;
|
|
175
|
+
constructor(obj: Object3D, cannonConstraint?: Constraint | undefined);
|
|
176
|
+
get name(): string;
|
|
177
|
+
removeFrom(world: World): void;
|
|
178
|
+
}
|
|
179
|
+
export declare class SyncConstraint extends ThreeJsCannonEsConstraint {
|
|
180
|
+
readonly body: Body;
|
|
181
|
+
private offsetPos;
|
|
182
|
+
private offsetQuat;
|
|
183
|
+
private hasInit;
|
|
184
|
+
constructor(obj: Object3D, body: Body);
|
|
185
|
+
private initOffsets;
|
|
186
|
+
update(): void;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Creates a cable ( a skinned mesh with a chain on bones that copies a chain of connected cannon-es bodies connected via point to point contraint )
|
|
190
|
+
*/
|
|
191
|
+
export declare class CableConstraint extends ThreeJsCannonEsConstraint {
|
|
192
|
+
readonly world: World;
|
|
193
|
+
readonly cable: CannonTubeRig;
|
|
194
|
+
private lockToA;
|
|
195
|
+
private lockToB;
|
|
196
|
+
constructor(world: World, obj: Object3D, cable: CannonTubeRig, stickToA?: Body, stickToB?: Body);
|
|
197
|
+
private lockXTo;
|
|
198
|
+
/**
|
|
199
|
+
* Locks or Releases the head to or from a body. `PointToPointConstraint` will be used to lock.
|
|
200
|
+
* @param body
|
|
201
|
+
*/
|
|
202
|
+
lockHeadTo(body: Body | undefined): void;
|
|
203
|
+
/**
|
|
204
|
+
* Locks or Releases the tail to or from a body. `PointToPointConstraint` will be used to lock.
|
|
205
|
+
* @param body
|
|
206
|
+
*/
|
|
207
|
+
lockTailTo(body: Body | undefined): void;
|
|
208
|
+
enable(): void;
|
|
209
|
+
disable(): void;
|
|
210
|
+
update(): void;
|
|
211
|
+
removeFrom(world: World): void;
|
|
212
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { Box as f, Vec3 as l, Sphere as W, Quaternion as g, LockConstraint as P, HingeConstraint as Q, PointToPointConstraint as v, DistanceConstraint as _, Body as A } from "cannon-es";
|
|
2
|
+
import { Vector3 as r, Quaternion as c, MeshNormalMaterial as z, Mesh as F, Matrix4 as T } from "three";
|
|
3
|
+
import { CannonTubeRig as S } from "threejs-cannones-tube";
|
|
4
|
+
function k(d) {
|
|
5
|
+
return d.reduce((n, t, e) => n | t << e, 0);
|
|
6
|
+
}
|
|
7
|
+
const m = new r(), y = new r(), M = new c(), q = new c();
|
|
8
|
+
class O {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new ThreeJsCannonEsSceneRigger.
|
|
11
|
+
* @param world The Cannon-es physics world.
|
|
12
|
+
* @param scene (Optional) The Three.js scene or root object to rig immediately.
|
|
13
|
+
*/
|
|
14
|
+
constructor(n, t) {
|
|
15
|
+
this.world = n, t && this.rigScene(t);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Keeps a relation between the object3D that defined the collider and the collider itself.
|
|
19
|
+
*/
|
|
20
|
+
obj2bod = /* @__PURE__ */ new Map();
|
|
21
|
+
constraints = [];
|
|
22
|
+
customId2ConstraintFactory = /* @__PURE__ */ new Map();
|
|
23
|
+
customConstraints = /* @__PURE__ */ new Map();
|
|
24
|
+
/**
|
|
25
|
+
* You can create a custom constrain using this method. So when you select "Custom Constrain" in blender, this function
|
|
26
|
+
* will be called when parsing that constraint allowing you to extend the functionality to suit your needs.
|
|
27
|
+
*
|
|
28
|
+
* @param id what you type in "custom ID" on blender side...
|
|
29
|
+
* @param creator the function that creates your custom constraint
|
|
30
|
+
*/
|
|
31
|
+
registerCustomConstraint(n, t) {
|
|
32
|
+
this.customId2ConstraintFactory.set(n, t);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns a constraint by its name.
|
|
36
|
+
* @param name The name of the constraint.
|
|
37
|
+
* @returns The constraint instance, or undefined if not found.
|
|
38
|
+
*/
|
|
39
|
+
getConstraintByName(n) {
|
|
40
|
+
return this.constraints.find((t) => t.name == n);
|
|
41
|
+
}
|
|
42
|
+
getCustomConstraint(n) {
|
|
43
|
+
return this.customConstraints.get(n);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns a CableConstraint by name.
|
|
47
|
+
* @param name The name of the constraint.
|
|
48
|
+
*/
|
|
49
|
+
getCableConstraint(n) {
|
|
50
|
+
return this.getConstraintByName(n);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Returns a SyncConstraint by name.
|
|
54
|
+
* @param name The name of the constraint.
|
|
55
|
+
*/
|
|
56
|
+
getSyncConstraint(n) {
|
|
57
|
+
return this.getConstraintByName(n);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns a LockConstraint (Cannon-es) by name.
|
|
61
|
+
* @param name The name of the constraint.
|
|
62
|
+
*/
|
|
63
|
+
getLockConstraint(n) {
|
|
64
|
+
return this.getConstraintByName(n)?.cannonConstraint;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns a HingeConstraint (Cannon-es) by name.
|
|
68
|
+
* @param name The name of the constraint.
|
|
69
|
+
*/
|
|
70
|
+
getHingeConstraint(n) {
|
|
71
|
+
return this.getConstraintByName(n)?.cannonConstraint;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns a PointToPointConstraint (Cannon-es) by name.
|
|
75
|
+
* @param name The name of the constraint.
|
|
76
|
+
*/
|
|
77
|
+
getPointConstraint(n) {
|
|
78
|
+
return this.getConstraintByName(n)?.cannonConstraint;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Returns a DistanceConstraint (Cannon-es) by name.
|
|
82
|
+
* @param name The name of the constraint.
|
|
83
|
+
*/
|
|
84
|
+
getDistanceConstraint(n) {
|
|
85
|
+
return this.getConstraintByName(n)?.cannonConstraint;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns a Cannon body by the name of its associated Three.js object.
|
|
89
|
+
* @param name The name to search for (from userData.name).
|
|
90
|
+
* @returns The Cannon body, or undefined if not found.
|
|
91
|
+
*/
|
|
92
|
+
getBodyByName(n) {
|
|
93
|
+
for (const [t, e] of this.obj2bod.entries())
|
|
94
|
+
if (t.userData.name === n) return e;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Scans the scene and creates physics bodies and constraints based on object userData.
|
|
98
|
+
* @param scene The Three.js scene or root object to rig.
|
|
99
|
+
*/
|
|
100
|
+
rigScene(n) {
|
|
101
|
+
n.traverse((t) => {
|
|
102
|
+
let e;
|
|
103
|
+
t.userData.threejscannones_cgroup && (t.userData.threejscannones_cgroup = k(t.userData.threejscannones_cgroup)), t.userData.threejscannones_cwith && (t.userData.threejscannones_cwith = k(t.userData.threejscannones_cwith)), t.userData.threejscannones_type == 1 ? e = this.createCollider(new f(new l(t.scale.x, t.scale.y, t.scale.z)), t) : t.userData.threejscannones_type == 2 ? e = this.createCollider(new W(t.scale.x), t) : t.userData.threejscannones_type == 3 && (e = this.createCollider(void 0, t), this.addCompoundShapes(e, t));
|
|
104
|
+
}), n.traverse((t) => {
|
|
105
|
+
const e = this.getBodyByName(t.userData.threejscannones_A?.name), o = this.getBodyByName(t.userData.threejscannones_B?.name);
|
|
106
|
+
let s;
|
|
107
|
+
switch (t.userData.threejscannones_type) {
|
|
108
|
+
case 7:
|
|
109
|
+
s = this.createDistanceConstraint(e, o);
|
|
110
|
+
break;
|
|
111
|
+
case 6:
|
|
112
|
+
s = this.createPointConstraint(e, o, t);
|
|
113
|
+
break;
|
|
114
|
+
case 5:
|
|
115
|
+
s = this.createHingeConstraint(e, o, t);
|
|
116
|
+
break;
|
|
117
|
+
case 4:
|
|
118
|
+
s = this.createLockConstraint(e, o);
|
|
119
|
+
break;
|
|
120
|
+
case 8:
|
|
121
|
+
const a = this.getBodyByName(t.userData.threejscannones_syncSource?.name);
|
|
122
|
+
this.createSyncBetween(t, a);
|
|
123
|
+
break;
|
|
124
|
+
case 9:
|
|
125
|
+
this.createCable(t, e, o);
|
|
126
|
+
break;
|
|
127
|
+
case 10:
|
|
128
|
+
this.createCustomConstraint(t, e, o);
|
|
129
|
+
}
|
|
130
|
+
s && this.constraints.push(new b(t, s));
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Removes all (known) created bodies and constraints from the world and clears internal state.
|
|
135
|
+
*/
|
|
136
|
+
clear() {
|
|
137
|
+
for (m.set(0, 0, 0), y.set(0, 0, 0), M.set(0, 0, 0, 0), q.set(0, 0, 0, 0); this.constraints.length; )
|
|
138
|
+
this.constraints.pop().removeFrom(this.world);
|
|
139
|
+
for (const [n, t] of this.obj2bod.entries())
|
|
140
|
+
this.world.removeBody(t);
|
|
141
|
+
this.obj2bod.clear();
|
|
142
|
+
}
|
|
143
|
+
addCompoundShapes(n, t) {
|
|
144
|
+
const e = t.children;
|
|
145
|
+
t.updateMatrixWorld();
|
|
146
|
+
const o = t.getWorldPosition(new r()), s = t.getWorldQuaternion(new c()), a = s.clone().invert();
|
|
147
|
+
for (const i of e) {
|
|
148
|
+
i.updateMatrixWorld();
|
|
149
|
+
const u = i.getWorldPosition(new r()), C = i.getWorldQuaternion(new c()), p = i.getWorldScale(new r()), w = u.clone().sub(o).applyQuaternion(a), h = a.clone().multiply(C), x = new l(w.x, w.y, w.z), B = new g(h.x, h.y, h.z, h.w), j = new l(p.x, p.y, p.z), D = new f(j);
|
|
150
|
+
n.addShape(D, x, B);
|
|
151
|
+
}
|
|
152
|
+
return n.position.copy(new l(o.x, o.y, o.z)), n.quaternion.copy(new g(s.x, s.y, s.z, s.w)), n;
|
|
153
|
+
}
|
|
154
|
+
createCustomConstraint(n, t, e) {
|
|
155
|
+
let o = n.userData.threejscannones_cgroup ?? 1, s = n.userData.threejscannones_cwith ?? 1;
|
|
156
|
+
const a = n.userData.threejscannones_customId;
|
|
157
|
+
if (!a)
|
|
158
|
+
throw new Error("A custom constraint MUST have an id...");
|
|
159
|
+
const i = this.customId2ConstraintFactory.get(a);
|
|
160
|
+
if (!i)
|
|
161
|
+
throw new Error(`Custom constraint with id ${a} not found. Dis you forgot to call registerCustomConstraint?`);
|
|
162
|
+
const u = i({
|
|
163
|
+
obj: n,
|
|
164
|
+
collisionGroup: o,
|
|
165
|
+
collisionMask: s,
|
|
166
|
+
A: t,
|
|
167
|
+
B: e
|
|
168
|
+
});
|
|
169
|
+
this.customConstraints.set(n.userData.name, u);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Creates a cable constraint between objects.
|
|
173
|
+
* @param obj The Three.js object representing the cable.
|
|
174
|
+
* @param stickToA (Optional) The body to attach the cable head to.
|
|
175
|
+
* @param stickToB (Optional) The body to attach the cable tail to.
|
|
176
|
+
*/
|
|
177
|
+
createCable(n, t, e) {
|
|
178
|
+
let o = 1, s = !1, a = new r();
|
|
179
|
+
n.children.length > 1 && (n.children[0].getWorldPosition(m), n.children[1].localToWorld(y), o = m.distanceTo(y), s = !0, a.copy(y));
|
|
180
|
+
const i = new S(
|
|
181
|
+
o,
|
|
182
|
+
// length in world units
|
|
183
|
+
20,
|
|
184
|
+
// resolution along the segment's length
|
|
185
|
+
0.1,
|
|
186
|
+
// radius of the tube
|
|
187
|
+
8
|
|
188
|
+
// resolution along the radius
|
|
189
|
+
);
|
|
190
|
+
i.material = n instanceof F ? n.material : new z(), n.parent?.add(i), s && (i.position.copy(m), i.lookAt(y)), i.addToPhysicalWorld(this.world), i.syncRig(), this.constraints.push(new N(this.world, n, i, t, e));
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Synchronizes a Three.js object with a Cannon body.
|
|
194
|
+
* @param obj The Three.js object to sync.
|
|
195
|
+
* @param body The Cannon body to sync with.
|
|
196
|
+
*/
|
|
197
|
+
createSyncBetween(n, t) {
|
|
198
|
+
if (!t) {
|
|
199
|
+
console.warn(`Object ${n.name} points to a non existent collider for it's sync constrain.`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const e = new L(n, t);
|
|
203
|
+
this.constraints.push(e);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Creates a lock constraint between two bodies.
|
|
207
|
+
* @param a The first body.
|
|
208
|
+
* @param b The second body.
|
|
209
|
+
* @returns The created LockConstraint.
|
|
210
|
+
*/
|
|
211
|
+
createLockConstraint(n, t) {
|
|
212
|
+
const e = new P(n, t);
|
|
213
|
+
return e.collideConnected = !1, this.world.addConstraint(e), e;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Creates a hinge constraint between two bodies.
|
|
217
|
+
* @param a The first body.
|
|
218
|
+
* @param b The second body.
|
|
219
|
+
* @param o The Three.js object representing the hinge.
|
|
220
|
+
* @returns The created HingeConstraint.
|
|
221
|
+
*/
|
|
222
|
+
createHingeConstraint(n, t, e) {
|
|
223
|
+
const o = e.getWorldPosition(new r()), s = new r(0, 1, 0).applyQuaternion(e.getWorldQuaternion(new c())).normalize(), a = new l(o.x, o.y, o.z), i = new l(s.x, s.y, s.z), u = n.pointToLocalFrame(a), C = t.pointToLocalFrame(a), p = n.vectorToLocalFrame(i), w = t.vectorToLocalFrame(i), h = new Q(n, t, {
|
|
224
|
+
pivotA: u,
|
|
225
|
+
pivotB: C,
|
|
226
|
+
axisA: p,
|
|
227
|
+
axisB: w,
|
|
228
|
+
collideConnected: !1
|
|
229
|
+
//maxForce: 1e9,
|
|
230
|
+
});
|
|
231
|
+
return this.world.addConstraint(h), h;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Creates a point-to-point constraint between two bodies.
|
|
235
|
+
* @param a The first body.
|
|
236
|
+
* @param b The second body.
|
|
237
|
+
* @param o The Three.js object representing the constraint.
|
|
238
|
+
* @returns The created PointToPointConstraint.
|
|
239
|
+
*/
|
|
240
|
+
createPointConstraint(n, t, e) {
|
|
241
|
+
const o = e.getWorldPosition(new r()), s = n.pointToLocalFrame(new l(o.x, o.y, o.z)), a = t.pointToLocalFrame(new l(o.x, o.y, o.z)), i = new v(n, s, t, a);
|
|
242
|
+
return this.world.addConstraint(i), i;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Creates a distance constraint between two bodies.
|
|
246
|
+
* @param a The first body.
|
|
247
|
+
* @param b The second body.
|
|
248
|
+
* @returns The created DistanceConstraint.
|
|
249
|
+
*/
|
|
250
|
+
createDistanceConstraint(n, t) {
|
|
251
|
+
const e = new _(n, t);
|
|
252
|
+
return this.world.addConstraint(e), e;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Creates a Cannon body for a given Three.js object and shape.
|
|
256
|
+
* @param shape The Cannon-es shape.
|
|
257
|
+
* @param o The Three.js object.
|
|
258
|
+
* @returns The created Cannon body.
|
|
259
|
+
*/
|
|
260
|
+
createCollider(n, t) {
|
|
261
|
+
t.visible = !1;
|
|
262
|
+
let e = t.userData.threejscannones_cgroup ?? 1, o = t.userData.threejscannones_cwith ?? 1;
|
|
263
|
+
const s = new A({
|
|
264
|
+
shape: n,
|
|
265
|
+
mass: t.userData.threejscannones_mass ?? 0,
|
|
266
|
+
collisionFilterMask: o,
|
|
267
|
+
collisionFilterGroup: e
|
|
268
|
+
}), a = t.getWorldPosition(new r());
|
|
269
|
+
s.position.set(a.x, a.y, a.z);
|
|
270
|
+
const i = t.getWorldQuaternion(new c());
|
|
271
|
+
return s.quaternion.set(i.x, i.y, i.z, i.w), this.world.addBody(s), this.obj2bod.set(t, s), s;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Updates all constraints. Should be called every frame.
|
|
275
|
+
* @param delta The time step.
|
|
276
|
+
*/
|
|
277
|
+
update(n) {
|
|
278
|
+
for (let t = 0; t < this.constraints.length; t++)
|
|
279
|
+
this.constraints[t].update(n);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
class b {
|
|
283
|
+
constructor(n, t) {
|
|
284
|
+
this.obj = n, this.cannonConstraint = t;
|
|
285
|
+
}
|
|
286
|
+
enable() {
|
|
287
|
+
this.cannonConstraint?.enable();
|
|
288
|
+
}
|
|
289
|
+
disable() {
|
|
290
|
+
this.cannonConstraint?.disable();
|
|
291
|
+
}
|
|
292
|
+
update(n) {
|
|
293
|
+
}
|
|
294
|
+
get name() {
|
|
295
|
+
return this.obj.userData.name;
|
|
296
|
+
}
|
|
297
|
+
removeFrom(n) {
|
|
298
|
+
this.cannonConstraint && n.removeConstraint(this.cannonConstraint);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
class L extends b {
|
|
302
|
+
constructor(n, t) {
|
|
303
|
+
super(n), this.body = t;
|
|
304
|
+
}
|
|
305
|
+
offsetPos = new r();
|
|
306
|
+
offsetQuat = new c();
|
|
307
|
+
hasInit = !1;
|
|
308
|
+
initOffsets() {
|
|
309
|
+
const n = new r(this.body.position.x, this.body.position.y, this.body.position.z), t = new c(this.body.quaternion.x, this.body.quaternion.y, this.body.quaternion.z, this.body.quaternion.w), e = new r(), o = new c();
|
|
310
|
+
this.obj.getWorldPosition(e), this.obj.getWorldQuaternion(o), this.offsetPos.copy(e).sub(n).applyQuaternion(t.clone().invert()), this.offsetQuat.copy(t.clone().invert().multiply(o)), this.hasInit = !0;
|
|
311
|
+
}
|
|
312
|
+
update() {
|
|
313
|
+
this.hasInit || this.initOffsets();
|
|
314
|
+
const n = new r(this.body.position.x, this.body.position.y, this.body.position.z), t = new c(this.body.quaternion.x, this.body.quaternion.y, this.body.quaternion.z, this.body.quaternion.w), e = t.clone().multiply(this.offsetQuat), o = this.offsetPos.clone().applyQuaternion(t).add(n);
|
|
315
|
+
if (this.obj.parent) {
|
|
316
|
+
this.obj.parent.updateMatrixWorld(!0);
|
|
317
|
+
const s = new T().compose(o, e, new r(1, 1, 1)), a = new T().copy(this.obj.parent.matrixWorld).invert();
|
|
318
|
+
s.premultiply(a), s.decompose(this.obj.position, this.obj.quaternion, new r());
|
|
319
|
+
} else
|
|
320
|
+
this.obj.position.copy(o), this.obj.quaternion.copy(e);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
class N extends b {
|
|
324
|
+
constructor(n, t, e, o, s) {
|
|
325
|
+
super(t), this.world = n, this.cable = e, o && this.lockHeadTo(o), s && this.lockTailTo(s);
|
|
326
|
+
}
|
|
327
|
+
lockToA;
|
|
328
|
+
lockToB;
|
|
329
|
+
lockXTo(n, t, e) {
|
|
330
|
+
let o;
|
|
331
|
+
return n && this.world.removeConstraint(n), e && (o = new v(t, new l(), e, e.pointToLocalFrame(t.position)), o.collideConnected = !1, this.world.addConstraint(o)), o;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Locks or Releases the head to or from a body. `PointToPointConstraint` will be used to lock.
|
|
335
|
+
* @param body
|
|
336
|
+
*/
|
|
337
|
+
lockHeadTo(n) {
|
|
338
|
+
this.lockToA = this.lockXTo(this.lockToA, this.cable.head, n);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Locks or Releases the tail to or from a body. `PointToPointConstraint` will be used to lock.
|
|
342
|
+
* @param body
|
|
343
|
+
*/
|
|
344
|
+
lockTailTo(n) {
|
|
345
|
+
this.lockToB = this.lockXTo(this.lockToB, this.cable.tail, n);
|
|
346
|
+
}
|
|
347
|
+
enable() {
|
|
348
|
+
this.lockToA?.enable(), this.lockToB?.enable(), this.cable.constraints.forEach((n) => n.enable());
|
|
349
|
+
}
|
|
350
|
+
disable() {
|
|
351
|
+
this.lockToA?.disable(), this.lockToB?.disable(), this.cable.constraints.forEach((n) => n.disable());
|
|
352
|
+
}
|
|
353
|
+
update() {
|
|
354
|
+
this.cable.syncRig();
|
|
355
|
+
}
|
|
356
|
+
removeFrom(n) {
|
|
357
|
+
this.lockToA && (n.removeConstraint(this.lockToA), this.lockToA = void 0), this.lockToB && (n.removeConstraint(this.lockToB), this.lockToA = void 0), this.cable.removeFromPhysicalWorld(n), super.removeFrom(n);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export {
|
|
361
|
+
N as CableConstraint,
|
|
362
|
+
L as SyncConstraint,
|
|
363
|
+
b as ThreeJsCannonEsConstraint,
|
|
364
|
+
O as ThreeJsCannonEsSceneRigger
|
|
365
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "threejs-cannones-rigger",
|
|
3
|
+
"author": {
|
|
4
|
+
"name": "pablo bandinopla",
|
|
5
|
+
"url": "https://github.com/bandinopla"
|
|
6
|
+
},
|
|
7
|
+
"description": "Create and position physics colliders in Blender then export them as GLB and load them automatically in ThreeJs with Cannon-es.",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"packageManager": "pnpm@8.15.4",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"main": "./dist/threejs-cannones-rigger.cjs",
|
|
15
|
+
"module": "./dist/threejs-cannones-rigger.js",
|
|
16
|
+
"types": "./dist/threejs-cannones-rigger.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/threejs-cannones-rigger.js",
|
|
20
|
+
"require": "./dist/threejs-cannones-rigger.cjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "vite --config vite.config.site.js",
|
|
25
|
+
"build": "tsc && vite build",
|
|
26
|
+
"build-test-app": "vite build --config vite.config.site.js",
|
|
27
|
+
"preview": "vite preview --config vite.config.site.js"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/three": "^0.178.1",
|
|
31
|
+
"cannon-es": "^0.20.0",
|
|
32
|
+
"cannon-es-debugger": "^1.0.0",
|
|
33
|
+
"three": "^0.179.0",
|
|
34
|
+
"threejs-cannones-tube": "^0.0.9",
|
|
35
|
+
"typescript": "~5.8.3",
|
|
36
|
+
"vite": "^7.0.4",
|
|
37
|
+
"vite-plugin-dts": "^4.5.4"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"cannon-es": ">=0.20.0 <1.0.0",
|
|
41
|
+
"three": ">=0.178.0 <1.0.0",
|
|
42
|
+
"threejs-cannones-tube": ">=0.0.7 <1.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# ThreeJs / Cannon-es Scene Rigger
|
|
4
|
+
### Design in Blender, simulate in Three!
|
|
5
|
+
|
|
6
|
+
Create and place physics colliders in [Blender](http://blender.org/), export as GLB, and automatically set them up in [three.js](https://threejs.org/) with [cannon-es](https://github.com/pmndrs/cannon-es).
|
|
7
|
+
|
|
8
|
+
This solution includes two tools:
|
|
9
|
+
|
|
10
|
+
1) A Blender addon for creating colliders in Blender
|
|
11
|
+
2) An NPM package to rig the physics in Three.js using cannon-es
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- Automatically creates Cannon bodies for Three.js objects defined inside of Blender ( _using the addon_ ).
|
|
17
|
+
- Supports:
|
|
18
|
+
- box and sphere colliders
|
|
19
|
+
- hinge
|
|
20
|
+
- point
|
|
21
|
+
- distance
|
|
22
|
+
- lock
|
|
23
|
+
- glue childrens
|
|
24
|
+
- [cable](https://www.npmjs.com/package/threejs-cannones-tube)
|
|
25
|
+
- synchronization (copy location & rotation)
|
|
26
|
+
- :sparkles: your own custom constraint :sparkles:
|
|
27
|
+
|
|
28
|
+
## Test your rig
|
|
29
|
+
You can quicly test your rig here: https://threejs-cannones-rigger.vercel.app
|
|
30
|
+
Just make sure your glb has a camera and it is in the right angle where you want to focus on (it will use the camera in the file... so if you don't see anything maybe you forgot to export the cameras too)
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
### 1) Install the blender addon
|
|
35
|
+
Blender → Preferences → Add-ons → Install → select `threejs-cannones-addon.py`
|
|
36
|
+
> Blender addon : [threejs-cannones-addon.py](https://github.com/bandinopla/threejs-cannones-rigger/raw/refs/heads/main/threejs-cannones-addon.py)
|
|
37
|
+
|
|
38
|
+
After installing, when you select an object in the scene inside of blender, you should see new expandable box appear in the Object's tab.
|
|
39
|
+
|
|
40
|
+
### 2) Scene Rigging
|
|
41
|
+
|
|
42
|
+
To create the colliders you work with Empty objects. Their scale is used to define the size of the collider. Sphere colliders can only have uniform scale. Typically you will/should use a Cube or Sphere empty for colliders, and any shape you want for the constraints (since their transform won't be used anyways just their data)
|
|
43
|
+
|
|
44
|
+
In Blender, when you select an object you will see on the right a panel titled **"ThreeJs / Cannon-es (Physics)"** From there, create empty objects or boxes or spheres and assign the corresponding options.
|
|
45
|
+
|
|
46
|
+
### 3) Export `.glb`
|
|
47
|
+
when you export as GLB make sure you tick the option **"include custom properties"**
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
### 4) Load in ThreeJs
|
|
51
|
+
```
|
|
52
|
+
npm install threejs-cannones-rigger
|
|
53
|
+
```
|
|
54
|
+
#### The load the GLB exported from blender like this...
|
|
55
|
+
> See `src/main.ts` for a usage example.
|
|
56
|
+
```typescript
|
|
57
|
+
import { World } from 'cannon-es';
|
|
58
|
+
import { ThreeJsCannonEsSceneRigger } from 'threejs-cannones-rigger';
|
|
59
|
+
|
|
60
|
+
// Assume you have a Cannon world and a loaded Three.js scene
|
|
61
|
+
const world = new World({ gravity: new Vec3(0, -10, 0) });
|
|
62
|
+
const scene = new THREE.Scene();
|
|
63
|
+
|
|
64
|
+
// Create the rigger and rig the scene
|
|
65
|
+
const rigger = new ThreeJsCannonEsSceneRigger(world, scene);
|
|
66
|
+
|
|
67
|
+
new GLTFLoader().load(url, file => {
|
|
68
|
+
scene.add(file.scene)
|
|
69
|
+
rigger.rigScene(file.scene); // *** this is where the magic happens ***
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// To update physics and sync with Three.js in your animation loop:
|
|
73
|
+
function yourRenderLoop() {
|
|
74
|
+
// ... update world, render scene, etc.
|
|
75
|
+
rigger.update(deltaTime);
|
|
76
|
+
//...
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
# API
|
|
82
|
+
|
|
83
|
+
### `ThreeJsCannonEsSceneRigger`
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
constructor(world: World, scene?: Object3D)
|
|
87
|
+
```
|
|
88
|
+
- `world`: The Cannon-es physics world.
|
|
89
|
+
- `scene` (optional): The Three.js scene or root object to rig immediately.
|
|
90
|
+
|
|
91
|
+
### Methods
|
|
92
|
+
|
|
93
|
+
- `rigScene(scene: Object3D)`: Scans the scene and creates physics bodies and constraints based on object `userData`.
|
|
94
|
+
- `clear()`: Removes all created bodies and constraints from the world and clears internal state.
|
|
95
|
+
- `getBodyByName(name: string)`: Returns a Cannon body by the name (the name in `userData.name` )
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
# Constraints
|
|
99
|
+
In all cases, when you call `get___Constraint( name )` the expected name is the name of the object as you read it in blender. Which is automatically put in `userData.name` when you export to glb.
|
|
100
|
+
|
|
101
|
+
#### Box / Sphere Collider
|
|
102
|
+
> Use a default Cube or UV Sphere. Scale and rotate as needed. Only spheres must be scaled uniformly; boxes can be stretched freely.
|
|
103
|
+
```js
|
|
104
|
+
rigger.getBodyByName(name) //-> CANNON.Body
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Compound Collider
|
|
108
|
+
> Assign this to an empty. All children will be glued into one collider/Body.
|
|
109
|
+
|
|
110
|
+
#### Glue/Lock Colliders
|
|
111
|
+
> Connect two colliders (A & B) so they behave as a single rigid body. Creates a LockConstraint...
|
|
112
|
+
```js
|
|
113
|
+
rigger.getLockConstraint(name) //-> CANNON.LockConstraint
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
#### Hinge Constraint
|
|
117
|
+
> Assign to an empty. Local Z axis defines the hinge (like a door axis).
|
|
118
|
+
```js
|
|
119
|
+
rigger.getHingeConstraint(name) //-> CANNON.HingeConstraint
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Point Constraint
|
|
123
|
+
> Links A and B via a shared point. Each keeps its relative position to it.
|
|
124
|
+
```js
|
|
125
|
+
rigger.getPointConstraint(name) //-> CANNON.PointToPointConstraint
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
#### Keep This Distance
|
|
129
|
+
>Maintains the initial distance between A and B, allowing movement but preserving separation.
|
|
130
|
+
```js
|
|
131
|
+
rigger.getDistanceConstraint(name) //-> CANNON.DistanceConstraint
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
#### Sync Constraint
|
|
135
|
+
> Use this on a visible object (e.g. mesh) to match the position & rotation of a physics collider.
|
|
136
|
+
```js
|
|
137
|
+
rigger.getSyncConstraint(name) //-> SyncConstraint
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### Tube / Cable
|
|
141
|
+
>Creates a flexible cable using [threejs-cannones-tube](https://www.npmjs.com/package/threejs-cannones-tube).
|
|
142
|
+
Add two child empties to the constraint object — one for the head, one for the tail. A and B can optionally anchor the ends.
|
|
143
|
+
|
|
144
|
+
**Material** : If the constraint body is a mesh (like a Box) it will use whatever material that mesh has and assign it to the mesh of the tube.
|
|
145
|
+
```js
|
|
146
|
+
rigger.getCableConstraint(name) //-> CableConstraint
|
|
147
|
+
rigger.getCableConstraint(name).cable //-> CannonTubeRig
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
# :sparkles: Custom constaint
|
|
151
|
+
In blender you can select "**Custom Constraint**" and pass a custom id (an arbitraty string of your choosing) then in javascript side, you define it like so:
|
|
152
|
+
|
|
153
|
+
```js
|
|
154
|
+
rigger.registerCustomConstraint("myCustomID", config => {
|
|
155
|
+
// this function will be called when parsing the scene and your custom id is detected...
|
|
156
|
+
return yourConstraintInstance;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
rigger.getCustomConstraint(name) //=>yourConstraintInstance
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Your constraint should implement `IConstraint`.
|
|
163
|
+
|
|
164
|
+
The `config` you get has this shape:
|
|
165
|
+
| Property | Description |
|
|
166
|
+
|------------|-------------|
|
|
167
|
+
| `obj` | The Three.js object that defines the constraint in the scene. |
|
|
168
|
+
| `collisionGroup` | (Optional) Bitmask, If you define it in blender side... |
|
|
169
|
+
| `collisionMask` | (Optional) Bitmask, If you define it in blender side... |
|
|
170
|
+
| `A` | (Optional) The Cannon body referenced as A (if set in Blender). |
|
|
171
|
+
| `B` | (Optional) The Cannon body referenced as B (if set in Blender). |
|
|
172
|
+
|
|
173
|
+
To get a reference to your constrain after creation call:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
rigger.getCustomConstraint("objectName") as MySuperCoolCustomConstraint
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|