@dimension-mismatch/2dphysics 0.0.1
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/body.ts +252 -0
- package/collision.ts +284 -0
- package/constraints/constraint.ts +17 -0
- package/constraints/springs.ts +93 -0
- package/constraints/wheel.ts +191 -0
- package/geometry.ts +98 -0
- package/package.json +11 -0
- package/solver.ts +95 -0
- package/tsconfig.json +10 -0
- package/world.ts +126 -0
package/body.ts
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Rotation, vec2 } from "../vec2/calc.js";
|
|
2
|
+
import { Circle, Polygon, Shape, ShapeType } from "./geometry.js";
|
|
3
|
+
let uniqueID: number = 0;
|
|
4
|
+
|
|
5
|
+
export class Material{
|
|
6
|
+
bounciness: number;
|
|
7
|
+
friction: number;
|
|
8
|
+
staticFriction: number;
|
|
9
|
+
density: number;
|
|
10
|
+
|
|
11
|
+
constructor(bounciness: number, friction: number, staticFriction: number, density: number){
|
|
12
|
+
this.bounciness = bounciness;
|
|
13
|
+
this.friction = friction;
|
|
14
|
+
this.staticFriction = staticFriction;
|
|
15
|
+
this.density = density;
|
|
16
|
+
}
|
|
17
|
+
static default(){
|
|
18
|
+
return new Material(0.2, 0.3, 0.4, 1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export interface GameObject{
|
|
22
|
+
uniqueID: number;
|
|
23
|
+
isCollection: boolean;
|
|
24
|
+
getAllObjects(): any;
|
|
25
|
+
}
|
|
26
|
+
export class Collection implements GameObject{
|
|
27
|
+
isCollection = true;
|
|
28
|
+
children: GameObject[] = [];
|
|
29
|
+
uniqueID: number;
|
|
30
|
+
|
|
31
|
+
constructor(children: GameObject[]){
|
|
32
|
+
this.uniqueID = uniqueID++;
|
|
33
|
+
this.children = children;
|
|
34
|
+
}
|
|
35
|
+
static ofObjects(...objects: GameObject[]){
|
|
36
|
+
return new Collection(objects)
|
|
37
|
+
}
|
|
38
|
+
addObjects(...objects: GameObject[]){
|
|
39
|
+
this.children.push(...objects);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
getAllObjects(): PhysicsObject[]{
|
|
43
|
+
let objects = [];
|
|
44
|
+
for(let i = 0; i < this.children.length; i++){
|
|
45
|
+
if( this.children[i].isCollection){
|
|
46
|
+
objects.push(this.children[i].getAllObjects())
|
|
47
|
+
}
|
|
48
|
+
else{
|
|
49
|
+
objects.push(this.children[i]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return objects;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export interface PhysicsObjectOptions{
|
|
56
|
+
angle?: Rotation;
|
|
57
|
+
velocity?: vec2;
|
|
58
|
+
angularVelocity?: Rotation;
|
|
59
|
+
bounciness?: number;
|
|
60
|
+
staticFriction?: number;
|
|
61
|
+
friction?: number;
|
|
62
|
+
density?: number;
|
|
63
|
+
mass?: number;
|
|
64
|
+
material?: Material;
|
|
65
|
+
static?: boolean;
|
|
66
|
+
skipCOMcalc?: boolean;
|
|
67
|
+
|
|
68
|
+
}
|
|
69
|
+
export class PhysicsObject implements GameObject{
|
|
70
|
+
getAllObjects() {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
isCollection = false;
|
|
74
|
+
position: vec2;
|
|
75
|
+
angle: Rotation;
|
|
76
|
+
uniqueID: number;
|
|
77
|
+
velocity: vec2;
|
|
78
|
+
angularVelocity: Rotation;
|
|
79
|
+
|
|
80
|
+
lastPosition: vec2;
|
|
81
|
+
lastAngle: Rotation;
|
|
82
|
+
|
|
83
|
+
deltaTime: number = 1;
|
|
84
|
+
|
|
85
|
+
acceleration: vec2 = vec2.zero();
|
|
86
|
+
angularAccerleration: Rotation = Rotation.zero();
|
|
87
|
+
|
|
88
|
+
mass: number;
|
|
89
|
+
inverseMass: number;
|
|
90
|
+
|
|
91
|
+
inertia: number;
|
|
92
|
+
inverseInertia: number;
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
material: Material;
|
|
96
|
+
|
|
97
|
+
colliders: Shape[];
|
|
98
|
+
constructor(position: vec2, colliders: Shape[] | Shape, options?: PhysicsObjectOptions){
|
|
99
|
+
this.uniqueID = uniqueID++;
|
|
100
|
+
this.position = position;
|
|
101
|
+
if(options && options.angle){
|
|
102
|
+
this.angle = options.angle;
|
|
103
|
+
}
|
|
104
|
+
else{
|
|
105
|
+
this.angle = Rotation.zero();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
this.lastPosition = position.copy();
|
|
110
|
+
this.lastAngle = this.angle.copy();
|
|
111
|
+
|
|
112
|
+
this.velocity = vec2.zero();
|
|
113
|
+
this.angularVelocity = Rotation.zero();
|
|
114
|
+
|
|
115
|
+
if(Array.isArray(colliders)){
|
|
116
|
+
this.colliders = colliders;
|
|
117
|
+
}
|
|
118
|
+
else{
|
|
119
|
+
this.colliders = [colliders];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if(options){
|
|
123
|
+
if(options.velocity){
|
|
124
|
+
this.lastPosition.subtract(options.velocity);
|
|
125
|
+
this.velocity = options.velocity.copy();
|
|
126
|
+
}
|
|
127
|
+
if(options.angularVelocity){
|
|
128
|
+
this.lastAngle.subtract(options.angularVelocity);
|
|
129
|
+
this.angularVelocity = options.angularVelocity.copy();
|
|
130
|
+
}
|
|
131
|
+
if(options.material){
|
|
132
|
+
this.material = options.material;
|
|
133
|
+
}
|
|
134
|
+
else{
|
|
135
|
+
this.material = Material.default();
|
|
136
|
+
}
|
|
137
|
+
if(options.bounciness){
|
|
138
|
+
this.material.bounciness = options.bounciness;
|
|
139
|
+
}
|
|
140
|
+
if(options.density){
|
|
141
|
+
this.material.density = options.density;
|
|
142
|
+
}
|
|
143
|
+
if(options.staticFriction){
|
|
144
|
+
this.material.staticFriction = options.staticFriction;
|
|
145
|
+
}
|
|
146
|
+
if(options.friction){
|
|
147
|
+
this.material.friction = options.friction;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
else{
|
|
151
|
+
this.material = Material.default();
|
|
152
|
+
}
|
|
153
|
+
this.calculateProperties(options && options.skipCOMcalc);
|
|
154
|
+
if(options && options.static){
|
|
155
|
+
this.inverseMass = 0;
|
|
156
|
+
this.inverseInertia = 0;
|
|
157
|
+
}
|
|
158
|
+
if(options?.mass){
|
|
159
|
+
let factor = options.mass / this.mass;
|
|
160
|
+
this.mass *= factor;
|
|
161
|
+
this.inertia *= factor;
|
|
162
|
+
this.inverseMass /= factor;
|
|
163
|
+
this.inverseInertia /= factor;
|
|
164
|
+
console.log(this);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
private calculateProperties(skipCOM?: boolean){
|
|
168
|
+
let COM = new vec2(0,0);
|
|
169
|
+
let totalArea = 0;
|
|
170
|
+
for(let i = 0; i < this.colliders.length; i++){
|
|
171
|
+
totalArea += this.colliders[i].area;
|
|
172
|
+
COM.add(vec2.times(this.colliders[i].COM, this.colliders[i].area));
|
|
173
|
+
}
|
|
174
|
+
COM.divideBy(totalArea);
|
|
175
|
+
this.mass = totalArea * this.material.density;
|
|
176
|
+
if(skipCOM){
|
|
177
|
+
COM = new vec2(0,0);
|
|
178
|
+
}
|
|
179
|
+
let inertia = 0;
|
|
180
|
+
for(let i = 0; i < this.colliders.length; i++){
|
|
181
|
+
this.colliders[i].translate(COM.inverse());
|
|
182
|
+
inertia += this.colliders[i].inertia;
|
|
183
|
+
}
|
|
184
|
+
this.inertia = inertia * this.material.density;
|
|
185
|
+
|
|
186
|
+
this.inverseMass = 1/this.mass;
|
|
187
|
+
this.inverseInertia = 1/this.inertia;
|
|
188
|
+
}
|
|
189
|
+
worldToLocalSpace(v: vec2): vec2{
|
|
190
|
+
return vec2.worldToLocalSpace(v, this.position, this.angle);
|
|
191
|
+
}
|
|
192
|
+
localToWorldSpace(v: vec2): vec2{
|
|
193
|
+
return vec2.localToWorldSpace(v, this.position, this.angle);
|
|
194
|
+
}
|
|
195
|
+
localToAASpace(v: vec2): vec2{
|
|
196
|
+
return vec2.localToWorldSpace(v, vec2.zero(), this.angle);
|
|
197
|
+
}
|
|
198
|
+
applyForce(force: vec2, location: vec2){
|
|
199
|
+
this.acceleration.add(vec2.times(force, this.inverseMass));
|
|
200
|
+
this.angularAccerleration.add(Rotation.new(vec2.cross(location, force) * this.inverseInertia));
|
|
201
|
+
}
|
|
202
|
+
translate(t: vec2, updateVelocity: boolean){
|
|
203
|
+
this.position.add(t);
|
|
204
|
+
if(!updateVelocity){
|
|
205
|
+
this.lastPosition.add(t);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
setPosition(p: vec2, updateVelocity: boolean){
|
|
209
|
+
if(!updateVelocity){
|
|
210
|
+
this.lastPosition.add(vec2.minus(p, this.position));
|
|
211
|
+
this.velocity.add(vec2.minus(p, this.position));
|
|
212
|
+
}
|
|
213
|
+
this.position = p;
|
|
214
|
+
}
|
|
215
|
+
setVelocity(v: vec2){
|
|
216
|
+
this.lastPosition = vec2.minus(this.position, v);
|
|
217
|
+
this.velocity = v.copy();
|
|
218
|
+
}
|
|
219
|
+
rotate(r: Rotation, updateVelocity: boolean){
|
|
220
|
+
this.angle.add(r);
|
|
221
|
+
if(!updateVelocity){
|
|
222
|
+
this.lastAngle.add(r);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
getVelocity(): vec2{
|
|
226
|
+
return this.velocity.copy();
|
|
227
|
+
return vec2.minus(this.position, this.lastPosition).divideBy(this.deltaTime);
|
|
228
|
+
}
|
|
229
|
+
getAngularVelocity(): Rotation{
|
|
230
|
+
return this.angularVelocity.copy();
|
|
231
|
+
return Rotation.times(Rotation.minus(this.angle, this.lastAngle),1 / this.deltaTime);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getVelocityOfPoint(point: vec2): vec2{
|
|
235
|
+
return vec2.plus(this.getVelocity(), vec2.times(vec2.rotatedBy(point, Rotation.ccw90deg()), this.getAngularVelocity().angle));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
getVelocityOfLocalPoint(point: vec2): vec2{
|
|
239
|
+
return vec2.plus(this.getVelocity(), vec2.times(vec2.rotatedBy(point, Rotation.plus(this.angle, Rotation.ccw90deg())), this.getAngularVelocity().angle));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
//static readonly empty = new PhysicsObject(vec2.zero, Rotation.zero, [], {density: Infinity});
|
|
243
|
+
static rectangle(position: vec2, width: number, height: number, options?: PhysicsObjectOptions): PhysicsObject{
|
|
244
|
+
return new PhysicsObject(position, [Polygon.rectangle(vec2.zero(), width, height)], options);
|
|
245
|
+
}
|
|
246
|
+
static regularPolygon(position: vec2, radius: number, sides: number, options?: PhysicsObjectOptions): PhysicsObject{
|
|
247
|
+
return new PhysicsObject(position, [Polygon.regularPolygon(vec2.zero(), radius, sides)], options);
|
|
248
|
+
}
|
|
249
|
+
static circle(position: vec2, radius: number, options?: PhysicsObjectOptions): PhysicsObject{
|
|
250
|
+
return new PhysicsObject(position, [new Circle(vec2.zero(), radius)], options);
|
|
251
|
+
}
|
|
252
|
+
}
|
package/collision.ts
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { PhysicsObject } from "./body.js";
|
|
2
|
+
import { Rotation, vec2 } from "../vec2/calc.js";
|
|
3
|
+
import { Circle, Polygon, Shape, ShapeType, Vertex } from "./geometry.js";
|
|
4
|
+
|
|
5
|
+
export interface Contact{
|
|
6
|
+
depth: number;
|
|
7
|
+
normal: vec2;
|
|
8
|
+
objectA: PhysicsObject;
|
|
9
|
+
objectB: PhysicsObject;
|
|
10
|
+
|
|
11
|
+
shapeA: Shape;
|
|
12
|
+
shapeB: Shape;
|
|
13
|
+
|
|
14
|
+
contactPoints: vec2[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SATresult{
|
|
18
|
+
axis: vec2;
|
|
19
|
+
depth: number;
|
|
20
|
+
Aindex: number;
|
|
21
|
+
Bindex: number;
|
|
22
|
+
}
|
|
23
|
+
function invert(r: Contact){
|
|
24
|
+
r.normal = r.normal.inverse();
|
|
25
|
+
|
|
26
|
+
let objectB = r.objectB;
|
|
27
|
+
r.objectB = r.objectA;
|
|
28
|
+
r.objectA = objectB;
|
|
29
|
+
|
|
30
|
+
let shapeB = r.shapeB;
|
|
31
|
+
r.shapeB = r.shapeA;
|
|
32
|
+
r.shapeA = shapeB;
|
|
33
|
+
}
|
|
34
|
+
function objectToVertexSpace(v: vec2, vert: Vertex): vec2{
|
|
35
|
+
return vec2.worldToLocalSpace(v , vert.position, new Rotation(0, vert.normal.x, vert.normal.y))
|
|
36
|
+
}
|
|
37
|
+
function worldToVertexSpace(v: vec2, objectA: PhysicsObject, objectB: PhysicsObject, vert: Vertex): vec2{
|
|
38
|
+
return objectToVertexSpace(objectB.worldToLocalSpace(objectA.localToWorldSpace(v)), vert);
|
|
39
|
+
}
|
|
40
|
+
function vertexToWorldSpace(v: vec2, objectB: PhysicsObject, vert: Vertex){
|
|
41
|
+
return objectB.localToWorldSpace(vec2.localToWorldSpace(v , vert.position, {angle: 0, cos: vert.normal.x, sin: vert.normal.y} as Rotation));
|
|
42
|
+
}
|
|
43
|
+
function SAT(shapeA: Polygon, shapeB: Polygon, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
44
|
+
let bestResult: SATresult = {axis: new vec2(0,0), depth: Infinity, Aindex: 0, Bindex: 0}
|
|
45
|
+
|
|
46
|
+
//find which normal of shapeB has the least overlap
|
|
47
|
+
for(let axis = 0; axis < shapeB.vertices.length; axis++){
|
|
48
|
+
|
|
49
|
+
let AminProjection = Infinity;
|
|
50
|
+
let mindex: number;
|
|
51
|
+
|
|
52
|
+
let normal = shapeB.vertices[axis].normal;
|
|
53
|
+
let BmaxProjection = vec2.dot(shapeB.vertices[axis].position, normal);
|
|
54
|
+
|
|
55
|
+
for(let vertex = 0; vertex < shapeA.vertices.length; vertex++){
|
|
56
|
+
let worldPosition = objectA.localToWorldSpace(shapeA.vertices[vertex].position);
|
|
57
|
+
let localPosition = objectB.worldToLocalSpace(worldPosition);
|
|
58
|
+
|
|
59
|
+
let proj = vec2.dot(localPosition, normal);
|
|
60
|
+
if(proj < AminProjection){
|
|
61
|
+
AminProjection = proj;
|
|
62
|
+
mindex = vertex;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
if(AminProjection > BmaxProjection){
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if(shapeB.vertices[axis].isInternal){
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if(BmaxProjection - AminProjection < bestResult.depth){
|
|
73
|
+
bestResult.depth = BmaxProjection - AminProjection;
|
|
74
|
+
bestResult.axis = normal;
|
|
75
|
+
bestResult.Bindex = axis;
|
|
76
|
+
bestResult.Aindex = mindex;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//find which face of shapeA is intersecting shapeB
|
|
80
|
+
let n1idx = bestResult.Aindex;
|
|
81
|
+
let n1 = shapeA.vertices[n1idx].normal;
|
|
82
|
+
|
|
83
|
+
let n2idx = (n1idx + 1) % shapeA.vertices.length;
|
|
84
|
+
let n0idx = (n1idx - 1) % shapeA.vertices.length;
|
|
85
|
+
|
|
86
|
+
let b1idx = bestResult.Bindex == 0? shapeB.vertices.length - 1 :bestResult.Bindex - 1;
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
let n2 = shapeA.vertices[n1idx].normal;
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
n1 = vec2.rotatedBy(n1, objectA.angle).rotateBy(Rotation.inverse(objectB.angle));
|
|
93
|
+
n2 = vec2.rotatedBy(n2, objectA.angle).rotateBy(Rotation.inverse(objectB.angle));
|
|
94
|
+
|
|
95
|
+
let contactPoints: vec2[];
|
|
96
|
+
if(vec2.dot(n1, bestResult.axis) < vec2.dot(n2, bestResult.axis)){
|
|
97
|
+
contactPoints = [shapeA.vertices[n0idx].position, shapeA.vertices[n1idx].position];
|
|
98
|
+
}
|
|
99
|
+
else{
|
|
100
|
+
contactPoints = [shapeA.vertices[n1idx].position, shapeA.vertices[n2idx].position];
|
|
101
|
+
}
|
|
102
|
+
contactPoints = contactPoints.map((v) => (worldToVertexSpace(v, objectA, objectB, shapeB.vertices[bestResult.Bindex])));
|
|
103
|
+
let b2 = objectToVertexSpace(shapeB.vertices[b1idx].position, shapeB.vertices[bestResult.Bindex]);
|
|
104
|
+
|
|
105
|
+
for(let i = contactPoints.length - 1; i >= 0; i--){
|
|
106
|
+
if(contactPoints[i].x > 0){
|
|
107
|
+
contactPoints.splice(i);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
else{
|
|
111
|
+
contactPoints[i].x = 0;
|
|
112
|
+
}
|
|
113
|
+
if(contactPoints[i].y > 0){
|
|
114
|
+
contactPoints[i].y = 0;
|
|
115
|
+
}
|
|
116
|
+
if(contactPoints[i].y < b2.y){
|
|
117
|
+
contactPoints[i].y = b2.y;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
contactPoints = contactPoints.map((v) => (vertexToWorldSpace(v, objectB, shapeB.vertices[bestResult.Bindex])));
|
|
121
|
+
return {
|
|
122
|
+
shapeA: shapeA,
|
|
123
|
+
shapeB: shapeB,
|
|
124
|
+
|
|
125
|
+
objectA: objectA,
|
|
126
|
+
objectB: objectB,
|
|
127
|
+
|
|
128
|
+
normal: vec2.rotatedBy(bestResult.axis,objectB.angle),
|
|
129
|
+
depth: bestResult.depth,
|
|
130
|
+
|
|
131
|
+
contactPoints: contactPoints
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
//return bestResult;
|
|
135
|
+
}
|
|
136
|
+
export function PolygonCollsion(shapeA: Polygon, shapeB: Polygon, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
137
|
+
let rA = SAT(shapeA, shapeB, objectA, objectB);
|
|
138
|
+
if(!rA){
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
let rB = SAT(shapeB, shapeA, objectB, objectA);
|
|
142
|
+
if(!rB){
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if(rB.depth < rA.depth){
|
|
146
|
+
invert(rB);
|
|
147
|
+
return rB;
|
|
148
|
+
}
|
|
149
|
+
else{
|
|
150
|
+
return rA;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function CirclePolygonCollision(shapeA: Circle, shapeB: Polygon, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
154
|
+
let localCenter = objectB.worldToLocalSpace(objectA.localToWorldSpace(shapeA.COM));
|
|
155
|
+
|
|
156
|
+
let bestResult: {distance: number, normal: vec2, contact: vec2};
|
|
157
|
+
bestResult = {distance: Infinity, normal: vec2.zero(), contact: vec2.zero()};
|
|
158
|
+
|
|
159
|
+
let lastPoint = shapeB.vertices[shapeB.vertices.length - 1].position;
|
|
160
|
+
for(let i = 0; i < shapeB.vertices.length; i++){
|
|
161
|
+
let vertex = shapeB.vertices[i];
|
|
162
|
+
|
|
163
|
+
let relativeCenter = objectToVertexSpace(localCenter, vertex);
|
|
164
|
+
let relativeLast = objectToVertexSpace(lastPoint, vertex);
|
|
165
|
+
lastPoint = vertex.position;
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
let currentResult: {distance: number, normal: vec2, contact: vec2};
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if(relativeCenter.y > 0){
|
|
172
|
+
currentResult = {
|
|
173
|
+
distance: relativeCenter.mag(),
|
|
174
|
+
contact: new vec2(0,0),
|
|
175
|
+
normal: vec2.asUnitVector(vec2.minus(localCenter, vertex.position))}
|
|
176
|
+
|
|
177
|
+
}
|
|
178
|
+
else{
|
|
179
|
+
if(relativeCenter.x > shapeA.radius){
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
if(relativeCenter.y < relativeLast.y || vertex.isInternal){
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
currentResult = {
|
|
186
|
+
distance: relativeCenter.x,
|
|
187
|
+
contact: new vec2(0, relativeCenter.y),
|
|
188
|
+
normal: vertex.normal}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if(Math.abs(currentResult.distance) < Math.abs(bestResult.distance)){
|
|
197
|
+
bestResult.distance = currentResult.distance;
|
|
198
|
+
bestResult.normal = currentResult.normal;
|
|
199
|
+
bestResult.contact = vertexToWorldSpace(currentResult.contact, objectB, vertex);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if(bestResult.distance > shapeA.radius){
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
shapeA: shapeA,
|
|
207
|
+
shapeB: shapeB,
|
|
208
|
+
|
|
209
|
+
objectA: objectA,
|
|
210
|
+
objectB: objectB,
|
|
211
|
+
|
|
212
|
+
normal: vec2.rotatedBy(bestResult.normal, objectB.angle),
|
|
213
|
+
depth: shapeA.radius - bestResult.distance,
|
|
214
|
+
|
|
215
|
+
contactPoints: [bestResult.contact]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
export function PolygonCircleCollsion(shapeA: Polygon, shapeB: Circle, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
219
|
+
let ret = CirclePolygonCollision(shapeB, shapeA, objectB, objectA);
|
|
220
|
+
if(!ret){
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
else{
|
|
224
|
+
invert(ret);
|
|
225
|
+
return ret;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function CircleCircleCollision(shapeA: Circle, shapeB: Circle, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
230
|
+
let Acenter = objectB.worldToLocalSpace(objectA.localToWorldSpace(shapeA.COM));
|
|
231
|
+
let between = vec2.minus(Acenter, shapeB.COM);
|
|
232
|
+
let dist = between.mag();
|
|
233
|
+
if(between.x == 0 && between.y == 0){
|
|
234
|
+
between.y = 1;
|
|
235
|
+
}
|
|
236
|
+
if(dist > shapeA.radius + shapeB.radius){
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
else{
|
|
240
|
+
let normal = vec2.dividedBy(between, dist);
|
|
241
|
+
return{
|
|
242
|
+
depth: shapeA.radius + shapeB.radius - dist,
|
|
243
|
+
normal: normal.rotateBy(objectB.angle),
|
|
244
|
+
|
|
245
|
+
shapeA: shapeA,
|
|
246
|
+
shapeB: shapeB,
|
|
247
|
+
|
|
248
|
+
objectA: objectA,
|
|
249
|
+
objectB: objectB,
|
|
250
|
+
|
|
251
|
+
contactPoints: [objectB.localToWorldSpace(vec2.times(normal, shapeB.radius))]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function ShapeCollision(shapeA: Shape, shapeB: Shape, objectA: PhysicsObject, objectB: PhysicsObject): Contact | false{
|
|
256
|
+
if(shapeA.type == ShapeType.CIRCLE){
|
|
257
|
+
if(shapeB.type == ShapeType.CIRCLE){
|
|
258
|
+
return CircleCircleCollision(shapeA as Circle, shapeB as Circle, objectA, objectB);
|
|
259
|
+
}
|
|
260
|
+
else{
|
|
261
|
+
return CirclePolygonCollision(shapeA as Circle, shapeB as Polygon, objectA, objectB);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else{
|
|
265
|
+
if(shapeB.type == ShapeType.CIRCLE){
|
|
266
|
+
return PolygonCircleCollsion(shapeA as Polygon, shapeB as Circle, objectA, objectB);
|
|
267
|
+
}
|
|
268
|
+
else{
|
|
269
|
+
return PolygonCollsion(shapeA as Polygon, shapeB as Polygon, objectA, objectB);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function Collision(objectA: PhysicsObject, objectB: PhysicsObject): Contact[]{
|
|
274
|
+
let results: Contact[] = [];
|
|
275
|
+
for(let i = 0; i < objectA.colliders.length; i++){
|
|
276
|
+
for(let j = 0; j < objectB.colliders.length; j++){
|
|
277
|
+
let res = ShapeCollision(objectA.colliders[i], objectB.colliders[j], objectA, objectB);
|
|
278
|
+
if(res){
|
|
279
|
+
results.push(res);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PhysicsObject } from "../body.js";
|
|
2
|
+
import { Rotation, vec2 } from "../../vec2/calc.js";
|
|
3
|
+
export enum ConstraintType{
|
|
4
|
+
Spring,
|
|
5
|
+
Mouse,
|
|
6
|
+
Wheel,
|
|
7
|
+
Motor,
|
|
8
|
+
}
|
|
9
|
+
export interface Constraint{
|
|
10
|
+
solveVelocity(dt: number): void;
|
|
11
|
+
solvePosition(dt: number): void;
|
|
12
|
+
constraintType: ConstraintType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PhysicsObject } from "../body.js";
|
|
2
|
+
import { vec2 } from "../../vec2/calc.js";
|
|
3
|
+
import { Constraint, ConstraintType } from "./constraint.js";
|
|
4
|
+
export interface SpringOptions{
|
|
5
|
+
pointA?: vec2;
|
|
6
|
+
pointB?: vec2;
|
|
7
|
+
k?: number;
|
|
8
|
+
damping?: number;
|
|
9
|
+
length?: number;
|
|
10
|
+
}
|
|
11
|
+
export class Spring implements Constraint{
|
|
12
|
+
constraintType = ConstraintType.Spring;
|
|
13
|
+
objectA: PhysicsObject;
|
|
14
|
+
objectB: PhysicsObject | null;
|
|
15
|
+
pointA: vec2;
|
|
16
|
+
pointB: vec2;
|
|
17
|
+
k: number;
|
|
18
|
+
damping: number;
|
|
19
|
+
length: number;
|
|
20
|
+
constructor(objectA: PhysicsObject, objectB: PhysicsObject | null, options?: SpringOptions){
|
|
21
|
+
this.objectA = objectA;
|
|
22
|
+
this.objectB = objectB;
|
|
23
|
+
this.pointA = options?.pointA? options.pointA : vec2.zero();
|
|
24
|
+
this.pointB = options?.pointB? options.pointB : vec2.zero();
|
|
25
|
+
this.k = options?.k? options.k : 1;
|
|
26
|
+
this.damping = options?.damping? options.damping: 0;
|
|
27
|
+
this.length = options?.length? options.length: 0;
|
|
28
|
+
}
|
|
29
|
+
getPointA(){
|
|
30
|
+
return this.objectA.localToWorldSpace(this.pointA);
|
|
31
|
+
}
|
|
32
|
+
getPointB(){
|
|
33
|
+
return this.objectB? this.objectB.localToWorldSpace(this.pointB) : this.pointB;
|
|
34
|
+
}
|
|
35
|
+
getVelA(){
|
|
36
|
+
return this.objectA.getVelocityOfLocalPoint(this.pointA);
|
|
37
|
+
}
|
|
38
|
+
getVelB(){
|
|
39
|
+
return this.objectB? this.objectB.getVelocityOfLocalPoint(this.pointB): vec2.zero();
|
|
40
|
+
}
|
|
41
|
+
solveVelocity(dt: number): void {
|
|
42
|
+
let displacement = vec2.minus(this.getPointB(), this.getPointA());
|
|
43
|
+
let direction = displacement.normalize();
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
let relativeVelocity = vec2.minus(this.getVelB(), this.getVelA());
|
|
47
|
+
let normalVelocity = vec2.dot(direction, relativeVelocity);
|
|
48
|
+
//F + gx' + kx. 0
|
|
49
|
+
//hooke's law: F = -kx
|
|
50
|
+
let force = this.k * (displacement.mag() - this.length);
|
|
51
|
+
let v = vec2.plus(vec2.times(direction, force * dt), vec2.times(relativeVelocity, this.damping * dt));
|
|
52
|
+
this.objectA.applyForce(v, this.objectA.localToAASpace(this.pointA));
|
|
53
|
+
if(this.objectB){
|
|
54
|
+
this.objectB.applyForce(vec2.times(v, -1), this.objectB.localToAASpace(this.pointB));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
solvePosition(): void {
|
|
58
|
+
//springs only apply forces
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class MouseConstraint implements Constraint{
|
|
63
|
+
constraintType = ConstraintType.Mouse;
|
|
64
|
+
spring: Spring;
|
|
65
|
+
enabled: boolean
|
|
66
|
+
constructor(){
|
|
67
|
+
this.enabled = false;
|
|
68
|
+
}
|
|
69
|
+
solveVelocity(dt: number): void {
|
|
70
|
+
if(this.enabled){
|
|
71
|
+
this.spring.solveVelocity(dt);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
solvePosition(dt: number): void {
|
|
75
|
+
if(this.enabled){
|
|
76
|
+
this.spring.solvePosition();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
enable(object: PhysicsObject, location: vec2, mousePosition: vec2){
|
|
80
|
+
this.spring = new Spring(object,null, {pointA: location, pointB: mousePosition, k: 100 * object.mass, damping: 8 * object.mass});
|
|
81
|
+
this.enabled = true;
|
|
82
|
+
}
|
|
83
|
+
disable(){
|
|
84
|
+
this.spring = null;
|
|
85
|
+
this.enabled = false;
|
|
86
|
+
}
|
|
87
|
+
update(mousePosition: vec2){
|
|
88
|
+
if(this.enabled){
|
|
89
|
+
this.spring.pointB = mousePosition;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { PhysicsObject } from "../body.js";
|
|
2
|
+
import { vec2, Rotation } from "../../vec2/calc.js";
|
|
3
|
+
import { Constraint, ConstraintType } from "./constraint.js";
|
|
4
|
+
export interface MotorDrivable extends Constraint{
|
|
5
|
+
applyMotorTorque(torque: number): void;
|
|
6
|
+
getAngularVelocity(): number;
|
|
7
|
+
}
|
|
8
|
+
export interface WheelOptions{
|
|
9
|
+
cof?: number;
|
|
10
|
+
static_cof?: number;
|
|
11
|
+
density?: number;
|
|
12
|
+
radius?: number;
|
|
13
|
+
wheelCount?: number;
|
|
14
|
+
angle?: Rotation;
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
export class Wheel implements MotorDrivable{
|
|
18
|
+
constraintType = ConstraintType.Wheel;
|
|
19
|
+
wheelCount: number = 1;
|
|
20
|
+
appliedTorque: number = 0;
|
|
21
|
+
frictionForce: vec2 = new vec2(0,0);
|
|
22
|
+
constructor(mountedTo: PhysicsObject, position: vec2, options?: WheelOptions){
|
|
23
|
+
this.mountedTo = mountedTo;
|
|
24
|
+
this.position = position.copy();
|
|
25
|
+
this.cof = 1.0;
|
|
26
|
+
this.static_cof = 1.2;
|
|
27
|
+
this.angularVelocity = 0;
|
|
28
|
+
this.radius = 1;
|
|
29
|
+
this.angle = Rotation.zero();
|
|
30
|
+
this.wheelCount = options?.wheelCount ? options.wheelCount : 1;
|
|
31
|
+
|
|
32
|
+
let density = 100;
|
|
33
|
+
if(options){
|
|
34
|
+
if(options.angle){
|
|
35
|
+
this.angle = options.angle.copy();
|
|
36
|
+
}
|
|
37
|
+
if(options.cof){
|
|
38
|
+
this.cof = options.cof;
|
|
39
|
+
}
|
|
40
|
+
if(options.density){
|
|
41
|
+
density = options.density;
|
|
42
|
+
}
|
|
43
|
+
if(options.radius){
|
|
44
|
+
this.radius = options.radius;
|
|
45
|
+
}
|
|
46
|
+
if(options.static_cof){
|
|
47
|
+
this.static_cof = options.static_cof;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
let area = Math.PI * this.radius * this.radius;
|
|
52
|
+
this.momentOfInertia = area * this.radius * this.radius * density;
|
|
53
|
+
}
|
|
54
|
+
applyMotorTorque(torque: number): void {
|
|
55
|
+
this.appliedTorque = torque;
|
|
56
|
+
}
|
|
57
|
+
getAngularVelocity(): number {
|
|
58
|
+
return this.angularVelocity;
|
|
59
|
+
}
|
|
60
|
+
solveVelocity(dt: number): void {
|
|
61
|
+
let axis = vec2.rotatedBy(this.angle.unitVector(), this.mountedTo.angle);
|
|
62
|
+
this.angularVelocity += this.appliedTorque / this.momentOfInertia * dt;
|
|
63
|
+
|
|
64
|
+
let relativeVelocity: vec2 = this.mountedTo.getVelocityOfLocalPoint(this.position);
|
|
65
|
+
let surfaceVelocity: vec2 = vec2.times(axis, this.angularVelocity * this.radius);
|
|
66
|
+
relativeVelocity.add(surfaceVelocity);
|
|
67
|
+
|
|
68
|
+
let AxN = vec2.cross(this.mountedTo.localToAASpace(this.position), vec2.asUnitVector(relativeVelocity));
|
|
69
|
+
let massFactor = this.mountedTo.inverseMass + this.mountedTo.inverseInertia * AxN * AxN;
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
let normalForce: number = 9.8 / massFactor / this.wheelCount;
|
|
73
|
+
|
|
74
|
+
let antiAlignedVelocity = vec2.cross( axis, relativeVelocity);
|
|
75
|
+
|
|
76
|
+
let stoppingForce = antiAlignedVelocity / massFactor / this.wheelCount;
|
|
77
|
+
|
|
78
|
+
let slidingForce = -Math.sign(antiAlignedVelocity) * normalForce * dt;
|
|
79
|
+
|
|
80
|
+
let antiAlignedForce = Math.abs(stoppingForce) < normalForce * this.static_cof? stoppingForce : slidingForce;
|
|
81
|
+
|
|
82
|
+
this.frictionForce = vec2.times(vec2.tangent(axis), antiAlignedForce);
|
|
83
|
+
|
|
84
|
+
this.mountedTo.applyForce(this.frictionForce, this.mountedTo.localToAASpace(this.position));
|
|
85
|
+
|
|
86
|
+
let alignedVelocity = vec2.dot(axis, relativeVelocity);
|
|
87
|
+
//1/2 I w_0^2 = 1/2mv^2 + 1/2 Iw^2, v = rw
|
|
88
|
+
//w = w_0 * sqrt(I / (I + mr^2))
|
|
89
|
+
let eqVelocity = this.radius * this.angularVelocity * Math.sqrt(this.momentOfInertia / (this.momentOfInertia + this.mountedTo.mass * this.radius * this.radius));
|
|
90
|
+
|
|
91
|
+
stoppingForce = (eqVelocity - alignedVelocity) / massFactor / this.wheelCount;
|
|
92
|
+
if(Math.abs(stoppingForce) < normalForce * this.static_cof){
|
|
93
|
+
let stoppingTorque = (eqVelocity - alignedVelocity) / this.radius * this.momentOfInertia / this.wheelCount;
|
|
94
|
+
|
|
95
|
+
this.mountedTo.applyForce(vec2.times(axis, stoppingForce), this.mountedTo.localToAASpace(this.position));
|
|
96
|
+
this.angularVelocity += stoppingTorque / this.momentOfInertia;
|
|
97
|
+
}
|
|
98
|
+
else{
|
|
99
|
+
slidingForce = -Math.sign(alignedVelocity) * normalForce * dt;
|
|
100
|
+
this.mountedTo.applyForce(vec2.times(axis, slidingForce), this.mountedTo.localToAASpace(this.position));
|
|
101
|
+
this.angularVelocity += slidingForce / this.momentOfInertia * this.radius;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
}
|
|
106
|
+
solvePosition(dt: number): void {
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
cof: number;
|
|
110
|
+
static_cof: number;
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
angularVelocity: number;
|
|
114
|
+
radius: number;
|
|
115
|
+
|
|
116
|
+
momentOfInertia: number;
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
mountedTo: PhysicsObject;
|
|
121
|
+
position: vec2;
|
|
122
|
+
angle: Rotation;
|
|
123
|
+
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DCMotorOptions{
|
|
127
|
+
nominalVoltage?: number;
|
|
128
|
+
stallTorque?: number;
|
|
129
|
+
stallCurrent?: number;
|
|
130
|
+
freeCurrent?: number;
|
|
131
|
+
freeSpeed?: number;
|
|
132
|
+
gearRatio?: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class DCMotor implements Constraint{
|
|
136
|
+
nominalVoltage: number;
|
|
137
|
+
stallTorque: number;
|
|
138
|
+
stallCurrent: number;
|
|
139
|
+
freeCurrent: number;
|
|
140
|
+
freeSpeed: number;
|
|
141
|
+
gearRatio: number;
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
internalResistance: number;
|
|
145
|
+
Kt: number; // N*m/A
|
|
146
|
+
Kv: number; // Rad/s/V
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
output: MotorDrivable;
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
inputVoltage: number = 0;
|
|
153
|
+
filteredVoltage: number = 0;
|
|
154
|
+
constructor(output: MotorDrivable, options?: DCMotorOptions){
|
|
155
|
+
//default values from KrakenX60
|
|
156
|
+
this.nominalVoltage = options?.nominalVoltage? options.nominalVoltage : 12;
|
|
157
|
+
this.stallTorque = options?.stallTorque? options.stallTorque : 7.09;
|
|
158
|
+
this.stallCurrent = options?.stallCurrent? options.stallCurrent : 366;
|
|
159
|
+
this.freeCurrent = options?.freeCurrent? options.freeCurrent : 2;
|
|
160
|
+
this.freeSpeed = options?.freeSpeed? options.freeSpeed : 6000 / 60 * 2 * Math.PI;
|
|
161
|
+
|
|
162
|
+
this.gearRatio = options?.gearRatio? options.gearRatio : 1;
|
|
163
|
+
this.output = output;
|
|
164
|
+
|
|
165
|
+
this.stallTorque *= this.gearRatio;
|
|
166
|
+
this.freeSpeed /= this.gearRatio;
|
|
167
|
+
|
|
168
|
+
this.internalResistance = this.nominalVoltage / this.stallCurrent; //Ohm's law: R = V/I
|
|
169
|
+
this.Kt = this.stallTorque / this.stallCurrent;
|
|
170
|
+
this.Kv = this.freeSpeed / (this.nominalVoltage - this.internalResistance * this.freeCurrent); //Find slope of the speed-voltage line
|
|
171
|
+
}
|
|
172
|
+
setInputVoltage(volts: number){
|
|
173
|
+
this.inputVoltage = volts;
|
|
174
|
+
}
|
|
175
|
+
solveVelocity(dt: number): void {
|
|
176
|
+
this.filteredVoltage = this.inputVoltage;
|
|
177
|
+
let angularVelocity = this.output.getAngularVelocity();
|
|
178
|
+
//input voltage minus back emf voltage?
|
|
179
|
+
let V = this.filteredVoltage - angularVelocity / this.Kv;
|
|
180
|
+
let I = V / this.internalResistance;
|
|
181
|
+
let T = I * this.Kt;
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
this.output.applyMotorTorque(T);
|
|
185
|
+
this.output.solveVelocity(dt);
|
|
186
|
+
}
|
|
187
|
+
solvePosition(dt: number): void {
|
|
188
|
+
this.output.solvePosition(dt);
|
|
189
|
+
}
|
|
190
|
+
constraintType = ConstraintType.Motor;
|
|
191
|
+
}
|
package/geometry.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Rotation, vec2 } from "../vec2/calc.js";
|
|
2
|
+
|
|
3
|
+
export class Vertex{
|
|
4
|
+
position: vec2;
|
|
5
|
+
normal: vec2;
|
|
6
|
+
isInternal: boolean;
|
|
7
|
+
}
|
|
8
|
+
export enum ShapeType{
|
|
9
|
+
POLYGON,
|
|
10
|
+
CIRCLE
|
|
11
|
+
}
|
|
12
|
+
export type Shape = Polygon | Circle;
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export class Polygon{
|
|
16
|
+
type = ShapeType.POLYGON;
|
|
17
|
+
area: number;
|
|
18
|
+
inertia: number;
|
|
19
|
+
COM: vec2;
|
|
20
|
+
vertices: Vertex[];
|
|
21
|
+
constructor(points: vec2[], internalEdges?: boolean[]){
|
|
22
|
+
let totalArea = 0;
|
|
23
|
+
this.COM = vec2.zero();
|
|
24
|
+
let lastPoint = points[points.length - 1];
|
|
25
|
+
this.vertices = [];
|
|
26
|
+
this.inertia = 0;
|
|
27
|
+
for(let i = 0; i < points.length; i++){
|
|
28
|
+
let triArea = vec2.cross(lastPoint, points[i]) / 2;
|
|
29
|
+
let triCom = vec2.times(vec2.plus(lastPoint, points[i]), triArea);
|
|
30
|
+
this.inertia += (lastPoint.magSqr() + vec2.dot(lastPoint, points[i]) + points[i].magSqr()) * triArea;
|
|
31
|
+
|
|
32
|
+
totalArea += triArea;
|
|
33
|
+
this.COM.add(triCom);
|
|
34
|
+
|
|
35
|
+
this.vertices.push(
|
|
36
|
+
{position: points[i],
|
|
37
|
+
normal: vec2.minus(points[i], lastPoint).normalize().rotateBy(Rotation.cw90deg()),
|
|
38
|
+
isInternal: internalEdges? internalEdges[i]: false});
|
|
39
|
+
lastPoint = points[i];
|
|
40
|
+
}
|
|
41
|
+
this.COM.divideBy(totalArea * 3);
|
|
42
|
+
this.inertia /= 6;
|
|
43
|
+
this.area = totalArea;
|
|
44
|
+
}
|
|
45
|
+
translate(t: vec2){
|
|
46
|
+
for(let i = 0; i < this.vertices.length; i++){
|
|
47
|
+
this.vertices[i].position.add(t);
|
|
48
|
+
}
|
|
49
|
+
this.inertia -= this.COM.magSqr() * this.area;
|
|
50
|
+
this.COM.add(t);
|
|
51
|
+
this.inertia += this.COM.magSqr() * this.area;
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
static rectangle(position: vec2, width: number , height: number){
|
|
55
|
+
let x = position.x;
|
|
56
|
+
let y = position.y;
|
|
57
|
+
let hw = width / 2;
|
|
58
|
+
let hh = height / 2;
|
|
59
|
+
return new Polygon(
|
|
60
|
+
[new vec2(x - hw, y - hh),
|
|
61
|
+
new vec2(x + hw, y - hh),
|
|
62
|
+
new vec2(x + hw, y + hh),
|
|
63
|
+
new vec2(x - hw, y + hh)]);
|
|
64
|
+
}
|
|
65
|
+
static cornerRect(c1: vec2, c2: vec2){
|
|
66
|
+
return Polygon.rectangle(vec2.plus(c1,c2).divideBy(2), Math.abs(c2.x - c1.x), Math.abs(c2.y - c1.y));
|
|
67
|
+
}
|
|
68
|
+
static regularPolygon(position: vec2, radius: number, sides: number){
|
|
69
|
+
let points = [];
|
|
70
|
+
let rot = Rotation.new(2 * Math.PI / sides)
|
|
71
|
+
let vertex = vec2.rotatedBy(new vec2(0, -radius), Rotation.times(rot, 0.5));
|
|
72
|
+
for(let i = 0; i < sides; i++){
|
|
73
|
+
points.push(vec2.plus(vertex, position));
|
|
74
|
+
vertex.rotateBy(rot);
|
|
75
|
+
}
|
|
76
|
+
return new Polygon(points);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export class Circle{
|
|
80
|
+
type = ShapeType.CIRCLE;
|
|
81
|
+
area: number;
|
|
82
|
+
inertia: number;
|
|
83
|
+
COM: vec2;
|
|
84
|
+
radius: number;
|
|
85
|
+
|
|
86
|
+
constructor(position: vec2 , radius: number){
|
|
87
|
+
this.area = Math.PI * radius * radius;
|
|
88
|
+
this.inertia = this.area * radius * radius + 0.5 * position.magSqr() * this.area;
|
|
89
|
+
this.COM = position;
|
|
90
|
+
this.radius = radius;
|
|
91
|
+
}
|
|
92
|
+
translate(t: vec2){
|
|
93
|
+
this.inertia -= this.COM.magSqr() * this.area;
|
|
94
|
+
this.COM.add(t);
|
|
95
|
+
this.inertia += this.COM.magSqr() * this.area;
|
|
96
|
+
|
|
97
|
+
}
|
|
98
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dimension-mismatch/2dphysics",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A simple 2d rigid-body physics engine",
|
|
5
|
+
"main": "svgtools.js",
|
|
6
|
+
"author": "dimension-mismatch",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@dimension-mismatch/vec2": "^0.0.1"
|
|
10
|
+
}
|
|
11
|
+
}
|
package/solver.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Rotation, vec2 } from "../vec2/calc.js";
|
|
2
|
+
import { Contact } from "./collision.js";
|
|
3
|
+
|
|
4
|
+
interface massFactor{
|
|
5
|
+
a: number;
|
|
6
|
+
b: number;
|
|
7
|
+
}
|
|
8
|
+
function getMassFactor(am: number, bm: number): massFactor{
|
|
9
|
+
let sum = 1 / (am + bm);
|
|
10
|
+
return {a: am * sum, b: bm * sum}
|
|
11
|
+
}
|
|
12
|
+
export class Solver{
|
|
13
|
+
positionIterations: number;
|
|
14
|
+
velocityIterations: number = 1;
|
|
15
|
+
positionCorrectionPercent: number = 0.6;
|
|
16
|
+
|
|
17
|
+
resolvePositions(contacts: Contact[]){
|
|
18
|
+
contacts.map((c) => this.resolvePosition(c));
|
|
19
|
+
}
|
|
20
|
+
resolveVelocities(contacts: Contact[]){
|
|
21
|
+
|
|
22
|
+
contacts.map((c) => this.applyImpulses(c));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resolvePosition(c: Contact){
|
|
26
|
+
let factor = getMassFactor(c.objectA.inverseMass, c.objectB.inverseMass);
|
|
27
|
+
|
|
28
|
+
c.objectA.translate(vec2.times(c.normal, factor.a * c.depth * this.positionCorrectionPercent), false);
|
|
29
|
+
c.objectB.translate(vec2.times(c.normal, -1 * factor.b * c.depth * this.positionCorrectionPercent), false);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
applyImpulses(c: Contact){
|
|
33
|
+
let objectA = c.objectA;
|
|
34
|
+
let objectB = c.objectB;
|
|
35
|
+
|
|
36
|
+
let normal = c.normal;
|
|
37
|
+
let tangent = vec2.rotatedBy(normal, Rotation.ccw90deg());
|
|
38
|
+
let depth = c.depth;
|
|
39
|
+
|
|
40
|
+
let nContacts = c.contactPoints.length;
|
|
41
|
+
|
|
42
|
+
// let iFactor: massFactor;
|
|
43
|
+
// let avgContact = vec2.zero();
|
|
44
|
+
// for(let i = 0; i < nContacts; i++){
|
|
45
|
+
|
|
46
|
+
// avgContact.add(c.contactPoints[i]);
|
|
47
|
+
// }
|
|
48
|
+
// avgContact.divideBy(nContacts);
|
|
49
|
+
// let avgA = vec2.minus(avgContact, objectA.position);
|
|
50
|
+
// let avgB = vec2.minus(avgContact, objectB.position);
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
for(let i = 0; i < nContacts; i++){
|
|
54
|
+
let contactA = vec2.minus(c.contactPoints[i], objectA.position);
|
|
55
|
+
let contactB = vec2.minus(c.contactPoints[i], objectB.position);
|
|
56
|
+
|
|
57
|
+
let relativeVelocity = vec2.minus(objectA.getVelocityOfPoint(contactA), objectB.getVelocityOfPoint(contactB));
|
|
58
|
+
|
|
59
|
+
let dot = vec2.dot(relativeVelocity, normal);
|
|
60
|
+
if(dot > 0){
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let restitution = Math.min(objectA.material.bounciness, objectB.material.bounciness);
|
|
65
|
+
let AxN = vec2.cross(contactA, normal);
|
|
66
|
+
let BxN = vec2.cross(contactB, normal);
|
|
67
|
+
let massFactors = nContacts * (objectA.inverseMass + objectB.inverseMass + objectA.inverseInertia * AxN * AxN + objectB.inverseInertia * BxN * BxN);
|
|
68
|
+
let impulseMagnitude = -(restitution + 1) * dot / massFactors;
|
|
69
|
+
|
|
70
|
+
objectA.applyForce(vec2.times(normal, impulseMagnitude), contactA);
|
|
71
|
+
objectB.applyForce(vec2.times(normal, -impulseMagnitude), contactB);
|
|
72
|
+
|
|
73
|
+
let cof = Math.sqrt(objectA.material.friction * objectB.material.friction);
|
|
74
|
+
let staticCof = Math.sqrt(objectA.material.staticFriction * objectB.material.staticFriction);
|
|
75
|
+
|
|
76
|
+
let tangentVelocity = vec2.dot(relativeVelocity, tangent);
|
|
77
|
+
let normalForce = impulseMagnitude;
|
|
78
|
+
let frictionMagnitude: number;
|
|
79
|
+
|
|
80
|
+
let staticForce = Math.abs(tangentVelocity / massFactors);
|
|
81
|
+
if(staticForce < staticCof * normalForce){
|
|
82
|
+
frictionMagnitude = staticForce * objectA.deltaTime * objectA.deltaTime * objectA.deltaTime;
|
|
83
|
+
}
|
|
84
|
+
else{
|
|
85
|
+
frictionMagnitude = cof * normalForce;
|
|
86
|
+
}
|
|
87
|
+
if(tangentVelocity > 0){
|
|
88
|
+
frictionMagnitude *= -1;
|
|
89
|
+
}
|
|
90
|
+
objectA.applyForce(vec2.times(tangent, frictionMagnitude), contactA);
|
|
91
|
+
objectB.applyForce(vec2.times(tangent, -frictionMagnitude), contactB);
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
package/tsconfig.json
ADDED
package/world.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Collection, GameObject, PhysicsObject } from "./body.js";
|
|
2
|
+
import { Rotation, vec2 } from "../vec2/calc.js";
|
|
3
|
+
import { Collision, Contact } from "./collision.js";
|
|
4
|
+
import { Constraint } from "./constraints/constraint.js";
|
|
5
|
+
import { Shape } from "./geometry.js";
|
|
6
|
+
import { Solver } from "./solver.js";
|
|
7
|
+
|
|
8
|
+
export class World{
|
|
9
|
+
root: Collection;
|
|
10
|
+
gravity: vec2 = new vec2(0, 0);
|
|
11
|
+
|
|
12
|
+
solver: Solver = new Solver();
|
|
13
|
+
preSolve: Function;
|
|
14
|
+
constraints: Constraint[] = [];
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
constructor(...objects: GameObject[]){
|
|
18
|
+
this.root = new Collection(objects);
|
|
19
|
+
this.preSolve = () => {};
|
|
20
|
+
}
|
|
21
|
+
addObjects(...objects: GameObject[]){
|
|
22
|
+
this.root.addObjects(...objects);
|
|
23
|
+
}
|
|
24
|
+
addConstraints(...constraints: Constraint[]){
|
|
25
|
+
this.constraints.push(...constraints);
|
|
26
|
+
}
|
|
27
|
+
step(dt: number, substeps: number){
|
|
28
|
+
for(let i = 0; i < substeps; i++){
|
|
29
|
+
this.substep(dt/substeps, i);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
substep(dt: number, stepCount: number){
|
|
33
|
+
|
|
34
|
+
let objects = this.root.getAllObjects();
|
|
35
|
+
|
|
36
|
+
//update objects positions based on velocity & acceleration
|
|
37
|
+
for(let i = 0; i < objects.length; i++){
|
|
38
|
+
let object = objects[i];
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// //verlet integration: p(t + 1) = 2 * p(t) - p(t - 1) + a(t) * dt^2
|
|
42
|
+
// let newPosition = vec2.minus(object.position, object.lastPosition).multiplyBy(dt / object.deltaTime).add(object.position);
|
|
43
|
+
// if(object.inverseMass != 0){
|
|
44
|
+
// newPosition.add(vec2.times(this.gravity, dt * dt));
|
|
45
|
+
// }
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
// let newAngle = Rotation.new(((object.angle.angle - object.lastAngle.angle) * dt / object.deltaTime) + object.angle.angle);
|
|
49
|
+
if(object.inverseMass != 0){
|
|
50
|
+
object.acceleration.add(vec2.times(this.gravity, dt));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
object.velocity = vec2.plus(object.velocity, object.acceleration);
|
|
54
|
+
object.angularVelocity = Rotation.plus(object.angularVelocity, object.angularAccerleration);
|
|
55
|
+
|
|
56
|
+
let newPosition = vec2.plus(object.position, vec2.times(object.velocity, dt));
|
|
57
|
+
let newAngle = Rotation.plus(object.angle, Rotation.times(object.angularVelocity, dt));
|
|
58
|
+
|
|
59
|
+
object.lastPosition = object.position.copy();
|
|
60
|
+
object.lastAngle = object.angle.copy();
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
object.position = newPosition;
|
|
64
|
+
object.angle = newAngle;
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
object.acceleration = vec2.zero();
|
|
69
|
+
object.angularAccerleration = Rotation.zero();
|
|
70
|
+
|
|
71
|
+
//console.log(object.deltaTime);
|
|
72
|
+
object.deltaTime = dt;
|
|
73
|
+
|
|
74
|
+
}
|
|
75
|
+
this.preSolve(stepCount);
|
|
76
|
+
//check for collisions
|
|
77
|
+
let contacts: Contact[] = [];
|
|
78
|
+
for(let i = 0; i < objects.length; i++){
|
|
79
|
+
let objectA = objects[i];
|
|
80
|
+
for(let j = i + 1; j < objects.length; j++){
|
|
81
|
+
let objectB = objects[j];
|
|
82
|
+
if(objectA.inverseMass == 0 && objectB.inverseMass == 0){
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
contacts.push(...Collision(objectA, objectB))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for(let i = 0; i < 3; i++){
|
|
89
|
+
//Apply collision impulse forces
|
|
90
|
+
this.solver.resolveVelocities(contacts);
|
|
91
|
+
for(let constraint of this.constraints){
|
|
92
|
+
constraint.solveVelocity(dt/3);
|
|
93
|
+
}
|
|
94
|
+
this.applyAccelerations(objects, dt);
|
|
95
|
+
}
|
|
96
|
+
//correct positions (stop colliding objects from overlapping)
|
|
97
|
+
this.solver.resolvePositions(contacts);
|
|
98
|
+
for(let constraint of this.constraints){
|
|
99
|
+
constraint.solvePosition(dt);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
applyAccelerations(objects: PhysicsObject[], dt: number){
|
|
104
|
+
for(let i = 0; i < objects.length; i++){
|
|
105
|
+
objects[i].lastPosition.subtract(vec2.times(objects[i].acceleration, dt));
|
|
106
|
+
objects[i].lastAngle.subtract(Rotation.times(objects[i].angularAccerleration, dt));
|
|
107
|
+
|
|
108
|
+
objects[i].velocity.add(vec2.times(objects[i].acceleration, 1))
|
|
109
|
+
objects[i].angularVelocity.add(Rotation.times(objects[i].angularAccerleration, 1))
|
|
110
|
+
|
|
111
|
+
objects[i].acceleration = vec2.zero();
|
|
112
|
+
objects[i].angularAccerleration = Rotation.zero();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
testHitbox(hitbox: Shape){
|
|
117
|
+
let dummyObject = new PhysicsObject(vec2.zero(), hitbox, {skipCOMcalc: true});
|
|
118
|
+
let contacts: Contact[] = [];
|
|
119
|
+
for(let o of this.root.getAllObjects()){
|
|
120
|
+
contacts.push(...Collision(o, dummyObject));
|
|
121
|
+
}
|
|
122
|
+
return contacts;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|