Wie erstelle ich 2D-Wasser mit dynamischen Wellen?


81

New Super Mario Bros hat wirklich cooles 2D-Wasser, das ich gerne erstellen lernen möchte.

Hier ist ein Video, das es zeigt. Ein illustrativer Teil:

Neue Super Mario Bros Wassereffekte

Dinge, die auf das Wasser treffen, erzeugen Wellen. Es gibt auch konstante "Hintergrund" -Wellen. Sie können sich die konstanten Wellen kurz nach 00:50 im Video genau ansehen, wenn sich die Kamera nicht bewegt.

Ich gehe davon aus, dass die Splash-Effekte wie im ersten Teil dieses Tutorials funktionieren .

In NSMB weist das Wasser jedoch auch konstante Wellen auf der Oberfläche auf, und die Spritzer sehen sehr unterschiedlich aus. Ein weiterer Unterschied besteht darin, dass beim Erstellen eines Spritzwassers im Lernprogramm zuerst ein tiefes "Loch" im Wasser am Ursprung des Spritzwassers erzeugt wird. Bei neuen Super Mario Bros fehlt dieses Loch oder ist viel kleiner. Ich beziehe mich auf die Spritzer, die der Spieler erzeugt, wenn er ins und aus dem Wasser springt.

Wie erstelle ich eine Wasseroberfläche mit konstanten Wellen und Spritzern?

Ich programmiere in XNA. Ich habe es selbst versucht, aber ich konnte die Sinuswellen im Hintergrund nicht wirklich erfassen dazu bringen, mit den dynamischen Wellen gut zusammenzuarbeiten.

Ich frage nicht, wie die Entwickler von New Super Mario Bros das genau gemacht haben - sie sind nur daran interessiert, wie sie einen solchen Effekt erzeugen können.

Antworten:


147

Ich versuchte es.

Spritzer (Federn)

Wie in diesem Tutorial erwähnt, ist die Wasseroberfläche wie ein Draht: Wenn Sie an einem Punkt des Drahtes ziehen, werden die Punkte neben diesem Punkt ebenfalls nach unten gezogen. Alle Punkte werden auch auf eine Grundlinie zurückgeführt.

Es sind im Grunde viele vertikale Federn nebeneinander, die sich auch gegenseitig anziehen.

Ich habe das in Lua mit LÖVE skizziert und folgendes bekommen:

Animation eines Splashs

Sieht plausibel aus. Oh Hooke , du hübsches Genie.

Wenn Sie damit spielen möchten, ist hier ein JavaScript-Port von Phil ! Mein Code ist am Ende dieser Antwort.

Hintergrundwellen (gestapelte Sinus)

Natürliche Hintergrundwellen sehen für mich aus wie ein Bündel von Sinuswellen (mit unterschiedlichen Amplituden, Phasen und Wellenlängen), die alle zusammen addiert werden. So sah das aus, als ich es schrieb:

Hintergrundwellen, die durch Sinusinterferenz erzeugt werden

Die Interferenzmuster sehen ziemlich plausibel aus.

Jetzt alle zusammen

Dann ist es ziemlich einfach, die Splash-Wellen und die Hintergrundwellen zusammenzufassen:

Hintergrundwellen, mit Spritzern

Wenn Spritzer auftreten, können Sie kleine graue Kreise sehen, die anzeigen, wo sich die ursprüngliche Hintergrundwelle befinden würde.

Es sieht dem Video, das Sie verlinkt haben , sehr ähnlich , daher würde ich dies als ein erfolgreiches Experiment betrachten.

Hier ist meine main.lua(die einzige Datei). Ich denke es ist ziemlich lesbar.

-- 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

Gute Antwort! Vielen Dank. Und danke, dass Sie meine Frage überarbeitet haben, kann ich sehen, wie klarer dies ist. Auch die Gifs sind sehr hilfreich. Kennen Sie zufällig einen Weg, um das große Loch, das beim Erzeugen eines Spritzwassers entsteht, zu verhindern? Es könnte sein, dass Mikael Högström dieses Recht bereits beantwortet hat, aber ich hatte es bereits vor dem Posten dieser Frage versucht und mein Ergebnis war, dass das Loch dreieckig wurde und sehr unrealistisch aussah.
Berry

Um die Tiefe des "Spritzlochs" abzuschneiden, können Sie die maximale Amplitude der Welle abdecken, dh wie weit ein Punkt von der Grundlinie abweichen darf.
Anko

3
Übrigens für alle Interessierten: Anstatt die Seiten des Wassers einzuwickeln, habe ich die Grundlinie verwendet, um die Seiten zu normalisieren. Andernfalls würde ein Spritzer rechts vom Wasser auch Wellen links vom Wasser erzeugen, was ich als unrealistisch empfand. Da ich die Wellen nicht gewickelt hatte, gingen die Hintergrundwellen sehr schnell flach. Deshalb habe ich mich dafür entschieden, diese nur grafisch darzustellen, wie Mikael Högström sagte, damit die Hintergrundwellen nicht in die Berechnungen für Geschwindigkeit und Beschleunigung einfließen.
Berry

1
Ich wollte dich nur wissen lassen. Wir haben darüber gesprochen, das "Splash-Hole" mit einer if-Anweisung abzuschneiden. Anfangs zögerte ich, dies zu tun. Aber jetzt ist mir aufgefallen, dass es tatsächlich perfekt funktioniert, da die Hintergrundwellen verhindern, dass die Oberfläche flach ist.
Berry

4
Ich habe diesen Wave-Code in JavaScript konvertiert und hier auf jsfiddle gesetzt: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick

11

Für die Lösung (mathematisch gesehen können Sie das Problem mit der Lösung von Differentialgleichungen lösen, aber ich bin mir sicher, dass sie es nicht so machen) der Erzeugung von Wellen haben Sie 3 Möglichkeiten (abhängig davon, wie detailliert es werden soll):

  1. Berechnen Sie die Wellen mit den trigonometrischen Funktionen (am einfachsten und am schnellsten)
  2. Mach es so, wie Anko es vorgeschlagen hat
  3. Lösen Sie die Differentialgleichungen
  4. Verwenden Sie Textur-Lookups

Lösung 1

Ganz einfach, wir berechnen für jede Welle den (absoluten) Abstand von jedem Punkt der Oberfläche zur Quelle und berechnen die 'Höhe' mit der Formel

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

wo

  • dist ist unsere Distanz
  • FaktorA ist ein Wert, der angibt, wie schnell / dicht die Wellen sein sollen
  • Phase ist die Phase der Welle, wir müssen sie mit der Zeit erhöhen, um eine animierte Welle zu erhalten

Beachten Sie, dass wir beliebig viele Begriffe addieren können (Überlagerungsprinzip).

Profi

  • Es ist sehr schnell zu berechnen
  • Ist einfach zu implementieren

Contra

  • Für (einfache) Reflexionen auf einer 1d-Oberfläche müssen "Geister" -Wellenquellen erstellt werden, um Reflexionen zu simulieren. Dies ist bei 2d-Oberflächen komplizierter und eine der Einschränkungen dieses einfachen Ansatzes

Lösung 2

Profi

  • Es ist auch einfach
  • Es ermöglicht die einfache Berechnung von Reflexionen
  • Es kann relativ einfach auf 2D- oder 3D-Raum erweitert werden

Contra

  • Kann numerisch instabil werden, wenn der Dumping-Wert zu hoch ist
  • benötigt mehr Rechenleistung als Lösung 1 (aber nicht so sehr wie Lösung 3 )

Lösung 3

Jetzt bin ich auf eine harte Wand gestoßen, das ist die komplizierteste Lösung.

Ich habe dieses nicht implementiert, aber es ist möglich, diese Monster zu lösen.

Hier finden Sie eine Präsentation über die Mathematik, es ist nicht einfach und es gibt auch Differentialgleichungen für verschiedene Arten von Wellen.

Hier ist eine nicht vollständige Liste mit einigen Differentialgleichungen zur Lösung speziellerer Fälle (Solitons, Peakons, ...)

Profi

  • Realistische Wellen

Contra

  • Für die meisten Spiele lohnt sich der Aufwand nicht
  • Benötigt die meiste Rechenzeit

Lösung 4

Etwas komplizierter als Lösung 1, aber nicht so kompliziert wie Lösung 3.

Wir verwenden vorberechnete Texturen und mischen sie zusammen. Danach verwenden wir ein Verschiebungs-Mapping (eigentlich eine Methode für 2D-Wellen, aber das Prinzip kann auch für 1D-Wellen funktionieren).

Das Spiel sturmovik hat diesen Ansatz verwendet, aber ich finde den Link zum Artikel darüber nicht.

Profi

  • es ist einfacher als 3
  • es wird gut aussehende Ergebnisse (für 2d)
  • es kann realistisch aussehen, wenn die künstler gut einen tollen job machen

Contra

  • schwer zu animieren
  • wiederholte Muster könnten am Horizont sichtbar werden

6

Um konstante Wellen hinzuzufügen, fügen Sie ein paar Sinuswellen hinzu, nachdem Sie die Dynamik berechnet haben. Der Einfachheit halber würde ich diese Verschiebung nur grafisch darstellen und nicht die Dynamik selbst beeinflussen lassen, aber Sie könnten beide Alternativen ausprobieren und sehen, welche am besten funktioniert.

Um das "Splashhole" zu verkleinern, würde ich vorschlagen, die Methode Splash (int index, float speed) so zu ändern, dass sie nicht nur den Index, sondern auch einige der nahen Scheitelpunkte direkt beeinflusst, um den Effekt zu verteilen, aber immer noch die gleiche " Energie". Die Anzahl der betroffenen Scheitelpunkte kann davon abhängen, wie breit Ihr Objekt ist. Wahrscheinlich müssen Sie den Effekt viel optimieren, bevor Sie ein perfektes Ergebnis erzielen.

Um die tieferen Teile des Wassers zu texturieren, können Sie entweder wie im Artikel beschrieben vorgehen und den tieferen Teil "blauer" machen, oder Sie können zwischen zwei Texturen interpolieren, abhängig von der Wassertiefe.


Danke für Ihre Antwort. Ich hatte tatsächlich gehofft, dass jemand anderes dies vor mir versucht hatte und mir eine spezifischere Antwort geben konnte. Aber auch Ihre Tipps werden sehr geschätzt. Ich bin eigentlich sehr beschäftigt, aber sobald ich Zeit dafür habe, werde ich die Dinge, die Sie erwähnt haben, ausprobieren und mit dem Code etwas mehr herumspielen.
Berry

1
Ok, aber wenn es etwas Bestimmtes gibt, bei dem Sie Hilfe brauchen, sagen Sie es einfach und ich werde sehen, ob ich etwas ausführlicher sein kann.
Mikael Högström

Vielen Dank! Es ist nur so, dass ich meine Frage nicht gut terminiert habe, da ich nächste Woche eine Prüfungswoche habe. Nach Abschluss meiner Prüfungen werde ich definitiv mehr Zeit mit dem Code verbringen und höchstwahrscheinlich mit genaueren Fragen zurückkehren.
Berry
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.