Comment créer de l'eau 2D avec des ondes dynamiques?

81

New Super Mario Bros a une eau 2D vraiment cool que j'aimerais apprendre à créer.

Voici une vidéo le montrant. Une partie illustrative:

Nouveaux effets de l'eau Super Mario Bros

Les objets frappant l'eau créent des vagues. Il y a aussi des vagues constantes de "fond". Vous pouvez bien regarder les vagues constantes juste après 00h50 dans la vidéo, lorsque la caméra ne bouge pas.

Je suppose que les effets de démarrage fonctionnent comme dans la première partie de ce didacticiel .

Cependant, dans NSMB, l'eau a également des vagues constantes à la surface et les éclaboussures sont très différentes. Une autre différence est que dans le didacticiel, si vous créez une éclaboussure, cela crée d'abord un "trou" profond dans l'eau à l'origine de l'éclaboussure. Dans les nouveaux super mario bros, ce trou est absent ou beaucoup plus petit. Je me réfère aux éclaboussures que le joueur crée en sautant dans et hors de l'eau.

Comment créer une surface d'eau avec des vagues constantes et des éclaboussures?

Je programme en XNA. J'ai essayé cela moi-même, mais je ne pouvais pas vraiment obtenir que les ondes sinusoïdales d'arrière - plan fonctionnent correctement avec les ondes dynamiques.

Je ne demande pas comment les développeurs de New Super Mario Bros ont fait cela exactement: ils veulent juste savoir comment recréer un effet comme celui-ci.

Baie
la source

Réponses:

147

Je l'ai essayé.

Éclaboussures

Comme le mentionne ce tutoriel , la surface de l'eau ressemble à un fil: si vous tirez sur un point du fil, les points situés à côté de ce point seront également abaissés. Tous les points sont également attirés vers une ligne de base.

En gros, il y a beaucoup de ressorts verticaux juxtaposés qui se tirent également.

J'ai dessiné ça à Lua avec LÖVE et j'ai obtenu ceci:

animation d'une éclaboussure

Cela semble plausible. Oh Hooke , mon beau génie.

Si vous voulez jouer avec, voici un portage JavaScript offert par Phil ! Mon code est à la fin de cette réponse.

Vagues de fond (sinus empilés)

Pour moi, les ondes de fond naturelles ressemblent à un groupe d’ondes sinusoïdales (d’amplitudes, de phases et de longueurs d’onde différentes) qui sont toutes combinées. Voici à quoi cela ressemblait quand je l'ai écrit:

ondes de fond produites par une interférence sine

Les modèles d'interférence semblent assez plausibles.

Tous ensemble maintenant

Il est donc assez simple de faire la somme des vagues splash et des vagues de fond:

vagues de fond, avec des éclaboussures

Lorsque des éclaboussures se produisent, vous pouvez voir de petits cercles gris indiquant l'emplacement de la vague de fond d'origine.

Cela ressemble beaucoup à la vidéo que vous avez liée , alors je considérerais cela comme une expérience réussie.

Voici mon main.lua(le seul fichier). Je pense que c'est assez lisible.

-- 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
Anko
la source
Très bonne réponse! Merci beaucoup. Et aussi, merci d'avoir révisé ma question, je peux voir en quoi cela est plus clair. Aussi les gifs sont très utiles. Connaissez-vous par hasard un moyen d’éviter également le grand trou qui se crée lorsque vous créez une éclaboussure? Il se peut que Mikael Högström ait déjà répondu à ce droit, mais j’avais essayé de le faire avant même de poser cette question et j’ai eu l’impression que le trou devenait de forme triangulaire et que cela paraissait très irréaliste.
Berry
Pour tronquer la profondeur du "trou d'éclaboussure", vous pouvez plafonner l'amplitude maximale de la vague, c'est-à-dire jusqu'à quel point tout point est autorisé à s'écarter de la ligne de base.
Anko
3
BTW pour toute personne intéressée: au lieu d’envelopper les bords de l’eau, j’ai choisi d’utiliser la ligne de base pour normaliser les bords. Sinon, si vous créez une éclaboussure à droite de l'eau, cela créerait également des vagues à gauche de l'eau, ce que j'ai trouvé irréaliste. De plus, comme je n'ai pas enveloppé les vagues, les ondes de fond deviendraient plates très rapidement. Par conséquent, j’ai choisi d’en faire un effet graphique uniquement, comme l’a dit Mikael Högström, afin que les ondes de fond ne soient pas incluses dans les calculs de vitesse et d’accélération.
Berry
1
Je voulais juste vous faire savoir. Nous avons parlé de tronquer le "splash-hole" avec une instruction if. Au début, j'étais réticent à le faire. Mais maintenant, j'ai remarqué que cela fonctionnait parfaitement, car les ondes de fond empêcheraient la surface d'être plate.
Berry
4
J'ai converti ce code wave en JavaScript et je l'ai mis sur jsfiddle ici: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick
11

Pour la solution (mathématiquement parlant, vous pouvez résoudre le problème de résolution d’équations différentielles, mais je suis sûr qu’ils ne le font pas de cette façon) en créant des vagues, vous avez 3 possibilités (selon le degré de détail à obtenir):

  1. Calculer les ondes avec les fonctions trigonométriques (les plus simples et les plus rapides)
  2. Faites comme Anko a proposé
  3. Résoudre les équations différentielles
  4. Utiliser des recherches de texture

Solution 1

Vraiment simple, pour chaque onde, on calcule la distance (absolue) de chaque point de la surface à la source et on calcule le "haut" avec la formule

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

  • dist est notre distance
  • FactorA est une valeur qui signifie à quelle vitesse / densité les vagues devraient être
  • La phase est la phase de la vague, il faut l'incrémenter avec le temps pour obtenir une vague animée

Notez que nous pouvons ajouter autant de termes que nous le souhaitons (principe de superposition).

Pro

  • C'est vraiment rapide à calculer
  • Est facile à mettre en œuvre

Contra

  • Pour les réflexions (simples) sur une surface 1d, nous devons créer des sources d’ondes "fantômes" pour simuler les réflexions. C’est plus compliqué pour les surfaces 2d et c’est l’une des limites de cette approche simple.

Solution 2

Pro

  • C'est simple aussi
  • Cela permet de calculer facilement les réflexions
  • Il peut être étendu à l'espace 2D ou 3D relativement facilement

Contra

  • Peut devenir numériquement instable si la valeur de dumping est trop élevée
  • nécessite plus de puissance de calcul que la Solution 1 (mais pas autant que la Solution 3 )

Solution 3

Maintenant, je frappe un mur dur, c'est la solution la plus compliquée.

Je n'ai pas implémenté celui-ci mais il est possible de résoudre ces monstres.

Ici vous pouvez trouver une présentation sur les mathématiques, ce n’est pas simple et il existe aussi des équations différentielles pour différents types d’ondes.

Voici une liste non complète avec quelques équations différentielles pour résoudre des cas plus particuliers (solitons, pics, ...)

Pro

  • Vagues réalistes

Contra

  • Pour la plupart des jeux ne vaut pas la peine
  • Nécessite le plus de temps de calcul

Solution 4

Un peu plus compliqué que la solution 1 mais pas si compliqué une solution 3.

Nous utilisons des textures précalculées et les mélangeons ensemble, après quoi nous utilisons la cartographie des déplacements (en fait, une méthode pour les ondes 2D mais le principe peut également fonctionner pour des ondes 1D).

Le jeu sturmovik a utilisé cette approche, mais je ne trouve pas le lien vers l'article à ce sujet.

Pro

  • c'est plus simple que 3
  • cela donne de beaux résultats (pour 2d)
  • cela peut paraître réaliste si les artistes font du bon travail

Contra

  • difficile à animer
  • des motifs répétés pourraient être visibles à l'horizon
Quonux
la source
6

Pour ajouter des ondes constantes, ajoutez quelques sinusoïdes après avoir calculé la dynamique. Pour des raisons de simplicité, je ferais de ce déplacement uniquement un effet graphique et je ne le laisserais pas affecter la dynamique elle-même, mais vous pouvez essayer les deux alternatives et voir celle qui fonctionne le mieux.

Pour réduire le "splashhole", je suggérerais de modifier la méthode Splash (index int, vitesse de flottement) afin qu'elle affecte directement non seulement l'index, mais également certains sommets proches, de manière à étaler l'effet tout en conservant le même effet " énergie". Le nombre de sommets affectés peut dépendre de la largeur de votre objet. Vous aurez probablement besoin de peaufiner l'effet avant d'obtenir un résultat parfait.

Pour texturer les parties les plus profondes de l'eau, vous pouvez soit procéder comme décrit dans l'article et rendre la partie plus profonde "plus bleue" ou interpoler entre deux textures en fonction de la profondeur de l'eau.

Mikael Högström
la source
Merci pour votre réponse. J'espérais en fait que quelqu'un d'autre l'avait essayé avant moi et pouvait me donner une réponse plus précise. Mais vos conseils sont également très appréciés. En fait, je suis très occupé, mais dès que j'en aurai le temps, je vais essayer les choses que vous avez mentionnées et jouer avec le code un peu plus.
Berry
1
Ok, mais si vous avez besoin d’aide pour quelque chose de spécifique, dites-le simplement et je verrai si je peux être un peu plus élaboré.
Mikael Högström
Merci beaucoup! C'est juste que je n'ai pas très bien chronométré ma question, car j'ai une semaine d'examen la semaine prochaine. Une fois mes examens terminés, je consacrerai plus de temps au code et je reviendrai probablement avec des questions plus spécifiques.
Berry