Як створити 2D воду з динамічними хвилями?


81

Новий Super Mario Bros має дійсно прохолодну 2D воду, яку я хотів би навчитися створювати.

Ось відео, де це показано. Ілюстративна частина:

Нові водні ефекти Super Mario Bros

Речі, що потрапляють у воду, створюють хвилі. Також є постійні "фонові" хвилі. Ви можете добре подивитися на постійні хвилі відразу після 00:50 у відео, коли камера не рухається.

Я припускаю, що ефекти сплеску спрацьовують як у першій частині цього підручника .

Однак у НСМБ вода також має постійні хвилі на поверхні, і бризки виглядають дуже інакше. Ще одна відмінність полягає в тому, що в підручнику, якщо ви створюєте сплеск, він спочатку створює глибоку «дірку» у воді біля витоку сплеску. У нових братах супер-маріо ця дірка відсутня або значно менша. Я маю на увазі бризки, які створює гравець, коли стрибає у воду та виходить із неї.

Як створити водну поверхню з постійними хвилями та бризками?

Я програмую в XNA. Я сам це спробував, але мені не вдалося змусити фонові синусоїди добре працювати разом з динамічними хвилями.

Я не запитую, як саме це зробили розробники New Super Mario Bros - просто цікаво, як відтворити подібний ефект.

Відповіді:


147

Я спробував це.

Бризки (пружини)

Як згадується в цьому підручнику , поверхня води - це як дріт. Якщо ви потягнете за якусь точку дроту, точки, розташовані поруч із цією точкою, також будуть зніматися. Усі бали також притягуються до базової лінії.

Це, як правило, безліч вертикальних пружин, розташованих поруч, які також тягнуться один за одним.

Я намалював це в Луа, використовуючи LÖVE, і отримав це:

анімація сплеск

Виглядає правдоподібно. О Гук , ти гарний геній.

Якщо ви хочете пограти з ним, ось вам надано JavaScript-порт від Філа ! Мій код знаходиться в кінці цієї відповіді.

Фонові хвилі (складені синуси)

Природні фонові хвилі виглядають як купа синусоїд (з різними амплітудами, фазами і довжинами хвиль), зведені разом. Ось як це виглядало, коли я це писав:

фонові хвилі, що виникають при синусоїдальних перешкодах

Шаблони перешкод виглядають досить правдоподібно.

Всі разом зараз

Тож досить просто підсумовувати хвилі сплеску та хвилі фону:

фонові хвилі, з бризками

Коли виникають бризки, ви можете побачити невеликі сірі кола, які показують, де була б початкова хвиля фону.

Це схоже на те відео, яке ви пов’язали , тому я вважаю це вдалим експериментом.

Ось мій main.lua(єдиний файл). Я думаю, що це досить читабельно.

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

Чудова відповідь! Дуже дякую. А також, дякую за перегляд мого питання, я бачу, як це зрозуміліше. Також дуже корисні подарунки. Чи випадково ви знаєте спосіб запобігти великій дірі, яка з’явиться і при створенні сплеску? Можливо, Мікаел Хьогстрьом вже відповів на це право, але я намагався це зробити ще до того, як ставити це питання, і мій результат полягав у тому, що отвір набув трикутної форми і виглядав дуже нереально.
Беррі

Щоб скоротити глибину «лунки бризок», ви можете обмежити максимальну амплітуду хвилі, тобто наскільки будь-яка точка може відхилитися від базової лінії.
Анко

3
BTW для всіх, хто цікавиться: Замість того, щоб загортати сторони води, я вирішив використовувати базову лінію для нормалізації сторін. В іншому випадку, якщо ви створите сплеск праворуч від води, він також створить хвилі зліва від води, що я вважав нереальним. Крім того, оскільки я не закручував хвилі, фонові хвилі дуже швидко виходили б з плоскості. Тому я вирішив зробити для них лише графічний ефект, як сказав Мікаел Хьогстрем, щоб фонові хвилі не були включені в розрахунки на швидкість і прискорення.
Беррі

1
Просто хотів вас повідомити. Ми говорили про обрізання "splash-hole" за допомогою заяви-if. Спочатку я неохоче робив це. Але зараз я помітив, що він насправді прекрасно працює, оскільки фонові хвилі запобігають рівній поверхні.
Беррі

4
Я перетворив цей хвильовий код у JavaScript і розмістив його на jsfiddle тут: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick

11

Для рішення (математично кажучи, ви можете вирішити проблему з вирішенням диференціальних рівнянь, але впевнений, що вони не роблять це так) створення хвиль у вас є три можливості (залежно від того, наскільки детально це має бути):

  1. Обчисліть хвилі за допомогою тригонометричних функцій (найпростіших і найшвидших)
  2. Зробіть так, як запропонував Анко
  3. Розв’яжіть диференціальні рівняння
  4. Використовуйте пошук текстур

Рішення 1

Дійсно просто: для кожної хвилі ми обчислюємо (абсолютну) відстань від кожної точки поверхні до джерела і обчислюємо «висоту» за формулою

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

де

  • dist - наша відстань
  • FactorA - це значення, яке означає, наскільки швидкими / щільними повинні бути хвилі
  • Фаза - це Фаза хвилі, нам потрібно наростити її з часом, щоб отримати анімовану хвилю

Зауважте, що ми можемо додавати стільки термінів разом, скільки нам подобається (принцип суперпозиції).

Про

  • Це дійсно швидко підрахувати
  • Це легко здійснити

Контраст

  • Для (простих) роздумів над 1d поверхнею нам потрібно створити хвильові джерела хвилі для імітації відображень, це складніше на 2d поверхнях і це одне з обмежень цього простого підходу

Рішення 2

Про

  • Його теж просто
  • Це дозволяє легко обчислити відображення
  • Його можна легко відновити до 2d або 3d простору

Контраст

  • Може стати нестабільним, якщо значення демпінгу занадто високе
  • потрібно більше розрахункової потужності, ніж рішення 1 (але не так, як рішення 3 )

Рішення 3

Зараз я потрапив у тверду стіну, це найскладніше рішення.

Я цього не реалізував, але ці монстри можна вирішити.

Тут ви можете знайти презентацію про математику, її непросту, а також існують диференціальні рівняння для різних видів хвиль.

Ось не повний список з деякими диференціальними рівняннями для вирішення більш спеціальних випадків (Солітони, Пікон, ...)

Про

  • Реалістичні хвилі

Контраст

  • Для більшості ігор не варто докладати зусиль
  • Потрібно найбільше часу обчислення

Рішення 4

Трохи складніше рішення 1, але не настільки складне рішення 3.

Ми використовуємо заздалегідь обчислені текстури та змішуємо їх разом, після чого використовуємо відображення зміщення (фактично це метод для 2d хвиль, але принцип також може працювати для 1d хвиль)

Гра sturmovik використовувала такий підхід, але я не знаходжу посилання на статтю про це.

Про

  • це простіше, ніж 3
  • він отримує гарні результати (за 2д)
  • це може виглядати реалістично, якщо художникам добре виконати роботу

Контраст

  • важко оживити
  • повторні візерунки могли видно на горизонті

6

Для додавання постійних хвиль додайте пару синусоїд після того, як ви обчислили динаміку. Для простоти я зробив би це переміщення лише графічним ефектом, і не дозволяв би вплинути на саму динаміку, але ви можете спробувати обидві альтернативи і побачити, що працює найкраще.

Щоб зменшити "вибух", я б запропонував змінити метод Splash (int index, float speed) так, щоб він безпосередньо впливав не тільки на індекс, але і на деякі близькі вершини, щоб поширити ефект, але все-таки мати однакові " енергія ». Кількість вражених вершин може залежати від того, наскільки широкий ваш об’єкт. Ймовірно, вам доведеться сильно виправити ефект, перш ніж матимете ідеальний результат.

Для текстурування глибших частин води ви можете зробити так, як описано в статті, і просто зробити більш глибоку частину "синішою", або ви могли інтерполювати між двома текстурами залежно від глибини води.


Дякую за відповідь Я справді сподівався, що хтось ще спробував це перед мною і міг би дати мені більш конкретну відповідь. Але ваші поради теж дуже вдячні. Я насправді дуже зайнятий, але як тільки у мене з’явиться час, я спробую все, що ви згадали, і ще трохи пограю з кодом.
Беррі

1
Гаразд, але якщо є якась конкретна, вам потрібна допомога, просто скажіть так, і я побачу, чи зможу я трохи детальніше.
Mikael Högström

Дуже дякую! Просто я ще не дуже приурочила своє запитання, оскільки в мене наступний тиждень іспит. Після закінчення іспитів я точно витрачу більше часу на код і, швидше за все, повернусь із більш конкретними питаннями.
Беррі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.