Working 3d simulation with various button actions

main
Antonio De Lucreziis 2 years ago
parent 734f89ce15
commit e60008d71a

@ -0,0 +1,10 @@
import { Vec3 } from './math.js'
export function enforceDistance(p1, p2, targetDistance) {
const v = Vec3.sub(p2, p1)
const d = Vec3.norm2(v)
const factor = (d - targetDistance) / d
return [Vec3.add(p1, Vec3.scale(v, 0.5 * factor)), Vec3.add(p2, Vec3.scale(v, -0.5 * factor))]
}

@ -51,6 +51,7 @@ export const Vec2 = {
return [x / norm, y / norm] return [x / norm, y / norm]
}, },
} }
export const Vec3 = { export const Vec3 = {
Mutate: { Mutate: {
add(target, [x, y, z]) { add(target, [x, y, z]) {
@ -98,6 +99,21 @@ export const Vec3 = {
const norm = Vec3.norm2([x, y, z]) const norm = Vec3.norm2([x, y, z])
return [x / norm, y / norm, z / norm] return [x / norm, y / norm, z / norm]
}, },
findNearest3d(curve, pt) {
let index = 0
let minDist = Infinity
for (let i = 0; i < curve.length; i++) {
const d = Vec3.distance2(curve[i], pt)
if (d < minDist) {
index = i
minDist = d
}
}
return [index, minDist]
},
} }
export function clamp(min, value, max) { export function clamp(min, value, max) {

@ -1,13 +1,14 @@
import { useEffect, useRef, useState } from 'preact/hooks' import { useEffect, useRef, useState } from 'preact/hooks'
import { resampleCurve, simplifyCurve } from '../../lib/math/curves.js' import { enforceDistance } from '../../lib/math/constraint.js'
import { Vec2 } from '../../lib/math/math.js' import { resampleCurve } from '../../lib/math/curves.js'
import { Vec2, Vec3 } from '../../lib/math/math.js'
function mod(i, modulus) { function mod(i, modulus) {
const r = i % modulus const r = i % modulus
return r < 0 ? r + modulus : r return r < 0 ? r + modulus : r
} }
function createSimulation2d(positions, velocities, accelerations) { function createSimulation3d(positions, velocities, accelerations) {
return { return {
positions, positions,
velocities, velocities,
@ -19,8 +20,8 @@ function createSimulation2d(positions, velocities, accelerations) {
const newVelocities = Array.from({ length: n }, () => []) const newVelocities = Array.from({ length: n }, () => [])
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
newPositions[i] = Vec2.add(Vec2.add(this.positions[i], this.velocities[i]), Vec2.scale(this.accelerations[i], 0.5)) newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5))
newVelocities[i] = Vec2.add(this.velocities[i], Vec2.scale(Vec2.add(this.accelerations[i], newAccelerations[i]), 0.5)) newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5))
} }
this.positions = newPositions this.positions = newPositions
@ -30,59 +31,80 @@ function createSimulation2d(positions, velocities, accelerations) {
} }
} }
const NODE_BALL_RADIUS = 30
const SEGMENT_LENGTH = 10
const PASSTHROUGH_MIN_INDICES = NODE_BALL_RADIUS / SEGMENT_LENGTH + 1
const INVERT_CROSSING_RADIUS = SEGMENT_LENGTH * 3
class KnotSimulation { class KnotSimulation {
constructor(knotRef) { constructor(knotRef, modeRef) {
/** ref allo stato esterno che maneggia anche Preact */ /** ref allo stato esterno che maneggia anche Preact */
this.knotRef = knotRef this.knotRef = knotRef
this.modeRef = modeRef
/** percorso temporaneo utilizzato mentre si disegna sul layer */ /** percorso temporaneo utilizzato mentre si disegna sul layer */
this.ghostPath = null this.ghostPath = null
/** position where to place the black hole while right-clicking the canvas */ /** position where to place the black hole while right-clicking the canvas */
this.blackHolePosition = null this.draggingIndex = null
/** particle simulation that holds positions, velocities and accelerations */ /** particle simulation that holds positions, velocities and accelerations */
this.particleSimulation = null this.particleSimulation = null
} }
onMouseDown(x, y, buttons) { onMouseDown(x, y, buttons) {
this.mousePosition = [x, y]
if (buttons === 1) { if (buttons === 1) {
this.ghostPath = [] this.ghostPath = []
} }
if (buttons === 2) { if (buttons === 2) {
this.blackHolePosition = [x, y] const [i] = Vec3.findNearest3d(this.particleSimulation.positions, [...this.mousePosition, 0])
this.draggingIndex = i
}
if (buttons === 4) {
const { positions } = this.particleSimulation
for (let i = 0; i < positions.length; i++) {
if (Vec2.distance2([x, y], positions[i]) < INVERT_CROSSING_RADIUS) {
positions[i][2] *= -1
}
}
} }
} }
onMouseDrag(x, y) { onMouseDrag(x, y) {
this.mousePosition = [x, y]
if (this.ghostPath) { if (this.ghostPath) {
this.ghostPath.push([x, y]) this.ghostPath.push([x, y])
} }
if (this.blackHolePosition) {
this.blackHolePosition = [x, y]
}
} }
onMouseUp() { onMouseUp() {
if (this.ghostPath) { if (this.ghostPath) {
this.ghostPath.push(this.ghostPath[0]) this.ghostPath.push(this.ghostPath[0])
const curve = resampleCurve(this.ghostPath, 10, 10) const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH)
curve.pop() curve.pop()
this.setPositions(curve) // convert the 2d curve to a 3d curve
// this.setPositions(curve.map(([x, y], i) => [x, y, 0.01 * (Math.random() * 2 - 1)]))
this.setPositions(curve.map(([x, y], i) => [x, y, 0.5 * i]))
this.ghostPath = null this.ghostPath = null
} }
this.blackHolePosition = null this.draggingIndex = null
} }
setPositions(positions) { setPositions(positions) {
this.particleSimulation = createSimulation2d( this.particleSimulation = createSimulation3d(
positions, positions,
Array.from({ length: positions.length }, () => [0, 0]), Array.from({ length: positions.length }, () => [0, 0, 0]),
Array.from({ length: positions.length }, () => [0, 0]) Array.from({ length: positions.length }, () => [0, 0, 0])
) )
} }
@ -94,45 +116,71 @@ class KnotSimulation {
const { positions } = this.particleSimulation const { positions } = this.particleSimulation
const N = positions.length const N = positions.length
const newAccelerations = Array.from({ length: N }, () => [0, 0]) const newAccelerations = Array.from({ length: N }, () => [0, 0, 0])
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
if (this.blackHolePosition) { Vec3.Mutate.add(newAccelerations[i], [0, 0, -0.1 * Math.sign(positions[i][2])])
const v = Vec2.sub(this.blackHolePosition, positions[i]) }
const dir = Vec2.normalize(v)
const dist = Vec2.norm2(v)
Vec2.Mutate.add(newAccelerations[i], Vec2.scale(dir, 1e6 / Math.max(dist, 75) ** 3)) // dragging point
if (this.draggingIndex !== null) {
this.particleSimulation.positions[this.draggingIndex] = [...this.mousePosition, 0]
}
// forzo la lunghezza delle barrette
for (let i = 0; i < 4; i++) {
for (let i = 0; i < N; i++) {
const curr = positions.at(i)
const next = positions.at((i + 1) % N)
const [pt1, pt2] = enforceDistance(curr, next, SEGMENT_LENGTH)
positions[i] = pt1
positions[mod(i + 1, N)] = pt2
} }
} }
// barrette rigide // raddrizzamento dei link
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
const prev = positions.at((i - 1) % N) const prev = positions.at((i - 1) % N)
const curr = positions.at(i) const curr = positions.at(i)
const next = positions.at((i + 1) % N) const next = positions.at((i + 1) % N)
const v = Vec2.sub(next, curr) const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5)
const d = Vec2.norm2(v) const jointFactor = 0.5
const factor = (d - 10) / d Vec3.Mutate.add(positions.at((i - 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor))
positions[mod(i, N)] = Vec2.add(curr, Vec2.scale(v, 0.5 * factor)) Vec3.Mutate.add(positions.at((i + 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
positions[mod(i + 1, N)] = Vec2.add(next, Vec2.scale(v, -0.5 * factor)) }
const baseForce = Vec2.scale(Vec2.add(Vec2.sub(prev, curr), Vec2.sub(next, curr)), 0.5) // repulsione per ogni coppia
const jointFactor = 1 for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
if (
i < j &&
Math.abs(i - j) > PASSTHROUGH_MIN_INDICES &&
Math.abs(i - j + N) > PASSTHROUGH_MIN_INDICES &&
Math.abs(i - j - N) > PASSTHROUGH_MIN_INDICES
) {
const p1 = positions[i]
const p2 = positions[j]
Vec2.Mutate.add(positions.at((i - 1) % N), Vec2.scale(baseForce, -jointFactor / 2)) const dist = Vec3.distance2(p1, p2)
Vec2.Mutate.add(positions[i], Vec2.scale(baseForce, jointFactor)) if (dist <= NODE_BALL_RADIUS) {
Vec2.Mutate.add(positions.at((i + 1) % N), Vec2.scale(baseForce, -jointFactor / 2)) const [newPt1, newPt2] = enforceDistance(p1, p2, NODE_BALL_RADIUS)
positions[i] = newPt1
positions[j] = newPt2
}
}
}
} }
this.particleSimulation.update(newAccelerations) this.particleSimulation.update(newAccelerations)
// attrito viscoso // attrito viscoso
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
Vec2.Mutate.scale(this.particleSimulation.velocities[i], 0.1) Vec3.Mutate.scale(this.particleSimulation.velocities[i], 0.5)
} }
this.knotRef.current.points = this.particleSimulation.positions this.knotRef.current.points = this.particleSimulation.positions
@ -186,17 +234,23 @@ class KnotSimulation {
const particleCount = positions.length const particleCount = positions.length
if (particleCount > 0) { if (particleCount > 0) {
g.beginPath() const sortedZ = [...positions]
const [x0, y0] = positions[0] .map(([, , z], i) => [i, z])
g.moveTo(x0, y0) .sort(([, z1], [, z2]) => z2 - z1)
.map(([i]) => i)
g.strokeStyle = '#333' g.strokeStyle = '#333'
for (let i = 0; i < particleCount; i++) { sortedZ.forEach(i => {
const prev2 = positions.at((i - 2) % particleCount) function toProjection([x, y, z]) {
const prev = positions.at((i - 1) % particleCount) // return [x + z, y + z]
const curr = positions.at(i % particleCount) return [x, y]
const next = positions.at((i + 1) % particleCount) }
const next2 = positions.at((i + 2) % particleCount)
const prev2 = toProjection(positions.at((i - 2) % particleCount))
const prev = toProjection(positions.at((i - 1) % particleCount))
const curr = toProjection(positions.at(i % particleCount))
const next = toProjection(positions.at((i + 1) % particleCount))
const next2 = toProjection(positions.at((i + 2) % particleCount))
g.save() g.save()
{ {
@ -229,15 +283,7 @@ class KnotSimulation {
g.stroke() g.stroke()
} }
g.restore() g.restore()
} })
// for (let i = 0; i < particleCount; i++) {
// g.fillStyle = '#f00'
// g.beginPath()
// g.ellipse(...positions[i], 2, 2, 0, 0, 2 * Math.PI)
// g.fill()
// }
} }
} }
} }
@ -264,7 +310,7 @@ export const KnotLayer = ({ knotRef }) => {
simTimerHandle = setInterval(() => { simTimerHandle = setInterval(() => {
knotSim.update() knotSim.update()
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext)) requestAnimationFrame(() => knotSim.render($canvas.graphicsContext))
}, 1000 / 30) }, 1000 / 60)
} }
return () => { return () => {

Loading…
Cancel
Save