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]
},
}
export const Vec3 = {
Mutate: {
add(target, [x, y, z]) {
@ -98,6 +99,21 @@ export const Vec3 = {
const norm = Vec3.norm2([x, y, z])
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) {

@ -1,13 +1,14 @@
import { useEffect, useRef, useState } from 'preact/hooks'
import { resampleCurve, simplifyCurve } from '../../lib/math/curves.js'
import { Vec2 } from '../../lib/math/math.js'
import { enforceDistance } from '../../lib/math/constraint.js'
import { resampleCurve } from '../../lib/math/curves.js'
import { Vec2, Vec3 } from '../../lib/math/math.js'
function mod(i, modulus) {
const r = i % modulus
return r < 0 ? r + modulus : r
}
function createSimulation2d(positions, velocities, accelerations) {
function createSimulation3d(positions, velocities, accelerations) {
return {
positions,
velocities,
@ -19,8 +20,8 @@ function createSimulation2d(positions, velocities, accelerations) {
const newVelocities = Array.from({ length: n }, () => [])
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))
newVelocities[i] = Vec2.add(this.velocities[i], Vec2.scale(Vec2.add(this.accelerations[i], newAccelerations[i]), 0.5))
newPositions[i] = Vec3.add(Vec3.add(this.positions[i], this.velocities[i]), Vec3.scale(this.accelerations[i], 0.5))
newVelocities[i] = Vec3.add(this.velocities[i], Vec3.scale(Vec3.add(this.accelerations[i], newAccelerations[i]), 0.5))
}
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 {
constructor(knotRef) {
constructor(knotRef, modeRef) {
/** ref allo stato esterno che maneggia anche Preact */
this.knotRef = knotRef
this.modeRef = modeRef
/** percorso temporaneo utilizzato mentre si disegna sul layer */
this.ghostPath = null
/** 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 */
this.particleSimulation = null
}
onMouseDown(x, y, buttons) {
this.mousePosition = [x, y]
if (buttons === 1) {
this.ghostPath = []
}
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) {
this.mousePosition = [x, y]
if (this.ghostPath) {
this.ghostPath.push([x, y])
}
if (this.blackHolePosition) {
this.blackHolePosition = [x, y]
}
}
onMouseUp() {
if (this.ghostPath) {
this.ghostPath.push(this.ghostPath[0])
const curve = resampleCurve(this.ghostPath, 10, 10)
const curve = resampleCurve(this.ghostPath, SEGMENT_LENGTH, SEGMENT_LENGTH)
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.blackHolePosition = null
this.draggingIndex = null
}
setPositions(positions) {
this.particleSimulation = createSimulation2d(
this.particleSimulation = createSimulation3d(
positions,
Array.from({ length: positions.length }, () => [0, 0]),
Array.from({ length: positions.length }, () => [0, 0])
Array.from({ length: positions.length }, () => [0, 0, 0]),
Array.from({ length: positions.length }, () => [0, 0, 0])
)
}
@ -94,45 +116,71 @@ class KnotSimulation {
const { positions } = this.particleSimulation
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++) {
if (this.blackHolePosition) {
const v = Vec2.sub(this.blackHolePosition, positions[i])
const dir = Vec2.normalize(v)
const dist = Vec2.norm2(v)
Vec3.Mutate.add(newAccelerations[i], [0, 0, -0.1 * Math.sign(positions[i][2])])
}
// 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)
Vec2.Mutate.add(newAccelerations[i], Vec2.scale(dir, 1e6 / Math.max(dist, 75) ** 3))
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++) {
const prev = positions.at((i - 1) % N)
const curr = positions.at(i)
const next = positions.at((i + 1) % N)
const v = Vec2.sub(next, curr)
const d = Vec2.norm2(v)
const factor = (d - 10) / d
const baseForce = Vec3.scale(Vec3.add(Vec3.sub(prev, curr), Vec3.sub(next, curr)), 0.5)
const jointFactor = 0.5
positions[mod(i, N)] = Vec2.add(curr, Vec2.scale(v, 0.5 * factor))
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)
const jointFactor = 1
Vec3.Mutate.add(positions.at((i - 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
Vec3.Mutate.add(positions[i], Vec3.scale(baseForce, jointFactor))
Vec3.Mutate.add(positions.at((i + 1) % N), Vec3.scale(baseForce, -jointFactor / 2))
}
Vec2.Mutate.add(positions.at((i - 1) % N), Vec2.scale(baseForce, -jointFactor / 2))
Vec2.Mutate.add(positions[i], Vec2.scale(baseForce, jointFactor))
Vec2.Mutate.add(positions.at((i + 1) % N), Vec2.scale(baseForce, -jointFactor / 2))
// repulsione per ogni coppia
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]
const dist = Vec3.distance2(p1, p2)
if (dist <= NODE_BALL_RADIUS) {
const [newPt1, newPt2] = enforceDistance(p1, p2, NODE_BALL_RADIUS)
positions[i] = newPt1
positions[j] = newPt2
}
}
}
}
this.particleSimulation.update(newAccelerations)
// attrito viscoso
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
@ -186,17 +234,23 @@ class KnotSimulation {
const particleCount = positions.length
if (particleCount > 0) {
g.beginPath()
const [x0, y0] = positions[0]
g.moveTo(x0, y0)
const sortedZ = [...positions]
.map(([, , z], i) => [i, z])
.sort(([, z1], [, z2]) => z2 - z1)
.map(([i]) => i)
g.strokeStyle = '#333'
for (let i = 0; i < particleCount; i++) {
const prev2 = positions.at((i - 2) % particleCount)
const prev = positions.at((i - 1) % particleCount)
const curr = positions.at(i % particleCount)
const next = positions.at((i + 1) % particleCount)
const next2 = positions.at((i + 2) % particleCount)
sortedZ.forEach(i => {
function toProjection([x, y, z]) {
// return [x + z, y + z]
return [x, y]
}
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()
{
@ -229,15 +283,7 @@ class KnotSimulation {
g.stroke()
}
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(() => {
knotSim.update()
requestAnimationFrame(() => knotSim.render($canvas.graphicsContext))
}, 1000 / 30)
}, 1000 / 60)
}
return () => {

Loading…
Cancel
Save