playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
488 lines (487 loc) • 14.4 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 { Trigger } from "./trigger.js";
const mat4 = new Mat4();
const p1 = new Vec3();
const p2 = new Vec3();
const p3 = new Vec3();
const quat = new Quat();
const quat2 = new Quat();
const _properties = [
"halfExtents",
"radius",
"axis",
"height",
"convexHull",
"model",
"asset",
"render",
"renderAsset",
"linearOffset",
"angularOffset",
"checkVertexDuplicates"
];
const collisionImpls = {
box: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "box",
halfExtents: component.halfExtents
})
},
sphere: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "sphere",
radius: component.radius
})
},
capsule: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "capsule",
axis: component.axis,
radius: component.radius,
height: component.height
})
},
cylinder: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "cylinder",
axis: component.axis,
radius: component.radius,
height: component.height
})
},
cone: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "cone",
axis: component.axis,
radius: component.radius,
height: component.height
})
},
mesh: {
// the model comes from assets - no placeholder is created
beforeInitialize() {
},
createPhysicalShape: createMeshShape,
recreatePhysicalShapes: recreateMeshShapes,
updateTransform: updateMeshTransform
},
compound: {
createPhysicalShape: (system, entity, component) => system.physicsWorld?.createShape({
type: "compound"
})
}
};
function getImpl(type) {
const impl = collisionImpls[type];
return impl;
}
function beforeInitialize(system, component) {
component._shape = null;
const model = new Model();
model.graph = new GraphNode();
component._model = model;
}
function afterInitialize(system, component) {
system.recreatePhysicalShapes(component);
component._initialized = true;
}
function recreateBody(entity) {
entity.rigidbody.disableSimulation();
entity.rigidbody.createBody();
if (entity.enabled && entity.rigidbody.enabled) {
entity.rigidbody.enableSimulation();
}
}
function recreateTrigger(system, entity, component) {
if (!entity.trigger) {
entity.trigger = new Trigger(system.app, component);
} else {
entity.trigger.initialize();
}
}
function destroyShape(system, component) {
if (component._shape) {
system.physicsWorld.destroyShape(component._shape);
component._shape = null;
}
}
function beforeRemove(system, entity, component) {
if (component._shape) {
if (component._compoundParent && !component._compoundParent.entity._destroying) {
system._removeCompoundChild(component._compoundParent, component._shape);
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
component._compoundParent = null;
destroyShape(system, component);
}
}
function recreateShapes(system, component) {
const entity = component.entity;
const world = system.physicsWorld;
if (world) {
if (entity.trigger) {
entity.trigger.destroy();
delete entity.trigger;
}
if (component._shape) {
if (component._compoundParent) {
if (component !== component._compoundParent) {
system._removeCompoundChild(component._compoundParent, component._shape);
}
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
destroyShape(system, component);
}
component._shape = getImpl(component._type).createPhysicalShape(system, entity, component);
const firstCompoundChild = !component._compoundParent;
if (component._type === "compound" && (!component._compoundParent || component === component._compoundParent)) {
component._compoundParent = component;
entity.forEach(system._addEachDescendant, component);
} else if (component._type !== "compound") {
if (!entity.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 && world.getCompoundChildCount(component._compoundParent.shape) === 0) {
system.recreatePhysicalShapes(component._compoundParent);
} else {
system.updateCompoundChildTransform(entity, true);
if (component._compoundParent.entity.rigidbody) {
component._compoundParent.entity.rigidbody.activate();
}
}
}
}
if (entity.rigidbody) {
recreateBody(entity);
} else if (!component._compoundParent) {
recreateTrigger(system, entity, component);
}
}
}
function updateShapeTransform(system, component, position, rotation, scale) {
if (component.entity.trigger) {
component.entity.trigger.updateTransform();
}
}
function createMeshSource(system, mesh, node, bakeScale, convexHull, checkDuplicates) {
let positions = null;
let stride = 0;
let indices = null;
let extracted = false;
const extract = () => {
if (extracted) return;
extracted = true;
if (convexHull) {
positions = [];
mesh.getPositions(positions);
stride = 3;
} else {
const vb = mesh.vertexBuffer;
const format = vb.getFormat();
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;
}
}
indices = [];
mesh.getIndices(indices);
}
};
const source = {
id: mesh.id,
get positions() {
extract();
return positions;
},
get stride() {
extract();
return stride;
},
get indices() {
extract();
return indices;
},
base: mesh.primitive[0].base,
count: mesh.primitive[0].count,
convexHull,
checkDuplicates,
bakeScale,
shapeScale: node ? system._getNodeScaling(node) : null,
position: new Vec3(),
rotation: new Quat()
};
if (node) {
system._getNodeTransform(node, null, source.position, source.rotation);
}
return source;
}
function createMeshShape(system, entity, component) {
const world = system.physicsWorld;
if (!world) return void 0;
if (component._model || component._render) {
const entityTransform = entity.getWorldTransform();
const scale = entityTransform.getScale();
const sources = [];
let shapeScale = null;
if (component._render) {
const meshes = component._render.meshes;
for (let i = 0; i < meshes.length; i++) {
sources.push(createMeshSource(system, meshes[i], null, scale, component._convexHull, component._checkVertexDuplicates));
}
} else if (component._model) {
const meshInstances = component._model.meshInstances;
for (let i = 0; i < meshInstances.length; i++) {
sources.push(createMeshSource(system, meshInstances[i].mesh, meshInstances[i].node, null, false, component._checkVertexDuplicates));
}
shapeScale = scale;
}
component._builtWorldScale = scale;
return world.createShape({
type: "mesh",
sources,
scale: shapeScale
});
}
return void 0;
}
function doRecreateMeshShape(system, component) {
const entity = component.entity;
if (component._model || component._render) {
destroyShape(system, component);
component._shape = createMeshShape(system, entity, component);
if (entity.rigidbody) {
recreateBody(entity);
} else {
recreateTrigger(system, entity, component);
}
} else {
beforeRemove(system, entity, component);
system.onRemove(entity);
}
}
function loadMeshAsset(system, component, id, property) {
const assets = system.app.assets;
const privateProperty = `_${property}`;
const previousPropertyValue = component[privateProperty];
const onAssetFullyReady = (asset2) => {
if (component[privateProperty] !== previousPropertyValue) {
return;
}
component[privateProperty] = asset2.resource;
doRecreateMeshShape(system, component);
};
const loadAndHandleAsset = (asset2) => {
asset2.ready((asset3) => {
if (asset3.data.containerAsset) {
const containerAsset = assets.get(asset3.data.containerAsset);
if (containerAsset.loaded) {
onAssetFullyReady(asset3);
} else {
containerAsset.ready(() => {
onAssetFullyReady(asset3);
});
assets.load(containerAsset);
}
} else {
onAssetFullyReady(asset3);
}
});
assets.load(asset2);
};
const asset = assets.get(id);
if (asset) {
loadAndHandleAsset(asset);
} else {
assets.once(`add:${id}`, loadAndHandleAsset);
}
}
function recreateMeshShapes(system, component) {
if (component._renderAsset || component._asset) {
if (component.enabled && component.entity.enabled) {
loadMeshAsset(
system,
component,
component._renderAsset || component._asset,
component._renderAsset ? "render" : "model"
);
return;
}
}
doRecreateMeshShape(system, component);
}
function updateMeshTransform(system, component, position, rotation, scale) {
if (component.shape && component._builtWorldScale) {
const entityTransform = component.entity.getWorldTransform();
const worldScale = entityTransform.getScale();
if (!worldScale.equals(component._builtWorldScale)) {
doRecreateMeshShape(system, component);
}
}
updateShapeTransform(system, component, position, rotation, scale);
}
class CollisionComponentSystem extends ComponentSystem {
constructor(app) {
super(app);
this.id = "collision";
this.ComponentType = CollisionComponent;
this.on("beforeremove", this.onBeforeRemove, this);
this.on("remove", this.onRemove, this);
}
get physicsWorld() {
return this.app.systems.rigidbody?.physicsWorld ?? null;
}
initializeComponentData(component, data) {
if (data.type) {
component._type = data.type;
}
let properties = _properties;
if (data.asset !== void 0) {
properties = properties.filter((p) => p !== "model" && p !== "render");
} else if (data.model !== void 0) {
properties = properties.filter((p) => p !== "asset");
}
for (const property of properties) {
if (data[property] !== void 0) {
component[property] = data[property];
}
}
const impl = getImpl(component._type);
(impl.beforeInitialize ?? beforeInitialize)(this, component);
super.initializeComponentData(component, data);
afterInitialize(this, component);
}
cloneComponent(entity, clone) {
const c = entity.collision;
const data = {
enabled: c.enabled,
type: c.type
};
for (const property of _properties) {
data[property] = c[property];
}
return this.addComponent(clone, data);
}
onBeforeRemove(entity, component) {
beforeRemove(this, entity, component);
component.onBeforeRemove();
if (this.app.systems.rigidbody) {
this.app.systems.rigidbody.clearEntityCollisions(entity);
}
}
onRemove(entity) {
if (entity.rigidbody && entity.rigidbody._body) {
entity.rigidbody.disableSimulation();
}
if (entity.trigger) {
entity.trigger.destroy();
delete entity.trigger;
}
}
updateCompoundChildTransform(entity, forceUpdate) {
const parentComponent = entity.collision._compoundParent;
if (parentComponent === entity.collision) return;
if (entity.enabled && entity.collision.enabled && (entity._dirtyLocal || forceUpdate)) {
this._getNodeTransform(entity, parentComponent.entity, p3, quat2);
this.physicsWorld.updateCompoundChild(parentComponent.shape, entity.collision.shape, p3, quat2);
}
}
_removeCompoundChild(collision, shape) {
this.physicsWorld.removeCompoundChild(collision.shape, shape);
}
onTransformChanged(component, position, rotation, scale) {
const impl = getImpl(component.type);
(impl.updateTransform ?? updateShapeTransform)(this, component, position, rotation, scale);
}
// Destroys the previous collision type and creates a new one based on the new type provided
changeType(component, previousType, newType) {
beforeRemove(this, component.entity, component);
this.onRemove(component.entity);
const impl = getImpl(newType);
(impl.beforeInitialize ?? beforeInitialize)(this, component);
afterInitialize(this, component);
}
// Recreates rigid bodies or triggers for the specified component
recreatePhysicalShapes(component) {
const impl = getImpl(component.type);
(impl.recreatePhysicalShapes ?? recreateShapes)(this, component);
}
doRecreatePhysicalShape(component) {
doRecreateMeshShape(this, component);
}
_addEachDescendant(entity) {
if (!entity.collision || entity.rigidbody) {
return;
}
entity.collision._compoundParent = this;
if (entity !== this.entity) {
entity.collision.system.recreatePhysicalShapes(entity.collision);
}
}
_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) {
return node.getWorldTransform().getScale();
}
_getNodeTransform(node, relative, position, rotation) {
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 component = node.collision;
if (component && component._hasOffset) {
const lo = component.linearOffset;
const ao = component.angularOffset;
const newOrigin = p2;
quat.copy(rot).transformVector(lo, newOrigin);
newOrigin.add(pos);
quat.copy(rot).mul(ao);
position.copy(newOrigin);
rotation.copy(quat);
} else {
position.copy(pos);
rotation.copy(rot);
}
}
}
export {
CollisionComponentSystem
};