Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 47 additions & 39 deletions src/js/3d/coolerParticles.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ export let particleSystems = []
// along z on the plane parrallel to the floor is constant
const accelerations = [0.5, 1.5, 1]

let frameCount = 0

/**
* createCoolerParticles creates and attaches a particle system to a cooler object.
*
* @param {THREE.Object3D} coolerObject - The cooler object to attach particles to.
* @returns {void}
*/
export function createCoolerParticles(coolerObject) {
const particleCount = 500
const particleCount = 200
const particlesGeometry = new THREE.BufferGeometry()
const positions = new Float32Array(particleCount * 3)
const worldPositions = new Float32Array(particleCount * 3)
Expand Down Expand Up @@ -83,6 +85,9 @@ export function createCoolerParticles(coolerObject) {
* @returns {void}
*/
export function updateCoolerParticles() {
frameCount++
const doRaycast = frameCount % 5 === 0 // Raycast every 5 frames

particleSystems.forEach((particleData) => {
const geometry = particleData.geometry
const positions = geometry.attributes.position.array
Expand Down Expand Up @@ -115,19 +120,19 @@ export function updateCoolerParticles() {
positions[i + 1] += velocities[i + 1] * accelerations[1]
positions[i + 2] += velocities[i + 2] * accelerations[2]

// Add some turbulence
velocities[i] += (Math.random() - 0.5) * 0.001
velocities[i + 1] += (Math.random() - 0.5) * 0.01
velocities[i + 2] += (Math.random() - 0.5) * 0.02 // it expands along z-axis orthogonal to main direction x
// Add some turbulence (reduced frequency)
if (doRaycast) {
velocities[i] += (Math.random() - 0.5) * 0.001
velocities[i + 1] += (Math.random() - 0.5) * 0.01
velocities[i + 2] += (Math.random() - 0.5) * 0.02
}

// Check for collisions
// Convert particle position from local cooler space to world space
const particlePosition = new THREE.Vector3(
positions[i],
positions[i + 1],
positions[i + 2]
)

// Convert particle position from local cooler space to world space
const worldPosition = particlePosition.clone()
particleData.cooler.localToWorld(worldPosition)

Expand All @@ -136,43 +141,46 @@ export function updateCoolerParticles() {
worldPositions[i + 1] = worldPosition['y']
worldPositions[i + 2] = worldPosition['z']

// Set up raycaster from particle position
const rayDirection = new THREE.Vector3(
velocities[i],
velocities[i + 1],
velocities[i + 2]
).normalize()
if (doRaycast) {
// Check for collisions
// Set up raycaster from particle position
const rayDirection = new THREE.Vector3(
velocities[i],
velocities[i + 1],
velocities[i + 2]
).normalize()

raycasterCollision.set(worldPosition, rayDirection)
raycasterCollision.set(worldPosition, rayDirection)

// Get all objects except the cooler itself
const filteredObjects = models.filter(
(obj) => obj !== particleData.cooler
)
// Take only the object itself without its particles if present
const filteredObjectsMeshes = filteredObjects.map((obj) =>
obj.getObjectByProperty('type', 'Mesh')
)
const objectsToTest = [...filteredObjectsMeshes, floor, ...walls]
// Get all objects except the cooler itself
const filteredObjects = models.filter(
(obj) => obj !== particleData.cooler
)
// Take only the object itself without its particles if present
const filteredObjectsMeshes = filteredObjects.map((obj) =>
obj.getObjectByProperty('type', 'Mesh')
)
const objectsToTest = [...filteredObjectsMeshes, floor, ...walls]

const intersects = raycasterCollision.intersectObjects(
objectsToTest,
false // do not check descendants
)
const intersects = raycasterCollision.intersectObjects(
objectsToTest,
false // do not check descendants
)

// Check if collision is close enough (within particle size)
if (intersects.length > 0 && intersects[0].distance < 0.1) {
// Change particle color on collision
colors[i] = 1.0 // Red
colors[i + 1] = 0.5 // Orange-ish
colors[i + 2] = 0.0 // Blue = 0
// Check if collision is close enough (within particle size)
if (intersects.length > 0 && intersects[0].distance < 0.1) {
// Change particle color on collision
colors[i] = 1.0 // Red
colors[i + 1] = 0.5 // Orange-ish
colors[i + 2] = 0.0 // Blue = 0

// Stop the particle completely on wall collision
velocities[i] = 0
velocities[i + 1] = 0
velocities[i + 2] = 0
// Stop the particle completely on wall collision
velocities[i] = 0
velocities[i + 1] = 0
velocities[i + 2] = 0

maxLifetime[particleIndex] *= 0.9 // Reduce maxLifetime to avoid accumulation
maxLifetime[particleIndex] *= 0.9 // Reduce maxLifetime to avoid accumulation
}
}
}

Expand Down
156 changes: 98 additions & 58 deletions src/js/3d/particleCollisions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@ import * as THREE from 'three'
import { particleSystems as rackParticleSystems } from './rackParticles.js'
import { particleSystems as coolerParticleSystems } from './coolerParticles.js'

const GRID_SIZE = 1.0 // Cell size for spatial partitioning
const COLLISION_DISTANCE = 0.1

// Spatial hash for particles
function getGridKey(x, y, z) {
const ix = Math.floor(x / GRID_SIZE)
const iy = Math.floor(y / GRID_SIZE)
const iz = Math.floor(z / GRID_SIZE)
return `${ix},${iy},${iz}`
}

function buildSpatialHash(particleSystems) {
const hash = new Map()
particleSystems.forEach((systemData) => {
const worldPositions = systemData.geometry.attributes.worldPosition.array
for (let i = 0; i < worldPositions.length; i += 3) {
const x = worldPositions[i]
const y = worldPositions[i + 1]
const z = worldPositions[i + 2]
const key = getGridKey(x, y, z)
if (!hash.has(key)) hash.set(key, [])
hash.get(key).push({ systemData, index: i })
}
})
return hash
}

function getNearbyCells(key) {
const [ix, iy, iz] = key.split(',').map(Number)
const cells = []
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
for (let dz = -1; dz <= 1; dz++) {
cells.push(`${ix + dx},${iy + dy},${iz + dz}`)
}
}
}
return cells
}

/**
* checkInterParticleCollisions checks for collisions between particles emitted from racks (red)
* and particles emitted from coolers (blue).
Expand All @@ -12,70 +52,68 @@ import { particleSystems as coolerParticleSystems } from './coolerParticles.js'
* @returns {void}
*/
export function checkInterParticleCollisions() {
rackParticleSystems.forEach((rackData) => {
coolerParticleSystems.forEach((coolerData) => {
checkCollisionsBetweenSystems(rackData, coolerData)
// Build spatial hashes for efficient collision detection
const rackHash = buildSpatialHash(rackParticleSystems)
const coolerHash = buildSpatialHash(coolerParticleSystems)

// Check collisions using spatial partitioning
const checkedPairs = new Set() // To avoid duplicate checks

rackHash.forEach((rackParticles, key) => {
const nearbyCells = getNearbyCells(key)
nearbyCells.forEach((cellKey) => {
if (coolerHash.has(cellKey)) {
const coolerParticles = coolerHash.get(cellKey)
rackParticles.forEach((rackParticle) => {
coolerParticles.forEach((coolerParticle) => {
const pairKey = `${rackParticle.systemData.system.id}-${rackParticle.index}-${coolerParticle.systemData.system.id}-${coolerParticle.index}`
if (!checkedPairs.has(pairKey)) {
checkedPairs.add(pairKey)
checkCollisionBetweenParticles(rackParticle, coolerParticle)
}
})
})
}
})
})
}

function checkCollisionsBetweenSystems(rackData, coolerData) {
const rackGeometry = rackData.geometry
const coolerGeometry = coolerData.geometry
function checkCollisionBetweenParticles(rackParticle, coolerParticle) {
const rackWorldPositions =
rackParticle.systemData.geometry.attributes.worldPosition.array
const coolerWorldPositions =
coolerParticle.systemData.geometry.attributes.worldPosition.array

const rackWorldPositions = rackGeometry.attributes.worldPosition.array
const coolerWorldPositions = coolerGeometry.attributes.worldPosition.array
const rackPos = new THREE.Vector3(
rackWorldPositions[rackParticle.index],
rackWorldPositions[rackParticle.index + 1],
rackWorldPositions[rackParticle.index + 2]
)

// Check for collisions between each rack particle and cooler particle
const collisionDistance = 0.1 // Same as used in individual particle collision detection
const coolerPos = new THREE.Vector3(
coolerWorldPositions[coolerParticle.index],
coolerWorldPositions[coolerParticle.index + 1],
coolerWorldPositions[coolerParticle.index + 2]
)

// Track which rack particles have already collided this frame
const collidedRackParticles = new Set()
const distance = rackPos.distanceTo(coolerPos)

for (
let rackIndex = 0;
rackIndex < rackWorldPositions.length;
rackIndex += 3
) {
// Skip if this rack particle already collided
if (collidedRackParticles.has(rackIndex)) {
continue
}

const rackPos = new THREE.Vector3(
rackWorldPositions[rackIndex],
rackWorldPositions[rackIndex + 1],
rackWorldPositions[rackIndex + 2]
if (distance < COLLISION_DISTANCE) {
// Collision detected!
handleCollision(
rackParticle.systemData,
rackParticle.index,
coolerParticle.systemData,
coolerParticle.index
)

for (
let coolerIndex = 0;
coolerIndex < coolerWorldPositions.length;
coolerIndex += 3
) {
const coolerPos = new THREE.Vector3(
coolerWorldPositions[coolerIndex],
coolerWorldPositions[coolerIndex + 1],
coolerWorldPositions[coolerIndex + 2]
)
const distance = rackPos.distanceTo(coolerPos)

if (distance < collisionDistance) {
// Collision detected!
handleCollision(rackData, rackIndex, coolerData, coolerIndex)
// Mark this rack particle as collided so it won't hit other particles
collidedRackParticles.add(rackIndex)
break // Stop checking this rack particle against other cooler particles
}
}
}
}

function handleCollision(
rackData,
rackParticleIndex,
rackArrayIndex,
coolerData,
coolerParticleIndex
coolerArrayIndex
) {
// Make both particles disappear immediately by setting their lifetime to max
const rackGeometry = rackData.geometry
Expand All @@ -86,18 +124,20 @@ function handleCollision(
const coolerMaxLifetimes = coolerGeometry.attributes.maxLifetime.array
const coolerColors = coolerGeometry.attributes.color.array

// Rember to multiply indexes by 3 as colors are defined 3 dimensional vectors
rackColors[rackParticleIndex] = 1.0 // Red
rackColors[rackParticleIndex + 1] = 0.5 // Yellow
rackColors[rackParticleIndex + 2] = 0.0 // Blue
// Colors are at arrayIndex
rackColors[rackArrayIndex] = 1.0 // Red
rackColors[rackArrayIndex + 1] = 0.5 // Yellow
rackColors[rackArrayIndex + 2] = 0.0 // Blue

coolerColors[coolerParticleIndex] = 1.0 // Red
coolerColors[coolerParticleIndex + 1] = 0.5 // Yellow
coolerColors[coolerParticleIndex + 2] = 0.0 // Blue
coolerColors[coolerArrayIndex] = 1.0 // Red
coolerColors[coolerArrayIndex + 1] = 0.5 // Yellow
coolerColors[coolerArrayIndex + 2] = 0.0 // Blue

// Reduce maxLifetimes to avoid accumulation
rackMaxLifetimes[rackParticleIndex / 3] *= 0.9
coolerMaxLifetimes[coolerParticleIndex / 3] *= 0.9
const rackParticleIndex = rackArrayIndex / 3
const coolerParticleIndex = coolerArrayIndex / 3
rackMaxLifetimes[rackParticleIndex] *= 0.9
coolerMaxLifetimes[coolerParticleIndex] *= 0.9

rackGeometry.attributes.maxLifetime.needsUpdate = true
coolerGeometry.attributes.maxLifetime.needsUpdate = true
Expand Down
Loading