Jak stworzyć wodę 2D z dynamicznymi falami?

81

Nowy Super Mario Bros ma naprawdę fajną wodę 2D, którą chciałbym nauczyć się tworzyć.

Oto wideo pokazujące to. Część ilustracyjna:

Nowe efekty wodne Super Mario Bros

Rzeczy uderzające w wodę tworzą fale. Istnieją również stałe fale „tła”. Możesz dobrze przyjrzeć się stałym falom zaraz po 00:50 na filmie, gdy kamera się nie porusza.

Zakładam, że efekty powitalne działają jak w pierwszej części tego samouczka .

Jednak w NSMB woda ma również stałe fale na powierzchni, a rozpryski wyglądają zupełnie inaczej. Inna różnica polega na tym, że w samouczku, jeśli utworzysz plusk, najpierw tworzy on głęboką „dziurę” w wodzie u źródła tego rozbryzgu. W nowych super mario bros dziura ta jest nieobecna lub znacznie mniejsza. Mam na myśli rozpryski, które gracz tworzy podczas wskakiwania i wychodzenia z wody.

Jak stworzyć powierzchnię wody ze stałymi falami i rozpryskami?

Programuję w XNA. Próbowałem tego sam, ale tak naprawdę nie mogłem sprawić, by sinusoidy tła działały dobrze razem z falami dynamicznymi.

Nie pytam, jak dokładnie to zrobili twórcy New Super Mario Bros - po prostu interesuje mnie, jak odtworzyć taki efekt.

Jagoda
źródło

Odpowiedzi:

147

Próbowałem tego.

Rozpryski (sprężyny)

Jak wspomniano w tym samouczku , powierzchnia wody jest jak drut: jeśli pociągniesz za jakiś punkt drutu, punkty znajdujące się obok tego punktu również zostaną pociągnięte w dół. Wszystkie punkty są również przyciągane z powrotem do linii bazowej.

Zasadniczo mnóstwo pionowych sprężyn obok siebie, które również się pociągają.

Naszkicowałem to w Lua za pomocą LÖVE i otrzymałem:

animacja plusku

Wygląda na wiarygodne. Oh Hooke , ty przystojny geniuszu.

Jeśli chcesz się nim bawić, oto port JavaScript dzięki uprzejmości Phila ! Mój kod znajduje się na końcu tej odpowiedzi.

Fale w tle (ułożone sinusoidy)

Naturalne fale tła wyglądają dla mnie jak wiązka fal sinusoidalnych (o różnych amplitudach, fazach i długościach fal) zsumowanych. Oto jak to wyglądało, kiedy to napisałem:

fale tła wytwarzane przez zakłócenia sinusoidalne

Wzorce interferencji wyglądają całkiem prawdopodobne.

Teraz wszyscy razem

Zatem podsumowanie fal powitalnych i fal tła jest dość proste:

fale tła, z plamami

Kiedy dochodzi do rozprysków, widać małe szare kółka pokazujące, gdzie byłaby pierwotna fala tła.

Wygląda bardzo podobnie do tego filmu, który połączyłeś , więc uważam to za udany eksperyment.

Oto mój main.lua(jedyny plik). Myślę, że to dość czytelne.

-- 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
źródło
Świetna odpowiedź! Dziękuję Ci bardzo. A także, dzięki za zrewidowanie mojego pytania, widzę, jak to jest bardziej jasne. Również gify są bardzo pomocne. Czy znasz przypadkiem sposób, aby zapobiec powstawaniu dużej dziury, która pojawia się również podczas tworzenia rozbryzgu? Możliwe, że Mikael Högström już odpowiedział na to prawo, ale próbowałem tego jeszcze przed zadaniem tego pytania, a moim rezultatem było to, że otwór miał kształt trójkąta i wyglądał bardzo nierealnie.
Berry,
Aby obciąć głębokość „dziury rozpryskowej”, możesz ograniczyć maksymalną amplitudę fali, tj. Jak daleko dowolny punkt może zboczyć z linii podstawowej.
Anko,
3
BTW dla wszystkich zainteresowanych: Zamiast owijać boki wody, zdecydowałem się użyć linii bazowej do normalizacji boków. W przeciwnym razie, jeśli utworzysz plusk po prawej stronie wody, spowoduje to również powstanie fal po lewej stronie wody, co uważam za nierealne. Ponadto, ponieważ nie owijałem fal, fale tła rozpadałyby się bardzo szybko. Dlatego postanowiłem zrobić z nich jedynie efekt graficzny, jak powiedział Mikael Högström, aby fale tła nie były uwzględniane w obliczeniach prędkości i przyspieszenia.
Berry,
1
Chciałem żebyś wiedział. Rozmawialiśmy o obcięciu „dziury” za pomocą instrukcji if. Na początku nie chciałem tego robić. Ale teraz zauważyłem, że faktycznie działa idealnie, ponieważ fale tła zapobiegną płaskości powierzchni.
Berry
4
Przekształciłem ten kod fali na JavaScript i umieściłem go w jsfiddle tutaj: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick
11

Dla rozwiązania (matematycznie możesz rozwiązać problem z rozwiązywaniem równań różniczkowych, ale jestem pewien, że nie robią tego w ten sposób) tworzenia fal masz 3 możliwości (w zależności od tego, jak szczegółowy powinien być):

  1. Oblicz fale za pomocą funkcji trygonometrycznych (najprostszych i najszybszych)
  2. Zrób to tak, jak zaproponowała Anko
  3. Rozwiąż równania różniczkowe
  4. Użyj wyszukiwania tekstur

Rozwiązanie 1

Naprawdę proste, dla każdej fali obliczamy (absolutną) odległość od każdego punktu powierzchni do źródła i obliczamy „wysokość” za pomocą wzoru

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

gdzie

  • dist to nasza odległość
  • FactorA to wartość, która oznacza, jak szybko / gęste powinny być fale
  • Faza jest fazą fali, musimy ją zwiększać z czasem, aby uzyskać animowaną falę

Pamiętaj, że możemy dodać tyle terminów, ile chcemy (zasada superpozycji).

Zawodowiec

  • Jest naprawdę szybki do obliczenia
  • Jest łatwy do wdrożenia

Contra

  • W przypadku (prostych) odbić na powierzchni 1d musimy utworzyć źródła fali „ducha” w celu symulacji odbić, jest to bardziej skomplikowane na powierzchniach 2d i jest to jedno z ograniczeń tego prostego podejścia

Rozwiązanie 2

Zawodowiec

  • To też jest proste
  • Umożliwia łatwe obliczanie odbić
  • Można go stosunkowo łatwo rozszerzyć do przestrzeni 2d lub 3d

Contra

  • Może stać się niestabilny numerycznie, jeśli wartość zrzutu jest zbyt wysoka
  • wymaga większej mocy obliczeniowej niż Rozwiązanie 1 (ale nie tak bardzo jak Rozwiązanie 3 )

Rozwiązanie 3

Teraz uderzyłem w twardą ścianę, jest to najbardziej skomplikowane rozwiązanie.

Nie wdrożyłem tego, ale możliwe jest rozwiązanie tych potworów.

Tutaj znajdziesz prezentację dotyczącą jego matematyki, nie jest to proste i istnieją również równania różniczkowe dla różnych rodzajów fal.

Oto niekompletna lista z pewnymi równaniami różniczkowymi do rozwiązania bardziej szczególnych przypadków (Solitons, Peakons, ...)

Zawodowiec

  • Realistyczne fale

Contra

  • W przypadku większości gier nie warte wysiłku
  • Potrzebuje najwięcej czasu na obliczenia

Rozwiązanie 4

Trochę bardziej skomplikowane niż rozwiązanie 1, ale nie tak skomplikowane rozwiązanie 3.

Używamy wstępnie obliczonych tekstur i łączymy je ze sobą, a następnie używamy mapowania przemieszczenia (właściwie metoda dla fal 2d, ale zasada może również działać dla fal 1d)

W tej grze wykorzystano takie podejście, ale nie znalazłem linku do artykułu na ten temat.

Zawodowiec

  • jest to prostsze niż 3
  • osiąga dobre wyniki (dla 2d)
  • może wyglądać realistycznie, jeśli artyści wykonają świetną robotę

Contra

  • trudne do animacji
  • powtarzające się wzory mogą być widoczne na horyzoncie
Quonux
źródło
6

Aby dodać fale stałe, dodaj kilka fal sinusoidalnych po obliczeniu dynamiki. Dla uproszczenia chciałbym, aby to przesunięcie było jedynie efektem graficznym i nie pozwalało, aby miało to wpływ na samą dynamikę, ale można wypróbować obie alternatywy i przekonać się, które działa najlepiej.

Aby zmniejszyć rozmiar „splashhole”, sugerowałbym zmianę metody Splash (indeks wewnętrzny, prędkość zmiennoprzecinkowa), aby bezpośrednio wpływał nie tylko na indeks, ale także na niektóre bliskie wierzchołki, aby rozłożyć efekt, ale nadal mieć ten sam „ energia". Liczba dotkniętych wierzchołków może zależeć od tego, jak szeroki jest twój obiekt. Prawdopodobnie będziesz musiał dużo ulepszyć efekt, aby uzyskać doskonały wynik.

Aby teksturować głębsze części wody, możesz zrobić to tak, jak opisano w artykule i po prostu uczynić głębszą część „bardziej niebieskim” lub możesz interpolować między dwiema teksturami w zależności od głębokości wody.

Mikael Högström
źródło
Dziękuję za odpowiedź. Miałem nadzieję, że ktoś inny spróbował tego przede mną i mógł dać mi bardziej szczegółową odpowiedź. Ale twoje wskazówki też są bardzo mile widziane. Jestem bardzo zajęty, ale jak tylko będę miał na to czas, wypróbuję rzeczy, o których wspomniałeś, i pobawię się kodem.
Berry
1
Ok, ale jeśli jest coś konkretnego, z czym potrzebujesz pomocy, po prostu powiedz to i zobaczę, czy mogę być nieco bardziej rozbudowany.
Mikael Högström,
Dziękuję Ci bardzo! Tyle tylko, że nie zadałem zbyt dobrze pytania, ponieważ mam tydzień egzaminacyjny w przyszłym tygodniu. Po zakończeniu egzaminów zdecydowanie spędzę więcej czasu na kodzie i najprawdopodobniej wrócę z bardziej szczegółowymi pytaniami.
Berry,