Come posso creare acqua 2D con onde dinamiche?


81

Il nuovo Super Mario Bros ha un'acqua 2D davvero fantastica che mi piacerebbe imparare a creare.

Ecco un video che lo mostra. Una parte illustrativa:

Nuovi effetti d'acqua Super Mario Bros

Le cose che colpiscono l'acqua creano onde. Ci sono anche onde "di fondo" costanti. Puoi vedere le onde costanti subito dopo le 00:50 nel video, quando la videocamera non si muove.

Presumo che gli effetti splash funzionino come nella prima parte di questo tutorial .

Tuttavia, in NSMB l'acqua ha anche onde costanti sulla superficie e gli schizzi sembrano molto diversi. Un'altra differenza è che nel tutorial, se crei uno splash, crea prima un profondo "buco" nell'acqua all'origine dello splash. Nel nuovo super mario bros questo buco è assente o molto più piccolo. Mi riferisco agli schizzi che il giocatore crea quando salta dentro e fuori dall'acqua.

Come posso creare una superficie d'acqua con onde e schizzi costanti?

Sto programmando in XNA. L'ho provato da solo, ma non sono riuscito a far funzionare bene le onde sinusoidali di sfondo insieme alle onde dinamiche.

Non sto chiedendo come gli sviluppatori di New Super Mario Bros abbiano fatto esattamente questo, ma sono solo interessato a ricreare un effetto simile.

Risposte:


147

L'ho provato.

Spruzzi (molle)

Come menziona quel tutorial , la superficie dell'acqua è come un filo: se tiri un punto del filo, anche i punti accanto a quel punto verranno abbassati. Tutti i punti sono anche attratti da una linea di base.

Fondamentalmente sono un sacco di molle verticali una accanto all'altra che si tirano anche a vicenda.

L'ho disegnato a Lua usando LÖVE e ho ottenuto questo:

animazione di uno splash

Sembra plausibile. Oh Hooke , bel genio.

Se vuoi giocarci, ecco una porta JavaScript per gentile concessione di Phil ! Il mio codice è alla fine di questa risposta.

Onde di sfondo (sine impilate)

Le onde di sfondo naturale mi sembrano un insieme di onde sinusoidali (con ampiezze, fasi e lunghezze d'onda diverse) tutte sommate. Ecco come appariva quando l'ho scritto:

onde di fondo prodotte dall'interferenza sinusoidale

I modelli di interferenza sembrano abbastanza plausibili.

Tutti insieme ora

Quindi è abbastanza semplice sommare le onde splash e le onde di sfondo:

onde di fondo, con spruzzi

Quando si verificano degli schizzi, puoi vedere piccoli cerchi grigi che mostrano dove si troverebbe l'onda di sfondo originale.

Assomiglia molto al video che hai collegato , quindi lo considero un esperimento riuscito.

Ecco il mio main.lua(l'unico file). Penso che sia abbastanza leggibile.

-- Resolution of simulation
NUM_POINTS = 50
-- Width of simulation
WIDTH = 400
-- Spring constant for forces applied by adjacent points
SPRING_CONSTANT = 0.005
-- Sprint constant for force applied to baseline
SPRING_CONSTANT_BASELINE = 0.005
-- Vertical draw offset of simulation
Y_OFFSET = 300
-- Damping to apply to speed changes
DAMPING = 0.98
-- Number of iterations of point-influences-point to do on wave per step
-- (this makes the waves animate faster)
ITERATIONS = 5

-- Make points to go on the wave
function makeWavePoints(numPoints)
    local t = {}
    for n = 1,numPoints do
        -- This represents a point on the wave
        local newPoint = {
            x    = n / numPoints * WIDTH,
            y    = Y_OFFSET,
            spd = {y=0}, -- speed with vertical component zero
            mass = 1
        }
        t[n] = newPoint
    end
    return t
end

-- A phase difference to apply to each sine
offset = 0

NUM_BACKGROUND_WAVES = 7
BACKGROUND_WAVE_MAX_HEIGHT = 5
BACKGROUND_WAVE_COMPRESSION = 1/5
-- Amounts by which a particular sine is offset
sineOffsets = {}
-- Amounts by which a particular sine is amplified
sineAmplitudes = {}
-- Amounts by which a particular sine is stretched
sineStretches = {}
-- Amounts by which a particular sine's offset is multiplied
offsetStretches = {}
-- Set each sine's values to a reasonable random value
for i=1,NUM_BACKGROUND_WAVES do
    table.insert(sineOffsets, -1 + 2*math.random())
    table.insert(sineAmplitudes, math.random()*BACKGROUND_WAVE_MAX_HEIGHT)
    table.insert(sineStretches, math.random()*BACKGROUND_WAVE_COMPRESSION)
    table.insert(offsetStretches, math.random()*BACKGROUND_WAVE_COMPRESSION)
end
-- This function sums together the sines generated above,
-- given an input value x
function overlapSines(x)
    local result = 0
    for i=1,NUM_BACKGROUND_WAVES do
        result = result
            + sineOffsets[i]
            + sineAmplitudes[i] * math.sin(
                x * sineStretches[i] + offset * offsetStretches[i])
    end
    return result
end

wavePoints = makeWavePoints(NUM_POINTS)

-- Update the positions of each wave point
function updateWavePoints(points, dt)
    for i=1,ITERATIONS do
    for n,p in ipairs(points) do
        -- force to apply to this point
        local force = 0

        -- forces caused by the point immediately to the left or the right
        local forceFromLeft, forceFromRight

        if n == 1 then -- wrap to left-to-right
            local dy = points[# points].y - p.y
            forceFromLeft = SPRING_CONSTANT * dy
        else -- normally
            local dy = points[n-1].y - p.y
            forceFromLeft = SPRING_CONSTANT * dy
        end
        if n == # points then -- wrap to right-to-left
            local dy = points[1].y - p.y
            forceFromRight = SPRING_CONSTANT * dy
        else -- normally
            local dy = points[n+1].y - p.y
            forceFromRight = SPRING_CONSTANT * dy
        end

        -- Also apply force toward the baseline
        local dy = Y_OFFSET - p.y
        forceToBaseline = SPRING_CONSTANT_BASELINE * dy

        -- Sum up forces
        force = force + forceFromLeft
        force = force + forceFromRight
        force = force + forceToBaseline

        -- Calculate acceleration
        local acceleration = force / p.mass

        -- Apply acceleration (with damping)
        p.spd.y = DAMPING * p.spd.y + acceleration

        -- Apply speed
        p.y = p.y + p.spd.y
    end
    end
end

-- Callback when updating
function love.update(dt)
    if love.keyboard.isDown"k" then
        offset = offset + 1
    end

    -- On click: Pick nearest point to mouse position
    if love.mouse.isDown("l") then
        local mouseX, mouseY = love.mouse.getPosition()
        local closestPoint = nil
        local closestDistance = nil
        for _,p in ipairs(wavePoints) do
            local distance = math.abs(mouseX-p.x)
            if closestDistance == nil then
                closestPoint = p
                closestDistance = distance
            else
                if distance <= closestDistance then
                    closestPoint = p
                    closestDistance = distance
                end
            end
        end

        closestPoint.y = love.mouse.getY()
    end

    -- Update positions of points
    updateWavePoints(wavePoints, dt)
end

local circle = love.graphics.circle
local line   = love.graphics.line
local color  = love.graphics.setColor
love.graphics.setBackgroundColor(0xff,0xff,0xff)

-- Callback for drawing
function love.draw(dt)

    -- Draw baseline
    color(0xff,0x33,0x33)
    line(0, Y_OFFSET, WIDTH, Y_OFFSET)

    -- Draw "drop line" from cursor

    local mouseX, mouseY = love.mouse.getPosition()
    line(mouseX, 0, mouseX, Y_OFFSET)
    -- Draw click indicator
    if love.mouse.isDown"l" then
        love.graphics.circle("line", mouseX, mouseY, 20)
    end

    -- Draw overlap wave animation indicator
    if love.keyboard.isDown "k" then
        love.graphics.print("Overlap waves PLAY", 10, Y_OFFSET+50)
    else
        love.graphics.print("Overlap waves PAUSED", 10, Y_OFFSET+50)
    end


    -- Draw points and line
    for n,p in ipairs(wavePoints) do
        -- Draw little grey circles for overlap waves
        color(0xaa,0xaa,0xbb)
        circle("line", p.x, Y_OFFSET + overlapSines(p.x), 2)
        -- Draw blue circles for final wave
        color(0x00,0x33,0xbb)
        circle("line", p.x, p.y + overlapSines(p.x), 4)
        -- Draw lines between circles
        if n == 1 then
        else
            local leftPoint = wavePoints[n-1]
            line(leftPoint.x, leftPoint.y + overlapSines(leftPoint.x), p.x, p.y + overlapSines(p.x))
        end
    end
end

Bella risposta! Grazie mille. Inoltre, grazie per aver rivisto la mia domanda, posso vedere come sia più chiaro. Anche le gif sono molto utili. Sai per caso un modo per prevenire il grande buco che emerge anche quando si crea uno splash? È possibile che Mikael Högström abbia già risposto a questa domanda, ma ci avevo provato anche prima di pubblicare questa domanda e il mio risultato era che il buco aveva una forma triangolare e sembrava molto realistico.
Berry,

Per troncare la profondità del "splash hole", è possibile limitare l'ampiezza massima dell'onda, vale a dire quanto è permesso a qualsiasi punto di allontanarsi dalla linea di base.
Anko,

3
A proposito per chiunque sia interessato: Invece di avvolgere i lati dell'acqua, ho scelto di utilizzare la linea di base per normalizzare i lati. Altrimenti, se crei una spruzzata a destra dell'acqua, creerebbe anche onde a sinistra dell'acqua, che ho trovato irrealistico. Inoltre, poiché non avvolgevo le onde, le onde di fondo si appiattivano molto rapidamente. Pertanto ho scelto di rendere questi effetti solo grafici, come ha detto Mikael Högström, in modo che le onde di fondo non vengano incluse nei calcoli per velocità e accelerazione.
Berry,

1
Volevo solo fartelo sapere. Abbiamo parlato di troncare lo "splash-hole" con un'istruzione if. All'inizio ero riluttante a farlo. Ma ora ho notato che in realtà funziona perfettamente, poiché le onde di fondo impediranno alla superficie di essere piatta.
Berry,

4
Ho convertito questo codice wave in JavaScript e l'ho messo su jsfiddle qui: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick il

11

Per la soluzione (matematicamente parlando puoi risolvere il problema con la risoluzione di equazioni differenziali, ma sono sicuro che non lo fanno in questo modo) di creare onde hai 3 possibilità (a seconda di quanto dettagliato dovrebbe ottenere):

  1. Calcola le onde con le funzioni trigonometriche (la più semplice e la più veloce)
  2. Fallo come ha proposto Anko
  3. Risolvi le equazioni differenziali
  4. Usa ricerche di texture

Soluzione 1

Davvero semplice, per ogni onda calcoliamo la distanza (assoluta) da ciascun punto della superficie alla sorgente e calcoliamo l'altezza con la formula

1.0f/(dist*dist) * sin(dist*FactorA + Phase)

dove

  • dist è la nostra distanza
  • Il fattore A è un valore che significa quanto dovrebbero essere veloci / dense le onde
  • La fase è la fase dell'onda, dobbiamo incrementarla con il tempo per ottenere un'onda animata

Si noti che possiamo aggiungere tutti i termini che vogliamo insieme (principio di sovrapposizione).

professionista

  • È davvero veloce da calcolare
  • È facile da implementare

Contra

  • Per (semplici) riflessi su una superficie 1d dobbiamo creare sorgenti d'onda "fantasma" per simulare i riflessi, questo è più complicato su superfici 2d ed è uno dei limiti di questo semplice approccio

Soluzione 2

professionista

  • È anche semplice
  • Permette di calcolare facilmente i riflessi
  • Può essere esteso facilmente allo spazio 2d o 3d

Contra

  • Può diventare numericamente instabile se il valore di dumping è troppo elevato
  • ha bisogno di più potenza di calcolo della soluzione 1 (ma non così tanto come la soluzione 3 )

Soluzione 3

Ora ho colpito un muro duro, questa è la soluzione più complicata.

Non ho implementato questo, ma è possibile risolvere questi mostri.

Qui puoi trovare una presentazione sulla sua matematica, non è semplice e esistono anche equazioni differenziali per diversi tipi di onde.

Ecco un elenco non completo con alcune equazioni differenziali per risolvere casi più speciali (Solitoni, Peakon, ...)

professionista

  • Onde realistiche

Contra

  • Per la maggior parte dei giochi non vale la pena
  • Richiede più tempo di calcolo

Soluzione 4

Un po 'più complicata della soluzione 1 ma non così complicata una soluzione 3.

Usiamo trame precalcolate e le mescoliamo insieme, dopodiché utilizziamo la mappatura di spostamento (in realtà un metodo per le onde 2d ma il principio può funzionare anche per le onde 1d)

Il gioco sturmovik ha usato questo approccio ma non trovo il link all'articolo a riguardo.

professionista

  • è più semplice di 3
  • ottiene buoni risultati (per 2d)
  • può sembrare realistico se gli artisti fanno un ottimo lavoro

Contra

  • difficile da animare
  • schemi ripetuti potrebbero diventare visibili all'orizzonte

6

Per aggiungere onde costanti aggiungere un paio di onde sinusoidali dopo aver calcolato la dinamica. Per semplicità renderei questo spostamento solo un effetto grafico e non lascerei che influisca sulla dinamica stessa, ma potresti provare entrambe le alternative e vedere quale funziona meglio.

Per rendere più piccolo lo "splashhole", suggerirei di alterare il metodo Splash (int index, float speed) in modo che influisca direttamente non solo sull'indice ma anche su alcuni vertici vicini, in modo da diffondere l'effetto ma avere comunque lo stesso " energia". Il numero di vertici interessati potrebbe dipendere dalla larghezza dell'oggetto. Probabilmente dovrai modificare molto l'effetto prima di ottenere un risultato perfetto.

Per strutturare le parti più profonde dell'acqua potresti fare come descritto nell'articolo e semplicemente rendere la parte più profonda "più blu" o potresti interpolare tra due trame a seconda della profondità dell'acqua.


Grazie per la risposta. In realtà speravo che qualcun altro avesse provato questo prima di me e potesse darmi una risposta più specifica. Ma i tuoi consigli sono anche molto apprezzati. In realtà sono molto occupato, ma non appena avrò tempo per farlo, proverò le cose che hai citato e giocherò ancora con il codice.
Berry

1
Ok, ma se c'è qualcosa di specifico con cui hai bisogno di aiuto, dillo e vedrò se posso essere un po 'più elaborato.
Mikael Högström,

Grazie mille! È solo che non ho valutato molto bene la mia domanda, dato che ho una settimana d'esame la settimana prossima. Dopo aver finito gli esami, trascorrerò sicuramente più tempo sul codice e molto probabilmente tornerò con domande più specifiche.
Berry,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.