Help stopping clumping from perlin noise into motion?

I was looking for a really processor-cheap way to simulate dust motes floating around.

The idea was to combine perlin noise with a little randomness to control motion:

-- Mote class
Mote = class()

function Mote:init(x, y)
    self.position = vec2(x, y)
    self.velocity = vec2(math.random() * 4 - 2, math.random() * 4 - 2)  -- Random velocity between -2 and 2
    self.maxSpeed = math.random(4) * math.random() * math.random()  -- Random max speed 
    self.noiseOffset = math.random() * 1000  -- Unique offset for Perlin noise
end

function Mote:update()
    local newPosition, newVelocity = wind(self)
    self.position = newPosition
    self.velocity = newVelocity
    
    -- Screen wrapping
    if self.position.x < 0 then
        self.position.x = self.position.x + WIDTH
    elseif self.position.x > WIDTH then
        self.position.x = self.position.x - WIDTH
    end
    
    if self.position.y < 0 then
        self.position.y = self.position.y + HEIGHT
    elseif self.position.y > HEIGHT then
        self.position.y = self.position.y - HEIGHT
    end
end


function Mote:draw()
    ellipse(self.position.x, self.position.y, 5)
end

-- Wind function using Perlin noise
function wind(mote)
    local scale = 0.01
    local offset = mote.noiseOffset
    
    -- Using Perlin noise for both direction and acceleration
    local angle = noise(mote.position.x * scale + offset, mote.position.y * scale + offset) * math.pi * 2
    local windForce = vec2(math.cos(angle), math.sin(angle))
    
    -- Random adjustment with correct floating-point generation
    local randomAdjustment = vec2(math.random() * 1 - 0.5, math.random() * 1 - 0.5)
    windForce = windForce + randomAdjustment
    
    local newVelocity = limit(mote.velocity + windForce, mote.maxSpeed)
    local newPosition = mote.position + newVelocity
    
    return newPosition, newVelocity
end

-- Limit the magnitude of a vector
function limit(vec, max)
    if vec:len() > max then
        return vec:normalize() * max
    end
    return vec
end

-- Global variables
local motes = {}
local numMotes = 3000  -- Adjust the number of motes as needed

function setup()
    for i = 1, numMotes do
        table.insert(motes, Mote(math.random(WIDTH), math.random(HEIGHT)))
    end
end

function draw()
    background(40, 40, 50)
    for i, mote in ipairs(motes) do
        mote:update()
        mote:draw()
    end
end

…it works pretty well but there’s a weird clumping around the edges of the screen that I can’t figure out. Can anyone see why?

That’s an interesting one!

The issue is with the wrapping. When a mote jumps from one side of the screen to the other, then its input to the noise function changes. So it can be that on one side of the screen then the noise function returns something very different to what it returns on the other. In particular, the following could happen:

  1. A mote is near the right edge of the screen,
  2. The noise function at its position pushes it further to the right,
  3. The wrap-around kicks in and the mote is transported to the left edge of the screen,
  4. The noise function at its new position pushes it now to the left,
  5. The wrap-around kicks in again and the mote now transports back to the right,
  6. This repeats …

One fix is to take the wrapping out of the Mote:update function and put it in the draw as:

function Mote:draw()
    ellipse(self.position.x%WIDTH, self.position.y%HEIGHT, 5)
end

this means that self.position never does big jumps and so the wind adjustment never swings wildly around.

An alternative would be to ensure that the period of the noise function is compatible with the screen size. That would involve having different scale factors for the horizontal and vertical and ensuring that they were suitable for the screen size. Off the top of my head, I don’t remember the natural period of the noise function (my guess would be 1) so that would mean setting the scales to be 1/WIDTH and 1/HEIGHT respectively.

1 Like

I’ll have to try the second way, I think, because ultimately the motes need to be able to sense each other, so they have to all be working off common x, y coordinates.