diff --git a/src/03-simulation/DemoSimulation.ts b/src/03-simulation/DemoSimulation.ts index 87a1f63190ebfd4ca799e13ff4c76438d75c16d2..1f968a8053a15ac536feb88e0996734a24b9f977 100644 --- a/src/03-simulation/DemoSimulation.ts +++ b/src/03-simulation/DemoSimulation.ts @@ -1,10 +1,18 @@ +import * as THREE from 'three'; import { PipelineData } from "../core/Pipeline"; +import { Particle } from "./cgPhysix/particle"; +import { SimulationSettings } from './SimulationSettings'; class SimulationDemo extends PipelineData { public constructor() { super(); } data(): void { - /* INIT DATA HERE */ + const sim = new SimulationSettings(); + this.addGUIElement(sim); + + SimulationSettings.generateTextile(); + this.scene.add(SimulationSettings.container); + } } diff --git a/src/03-simulation/SimulationSettings.ts b/src/03-simulation/SimulationSettings.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4b6d060c25ee9b41bd173993d7967ef9db42ccd --- /dev/null +++ b/src/03-simulation/SimulationSettings.ts @@ -0,0 +1,311 @@ +import * as THREE from 'three'; +import { GUI } from 'dat.gui'; +import { picked, PipelineGUI } from '../core/Pipeline'; +import { BendingSpring, ShearSpring, Spring, SpringConstants, StructuralSpring, } from './cgPhysix/Springs'; +import { Particle } from './cgPhysix/particle'; + + +class World { + public static readonly G = 9.807; + + public static derivation(x: Array<number>): Array<number> { + + let xDot = new Array<number>(x.length); + + for (let i = 0; i < x.length; i += 6) { + xDot[i] = x[i + 3]; + xDot[i + 1] = x[i + 4]; + xDot[i + 2] = x[i + 5]; + xDot[i + 3] = 0; + xDot[i + 4] = -.01; + xDot[i + 5] = 0; + } + + return xDot; + } + + +} + + +class SimulationSettings implements PipelineGUI { + + public static container: THREE.Group = new THREE.Group(); + + // cloth settings + public static readonly MAX_TOTAL_MASS = 10; + public static readonly MIN_TOTAL_MASS = 0.1; + + public static readonly MAX_WIDTH = 5; + public static readonly MIN_WIDTH = 0.01; + + public static readonly MAX_HEIGHT = 2; + public static readonly MIN_HEIGHT = 0.01; + + public static readonly MAX_RESOLUTION = 20; + public static readonly MIN_RESOLUTION = 2; + + public static totalMass: number = 1; + public static width: number = 1; + public static height: number = 1; + public static resolution: number = 5; + public static externalForce: THREE.Vector3 = new THREE.Vector3(0, 0, 0); + + // particls + public static pList = new Array<Array<Particle>>(); + public static pColorDefault: number = 0xffffff; + public static pColorNoPhysix: number = 0xff00ff; + + // springs + public static readonly MAX_CONSTANT = 10; + public static readonly MIN_CONSTANT = 0.1; + + public static sList = new Array<Spring>(); + public static sConstants: SpringConstants = { structural: 1, shear: 1, bend: 1 }; + public static sColorStructural: number = 0xff0000; + public static sColorShear: number = 0x00ff00; + public static sColorBend: number = 0x0000ff; + + // integrator + public static readonly INTEGRATOR_TYPES = ['Euler', 'Midpoint', 'RungeKutta']; + public static readonly STEP_SIZE = ['Fixed', 'Adaptive']; + + public static integratorType: string = 'Euler'; + public static stepSize: string = 'Fixed'; + + public static generateTextile(): void { + // 1. reset pList + SimulationSettings.pList = new Array<Array<Particle>>(); + + // 2. generate particles + for (let i = 0; i < SimulationSettings.resolution; i++) { + SimulationSettings.pList[i] = new Array<Particle>(); + for (let j = 0; j < SimulationSettings.resolution; j++) { + + const p = new Particle({ + position: new THREE.Vector3( + SimulationSettings.width * (i / SimulationSettings.resolution) - SimulationSettings.width / 2, + SimulationSettings.height * (j / SimulationSettings.resolution) - SimulationSettings.height / 2, + 0), + velocity: new THREE.Vector3(0, 0, 0), + mass: SimulationSettings.totalMass / (SimulationSettings.resolution * SimulationSettings.resolution), + usePhysics: true, + references: [], + radius: 0, + color: SimulationSettings.pColorDefault, + wireframe: true + }); + SimulationSettings.pList[i][j] = p; + } + } + + for (let i = 0; i < SimulationSettings.resolution; i++) { + SimulationSettings.pList[i][SimulationSettings.resolution - 1].usePhysics = false; + } + + // 3. set particle mass and color + SimulationSettings.changedParticleSize(); + SimulationSettings.changedParticleColor(); + + // 4. add particles to container + const pLinList = new Array<THREE.Mesh>(); + SimulationSettings.pList.forEach(pSub => { + pSub.forEach(p => { + pLinList.push(p.mesh); + }); + }); + + // 5. reset sList + SimulationSettings.sList = new Array<Spring>(); + + // 6. generate springs + for (let i = 0; i < SimulationSettings.resolution; i++) { + for (let j = 0; j < SimulationSettings.resolution; j++) { + + // structural springs (horizontal) + if (i < SimulationSettings.resolution - 1) { + const pA = SimulationSettings.pList[i][j]; + const pB = SimulationSettings.pList[i + 1][j]; + + const s = new StructuralSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimulationSettings.sConstants, + color: SimulationSettings.sColorStructural, + }); + SimulationSettings.sList.push(s); + + pA.references.push(s); + pB.references.push(s); + } + + // structural springs (vertical) + if (j < SimulationSettings.resolution - 1) { + const pA = SimulationSettings.pList[i][j]; + const pB = SimulationSettings.pList[i][j + 1]; + + const s = new StructuralSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimulationSettings.sConstants, + color: SimulationSettings.sColorStructural, + }); + SimulationSettings.sList.push(s); + + pA.references.push(s); + pB.references.push(s); + } + + + // // shear springs + // if (j < SimulationSettings.resolution - 1) { + // const s = new ShearSpring(SimulationSettings.pList[i][j], SimulationSettings.pList[i][j + 1], SimulationSettings.sConstants.shear); + // SimulationSettings.sList.push(s); + // } + + // // bending springs + // if (i < SimulationSettings.resolution - 1 && j < SimulationSettings.resolution - 1) { + // const s = new BendingSpring(SimulationSettings.pList[i][j], SimulationSettings.pList[i + 1][j + 1], SimulationSettings.sConstants.bend); + // SimulationSettings.sList.push(s); + // } + } + } + + // 7. add springs to container + const sLinList = new Array<THREE.Line>(); + SimulationSettings.sList.forEach(s => { + sLinList.push(s.line); + }); + + // 8. add particles and springs to container + while (SimulationSettings.container.children.length > 0) { + SimulationSettings.container.children.pop(); + } + + SimulationSettings.container.add(...pLinList, ...sLinList); + } + + public static changedParticleSize(): void { + // 1. calculate current mass + const pCount = SimulationSettings.resolution * SimulationSettings.resolution; + const pMass = SimulationSettings.totalMass / pCount; + + // 2. calculate max pSize + const maxSize = Math.min(SimulationSettings.width, SimulationSettings.height); + + // 3. normalize pMass + const pMassNorm = (pMass + SimulationSettings.MIN_TOTAL_MASS) / + (SimulationSettings.MAX_TOTAL_MASS + SimulationSettings.MIN_TOTAL_MASS); + + // 4. calculate pSize + const pSize = maxSize * pMassNorm; + + // 5. set pSize + SimulationSettings.pList.forEach(pSub => { pSub.forEach(p => p.setRadius(pSize)) }); + } + + public static changedDimensions(): void { + // 1. reset positions of particles + for (let i = 0; i < SimulationSettings.resolution; i++) { + for (let j = 0; j < SimulationSettings.resolution; j++) { + SimulationSettings.pList[i][j].setPosition( + SimulationSettings.width * (i / SimulationSettings.resolution) - SimulationSettings.width / 2, + SimulationSettings.height * (j / SimulationSettings.resolution) - SimulationSettings.height / 2, + 0); + } + } + + // 2. set size based on particle mass + SimulationSettings.changedParticleSize(); + } + + public static changedParticleColor(): void { + // iterate over all particles and set color based on current internal state + SimulationSettings.pList.forEach(pSub => { + pSub.forEach(p => { + if (p.usePhysics) { + p.setColor(SimulationSettings.pColorDefault); + } else { + p.setColor(SimulationSettings.pColorNoPhysix); + } + }) + }); + } + + public static changedSpringColor(): void { + // iterate over all particles and set color based on current internal state + SimulationSettings.sList.forEach(s => { + if (s instanceof StructuralSpring) { + s.setColor(SimulationSettings.sColorStructural); + } else if (s instanceof ShearSpring) { + s.setColor(SimulationSettings.sColorShear); + } else if (s instanceof BendingSpring) { + s.setColor(SimulationSettings.sColorBend); + } + }); + } + + public static switchParticlePhysics(): void { + // 1. is picked object not null + if (!picked) { + return; + } + + // 2. compare mesh to all particles => find particle and change usePhysics + SimulationSettings.pList.forEach(pSub => { + pSub.forEach(p => { + if (p.mesh === picked) { + p.usePhysics = !p.usePhysics; + p.setColor(p.usePhysics ? SimulationSettings.pColorDefault + : SimulationSettings.pColorNoPhysix); + } + }) + }); + } + + // callback overrides by other classes + public callbackSpringColorStructural(_value: number): void { } + public callbackSpringColorShear(_value: number): void { } + public callbackSpringColorBend(_value: number): void { } + + gui(gui: GUI): void { + const general = gui.addFolder('General'); + general.add(SimulationSettings, 'totalMass', SimulationSettings.MIN_TOTAL_MASS, SimulationSettings.MAX_TOTAL_MASS) + .step(0.1).name('Total Mass').onChange(SimulationSettings.changedParticleSize); + general.add(SimulationSettings, 'width', SimulationSettings.MIN_WIDTH, SimulationSettings.MAX_WIDTH) + .step(0.1).name('Width').onChange(SimulationSettings.changedDimensions); + general.add(SimulationSettings, 'height', SimulationSettings.MIN_HEIGHT, SimulationSettings.MAX_HEIGHT) + .step(0.1).name('Height').onChange(SimulationSettings.changedDimensions); + general.add(SimulationSettings, 'resolution', SimulationSettings.MIN_RESOLUTION, SimulationSettings.MAX_RESOLUTION) + .step(1).name('Resolution').onChange(SimulationSettings.generateTextile); + general.open(); + + const extForce = general.addFolder('External Force'); + extForce.add(SimulationSettings.externalForce, 'x',).step(0.01); + extForce.add(SimulationSettings.externalForce, 'y',).step(0.01); + extForce.add(SimulationSettings.externalForce, 'z',).step(0.01); + extForce.open(); + + const particles = gui.addFolder('Particles'); + particles.add(SimulationSettings, 'switchParticlePhysics').name('Switch Particle Physics'); + particles.addColor(SimulationSettings, 'pColorDefault').name('Default').onChange(SimulationSettings.changedParticleColor); + particles.addColor(SimulationSettings, 'pColorNoPhysix').name('No Phyisx').onChange(SimulationSettings.changedParticleColor); + particles.open(); + + const springs = gui.addFolder('Springs'); + springs.add(SimulationSettings.sConstants, 'structural', SimulationSettings.MIN_CONSTANT, SimulationSettings.MAX_CONSTANT) + .step(0.1).name('Structural'); + springs.add(SimulationSettings.sConstants, 'shear', SimulationSettings.MIN_CONSTANT, SimulationSettings.MAX_CONSTANT) + .step(0.1).name('Shear'); + springs.add(SimulationSettings.sConstants, 'bend', SimulationSettings.MIN_CONSTANT, SimulationSettings.MAX_CONSTANT) + .step(0.1).name('Bend'); + springs.addColor(SimulationSettings, 'sColorStructural').name('Structural').onChange(SimulationSettings.changedSpringColor); + springs.addColor(SimulationSettings, 'sColorShear').name('Shear').onChange(SimulationSettings.changedSpringColor); + springs.addColor(SimulationSettings, 'sColorBend').name('Bend').onChange(SimulationSettings.changedSpringColor); + springs.open(); + } +} + +export { SimulationSettings }; + + diff --git a/src/03-simulation/physics/Integrators.ts b/src/03-simulation/physics/Integrators.ts new file mode 100644 index 0000000000000000000000000000000000000000..39da71ccb6ecd91bee3371acb85b322fdba2b1cd --- /dev/null +++ b/src/03-simulation/physics/Integrators.ts @@ -0,0 +1,57 @@ + +abstract class Integrator { + abstract step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number>; + + public static adaptiveStep(integrator: Integrator, x: Array<number>, h: number, + f: (x: Array<number>) => Array<number>, errorMax: number): number { + let xFull = integrator.step(x, h, f); + let xHalf = integrator.step(x, h / 2, f); + let error: Array<number> = []; + for (let i = 0; i < xFull.length; i++) { + error.push(Math.abs(xFull[i] - xHalf[i])); + } + let errorApproxMax = error.reduce((a, b) => Math.max(a, b)); + return h * Math.sqrt(errorMax / errorApproxMax); + } +} + +class EulerIntegrator extends Integrator { + step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> + { + let y = f(x); + let x_new = x.map((_, i) => x[i] + h * y[i]); + return x_new; + } +} + +class MidpointIntegrator extends Integrator { + + private euler: EulerIntegrator; + + constructor() { + super(); + this.euler = new EulerIntegrator(); + } + + step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> + { + let euler_step = this.euler.step(x, h / 2, f); + let y = f(euler_step); + let x_new = x.map((_, i) => x[i] + h * y[i]); + return x_new; + } +} + +class RungeKuttaIntegrator extends Integrator { + step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> + { + let k1 = f(x); + let k2 = f(x.map((_, i) => x[i] + h / 2 * k1[i])); + let k3 = f(x.map((_, i) => x[i] + h / 2 * k2[i])); + let k4 = f(x.map((_, i) => x[i] + h * k3[i])); + let x_new = x.map((_, i) => x[i] + h / 6 * (k1[i] + 2 * k2[i] + 2 * k3[i] + k4[i])); + return x_new; + } +} + +export { Integrator, EulerIntegrator, MidpointIntegrator, RungeKuttaIntegrator }; diff --git a/src/03-simulation/physics/Particle.ts b/src/03-simulation/physics/Particle.ts new file mode 100644 index 0000000000000000000000000000000000000000..6b028deb40e66f86d7c98366c25a1658b68d4acc --- /dev/null +++ b/src/03-simulation/physics/Particle.ts @@ -0,0 +1,63 @@ +import * as THREE from 'three'; +import { PipelineRenderable } from '../../core/Pipeline'; +import './Springs'; +import { Spring } from './Springs'; + +export interface ParticleParameters { + // particle traits + position: THREE.Vector3; + velocity: THREE.Vector3; + + // internal parameters + usePhysics: boolean; + mass: number; + references: Spring[]; + + // visual parameters + radius: number; + color: number; + wireframe: boolean; +} + +export class Particle implements PipelineRenderable { + public position: THREE.Vector3; + public velocity: THREE.Vector3; + + public mass: number; + public usePhysics: boolean; + + public references: Spring[]; + public mesh: THREE.Mesh; + + constructor(params: ParticleParameters) { + this.position = params.position; + this.velocity = params.velocity; + this.mass = params.mass; + this.usePhysics = params.usePhysics; + this.references = params.references; + + this.mesh = new THREE.Mesh( + new THREE.SphereGeometry(params.radius, 4, 2), + new THREE.MeshBasicMaterial({ color: params.color, wireframe: params.wireframe }) + ); + this.mesh.position.copy(this.position); + } + + public setColor(color: number): void { + (this.mesh.material as THREE.MeshBasicMaterial).color.set(color); + } + + public setPosition(x: number, y: number, z: number): void { + this.position.set(x, y, z); + this.mesh.position.copy(this.position); + this.references.forEach(spring => spring.notify()); + } + + public setRadius(radius: number): void { + this.mesh.geometry = new THREE.SphereGeometry(radius, 4, 2); + } + + public object(): THREE.Object3D<THREE.Event> { + return this.mesh; + } +} \ No newline at end of file diff --git a/src/03-simulation/physics/Springs.ts b/src/03-simulation/physics/Springs.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7fccd103b9b7ea594a9cf5112902c3b6e8b7174 --- /dev/null +++ b/src/03-simulation/physics/Springs.ts @@ -0,0 +1,105 @@ +import * as THREE from 'three'; + +export interface SpringParameters { + // spring traits + positionA: THREE.Vector3; + positionB: THREE.Vector3; + + // internal parameters + constants: SpringConstants; + + // visuals + color: number; +} + +export abstract class Spring { + + public positionA: THREE.Vector3; + public positionB: THREE.Vector3; + public restingLength: number; + + public line: THREE.Line; + + constructor(params: SpringParameters) { + this.positionA = params.positionA; + this.positionB = params.positionB; + this.restingLength = this.positionA.distanceTo(this.positionB); + + this.line = new THREE.Line( + new THREE.BufferGeometry().setFromPoints([this.positionA, this.positionB]), + new THREE.LineBasicMaterial({ color: params.color }) + ); + } + + abstract get springConstant(): number; + + public force(x: number, y: number, z: number): THREE.Vector3 { + let direction = this.positionB.clone().sub(this.positionA); + const distance = direction.length(); + direction.normalize(); + const sign = distance / Math.abs(distance); + const result = direction.multiplyScalar(this.springConstant * (distance - this.restingLength) * sign); + + if (this.positionA === new THREE.Vector3(x, y, z)) { + return result; + } + + return result.multiplyScalar(-1); + } + + public setColor(color: number): void { + (this.line.material as THREE.LineBasicMaterial).color.set(color); + } + + notify(): void { + this.line.geometry.setFromPoints([this.positionA, this.positionB]); + } +} + +export class SpringConstants { + public structural: number = 1; + public shear: number = 1; + public bend: number = 1; +} + +export class StructuralSpring extends Spring { + + public constants: SpringConstants; + + constructor(params: SpringParameters) { + super(params); + this.constants = params.constants; + } + + get springConstant(): number { + return this.constants.structural; + } +} + +export class ShearSpring extends Spring { + + public constants: SpringConstants; + + constructor(params: SpringParameters) { + super(params); + this.constants = params.constants; + } + + get springConstant(): number { + return this.constants.shear; + } +} + +export class BendingSpring extends Spring { + + public constants: SpringConstants; + + constructor(params: SpringParameters) { + super(params); + this.constants = params.constants; + } + + get springConstant(): number { + return this.constants.bend; + } +} diff --git a/src/core/Pipeline.ts b/src/core/Pipeline.ts index 3546ad639de5d133cc9dbd4541b96ff6d3790f4b..08464fa52948d6ee17563633bc4f2a179eef6ed5 100644 --- a/src/core/Pipeline.ts +++ b/src/core/Pipeline.ts @@ -4,6 +4,8 @@ import * as THREE from "three"; import { DragControls } from "three/examples/jsm/controls/DragControls"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls"; +export let picked: THREE.Object3D | null; + /** * PipelineObserver is an abstract class that is used to observe the renderer. */ @@ -27,11 +29,6 @@ interface PipelineRenderable { * Access the THREE.js object. */ object(): THREE.Object3D; - - /** - * Access the position of the THREE.js object directly. - */ - position(): THREE.Vector3; } /** @@ -102,7 +99,7 @@ abstract class PipelineData { /** * Define objects that should be added to the GUI. */ - protected addGUI(guiElement: PipelineGUI) { + protected addGUIElement(guiElement: PipelineGUI) { this.guiElements.push(guiElement); } @@ -190,6 +187,25 @@ class Pipeline { data.draggables.forEach(draggable => { this.addDraggable(draggable); }); data.observers.forEach(observer => { this.addPipelineObserver(observer); }); data.guiElements.forEach(guiElement => { this.addGUI(guiElement); }); + + document.addEventListener( 'mousedown', (event) => { + event.preventDefault(); + let mouse3D = new THREE.Vector3( ( event.clientX / window.innerWidth ) * 2 - 1, + -( event.clientY / window.innerHeight ) * 2 + 1, + 0.5 ); + let raycaster = new THREE.Raycaster(); + raycaster.setFromCamera( mouse3D, this.camera ); + let intersects = raycaster.intersectObjects( data.scene.children ); + if (intersects.length > 0) { + intersects.forEach(intersect => { + if (intersect.object instanceof THREE.Mesh) { + picked = intersect.object; + } + }); + + } + console.log(picked); + }); } public addPipelineObserver(object: PipelineObserver): void {