Animating grass with perlin noise

Hello everyone!
Below is a mockup for the new look of my WIP Eclipse. I thought about animating the grass with a mix of pre-rendered assets and perlin noise. Any idea on how you'd go about doing that? Thanks in advance!

Mockup

3 Likes

For anyone still interested, following is my first, very rough approach:

I use the following image as matrix image table (e.g. 20x24), pick a random sub-image and place them next to each other with a random overlap.
grass-table-20-24
(Image created using a random brush in Gimp)

I then use a perlin noise generator and a timer callback to randomly offset the placement of each of the sub-images on every timer tick.
grass

Edit: Following is a slightly improved approach, where the image is split once vertically and the top half moves twice as much as the lower half.
grass2

1 Like

Nice effect! Are you applying noise independently/randomly to each tuft or sampling the same noise function over the entire field based on x coordinate? I wonder if you could make it look more like breeze by scaling up the noise pattern so that more of the tufts in the same area appear to sway together in waves.

I once used Perlin noise to simulate gentle rolling waves in a bowl of literal (though of course virtual) alphabet soup, with the letters bobbing according to their position, so I know the wave effect can work nice and gently.

1 Like

So below is the test code for the effect. Im still figuring out how to effectively use the perlin noise API in the SDK, so if you have any idea on how to improve it, id greatly appreciate that ^^

local img = gfx.imagetable.new("assets/images/grass-small")
local x = 0
local y = 200
local sway = 4
local grass = {}
while(x < dsp.getWidth()+sway) do
    local r = math.random(img:getLength()/2)
    local s1 = gfx.sprite.new(img:getImage(r, 1))
    local s2 = gfx.sprite.new(img:getImage(r, 2))
    x = math.max(0, x - math.random(sway))
    s1:moveTo(x, y)
    s1:add()
    s2:moveTo(x, y+s1.height)
    s2:add()
    table.insert(grass, {s1, s2, x})
    x += s1.width
end

local pc = 1
local t = pd.timer.keyRepeatTimerWithDelay(1000, 100, function ()
    local noise1 = gfx.perlinArray(#grass, pc, 1, 0.5, 0)
    for i, p in ipairs(grass) do
        local s1, s2, x = p[1], p[2], p[3]
        s1:moveTo(x + noise1[i]*2*sway - sway, s1.y)
        s2:moveTo(x + noise1[i]*sway - sway/2, s2.y)
    end
    pc += 1
end)

So I sample one line of noise with a sample for each tuft and scroll the function along the x-axis (if that is indeed the proper use of the function). I think you'd get a better result by dividing the image into more vertical section and extend the noise function by that many sections in the y-direction.

1 Like

Cool, your approach looks sound, and you are applying the noise field across your grass smoothly given use of the perlinArray function. I’d play with the dx parameter (currently 0.5) and try decreasing it steadily until you start to see more of the grass tufts swaying together. (I haven’t tested this, but my intuition is that as that value trends to 0 all the grass will sway together.)

I’d also try only animating the top half of the grass, or split it into thirds using 1x and 2x sway for the top 2/3 and leaving the bottom fixed. That way it won’t look like the grass is sliding relative to the ground.

One more thought: Right now you’re subtracting a constant amount (sway or sway/2) to center the noise so the grass can sway in either direction. You could also apply a separate Perlin value to that parameter so that the direction of the breeze biases in one direction or the other over time to simulate gusts or shifts in wind direction.

1 Like

These are all very good remarks, thank you! Ill try it out tomorrow and post the results :slight_smile:

I've implemented all of your remarks and now the grass looks really organic and very dynamic. The additional perlin noise for direction gives the impression of more intense gusts of wind. I think I'm very satisfied with the result. :slight_smile:

grass_final

6 Likes

Very cool!

(If it helps performance of the game, you could use your code to pre-render a series of frames at each launch, into a table of long narrow bitmaps, and then cycle those during gameplay. Maybe in ping-pong order if they won’t loop nicely. I’m always underestimating the sheer volume of bitmaps Playdate can hold in memory!)

2 Likes

Just chiming in here out of curiosity. I wonder if you could modify my Fluid class to handle this animation and avoid generating Perlin noise entirely. This way you could randomly “touch” parts of the scene and then just map the fluid’s y offset to a x offset instead. So when the fluid is at its peak, the grass would be offset most and propagate left and/or right. Maybe! Not sure exactly what this would look like in practice, but I am curious now! :slight_smile:

1 Like

Prerendering is always a good idea, but I think its not necessary in this case, since only around a dozen points of noise are sampled so it doesnt take up much cpu time.

2 Likes