diff --git a/src/03-simulation/DemoSimulation.ts b/src/03-simulation/DemoSimulation.ts index 94def8985f907c323a5fa87d86078135b8671c37..bc7d53f765be8589105db808b16082452bc59ab7 100644 --- a/src/03-simulation/DemoSimulation.ts +++ b/src/03-simulation/DemoSimulation.ts @@ -1,12 +1,12 @@ import { PipelineData } from "../core/Pipeline"; import { Simulation } from "./Simulation"; -import { SimulationData } from './SimulationData'; +import { SimData } from './SimData'; class SimulationDemo extends PipelineData { public constructor() { super(); } data(): void { - const data = new SimulationData(); + const data = new SimData(); this.addGUIElement(data); this.addRenderable(data); diff --git a/src/03-simulation/SimData.ts b/src/03-simulation/SimData.ts new file mode 100644 index 0000000000000000000000000000000000000000..e2989ba2e3d6a6aec1d94f6e2fb31a020018f091 --- /dev/null +++ b/src/03-simulation/SimData.ts @@ -0,0 +1,546 @@ +import * as THREE from 'three'; +import { GUI } from 'dat.gui'; +import { picked, PipelineGUI, PipelineRenderable } from '../core/Pipeline'; +import { BendingSpring, ShearSpring, Spring, SpringConstants, StructuralSpring, } from './physics/Springs'; +import { Particle } from './physics/particle'; +import { EulerIntegrator, Integrator, MidpointIntegrator, RungeKuttaIntegrator } from './physics/Integrators'; + +document.addEventListener('keydown', (event: KeyboardEvent) => { + + if (event.key === 'e') { + SimData.switchParticlePhysics(); + } else if (event.key === 'w') { + SimData.paused = !SimData.paused; + } +}); + +class SimData implements PipelineGUI, PipelineRenderable { + + public static container: THREE.Group = new THREE.Group(); + public static paused: boolean = false; + + // constants + 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 readonly MAX_SPRING_CONSTANT = 10; + public static readonly MIN_SPRING_CONSTANT = 0.1; + + public static readonly INTEGRATOR_OPTIONS = { 'Euler': 0, 'Midpoint': 1, 'RungeKutta': 2 } + public static readonly STEP_SIZE_OPTIONS = { 'Fixed': 0, 'Adaptive': 1, } + + public static readonly MAX_EXPONENT = 10; + public static readonly MIN_EXPONENT = -10; + + // general + public static totalMass: number = 1; + public static width: number = .2; + public static height: number = .2; + public static resolution: number = 5; + + // integrator + public static readonly INTEGRATORS: Integrator[] = [ + new EulerIntegrator(), + new MidpointIntegrator(), + new RungeKuttaIntegrator() + ] + + public static integrationMethod: number = 1; + public static stepSizeMethod: number = 0; + + public static stepSize: number = .001; + + public static hMax: number = .05; + public static hMin: number = .00005; + + public static hFactor: number = 5; + public static hMaxExponent: number = -2; + public static hMinExponent: number = -5; + + public static eOrder: number = 3; + public static eFactor: number = 1; + public static eMaxExponent: number = -14; + + public static damping: number = .1; + + + // Force Field + public static forceFieldStrength: THREE.Vector3 = new THREE.Vector3(0, 0, 0); + public static forceFieldPosition: 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 sList?= new Array<Spring>(); + public static sConstants: SpringConstants = { structural: 5, shear: 2, bend: 4 }; + public static sColorStructural: number = 0xff0000; + public static sColorShear: number = 0x00ff00; + public static sColorBend: number = 0x0000ff; + + public static resetForceField(): void { + SimData.forceFieldStrength.x = 0; + SimData.forceFieldStrength.y = 0; + SimData.forceFieldStrength.z = 0; + } + + public static generateTextile(): void { + // 1. reset pList + delete SimData.pList; + SimData.pList = new Array<Array<Particle>>(); + + // 2. generate particles + for (let i = 0; i < SimData.resolution; i++) { + SimData.pList[i] = new Array<Particle>(); + for (let j = 0; j < SimData.resolution; j++) { + const p = new Particle({ + position: new THREE.Vector3( + SimData.width * (i / SimData.resolution) - SimData.width / 2, + SimData.height * (j / SimData.resolution) - SimData.height / 2, + 0), + velocity: new THREE.Vector3(0, 0, 0), + mass: SimData.totalMass / (SimData.resolution * SimData.resolution), + usePhysics: true, + references: [], + color: SimData.pColorDefault, + wireframe: true + }); + SimData.pList[i].push(p); + } + } + + for (let i = 0; i < SimData.resolution; i++) { + SimData.pList[i][SimData.resolution - 1].usePhysics = false; + } + + // 3. set particle mass and color + SimData.changedParticleSize(); + SimData.changedParticleColor(); + + // 4. add particles to container + const pLinList = new Array<THREE.Mesh>(); + SimData.pList.forEach(pSub => { + pSub.forEach(p => { + pLinList.push(p.mesh); + }); + }); + + // 5. reset sList + delete SimData.sList; + SimData.sList = new Array<Spring>(); + + // 6. generate springs + for (let i = 0; i < SimData.resolution; i++) { + for (let j = 0; j < SimData.resolution; j++) { + + // structural springs (horizontal) + if (i < SimData.resolution - 1) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i + 1][j]; + + const s = new StructuralSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorStructural, + }); + SimData.sList.push(s); + s.line.renderOrder = 2; + + pA.references.push(s); + pB.references.push(s); + } + + // structural springs (vertical) + if (j < SimData.resolution - 1) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i][j + 1]; + + const s = new StructuralSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorStructural, + }); + SimData.sList.push(s); + s.line.renderOrder = 2; + + pA.references.push(s); + pB.references.push(s); + } + + // shear springs (bottom-left to top-right) + if (i < SimData.resolution - 1 && j < SimData.resolution - 1) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i + 1][j + 1]; + + const s = new ShearSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorShear, + }); + SimData.sList.push(s); + s.line.renderOrder = 1; + + pA.references.push(s); + pB.references.push(s); + } + + // shear springs (top-left to bottom-right) + if (i < SimData.resolution - 1 && j > 0) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i + 1][j - 1]; + + const s = new ShearSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorShear, + }); + SimData.sList.push(s); + s.line.renderOrder = 1; + + pA.references.push(s); + pB.references.push(s); + } + + // bend springs (horizontal) + if (i < SimData.resolution - 2) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i + 2][j]; + + const s = new BendingSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorBend, + }); + SimData.sList.push(s); + s.line.renderOrder = 0; + + pA.references.push(s); + pB.references.push(s); + } + + // bend springs (vertical) + if (j < SimData.resolution - 2) { + const pA = SimData.pList[i][j]; + const pB = SimData.pList[i][j + 2]; + + const s = new BendingSpring({ + positionA: pA.position, + positionB: pB.position, + constants: SimData.sConstants, + color: SimData.sColorBend, + }); + SimData.sList.push(s); + s.line.renderOrder = 0; + + pA.references.push(s); + pB.references.push(s); + } + } + } + + // 7. add springs to container + const sLinList = new Array<THREE.Line>(); + SimData.sList.forEach(s => { + sLinList.push(s.line); + }); + + // 8. add particles and springs to container + while (SimData.container.children.length > 0) { + let rm = SimData.container.children.pop(); + if (rm instanceof THREE.Line) { + rm.geometry.dispose(); + rm.material.dispose(); + } else if (rm instanceof THREE.Mesh) { + rm.geometry.dispose(); + rm.material.dispose(); + } + } + + SimData.container.add(...pLinList, ...sLinList); + } + + public static changedParticleSize(): void { + // 1. calculate current mass + const pCount = SimData.resolution * SimData.resolution; + const pMass = SimData.totalMass / pCount; + + // 1.1 update particle mass + SimData.pList?.forEach(pSub => { + pSub.forEach(p => { + p.mass = pMass; + }); + }); + + // 2. calculate max pSize + const maxSize = Math.min(SimData.width, SimData.height); + + // 3. normalize pMass + const pMassNorm = (pMass - SimData.MIN_TOTAL_MASS) / + (SimData.MAX_TOTAL_MASS + SimData.MIN_TOTAL_MASS); + + // 4. calculate pSize + const pSize = maxSize * pMassNorm; + + // 5. set pSize + SimData.pList?.forEach(pSub => { pSub.forEach(p => p.setRadius(pSize)) }); + } + + public static changedDimensions(): void { + if (!SimData.pList) + return; + + // 1. reset positions of particles + for (let i = 0; i < SimData.resolution; i++) { + for (let j = 0; j < SimData.resolution; j++) { + SimData.pList[i][j].setPosition( + SimData.width * (i / SimData.resolution) - SimData.width / 2, + SimData.height * (j / SimData.resolution) - SimData.height / 2, + 0); + } + } + + // 2. set size based on particle mass + SimData.changedParticleSize(); + } + + public static changedParticleColor(): void { + // iterate over all particles and set color based on current internal state + SimData.pList?.forEach(pSub => { + pSub.forEach(p => { + if (p.usePhysics) { + p.setColor(SimData.pColorDefault); + } else { + p.setColor(SimData.pColorNoPhysix); + } + }) + }); + } + + public static changedSpringColor(): void { + // iterate over all particles and set color based on current internal state + SimData.sList?.forEach(s => { + if (s instanceof StructuralSpring) { + s.setColor(SimData.sColorStructural); + } else if (s instanceof ShearSpring) { + s.setColor(SimData.sColorShear); + } else if (s instanceof BendingSpring) { + s.setColor(SimData.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 + SimData.pList?.forEach(pSub => { + pSub.forEach(p => { + if (p.mesh === picked) { + p.usePhysics = !p.usePhysics; + p.setColor(p.usePhysics ? SimData.pColorDefault + : SimData.pColorNoPhysix); + } + }) + }); + } + + gui(gui: GUI): void { + + // GENERAL SETTINGS + const general = gui.addFolder('General'); + general.open(); + + general.add(SimData, 'paused') + .name("Pause Simulation") + .listen(); + + general.add(SimData, 'switchParticlePhysics') + .name('Switch Particle Physics'); + + general.add(SimData, 'totalMass') + .name("Total Mass") + .max(SimData.MAX_TOTAL_MASS) + .min(SimData.MIN_TOTAL_MASS) + .step(0.1) + .onChange(SimData.changedParticleSize); + + general.add(SimData, 'width') + .name("Width") + .max(SimData.MAX_WIDTH) + .min(SimData.MIN_WIDTH) + .step(0.1) + .onChange(SimData.changedDimensions); + + general.add(SimData, 'height') + .name("Height") + .max(SimData.MAX_HEIGHT) + .min(SimData.MIN_HEIGHT) + .step(0.1) + .onChange(SimData.changedDimensions); + + general.add(SimData, 'resolution') + .name("Resolution") + .max(SimData.MAX_RESOLUTION) + .min(SimData.MIN_RESOLUTION) + .step(1) + .onFinishChange(SimData.generateTextile); + + // INTEGRATOR SETTINGS + const integrator = general.addFolder('Integrator Settings'); + integrator.open(); + + integrator.add(SimData, 'integrationMethod') + .options(SimData.INTEGRATOR_OPTIONS) + .name("Method") + .onChange((e: number) => { + if (e === 0) { + SimData.eOrder = 2; + } else if (e === 1) { + SimData.eOrder = 3; + } else if (e === 2) { + SimData.eOrder = 5; + } + }); + + integrator.add(SimData, 'stepSizeMethod') + .options(SimData.STEP_SIZE_OPTIONS) + .name("Step Size Method") + + integrator.add(SimData, 'stepSize') + .name("Fixed Size") + .step(0.00001) + + + const adaptiveSettings = integrator.addFolder('Adaptive Settings'); + adaptiveSettings.open(); + + const stepSizeSettings = adaptiveSettings.addFolder('Step Size'); + + stepSizeSettings.add(SimData, 'hFactor') + .name("h Factor") + .step(1) + .max(9) + .min(1) + + stepSizeSettings.add(SimData, 'hMaxExponent') + .name("h Max Exponent") + .step(1) + .max(SimData.MAX_EXPONENT) + .min(SimData.MIN_EXPONENT) + + stepSizeSettings.add(SimData, 'hMinExponent') + .name("h Min Exponent") + .step(1) + .max(SimData.MAX_EXPONENT) + .min(SimData.MIN_EXPONENT) + + const errorSettings = adaptiveSettings.addFolder('Error'); + + errorSettings.add(SimData, 'eFactor') + .name("e Factor") + .step(1) + .max(9) + .min(1) + + errorSettings.add(SimData, 'eMaxExponent') + .name("e Max Exponent") + .step(1) + .max(-10) + .min(-20) + + // SPRING SETTINGS + const springs = general.addFolder('Spring Constants'); + springs.open(); + + springs.add(SimData.sConstants, 'structural') + .name("Structural") + .step(0.1) + .max(SimData.MAX_SPRING_CONSTANT) + .min(SimData.MIN_SPRING_CONSTANT); + + springs.add(SimData.sConstants, 'shear') + .name("Shear") + .step(0.1) + .max(SimData.MAX_SPRING_CONSTANT) + .min(SimData.MIN_SPRING_CONSTANT); + + springs.add(SimData.sConstants, 'bend') + .name("Bend") + .step(0.1) + .max(SimData.MAX_SPRING_CONSTANT) + .min(SimData.MIN_SPRING_CONSTANT); + + springs.add(SimData, 'damping') + .name("Damping Factor") + .step(0.001) + + // APPEARANCE SETTINGS + const appearance = general.addFolder('Appearance'); + + appearance.addColor(SimData, 'pColorDefault') + .name('Particle Default') + .onChange(SimData.changedParticleColor); + + appearance.addColor(SimData, 'pColorNoPhysix') + .name('Particle Pinned') + .onChange(SimData.changedParticleColor); + + appearance.addColor(SimData, 'sColorStructural') + .name('Structural') + .onChange(SimData.changedSpringColor); + + appearance.addColor(SimData, 'sColorShear') + .name('Shear') + .onChange(SimData.changedSpringColor); + + appearance.addColor(SimData, 'sColorBend') + .name('Bend') + .onChange(SimData.changedSpringColor); + + const forceField = general.addFolder('Point Force Field'); + + const ffStrength = forceField.addFolder('Strength'); + ffStrength.open(); + + ffStrength.add(SimData, 'resetForceField') + .name('Reset Strength') + .onFinishChange(() => ffStrength.__controllers.forEach(c => c.updateDisplay())); + ffStrength.add(SimData.forceFieldStrength, 'x',).step(0.01); + ffStrength.add(SimData.forceFieldStrength, 'y',).step(0.01); + ffStrength.add(SimData.forceFieldStrength, 'z',).step(0.01); + + const ffPosition = forceField.addFolder('Position'); + ffPosition.open(); + + ffPosition.add(SimData.forceFieldPosition, 'x',).step(0.01); + ffPosition.add(SimData.forceFieldPosition, 'y',).step(0.01); + ffPosition.add(SimData.forceFieldPosition, 'z',).step(0.01); + } + + object(): THREE.Object3D<THREE.Event> { + return SimData.container; + } +} + +export { SimData }; + + diff --git a/src/03-simulation/Simulation.ts b/src/03-simulation/Simulation.ts index 340ece80290ba376ac9781fc762ce579ac19669b..c15d5cf6161a417ac3510e497934b8de3030ab44 100644 --- a/src/03-simulation/Simulation.ts +++ b/src/03-simulation/Simulation.ts @@ -1,90 +1,73 @@ import * as THREE from 'three'; import { PipelineObserver } from "../core/Pipeline"; import { Particle } from './physics/particle'; -import { SimulationData } from "./SimulationData"; - - -function adaptive(x: Array<number>, h: number, eMax = 1e-10, n: number = 5): number { - const rk4 = SimulationData.INTEGRATORS[2]; - - // 1. calculate approximate - let xFull = rk4.step([...x], h, Simulation.derivation); - let xHalf = rk4.step([...x], h/2, Simulation.derivation); - xHalf = rk4.step(xHalf, h/2, Simulation.derivation); - - // 2. calculate error - let eList = []; - for (let i = 0; i < x.length; i++) { - eList.push(Math.abs(xFull[i] - xHalf[i])); - } - let e = 0; - for (let i = 0; i < eList.length; i++) { - e += Math.pow(eList[i], 2); - } - - if (e < 1e-8) { - return h; - } - - e = Math.sqrt(e); // total error - - // 3. e = k * h^n - // e.g. h = 0.001 mit n = 5 => e = k * 0.001^5 - // => k = e / h^n - // => k * h^n = eMax - // <=> h = pow(eMax / k, 1/n) - // => h = pow(eMax / (e / h^n), 1/n) - - let newH = Math.pow(eMax / (e / Math.pow(h, n)), 1 / n); - if (isNaN(newH) || !isFinite(newH)) { - newH = h; - } - return newH; -} +import { SimData } from "./SimData"; export class Simulation implements PipelineObserver { public static readonly G = new THREE.Vector3(0, -9.807, 0); public static current: Particle; - public static h = .001; + + public static timeAccumulator = 0; constructor() { - SimulationData.generateTextile(); + SimData.generateTextile(); } - update(_deltaTime: number): void { - if (SimulationData.paused) { + if (SimData.paused) { return; } - - for (let t = 0; t < _deltaTime; t += Simulation.h) { - SimulationData.pList?.forEach(pSub => { + + const integrator = SimData.INTEGRATORS[SimData.integrationMethod]; + Simulation.timeAccumulator += _deltaTime; + + // 1. find smallest step siz + let h = SimData.stepSize; + if (SimData.stepSizeMethod == 1) { + let hMin = Number.MAX_VALUE; + SimData.pList?.forEach(pSub => { + pSub.forEach(p => { + let x = [p.position.x, p.position.y, p.position.z, + p.velocity.x, p.velocity.y, p.velocity.z]; + + let hTmp = integrator.adaptive(x, h, Simulation.derivation, SimData.eOrder, + SimData.eFactor * Math.pow(10, SimData.eMaxExponent), + SimData.hFactor * Math.pow(10, SimData.hMaxExponent), + SimData.hFactor * Math.pow(10, SimData.hMinExponent)); + if (hTmp < hMin) { + hMin = hTmp; + } + }); + }) + + h = hMin; + + if (Simulation.timeAccumulator > 1) { + Simulation.timeAccumulator = 0; + console.log(h); + } + } + + // 2. apply simualtion + for (let t = 0, i = 0; t < _deltaTime && i < 10000; t += h, i++) { + SimData.pList?.forEach(pSub => { pSub.forEach(p => { if (p.usePhysics) { Simulation.current = p; let x = [p.position.x, p.position.y, p.position.z, p.velocity.x, p.velocity.y, p.velocity.z]; - if (SimulationData.stepSizeMethod == 1) { - Simulation.h = adaptive(x, Simulation.h); - } - - x = SimulationData.INTEGRATORS[SimulationData.integrationMethod] - .step(x, Simulation.h, Simulation.derivation); + x = integrator.step(x, h, Simulation.derivation); p.setState(x[0], x[1], x[2], x[3], x[4], x[5]); } }); }) } - - console.log(Simulation.h); - } - public static derivation(x: Array<number>): Array<number> { // 1. create new array to store derivatives @@ -99,10 +82,13 @@ export class Simulation implements PipelineObserver { // 3.1 accumulate forces let totalForce = new THREE.Vector3(0, 0, 0); - totalForce.add(SimulationData.externalForce); - totalForce.divideScalar(Math.pow(SimulationData.resolution, 2)); - totalForce.divideScalar(Math.sqrt(Math.pow(x[0], 2) + Math.pow(x[1], 2) + Math.pow(x[2], 2))); - // totalForce.multiplyScalar(Math.sin(2 * Math.PI * Math.random()) + .5); + totalForce.add(SimData.forceFieldStrength); + totalForce.divideScalar(Math.pow(SimData.resolution, 2)); + + const dist = SimData.forceFieldPosition.distanceTo(new THREE.Vector3(x[0], x[1], x[2])); + if (dist > 0) { + totalForce.divideScalar(dist); + } // 3.2 accumulate spring forces Simulation.current.references.forEach(spring => { @@ -111,7 +97,7 @@ export class Simulation implements PipelineObserver { }); // 3.3 damping force - const dampingForce = Simulation.current.velocity.clone().multiplyScalar(-SimulationData.damping); + const dampingForce = Simulation.current.velocity.clone().multiplyScalar(-SimData.damping); totalForce.add(dampingForce); // 3.3 acceleration = force / mass diff --git a/src/03-simulation/SimulationData.ts b/src/03-simulation/SimulationData.ts deleted file mode 100644 index 03a81b9ce1315a81b392c325cc70d96e251dc70e..0000000000000000000000000000000000000000 --- a/src/03-simulation/SimulationData.ts +++ /dev/null @@ -1,400 +0,0 @@ -import * as THREE from 'three'; -import { GUI } from 'dat.gui'; -import { picked, PipelineGUI, PipelineRenderable } from '../core/Pipeline'; -import { BendingSpring, ShearSpring, Spring, SpringConstants, StructuralSpring, } from './physics/Springs'; -import { Particle } from './physics/particle'; -import { EulerIntegrator, Integrator, MidpointIntegrator, RungeKuttaIntegrator } from './physics/Integrators'; - -document.addEventListener('keydown', (event: KeyboardEvent) => { - - if (event.key === 'e') { - SimulationData.switchParticlePhysics(); - } else if (event.key === 'w') { - SimulationData.paused = !SimulationData.paused; - } -}); - -class SimulationData implements PipelineGUI, PipelineRenderable { - - public static container: THREE.Group = new THREE.Group(); - public static paused: boolean = false; - - // 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 = .2; - public static height: number = .2; - public static resolution: number = 5; - public static damping: number = .1; - 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: 10, shear: 2, bend: 4 }; - public static sColorStructural: number = 0xff0000; - public static sColorShear: number = 0x00ff00; - public static sColorBend: number = 0x0000ff; - - // integrator - public static readonly INTEGRATORS: Integrator[] = [ - new EulerIntegrator(), - new MidpointIntegrator(), - new RungeKuttaIntegrator() - ] - - public static integrationMethod: number = 2; - public static stepSizeMethod: number = 0; - public static delta: number = 1e-3; - - public static resetExternalForce(): void { - SimulationData.externalForce.x = 0; - SimulationData.externalForce.y = 0; - SimulationData.externalForce.z = 0; - } - - public static generateTextile(): void { - // 1. reset pList - delete SimulationData.pList; - SimulationData.pList = new Array<Array<Particle>>(); - - // 2. generate particles - for (let i = 0; i < SimulationData.resolution; i++) { - SimulationData.pList[i] = new Array<Particle>(); - for (let j = 0; j < SimulationData.resolution; j++) { - - const p = new Particle({ - position: new THREE.Vector3( - SimulationData.width * (i / SimulationData.resolution) - SimulationData.width / 2, - SimulationData.height * (j / SimulationData.resolution) - SimulationData.height / 2, - 0), - velocity: new THREE.Vector3(0, 0, 0), - mass: SimulationData.totalMass / (SimulationData.resolution * SimulationData.resolution), - usePhysics: true, - references: [], - radius: 0, - color: SimulationData.pColorDefault, - wireframe: true - }); - SimulationData.pList[i][j] = p; - } - } - - for (let i = 0; i < SimulationData.resolution; i++) { - SimulationData.pList[i][SimulationData.resolution - 1].usePhysics = false; - } - - // 3. set particle mass and color - SimulationData.changedParticleSize(); - SimulationData.changedParticleColor(); - - // 4. add particles to container - const pLinList = new Array<THREE.Mesh>(); - SimulationData.pList.forEach(pSub => { - pSub.forEach(p => { - pLinList.push(p.mesh); - }); - }); - - // 5. reset sList - delete SimulationData.sList; - SimulationData.sList = new Array<Spring>(); - - // 6. generate springs - for (let i = 0; i < SimulationData.resolution; i++) { - for (let j = 0; j < SimulationData.resolution; j++) { - - // structural springs (horizontal) - if (i < SimulationData.resolution - 1) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i + 1][j]; - - const s = new StructuralSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorStructural, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 2; - - pA.references.push(s); - pB.references.push(s); - } - - // structural springs (vertical) - if (j < SimulationData.resolution - 1) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i][j + 1]; - - const s = new StructuralSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorStructural, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 2; - - pA.references.push(s); - pB.references.push(s); - } - - - // shear springs (bottom-left to top-right) - if (i < SimulationData.resolution - 1 && j < SimulationData.resolution - 1) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i + 1][j + 1]; - - const s = new ShearSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorShear, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 1; - - pA.references.push(s); - pB.references.push(s); - } - - // shear springs (top-left to bottom-right) - if (i < SimulationData.resolution - 1 && j > 0) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i + 1][j - 1]; - - const s = new ShearSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorShear, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 1; - - pA.references.push(s); - pB.references.push(s); - } - - // bend springs (horizontal) - if (i < SimulationData.resolution - 2) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i + 2][j]; - - const s = new BendingSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorBend, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 0; - - pA.references.push(s); - pB.references.push(s); - } - - // bend springs (vertical) - if (j < SimulationData.resolution - 2) { - const pA = SimulationData.pList[i][j]; - const pB = SimulationData.pList[i][j + 2]; - - const s = new BendingSpring({ - positionA: pA.position, - positionB: pB.position, - constants: SimulationData.sConstants, - color: SimulationData.sColorBend, - }); - SimulationData.sList.push(s); - s.line.renderOrder = 0; - - pA.references.push(s); - pB.references.push(s); - } - } - } - - // 7. add springs to container - const sLinList = new Array<THREE.Line>(); - SimulationData.sList.forEach(s => { - sLinList.push(s.line); - }); - - // 8. add particles and springs to container - while (SimulationData.container.children.length > 0) { - let rm = SimulationData.container.children.pop(); - if (rm instanceof THREE.Line) { - rm.geometry.dispose(); - rm.material.dispose(); - } else if (rm instanceof THREE.Mesh) { - rm.geometry.dispose(); - rm.material.dispose(); - } - } - - SimulationData.container.add(...pLinList, ...sLinList); - } - - public static changedParticleSize(): void { - // 1. calculate current mass - const pCount = SimulationData.resolution * SimulationData.resolution; - const pMass = SimulationData.totalMass / pCount; - - // 1.1 update particle mass - SimulationData.pList?.forEach(pSub => { - pSub.forEach(p => { - p.mass = pMass; - }); - }); - - // 2. calculate max pSize - const maxSize = Math.min(SimulationData.width, SimulationData.height); - - // 3. normalize pMass - const pMassNorm = (pMass + SimulationData.MIN_TOTAL_MASS) / - (SimulationData.MAX_TOTAL_MASS + SimulationData.MIN_TOTAL_MASS); - - // 4. calculate pSize - const pSize = maxSize * pMassNorm; - - // 5. set pSize - SimulationData.pList?.forEach(pSub => { pSub.forEach(p => p.setRadius(pSize)) }); - } - - public static changedDimensions(): void { - if (!SimulationData.pList) - return; - - // 1. reset positions of particles - for (let i = 0; i < SimulationData.resolution; i++) { - for (let j = 0; j < SimulationData.resolution; j++) { - SimulationData.pList[i][j].setPosition( - SimulationData.width * (i / SimulationData.resolution) - SimulationData.width / 2, - SimulationData.height * (j / SimulationData.resolution) - SimulationData.height / 2, - 0); - } - } - - // 2. set size based on particle mass - SimulationData.changedParticleSize(); - } - - public static changedParticleColor(): void { - // iterate over all particles and set color based on current internal state - SimulationData.pList?.forEach(pSub => { - pSub.forEach(p => { - if (p.usePhysics) { - p.setColor(SimulationData.pColorDefault); - } else { - p.setColor(SimulationData.pColorNoPhysix); - } - }) - }); - } - - public static changedSpringColor(): void { - // iterate over all particles and set color based on current internal state - SimulationData.sList?.forEach(s => { - if (s instanceof StructuralSpring) { - s.setColor(SimulationData.sColorStructural); - } else if (s instanceof ShearSpring) { - s.setColor(SimulationData.sColorShear); - } else if (s instanceof BendingSpring) { - s.setColor(SimulationData.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 - SimulationData.pList?.forEach(pSub => { - pSub.forEach(p => { - if (p.mesh === picked) { - p.usePhysics = !p.usePhysics; - p.setColor(p.usePhysics ? SimulationData.pColorDefault - : SimulationData.pColorNoPhysix); - } - }) - }); - } - - gui(gui: GUI): void { - - - const general = gui.addFolder('General'); - general.add(SimulationData, 'paused').listen().name("Pause Simulation"); - general.add(SimulationData, 'totalMass', SimulationData.MIN_TOTAL_MASS, SimulationData.MAX_TOTAL_MASS) - .step(0.1).name('Total Mass').onChange(SimulationData.changedParticleSize); - general.add(SimulationData, 'width', SimulationData.MIN_WIDTH, SimulationData.MAX_WIDTH) - .step(0.1).name('Width').onChange(SimulationData.changedDimensions); - general.add(SimulationData, 'height', SimulationData.MIN_HEIGHT, SimulationData.MAX_HEIGHT) - .step(0.1).name('Height').onChange(SimulationData.changedDimensions); - general.add(SimulationData, 'resolution', SimulationData.MIN_RESOLUTION, SimulationData.MAX_RESOLUTION) - .step(1).name('Resolution').onChange(SimulationData.generateTextile); - general.open(); - - - const integration = gui.addFolder('Integrator'); - integration.add(SimulationData, 'integrationMethod', { 'Euler': 0, 'Midpoint': 1, 'Runge-Kutta': 2 }).name('Method'); - integration.add(SimulationData, 'stepSizeMethod', { 'Fixed': 0, 'Adaptive': 1 }).name('Step Size').onChange((e) => { SimulationData.stepSizeMethod = e; console.log(e);}); - integration.add(SimulationData, 'delta', 1e-10, 1e-6, 1e-10).name('Error Max'); - integration.open(); - - const extForce = general.addFolder('External Force'); - extForce.add(SimulationData.externalForce, 'x',).step(0.01); - extForce.add(SimulationData.externalForce, 'y',).step(0.01); - extForce.add(SimulationData.externalForce, 'z',).step(0.01); - extForce.open(); - - const particles = gui.addFolder('Particles'); - particles.add(SimulationData, 'switchParticlePhysics').name('Switch Particle Physics'); - particles.addColor(SimulationData, 'pColorDefault').name('Default').onChange(SimulationData.changedParticleColor); - particles.addColor(SimulationData, 'pColorNoPhysix').name('No Phyisx').onChange(SimulationData.changedParticleColor); - particles.open(); - - const springs = gui.addFolder('Springs'); - springs.add(SimulationData, 'damping', 0, 0.2, 0.0001).name('Damping Factor'); - springs.add(SimulationData.sConstants, 'structural', SimulationData.MIN_CONSTANT, SimulationData.MAX_CONSTANT) - .step(0.1).name('Structural'); - springs.add(SimulationData.sConstants, 'shear', SimulationData.MIN_CONSTANT, SimulationData.MAX_CONSTANT) - .step(0.1).name('Shear'); - springs.add(SimulationData.sConstants, 'bend', SimulationData.MIN_CONSTANT, SimulationData.MAX_CONSTANT) - .step(0.1).name('Bend'); - springs.addColor(SimulationData, 'sColorStructural').name('Structural').onChange(SimulationData.changedSpringColor); - springs.addColor(SimulationData, 'sColorShear').name('Shear').onChange(SimulationData.changedSpringColor); - springs.addColor(SimulationData, 'sColorBend').name('Bend').onChange(SimulationData.changedSpringColor); - springs.open(); - } - - object(): THREE.Object3D<THREE.Event> { - return SimulationData.container; - } -} - -export { SimulationData }; - - diff --git a/src/03-simulation/physics/Integrators.ts b/src/03-simulation/physics/Integrators.ts index f47ee3cda63caf5337b070b86aa24579cd34e37c..8ecdcee6471fe7c09f971b329bb2bae6b7be6bd6 100644 --- a/src/03-simulation/physics/Integrators.ts +++ b/src/03-simulation/physics/Integrators.ts @@ -1,12 +1,57 @@ abstract class Integrator { abstract step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number>; + + /** + * + * @param x current state + * @param h current step size + * @param f how to deriviate the state + * @param eMax tolerance for error + * @param eOrder error class of the used method + * @param hMax maximum step size + * @param hMin minimum step size + * @returns the next step size between hMax and hMin + */ + public adaptive(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>, + eOrder: number, eMax: number, hMax: number = 0.05, hMin: number = 0.00005): number { + // 1. calculate approximate + let xFull = this.step([...x], h, f); + let xHalf = this.step([...x], h / 2, f); + xHalf = this.step(xHalf, h / 2, f); + + // 2. calculate error + let eList = []; + for (let i = 0; i < x.length; i++) { + eList.push(Math.abs(xFull[i] - xHalf[i])); + } + let e = 0; + for (let i = 0; i < eList.length; i++) { + e += Math.pow(eList[i], 2); + } + + e = Math.sqrt(e); // total error + + // 3. e = k * h^n + // e.g. h = 0.001 mit n = 5 => e = k * 0.001^5 + // => k = e / h^n + // => k * h^n = eMax + // => h = pow(eMax / k, 1/n) + // => h = pow(eMax / (e / h^n), 1/n) + + let newH = Math.pow(eMax / (e / Math.pow(h, eOrder)), 1 / eOrder); + if (isNaN(newH) || !isFinite(newH)) { + newH = h; + } + + let clampedH = Math.min(hMax, Math.max(newH, hMin)); + return clampedH; + } } class EulerIntegrator extends Integrator { - step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> - { - let y = f(x); + 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; } @@ -21,8 +66,7 @@ class MidpointIntegrator extends Integrator { this.euler = new EulerIntegrator(); } - step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> - { + 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]); @@ -31,8 +75,7 @@ class MidpointIntegrator extends Integrator { } class RungeKuttaIntegrator extends Integrator { - step(x: Array<number>, h: number, f: (x: Array<number>) => Array<number>): Array<number> - { + 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])); diff --git a/src/03-simulation/physics/Particle.ts b/src/03-simulation/physics/Particle.ts index 8237d597c549f126e85ca2f2d0341636427d700a..2fd28b99468188cdb5b9e29f752497d70c66f72b 100644 --- a/src/03-simulation/physics/Particle.ts +++ b/src/03-simulation/physics/Particle.ts @@ -14,12 +14,13 @@ export interface ParticleParameters { references: Spring[]; // visual parameters - radius: number; color: number; wireframe: boolean; } export class Particle implements PipelineRenderable { + private static counter = 0; + public position: THREE.Vector3; public velocity: THREE.Vector3; @@ -37,10 +38,12 @@ export class Particle implements PipelineRenderable { this.references = params.references; this.mesh = new THREE.Mesh( - new THREE.SphereGeometry(params.radius, 4, 2), + new THREE.SphereGeometry(), new THREE.MeshBasicMaterial({ color: params.color, wireframe: params.wireframe }) ); this.mesh.position.copy(this.position); + this.mesh.name = `Particle ${Particle.counter++}`; + this.mesh.geometry.name = this.mesh.name; } public setColor(color: number): void { @@ -60,7 +63,8 @@ export class Particle implements PipelineRenderable { public setRadius(radius: number): void { this.mesh.geometry.dispose(); - this.mesh.geometry = new THREE.SphereGeometry(radius, 4, 2); + this.mesh.geometry = new THREE.SphereGeometry(Math.abs(radius), 4, 2); + this.mesh.geometry.name = this.mesh.name; } public object(): THREE.Object3D<THREE.Event> { diff --git a/src/03-simulation/physics/Springs.ts b/src/03-simulation/physics/Springs.ts index a43c7cf7220678ca45c981dd4d49649eec76ff37..5746ec187ef65c192b1d44c0b5824b62ef5b3604 100644 --- a/src/03-simulation/physics/Springs.ts +++ b/src/03-simulation/physics/Springs.ts @@ -13,6 +13,7 @@ export interface SpringParameters { } export abstract class Spring { + private static counter = 0; public positionA: THREE.Vector3; public positionB: THREE.Vector3; @@ -25,10 +26,19 @@ export abstract class Spring { this.positionB = params.positionB; this.restingLength = this.positionA.distanceTo(this.positionB); + const bufferGeometry = new THREE.BufferGeometry(); + bufferGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(6), 3)); + bufferGeometry.attributes.position.setXYZ(0, this.positionA.x, this.positionA.y, this.positionA.z); + bufferGeometry.attributes.position.setXYZ(1, this.positionB.x, this.positionB.y, this.positionB.z); + bufferGeometry.attributes.position.needsUpdate = true; + this.line = new THREE.Line( - new THREE.BufferGeometry().setFromPoints([this.positionA, this.positionB]), + bufferGeometry, new THREE.LineBasicMaterial({ color: params.color }) ); + + this.line.name = `Spring ${Spring.counter++}`; + this.line.geometry.name = this.line.name; } abstract get springConstant(): number; @@ -55,6 +65,7 @@ export abstract class Spring { this.line.geometry.attributes.position.setXYZ(0, this.positionA.x, this.positionA.y, this.positionA.z); this.line.geometry.attributes.position.setXYZ(1, this.positionB.x, this.positionB.y, this.positionB.z); this.line.geometry.attributes.position.needsUpdate = true; + this.line.geometry.name = this.line.name; } }