UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

736 lines (733 loc) 23.8 kB
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 };