UNPKG

@matematrolii/sketchbook

Version:

3D matematrolii playground built on three.js and cannon.js

349 lines (292 loc) 10.5 kB
import * as THREE from 'three'; // @ts-ignore import * as CANNON from 'cannon'; import * as _ from 'lodash'; import * as Utils from '../core/FunctionLibrary'; import { VectorSpringSimulator } from '../physics/spring_simulation/VectorSpringSimulator'; import { RelativeSpringSimulator } from '../physics/spring_simulation/RelativeSpringSimulator'; import { World } from '../world/World'; import { IWorldEntity } from '../interfaces/IWorldEntity'; import { CollisionGroups } from '../enums/CollisionGroups'; import { CapsuleCollider } from '../physics/colliders/CapsuleCollider'; import { EntityType } from '../enums/EntityType'; export class Item extends THREE.Object3D implements IWorldEntity { public updateOrder: number = 1; public entityType: EntityType = EntityType.Item; public height: number = 0; public tiltContainer: THREE.Group; public modelContainer: THREE.Group; public materials: THREE.Material[] = []; // Movement public velocity: THREE.Vector3 = new THREE.Vector3(); public arcadeVelocityInfluence: THREE.Vector3 = new THREE.Vector3(); public velocityTarget: THREE.Vector3 = new THREE.Vector3(); public arcadeVelocityIsAdditive: boolean = false; public defaultVelocitySimulatorDamping: number = 0.8; public defaultVelocitySimulatorMass: number = 50; public velocitySimulator: VectorSpringSimulator; public moveSpeed: number = 4; public orientation: THREE.Vector3 = new THREE.Vector3(0, 0, 1); public orientationTarget: THREE.Vector3 = new THREE.Vector3(0, 0, 1); public defaultRotationSimulatorDamping: number = 0.5; public defaultRotationSimulatorMass: number = 10; public rotationSimulator: RelativeSpringSimulator; public viewVector: THREE.Vector3; public itemBox: CapsuleCollider; public world: World; public isPicked: boolean = false; private physicsEnabled: boolean = true; private gltf: any = null; constructor(gltf: any, texture: string) { super(); this.gltf = gltf; this.readItemData(gltf, texture); // The visuals group is centered for easy character tilting this.tiltContainer = new THREE.Group(); this.add(this.tiltContainer); // Model container is used to reliably ground the character, as animation can alter the position of the model itself this.modelContainer = new THREE.Group(); this.modelContainer.position.y = -0.57; this.tiltContainer.add(this.modelContainer); this.modelContainer.add(gltf.scene); this.velocitySimulator = new VectorSpringSimulator(60, this.defaultVelocitySimulatorMass, this.defaultVelocitySimulatorDamping); this.rotationSimulator = new RelativeSpringSimulator(60, this.defaultRotationSimulatorMass, this.defaultRotationSimulatorDamping); this.viewVector = new THREE.Vector3(); // Physics // Player Capsule this.itemBox = new CapsuleCollider({ mass: 1, position: new CANNON.Vec3(), height: 0.5, radius: 0.25, segments: 8, friction: 1 }); // capsulePhysics.physical.collisionFilterMask = ~CollisionGroups.Trimesh; this.itemBox.body.shapes.forEach((shape) => { // tslint:disable-next-line: no-bitwise shape.collisionFilterMask = ~CollisionGroups.TrimeshColliders; }); this.itemBox.body.allowSleep = true; // Move character to different collision group for raycasting this.itemBox.body.collisionFilterGroup = 2; // Disable character rotation this.itemBox.body.fixedRotation = true; this.itemBox.body.updateMassProperties(); // Physics pre/post step callback bindings this.itemBox.body.preStep = (body: CANNON.Body) => { this.physicsPreStep(body, this); }; this.itemBox.body.postStep = (body: CANNON.Body) => { this.physicsPostStep(body, this); }; } public setArcadeVelocityInfluence(x: number, y: number = x, z: number = x): void { this.arcadeVelocityInfluence.set(x, y, z); } public setViewVector(vector: THREE.Vector3): void { this.viewVector.copy(vector).normalize(); } public setPosition(x: number, y: number, z: number): void { if (this.physicsEnabled) { this.itemBox.body.previousPosition = new CANNON.Vec3(x, y, z); this.itemBox.body.position = new CANNON.Vec3(x, y, z); this.itemBox.body.interpolatedPosition = new CANNON.Vec3(x, y, z); } else { this.position.x = x; this.position.y = y; this.position.z = z; } } public resetVelocity(): void { this.velocity.x = 0; this.velocity.y = 0; this.velocity.z = 0; this.itemBox.body.velocity.x = 0; this.itemBox.body.velocity.y = 0; this.itemBox.body.velocity.z = 0; this.velocitySimulator.init(); } public setArcadeVelocityTarget(velZ: number, velX: number = 0, velY: number = 0): void { this.velocityTarget.z = velZ; this.velocityTarget.x = velX; this.velocityTarget.y = velY; } public setOrientation(vector: THREE.Vector3, instantly: boolean = false): void { let lookVector = new THREE.Vector3().copy(vector).setY(0).normalize(); this.orientationTarget.copy(lookVector); if (instantly) { this.orientation.copy(lookVector); } } public setPhysicsEnabled(value: boolean): void { this.physicsEnabled = value; if (value === true) { this.world.physicsWorld.addBody(this.itemBox.body); } else { this.world.physicsWorld.remove(this.itemBox.body); } } public readItemData(gltf: any, textureUrl: string): void { const image = new Image(); image.crossOrigin = "Anonymous"; const texture = new THREE.Texture(image); image.onload = () => { texture.needsUpdate = true; }; image.src = textureUrl; texture.flipY = false; const material = new THREE.MeshPhongMaterial({ shininess: 0, color: 0xff0000 }); material.skinning = false; gltf.scene.traverse((child) => { if (child.isMesh) { //Utils.setupMeshProperties(child); if (child.material.isGLTFSpecularGlossinessMaterial) { child.onBeforeRender = function () {}; } child.material = material; child.castShadow = true; child.receiveShadow = true; const scale = 0.006; child.scale.set(scale, scale, scale); this.materials.push(material); } }); } public update(timeStep: number): void { this.rotation.y += 0.01; // Sync physics/graphics if (this.physicsEnabled) { this.position.set( this.itemBox.body.interpolatedPosition.x, this.itemBox.body.interpolatedPosition.y, this.itemBox.body.interpolatedPosition.z ); } else if (this.itemBox) { let newPos = new THREE.Vector3(); this.getWorldPosition(newPos); this.itemBox.body.position.copy(Utils.cannonVector(newPos)); this.itemBox.body.interpolatedPosition.copy(Utils.cannonVector(newPos)); } this.updateMatrixWorld(); } public physicsPreStep(body: CANNON.Body, character: Item): void { } public physicsPostStep(body: CANNON.Body, character: Item): void { // Get velocities let simulatedVelocity = new THREE.Vector3(body.velocity.x, body.velocity.y, body.velocity.z); // Take local velocity let arcadeVelocity = new THREE.Vector3().copy(character.velocity).multiplyScalar(character.moveSpeed); // Turn local into global arcadeVelocity = Utils.appplyVectorMatrixXZ(character.orientation, arcadeVelocity); let newVelocity = new THREE.Vector3(); // Additive velocity mode if (character.arcadeVelocityIsAdditive) { newVelocity.copy(simulatedVelocity); let globalVelocityTarget = Utils.appplyVectorMatrixXZ(character.orientation, character.velocityTarget); let add = new THREE.Vector3().copy(arcadeVelocity).multiply(character.arcadeVelocityInfluence); if (Math.abs(simulatedVelocity.x) < Math.abs(globalVelocityTarget.x * character.moveSpeed) || Utils.haveDifferentSigns(simulatedVelocity.x, arcadeVelocity.x)) { newVelocity.x += add.x; } if (Math.abs(simulatedVelocity.y) < Math.abs(globalVelocityTarget.y * character.moveSpeed) || Utils.haveDifferentSigns(simulatedVelocity.y, arcadeVelocity.y)) { newVelocity.y += add.y; } if (Math.abs(simulatedVelocity.z) < Math.abs(globalVelocityTarget.z * character.moveSpeed) || Utils.haveDifferentSigns(simulatedVelocity.z, arcadeVelocity.z)) { newVelocity.z += add.z; } } else { newVelocity = new THREE.Vector3( THREE.MathUtils.lerp(simulatedVelocity.x, arcadeVelocity.x, character.arcadeVelocityInfluence.x), THREE.MathUtils.lerp(simulatedVelocity.y, arcadeVelocity.y, character.arcadeVelocityInfluence.y), THREE.MathUtils.lerp(simulatedVelocity.z, arcadeVelocity.z, character.arcadeVelocityInfluence.z), ); } } public addToWorld(world: World): void { if (_.includes(world.items, this)) { console.warn('Adding item to a world in which it already exists.'); } else { // Set world this.world = world; // Register character world.items.push(this); // Register physics world.physicsWorld.addBody(this.itemBox.body); // Add to graphicsWorld world.graphicsWorld.add(this); // Shadow cascades this.materials.forEach((mat) => { world.sky.csm.setupMaterial(mat); }); } } public removeFromWorld(world: World): void { if (!_.includes(world.items, this)) { console.warn('Removing item from a world in which it isn\'t present.'); } else { this.world = undefined; // Remove from items _.pull(world.items, this); // Remove physics world.physicsWorld.remove(this.itemBox.body); // Remove visuals world.graphicsWorld.remove(this); this.modelContainer.remove(this.gltf.scene); this.tiltContainer.remove(this.modelContainer); this.remove(this.tiltContainer); this.materials.forEach(material => material.dispose()); this.itemBox = undefined; this.materials = undefined; this.modelContainer = undefined; this.tiltContainer = undefined; this.velocitySimulator = undefined; this.rotationSimulator = undefined; this.viewVector = undefined; this.updateOrder = undefined; this.entityType = undefined; this.height = undefined; this.velocity = undefined; this.arcadeVelocityInfluence = undefined; this.velocityTarget = undefined; this.arcadeVelocityIsAdditive = undefined; this.defaultVelocitySimulatorDamping = undefined; this.defaultVelocitySimulatorMass = undefined; this.moveSpeed = undefined; this.orientation = undefined; this.orientationTarget = undefined; this.defaultRotationSimulatorDamping = undefined; this.defaultRotationSimulatorMass = undefined; this.physicsEnabled = undefined; this.type = undefined; this.isPicked = undefined; } } }