@matematrolii/sketchbook
Version:
3D matematrolii playground built on three.js and cannon.js
394 lines (342 loc) • 10.3 kB
text/typescript
import * as THREE from "three";
// @ts-ignore
import * as CANNON from "cannon";
import * as _ from "lodash";
import { SimulationFrame } from "../physics/spring_simulation/SimulationFrame";
import { World } from "../world/World";
import { Side } from "../enums/Side";
import { Object3D } from "three";
import { Space } from "../enums/Space";
import { ModelProps, ModelType } from "../enums/WorldType";
import { IWorldOptions } from "../interfaces/IWorldSettings";
const rootMaterial = new THREE.MeshPhongMaterial({
shininess: 0,
skinning: true,
});
export function createCapsuleGeometry(
radius: number = 1,
height: number = 2,
N: number = 32
): THREE.Geometry {
const geometry = new THREE.Geometry();
const TWOPI = Math.PI * 2;
const PID2 = 1.570796326794896619231322;
const normals = [];
// top cap
for (let i = 0; i <= N / 4; i++) {
for (let j = 0; j <= N; j++) {
let theta = (j * TWOPI) / N;
let phi = -PID2 + (Math.PI * i) / (N / 2);
let vertex = new THREE.Vector3();
let normal = new THREE.Vector3();
vertex.x = radius * Math.cos(phi) * Math.cos(theta);
vertex.y = radius * Math.cos(phi) * Math.sin(theta);
vertex.z = radius * Math.sin(phi);
vertex.z -= height / 2;
normal.x = vertex.x;
normal.y = vertex.y;
normal.z = vertex.z;
geometry.vertices.push(vertex);
normals.push(normal);
}
}
// bottom cap
for (let i = N / 4; i <= N / 2; i++) {
for (let j = 0; j <= N; j++) {
let theta = (j * TWOPI) / N;
let phi = -PID2 + (Math.PI * i) / (N / 2);
let vertex = new THREE.Vector3();
let normal = new THREE.Vector3();
vertex.x = radius * Math.cos(phi) * Math.cos(theta);
vertex.y = radius * Math.cos(phi) * Math.sin(theta);
vertex.z = radius * Math.sin(phi);
vertex.z += height / 2;
normal.x = vertex.x;
normal.y = vertex.y;
normal.z = vertex.z;
geometry.vertices.push(vertex);
normals.push(normal);
}
}
for (let i = 0; i <= N / 2; i++) {
for (let j = 0; j < N; j++) {
let vec = new THREE.Vector4(
i * (N + 1) + j,
i * (N + 1) + (j + 1),
(i + 1) * (N + 1) + (j + 1),
(i + 1) * (N + 1) + j
);
if (i === N / 4) {
let face1 = new THREE.Face3(vec.x, vec.y, vec.z, [
normals[vec.x],
normals[vec.y],
normals[vec.z],
]);
let face2 = new THREE.Face3(vec.x, vec.z, vec.w, [
normals[vec.x],
normals[vec.z],
normals[vec.w],
]);
geometry.faces.push(face2);
geometry.faces.push(face1);
} else {
let face1 = new THREE.Face3(vec.x, vec.y, vec.z, [
normals[vec.x],
normals[vec.y],
normals[vec.z],
]);
let face2 = new THREE.Face3(vec.x, vec.z, vec.w, [
normals[vec.x],
normals[vec.z],
normals[vec.w],
]);
geometry.faces.push(face1);
geometry.faces.push(face2);
}
}
// if(i==(N/4)) break; // N/4 is when the center segments are solved
}
geometry.rotateX(Math.PI / 2);
geometry.computeVertexNormals();
geometry.computeFaceNormals();
return geometry;
}
//#endregion
//#region Math
/**
* Constructs a 2D matrix from first vector, replacing the Y axes with the global Y axis,
* and applies this matrix to the second vector. Saves performance when compared to full 3D matrix application.
* Useful for character rotation, as it only happens on the Y axis.
* @param {Vector3} a Vector to construct 2D matrix from
* @param {Vector3} b Vector to apply basis to
*/
export function appplyVectorMatrixXZ(
a: THREE.Vector3,
b: THREE.Vector3
): THREE.Vector3 {
return new THREE.Vector3(a.x * b.z + a.z * b.x, b.y, a.z * b.z + -a.x * b.x);
}
export function round(value: number, decimals: number = 0): number {
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
export function roundVector(
vector: THREE.Vector3,
decimals: number = 0
): THREE.Vector3 {
return new THREE.Vector3(
this.round(vector.x, decimals),
this.round(vector.y, decimals),
this.round(vector.z, decimals)
);
}
/**
* Finds an angle between two vectors
* @param {THREE.Vector3} v1
* @param {THREE.Vector3} v2
*/
export function getAngleBetweenVectors(
v1: THREE.Vector3,
v2: THREE.Vector3,
dotTreshold: number = 0.0005
): number {
let angle: number;
let dot = v1.dot(v2);
// If dot is close to 1, we'll round angle to zero
if (dot > 1 - dotTreshold) {
angle = 0;
} else {
// Dot too close to -1
if (dot < -1 + dotTreshold) {
angle = Math.PI;
} else {
// Get angle difference in radians
angle = Math.acos(dot);
}
}
return angle;
}
/**
* Finds an angle between two vectors with a sign relative to normal vector
*/
export function getSignedAngleBetweenVectors(
v1: THREE.Vector3,
v2: THREE.Vector3,
normal: THREE.Vector3 = new THREE.Vector3(0, 1, 0),
dotTreshold: number = 0.0005
): number {
let angle = this.getAngleBetweenVectors(v1, v2, dotTreshold);
// Get vector pointing up or down
let cross = new THREE.Vector3().crossVectors(v1, v2);
// Compare cross with normal to find out direction
if (normal.dot(cross) < 0) {
angle = -angle;
}
return angle;
}
export function haveSameSigns(n1: number, n2: number): boolean {
return n1 < 0 === n2 < 0;
}
export function haveDifferentSigns(n1: number, n2: number): boolean {
return n1 < 0 !== n2 < 0;
}
//#endregion
//#region Miscellaneous
export function setDefaults(options: {}, defaults: {}): {} {
return _.defaults({}, _.clone(options), defaults);
}
export function getGlobalProperties(prefix: string = ""): any[] {
let keyValues = [];
let global = window; // window for browser environments
for (let prop in global) {
// check the prefix
if (prop.indexOf(prefix) === 0) {
keyValues.push(prop /*+ "=" + global[prop]*/);
}
}
return keyValues; // build the string
}
export function spring(
source: number,
dest: number,
velocity: number,
mass: number,
damping: number
): SimulationFrame {
let acceleration = dest - source;
acceleration /= mass;
velocity += acceleration;
velocity *= damping;
let position = source + velocity;
return new SimulationFrame(position, velocity);
}
export function springV(
source: THREE.Vector3,
dest: THREE.Vector3,
velocity: THREE.Vector3,
mass: number,
damping: number
): void {
let acceleration = new THREE.Vector3().subVectors(dest, source);
acceleration.divideScalar(mass);
velocity.add(acceleration);
velocity.multiplyScalar(damping);
source.add(velocity);
}
export function threeVector(vec: CANNON.Vec3): THREE.Vector3 {
return new THREE.Vector3(vec.x, vec.y, vec.z);
}
export function cannonVector(vec: THREE.Vector3): CANNON.Vec3 {
return new CANNON.Vec3(vec.x, vec.y, vec.z);
}
export function threeQuat(quat: CANNON.Quaternion): THREE.Quaternion {
return new THREE.Quaternion(quat.x, quat.y, quat.z, quat.w);
}
export function cannonQuat(quat: THREE.Quaternion): CANNON.Quaternion {
return new CANNON.Quaternion(quat.x, quat.y, quat.z, quat.w);
}
export function setupMeshProperties(child: any, options: IWorldOptions): void {
const isRoad = child[ModelProps.USER_DATA].name?.toLowerCase().indexOf('road') !== -1;
child.castShadow = !isRoad;
child.receiveShadow = true;
if (child.material.name === ModelType.ROCKET) {
const image = new Image();
image.crossOrigin = "Anonymous";
const texture = new THREE.Texture(image);
image.onload = () => {
texture.needsUpdate = true;
};
image.src = options.rocketTexture;
texture.flipY = false;
const mat = rootMaterial.clone();
mat.map = texture;
mat.name = child.material.name;
mat.aoMap = child.material.aoMap;
child.material = mat;
} else if (child.material.map !== null) {
const mat = rootMaterial.clone();
mat.name = child.material.name;
mat.map = child.material.map;
mat.aoMap = child.material.aoMap;
mat.transparent = child.material.transparent;
child.material = mat;
} else {
const mat = rootMaterial.clone();
mat.color = child.material.color;
child.material = mat;
}
}
export function detectRelativeSide(from: Object3D, to: Object3D): Side {
const right = getRight(from, Space.Local);
const viewVector = to.position.clone().sub(from.position).normalize();
return right.dot(viewVector) > 0 ? Side.Left : Side.Right;
}
export function easeInOutSine(x: number): number {
return -(Math.cos(Math.PI * x) - 1) / 2;
}
export function easeOutQuad(x: number): number {
return 1 - (1 - x) * (1 - x);
}
export function getRight(
obj: THREE.Object3D,
space: Space = Space.Global
): THREE.Vector3 {
const matrix = getMatrix(obj, space);
return new THREE.Vector3(
matrix.elements[0],
matrix.elements[1],
matrix.elements[2]
);
}
export function getUp(
obj: THREE.Object3D,
space: Space = Space.Global
): THREE.Vector3 {
const matrix = getMatrix(obj, space);
return new THREE.Vector3(
matrix.elements[4],
matrix.elements[5],
matrix.elements[6]
);
}
export function getForward(
obj: THREE.Object3D,
space: Space = Space.Global
): THREE.Vector3 {
const matrix = getMatrix(obj, space);
return new THREE.Vector3(
matrix.elements[8],
matrix.elements[9],
matrix.elements[10]
);
}
export function getBack(
obj: THREE.Object3D,
space: Space = Space.Global
): THREE.Vector3 {
const matrix = getMatrix(obj, space);
return new THREE.Vector3(
-matrix.elements[8],
-matrix.elements[9],
-matrix.elements[10]
);
}
export function getMatrix(obj: THREE.Object3D, space: Space): THREE.Matrix4 {
switch (space) {
case Space.Local:
return obj.matrix;
case Space.Global:
return obj.matrixWorld;
}
}
export function countSleepyBodies(): any {
// let awake = 0;
// let sleepy = 0;
// let asleep = 0;
// this.physicsWorld.bodies.forEach((body) =>
// {
// if (body.sleepState === 0) awake++;
// if (body.sleepState === 1) sleepy++;
// if (body.sleepState === 2) asleep++;
// });
}
//#endregion