forked from c-frame/aframe-extras
-
Notifications
You must be signed in to change notification settings - Fork 0
/
kinematic-body.js
219 lines (188 loc) · 7.68 KB
/
kinematic-body.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
/* global CANNON */
/**
* Kinematic body.
*
* Managed dynamic body, which moves but is not affected (directly) by the
* physics engine. This is not a true kinematic body, in the sense that we are
* letting the physics engine _compute_ collisions against it and selectively
* applying those collisions to the object. The physics engine does not decide
* the position/velocity/rotation of the element.
*
* Used for the camera object, because full physics simulation would create
* movement that feels unnatural to the player. Bipedal movement does not
* translate nicely to rigid body physics.
*
* See: http://www.learn-cocos2d.com/2013/08/physics-engine-platformer-terrible-idea/
* And: http://oxleygamedev.blogspot.com/2011/04/player-physics-part-2.html
*/
const EPS = 0.000001;
module.exports = AFRAME.registerComponent('kinematic-body', {
dependencies: ['velocity'],
/*******************************************************************
* Schema
*/
schema: {
mass: { default: 5 },
radius: { default: 1.3 },
userHeight: { default: 1.6 },
linearDamping: { default: 0.05 },
enableSlopes: { default: true }
},
/*******************************************************************
* Lifecycle
*/
init: function () {
this.system = this.el.sceneEl.systems.physics;
this.system.addComponent(this);
const el = this.el,
data = this.data,
position = (new CANNON.Vec3()).copy(el.getAttribute('position'));
this.body = new CANNON.Body({
material: this.system.getMaterial('staticMaterial'),
position: position,
mass: data.mass,
linearDamping: data.linearDamping,
fixedRotation: true
});
this.body.addShape(
new CANNON.Sphere(data.radius),
new CANNON.Vec3(0, data.radius - data.height, 0)
);
this.body.el = this.el;
this.el.body = this.body;
this.system.addBody(this.body);
if (el.hasAttribute('wasd-controls')) {
console.warn('[kinematic-body] Not compatible with wasd-controls, use movement-controls.');
}
},
remove: function () {
this.system.removeBody(this.body);
this.system.removeComponent(this);
delete this.el.body;
},
/*******************************************************************
* Update
*/
/**
* Checks CANNON.World for collisions and attempts to apply them to the
* element automatically, in a player-friendly way.
*
* There's extra logic for horizontal surfaces here. The basic requirements:
* (1) Only apply gravity when not in contact with _any_ horizontal surface.
* (2) When moving, project the velocity against exactly one ground surface.
* If in contact with two ground surfaces (e.g. ground + ramp), choose
* the one that collides with current velocity, if any.
*/
beforeStep: function (t, dt) {
if (!dt) return;
const el = this.el;
const body = this.body;
body.velocity.copy(el.getAttribute('velocity'));
body.position.copy(el.getAttribute('position'));
body.position.y += this.data.userHeight;
},
step: (function () {
var velocity = new THREE.Vector3(),
normalizedVelocity = new THREE.Vector3(),
currentSurfaceNormal = new THREE.Vector3(),
groundNormal = new THREE.Vector3();
return function (t, dt) {
if (!dt) return;
let body = this.body,
data = this.data,
didCollide = false,
height, groundHeight = -Infinity,
groundBody,
contacts = this.system.getContacts();
dt = Math.min(dt, this.system.data.maxInterval * 1000);
groundNormal.set(0, 0, 0);
velocity.copy(this.el.getAttribute('velocity'));
body.velocity.copy(velocity);
for (var i = 0, contact; contact = contacts[i]; i++) {
// 1. Find any collisions involving this element. Get the contact
// normal, and make sure it's oriented _out_ of the other object and
// enabled (body.collisionReponse is true for both bodies)
if (!contact.enabled) { continue; }
if (body.id === contact.bi.id) {
contact.ni.negate(currentSurfaceNormal);
} else if (body.id === contact.bj.id) {
currentSurfaceNormal.copy(contact.ni);
} else {
continue;
}
didCollide = body.velocity.dot(currentSurfaceNormal) < -EPS;
if (didCollide && currentSurfaceNormal.y <= 0.5) {
// 2. If current trajectory attempts to move _through_ another
// object, project the velocity against the collision plane to
// prevent passing through.
velocity = velocity.projectOnPlane(currentSurfaceNormal);
} else if (currentSurfaceNormal.y > 0.5) {
// 3. If in contact with something roughly horizontal (+/- 45º) then
// consider that the current ground. Only the highest qualifying
// ground is retained.
height = body.id === contact.bi.id
? Math.abs(contact.rj.y + contact.bj.position.y)
: Math.abs(contact.ri.y + contact.bi.position.y);
if (height > groundHeight) {
groundHeight = height;
groundNormal.copy(currentSurfaceNormal);
groundBody = body.id === contact.bi.id ? contact.bj : contact.bi;
}
}
}
normalizedVelocity.copy(velocity).normalize();
if (groundBody && normalizedVelocity.y < 0.5) {
if (!data.enableSlopes) {
groundNormal.set(0, 1, 0);
} else if (groundNormal.y < 1 - EPS) {
groundNormal.copy(this.raycastToGround(groundBody, groundNormal));
}
// 4. Project trajectory onto the top-most ground object, unless
// trajectory is > 45º.
velocity = velocity.projectOnPlane(groundNormal);
} else if (this.system.driver.world) {
// 5. If not in contact with anything horizontal, apply world gravity.
// TODO - Why is the 4x scalar necessary.
// NOTE: Does not work if physics runs on a worker.
velocity.add(this.system.driver.world.gravity.scale(dt * 4.0 / 1000));
}
// 6. If the ground surface has a velocity, apply it directly to current
// position, not velocity, to preserve relative velocity.
if (groundBody && groundBody.el && groundBody.el.components.velocity) {
const groundVelocity = groundBody.el.getAttribute('velocity');
body.position.copy({
x: body.position.x + groundVelocity.x * dt / 1000,
y: body.position.y + groundVelocity.y * dt / 1000,
z: body.position.z + groundVelocity.z * dt / 1000
});
}
body.velocity.copy(velocity);
body.position.y -= data.userHeight;
this.el.setAttribute('velocity', body.velocity);
this.el.setAttribute('position', body.position);
};
}()),
/**
* When walking on complex surfaces (trimeshes, borders between two shapes),
* the collision normals returned for the player sphere can be very
* inconsistent. To address this, raycast straight down, find the collision
* normal, and return whichever normal is more vertical.
* @param {CANNON.Body} groundBody
* @param {CANNON.Vec3} groundNormal
* @return {CANNON.Vec3}
*/
raycastToGround: function (groundBody, groundNormal) {
let ray,
hitNormal,
vFrom = this.body.position,
vTo = this.body.position.clone();
vTo.y -= this.data.height;
ray = new CANNON.Ray(vFrom, vTo);
ray._updateDirection(); // TODO - Report bug.
ray.intersectBody(groundBody);
if (!ray.hasHit) return groundNormal;
// Compare ABS, in case we're projecting against the inside of the face.
hitNormal = ray.result.hitNormalWorld;
return Math.abs(hitNormal.y) > Math.abs(groundNormal.y) ? hitNormal : groundNormal;
}
});