playcanvas
Version:
PlayCanvas WebGL game engine
736 lines (733 loc) • 23.8 kB
JavaScript
import { Mat4 } from '../../../core/math/mat4.js';
import { Quat } from '../../../core/math/quat.js';
import { Vec3 } from '../../../core/math/vec3.js';
import { SEMANTIC_POSITION } from '../../../platform/graphics/constants.js';
import { GraphNode } from '../../../scene/graph-node.js';
import { Model } from '../../../scene/model.js';
import { ComponentSystem } from '../system.js';
import { CollisionComponent } from './component.js';
import { CollisionComponentData } from './data.js';
import { Trigger } from './trigger.js';
const mat4 = new Mat4();
const p1 = new Vec3();
const p2 = new Vec3();
const quat = new Quat();
const tempGraphNode = new GraphNode();
const _schema = [
'enabled',
'type',
'halfExtents',
'linearOffset',
'angularOffset',
'radius',
'axis',
'height',
'convexHull',
'asset',
'renderAsset',
'shape',
'model',
'render',
'checkVertexDuplicates'
];
class CollisionSystemImpl {
constructor(system){
this.system = system;
}
beforeInitialize(component, data) {
data.shape = null;
data.model = new Model();
data.model.graph = new GraphNode();
}
afterInitialize(component, data) {
this.recreatePhysicalShapes(component);
component.data.initialized = true;
}
reset(component, data) {
this.beforeInitialize(component, data);
this.afterInitialize(component, data);
}
recreatePhysicalShapes(component) {
const entity = component.entity;
const data = component.data;
if (typeof Ammo !== 'undefined') {
if (entity.trigger) {
entity.trigger.destroy();
delete entity.trigger;
}
if (data.shape) {
if (component._compoundParent) {
if (component !== component._compoundParent) {
this.system._removeCompoundChild(component._compoundParent, data.shape);
}
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
this.destroyShape(data);
}
data.shape = this.createPhysicalShape(component.entity, data);
const firstCompoundChild = !component._compoundParent;
if (data.type === 'compound' && (!component._compoundParent || component === component._compoundParent)) {
component._compoundParent = component;
entity.forEach(this._addEachDescendant, component);
} else if (data.type !== 'compound') {
if (!component.rigidbody) {
component._compoundParent = null;
let parent = entity.parent;
while(parent){
if (parent.collision && parent.collision.type === 'compound') {
component._compoundParent = parent.collision;
break;
}
parent = parent.parent;
}
}
}
if (component._compoundParent) {
if (component !== component._compoundParent) {
if (firstCompoundChild && component._compoundParent.shape.getNumChildShapes() === 0) {
this.system.recreatePhysicalShapes(component._compoundParent);
} else {
this.system.updateCompoundChildTransform(entity, true);
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
}
}
if (entity.rigidbody) {
entity.rigidbody.disableSimulation();
entity.rigidbody.createBody();
if (entity.enabled && entity.rigidbody.enabled) {
entity.rigidbody.enableSimulation();
}
} else if (!component._compoundParent) {
if (!entity.trigger) {
entity.trigger = new Trigger(this.system.app, component, data);
} else {
entity.trigger.initialize(data);
}
}
}
}
createPhysicalShape(entity, data) {
return undefined;
}
updateTransform(component, position, rotation, scale) {
if (component.entity.trigger) {
component.entity.trigger.updateTransform();
}
}
destroyShape(data) {
if (data.shape) {
Ammo.destroy(data.shape);
data.shape = null;
}
}
beforeRemove(entity, component) {
if (component.data.shape) {
if (component._compoundParent && !component._compoundParent.entity._destroying) {
this.system._removeCompoundChild(component._compoundParent, component.data.shape);
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
component._compoundParent = null;
this.destroyShape(component.data);
}
}
remove(entity, data) {
if (entity.rigidbody && entity.rigidbody.body) {
entity.rigidbody.disableSimulation();
}
if (entity.trigger) {
entity.trigger.destroy();
delete entity.trigger;
}
}
clone(entity, clone) {
const src = this.system.store[entity.getGuid()];
const data = {
enabled: src.data.enabled,
type: src.data.type,
halfExtents: [
src.data.halfExtents.x,
src.data.halfExtents.y,
src.data.halfExtents.z
],
linearOffset: [
src.data.linearOffset.x,
src.data.linearOffset.y,
src.data.linearOffset.z
],
angularOffset: [
src.data.angularOffset.x,
src.data.angularOffset.y,
src.data.angularOffset.z,
src.data.angularOffset.w
],
radius: src.data.radius,
axis: src.data.axis,
height: src.data.height,
convexHull: src.data.convexHull,
asset: src.data.asset,
renderAsset: src.data.renderAsset,
model: src.data.model,
render: src.data.render,
checkVertexDuplicates: src.data.checkVertexDuplicates
};
return this.system.addComponent(clone, data);
}
}
class CollisionBoxSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
if (typeof Ammo !== 'undefined') {
const he = data.halfExtents;
const ammoHe = new Ammo.btVector3(he ? he.x : 0.5, he ? he.y : 0.5, he ? he.z : 0.5);
const shape = new Ammo.btBoxShape(ammoHe);
Ammo.destroy(ammoHe);
return shape;
}
return undefined;
}
}
class CollisionSphereSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
if (typeof Ammo !== 'undefined') {
return new Ammo.btSphereShape(data.radius);
}
return undefined;
}
}
class CollisionCapsuleSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
const axis = data.axis ?? 1;
const radius = data.radius ?? 0.5;
const height = Math.max((data.height ?? 2) - 2 * radius, 0);
let shape = null;
if (typeof Ammo !== 'undefined') {
switch(axis){
case 0:
shape = new Ammo.btCapsuleShapeX(radius, height);
break;
case 1:
shape = new Ammo.btCapsuleShape(radius, height);
break;
case 2:
shape = new Ammo.btCapsuleShapeZ(radius, height);
break;
}
}
return shape;
}
}
class CollisionCylinderSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
const axis = data.axis ?? 1;
const radius = data.radius ?? 0.5;
const height = data.height ?? 1;
let halfExtents = null;
let shape = null;
if (typeof Ammo !== 'undefined') {
switch(axis){
case 0:
halfExtents = new Ammo.btVector3(height * 0.5, radius, radius);
shape = new Ammo.btCylinderShapeX(halfExtents);
break;
case 1:
halfExtents = new Ammo.btVector3(radius, height * 0.5, radius);
shape = new Ammo.btCylinderShape(halfExtents);
break;
case 2:
halfExtents = new Ammo.btVector3(radius, radius, height * 0.5);
shape = new Ammo.btCylinderShapeZ(halfExtents);
break;
}
}
if (halfExtents) {
Ammo.destroy(halfExtents);
}
return shape;
}
}
class CollisionConeSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
const axis = data.axis ?? 1;
const radius = data.radius ?? 0.5;
const height = data.height ?? 1;
let shape = null;
if (typeof Ammo !== 'undefined') {
switch(axis){
case 0:
shape = new Ammo.btConeShapeX(radius, height);
break;
case 1:
shape = new Ammo.btConeShape(radius, height);
break;
case 2:
shape = new Ammo.btConeShapeZ(radius, height);
break;
}
}
return shape;
}
}
class CollisionMeshSystemImpl extends CollisionSystemImpl {
beforeInitialize(component, data) {}
createAmmoHull(mesh, node, shape, scale) {
const hull = new Ammo.btConvexHullShape();
const point = new Ammo.btVector3();
const positions = [];
mesh.getPositions(positions);
for(let i = 0; i < positions.length; i += 3){
point.setValue(positions[i] * scale.x, positions[i + 1] * scale.y, positions[i + 2] * scale.z);
hull.addPoint(point, false);
}
Ammo.destroy(point);
hull.recalcLocalAabb();
hull.setMargin(0.01);
const transform = this.system._getNodeTransform(node);
shape.addChildShape(transform, hull);
Ammo.destroy(transform);
}
createAmmoMesh(mesh, node, shape, scale, checkDupes = true) {
const system = this.system;
let triMesh;
if (system._triMeshCache[mesh.id]) {
triMesh = system._triMeshCache[mesh.id];
} else {
const vb = mesh.vertexBuffer;
const format = vb.getFormat();
let stride, positions;
for(let i = 0; i < format.elements.length; i++){
const element = format.elements[i];
if (element.name === SEMANTIC_POSITION) {
positions = new Float32Array(vb.lock(), element.offset);
stride = element.stride / 4;
break;
}
}
const indices = [];
mesh.getIndices(indices);
const numTriangles = mesh.primitive[0].count / 3;
const v1 = new Ammo.btVector3();
let i1, i2, i3;
const base = mesh.primitive[0].base;
triMesh = new Ammo.btTriangleMesh();
system._triMeshCache[mesh.id] = triMesh;
const vertexCache = new Map();
const indexedArray = triMesh.getIndexedMeshArray();
indexedArray.at(0).m_numTriangles = numTriangles;
const sx = scale ? scale.x : 1;
const sy = scale ? scale.y : 1;
const sz = scale ? scale.z : 1;
const addVertex = (index)=>{
const x = positions[index * stride] * sx;
const y = positions[index * stride + 1] * sy;
const z = positions[index * stride + 2] * sz;
let idx;
if (checkDupes) {
const str = `${x}:${y}:${z}`;
idx = vertexCache.get(str);
if (idx !== undefined) {
return idx;
}
v1.setValue(x, y, z);
idx = triMesh.findOrAddVertex(v1, false);
vertexCache.set(str, idx);
} else {
v1.setValue(x, y, z);
idx = triMesh.findOrAddVertex(v1, false);
}
return idx;
};
for(let i = 0; i < numTriangles; i++){
i1 = addVertex(indices[base + i * 3]);
i2 = addVertex(indices[base + i * 3 + 1]);
i3 = addVertex(indices[base + i * 3 + 2]);
triMesh.addIndex(i1);
triMesh.addIndex(i2);
triMesh.addIndex(i3);
}
Ammo.destroy(v1);
}
const triMeshShape = new Ammo.btBvhTriangleMeshShape(triMesh, true);
if (!scale) {
const scaling = system._getNodeScaling(node);
triMeshShape.setLocalScaling(scaling);
Ammo.destroy(scaling);
}
const transform = system._getNodeTransform(node);
shape.addChildShape(transform, triMeshShape);
Ammo.destroy(transform);
}
createPhysicalShape(entity, data) {
if (typeof Ammo === 'undefined') return undefined;
if (data.model || data.render) {
const shape = new Ammo.btCompoundShape();
const entityTransform = entity.getWorldTransform();
const scale = entityTransform.getScale();
if (data.render) {
const meshes = data.render.meshes;
for(let i = 0; i < meshes.length; i++){
if (data.convexHull) {
this.createAmmoHull(meshes[i], tempGraphNode, shape, scale);
} else {
this.createAmmoMesh(meshes[i], tempGraphNode, shape, scale, data.checkVertexDuplicates);
}
}
} else if (data.model) {
const meshInstances = data.model.meshInstances;
for(let i = 0; i < meshInstances.length; i++){
this.createAmmoMesh(meshInstances[i].mesh, meshInstances[i].node, shape, null, data.checkVertexDuplicates);
}
const vec = new Ammo.btVector3(scale.x, scale.y, scale.z);
shape.setLocalScaling(vec);
Ammo.destroy(vec);
}
return shape;
}
return undefined;
}
recreatePhysicalShapes(component) {
const data = component.data;
if (data.renderAsset || data.asset) {
if (component.enabled && component.entity.enabled) {
this.loadAsset(component, data.renderAsset || data.asset, data.renderAsset ? 'render' : 'model');
return;
}
}
this.doRecreatePhysicalShape(component);
}
loadAsset(component, id, property) {
const data = component.data;
const assets = this.system.app.assets;
const previousPropertyValue = data[property];
const onAssetFullyReady = (asset)=>{
if (data[property] !== previousPropertyValue) {
return;
}
data[property] = asset.resource;
this.doRecreatePhysicalShape(component);
};
const loadAndHandleAsset = (asset)=>{
asset.ready((asset)=>{
if (asset.data.containerAsset) {
const containerAsset = assets.get(asset.data.containerAsset);
if (containerAsset.loaded) {
onAssetFullyReady(asset);
} else {
containerAsset.ready(()=>{
onAssetFullyReady(asset);
});
assets.load(containerAsset);
}
} else {
onAssetFullyReady(asset);
}
});
assets.load(asset);
};
const asset = assets.get(id);
if (asset) {
loadAndHandleAsset(asset);
} else {
assets.once(`add:${id}`, loadAndHandleAsset);
}
}
doRecreatePhysicalShape(component) {
const entity = component.entity;
const data = component.data;
if (data.model || data.render) {
this.destroyShape(data);
data.shape = this.createPhysicalShape(entity, data);
if (entity.rigidbody) {
entity.rigidbody.disableSimulation();
entity.rigidbody.createBody();
if (entity.enabled && entity.rigidbody.enabled) {
entity.rigidbody.enableSimulation();
}
} else {
if (!entity.trigger) {
entity.trigger = new Trigger(this.system.app, component, data);
} else {
entity.trigger.initialize(data);
}
}
} else {
this.beforeRemove(entity, component);
this.remove(entity, data);
}
}
updateTransform(component, position, rotation, scale) {
if (component.shape) {
const entityTransform = component.entity.getWorldTransform();
const worldScale = entityTransform.getScale();
const previousScale = component.shape.getLocalScaling();
if (worldScale.x !== previousScale.x() || worldScale.y !== previousScale.y() || worldScale.z !== previousScale.z()) {
this.doRecreatePhysicalShape(component);
}
}
super.updateTransform(component, position, rotation, scale);
}
destroyShape(data) {
if (!data.shape) {
return;
}
const numShapes = data.shape.getNumChildShapes();
for(let i = 0; i < numShapes; i++){
const shape = data.shape.getChildShape(i);
Ammo.destroy(shape);
}
Ammo.destroy(data.shape);
data.shape = null;
}
}
class CollisionCompoundSystemImpl extends CollisionSystemImpl {
createPhysicalShape(entity, data) {
if (typeof Ammo !== 'undefined') {
return new Ammo.btCompoundShape();
}
return undefined;
}
_addEachDescendant(entity) {
if (!entity.collision || entity.rigidbody) {
return;
}
entity.collision._compoundParent = this;
if (entity !== this.entity) {
entity.collision.system.recreatePhysicalShapes(entity.collision);
}
}
_updateEachDescendant(entity) {
if (!entity.collision) {
return;
}
if (entity.collision._compoundParent !== this) {
return;
}
entity.collision._compoundParent = null;
if (entity !== this.entity && !entity.rigidbody) {
entity.collision.system.recreatePhysicalShapes(entity.collision);
}
}
_updateEachDescendantTransform(entity) {
if (!entity.collision || entity.collision._compoundParent !== this.collision._compoundParent) {
return;
}
this.collision.system.updateCompoundChildTransform(entity, false);
}
}
class CollisionComponentSystem extends ComponentSystem {
constructor(app){
super(app);
this.id = 'collision';
this.ComponentType = CollisionComponent;
this.DataType = CollisionComponentData;
this.schema = _schema;
this.implementations = {};
this._triMeshCache = {};
this.on('beforeremove', this.onBeforeRemove, this);
this.on('remove', this.onRemove, this);
}
initializeComponentData(component, _data, properties) {
properties = [
'type',
'halfExtents',
'radius',
'axis',
'height',
'convexHull',
'shape',
'model',
'asset',
'render',
'renderAsset',
'enabled',
'linearOffset',
'angularOffset',
'checkVertexDuplicates'
];
const data = {};
for(let i = 0, len = properties.length; i < len; i++){
const property = properties[i];
data[property] = _data[property];
}
let idx;
if (_data.hasOwnProperty('asset')) {
idx = properties.indexOf('model');
if (idx !== -1) {
properties.splice(idx, 1);
}
idx = properties.indexOf('render');
if (idx !== -1) {
properties.splice(idx, 1);
}
} else if (_data.hasOwnProperty('model')) {
idx = properties.indexOf('asset');
if (idx !== -1) {
properties.splice(idx, 1);
}
}
if (!data.type) {
data.type = component.data.type;
}
component.data.type = data.type;
if (Array.isArray(data.halfExtents)) {
data.halfExtents = new Vec3(data.halfExtents);
}
if (Array.isArray(data.linearOffset)) {
data.linearOffset = new Vec3(data.linearOffset);
}
if (Array.isArray(data.angularOffset)) {
const values = data.angularOffset;
if (values.length === 3) {
data.angularOffset = new Quat().setFromEulerAngles(values[0], values[1], values[2]);
} else {
data.angularOffset = new Quat(data.angularOffset);
}
}
const impl = this._createImplementation(data.type);
impl.beforeInitialize(component, data);
super.initializeComponentData(component, data, properties);
impl.afterInitialize(component, data);
}
_createImplementation(type) {
if (this.implementations[type] === undefined) {
let impl;
switch(type){
case 'box':
impl = new CollisionBoxSystemImpl(this);
break;
case 'sphere':
impl = new CollisionSphereSystemImpl(this);
break;
case 'capsule':
impl = new CollisionCapsuleSystemImpl(this);
break;
case 'cylinder':
impl = new CollisionCylinderSystemImpl(this);
break;
case 'cone':
impl = new CollisionConeSystemImpl(this);
break;
case 'mesh':
impl = new CollisionMeshSystemImpl(this);
break;
case 'compound':
impl = new CollisionCompoundSystemImpl(this);
break;
}
this.implementations[type] = impl;
}
return this.implementations[type];
}
_getImplementation(entity) {
return this.implementations[entity.collision.data.type];
}
cloneComponent(entity, clone) {
return this._getImplementation(entity).clone(entity, clone);
}
onBeforeRemove(entity, component) {
this.implementations[component.data.type].beforeRemove(entity, component);
component.onBeforeRemove();
}
onRemove(entity, data) {
this.implementations[data.type].remove(entity, data);
}
updateCompoundChildTransform(entity, forceUpdate) {
const parentComponent = entity.collision._compoundParent;
if (parentComponent === entity.collision) return;
if (entity.enabled && entity.collision.enabled && (entity._dirtyLocal || forceUpdate)) {
const transform = this._getNodeTransform(entity, parentComponent.entity);
const idx = parentComponent.getCompoundChildShapeIndex(entity.collision.shape);
if (idx === null) {
parentComponent.shape.addChildShape(transform, entity.collision.data.shape);
} else {
parentComponent.shape.updateChildTransform(idx, transform, true);
}
Ammo.destroy(transform);
}
}
_removeCompoundChild(collision, shape) {
if (collision.shape.getNumChildShapes() === 0) {
return;
}
if (collision.shape.removeChildShape) {
collision.shape.removeChildShape(shape);
} else {
const ind = collision.getCompoundChildShapeIndex(shape);
if (ind !== null) {
collision.shape.removeChildShapeByIndex(ind);
}
}
}
onTransformChanged(component, position, rotation, scale) {
this.implementations[component.data.type].updateTransform(component, position, rotation, scale);
}
changeType(component, previousType, newType) {
this.implementations[previousType].beforeRemove(component.entity, component);
this.implementations[previousType].remove(component.entity, component.data);
this._createImplementation(newType).reset(component, component.data);
}
recreatePhysicalShapes(component) {
this.implementations[component.data.type].recreatePhysicalShapes(component);
}
_calculateNodeRelativeTransform(node, relative) {
if (node === relative) {
const scale = node.getWorldTransform().getScale();
mat4.setScale(scale.x, scale.y, scale.z);
} else {
this._calculateNodeRelativeTransform(node.parent, relative);
mat4.mul(node.getLocalTransform());
}
}
_getNodeScaling(node) {
const wtm = node.getWorldTransform();
const scl = wtm.getScale();
return new Ammo.btVector3(scl.x, scl.y, scl.z);
}
_getNodeTransform(node, relative) {
let pos, rot;
if (relative) {
this._calculateNodeRelativeTransform(node, relative);
pos = p1;
rot = quat;
mat4.getTranslation(pos);
rot.setFromMat4(mat4);
} else {
pos = node.getPosition();
rot = node.getRotation();
}
const ammoQuat = new Ammo.btQuaternion();
const transform = new Ammo.btTransform();
transform.setIdentity();
const origin = transform.getOrigin();
const component = node.collision;
if (component && component._hasOffset) {
const lo = component.data.linearOffset;
const ao = component.data.angularOffset;
const newOrigin = p2;
quat.copy(rot).transformVector(lo, newOrigin);
newOrigin.add(pos);
quat.copy(rot).mul(ao);
origin.setValue(newOrigin.x, newOrigin.y, newOrigin.z);
ammoQuat.setValue(quat.x, quat.y, quat.z, quat.w);
} else {
origin.setValue(pos.x, pos.y, pos.z);
ammoQuat.setValue(rot.x, rot.y, rot.z, rot.w);
}
transform.setRotation(ammoQuat);
Ammo.destroy(ammoQuat);
return transform;
}
destroy() {
for(const key in this._triMeshCache){
Ammo.destroy(this._triMeshCache[key]);
}
this._triMeshCache = null;
super.destroy();
}
}
export { CollisionComponentSystem };