@matematrolii/sketchbook
Version:
3D matematrolii playground built on three.js and cannon.js
349 lines (292 loc) • 10.5 kB
text/typescript
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;
}
}
}