Как мне создать 2D воду с динамическими волнами?

81

Новый Super Mario Bros имеет действительно классную 2D воду, которую я хотел бы научиться создавать.

Вот видео, показывающее это. Иллюстративная часть:

Новые водные эффекты Super Mario Bros

Вещи, попадающие в воду, создают волны. Есть также постоянные «фоновые» волны. Вы можете хорошо видеть постоянные волны сразу после 00:50 в видео, когда камера не движется.

Я предполагаю, что эффекты всплеска работают как в первой части этого урока .

Тем не менее, в NSMB вода также имеет постоянные волны на поверхности, и брызги выглядят очень по-разному. Другое отличие состоит в том, что в учебнике, если вы создаете всплеск, он сначала создает глубокую «дыру» в воде в начале всплеска. В новых супер марио, эта дыра отсутствует или намного меньше. Я имею в виду брызги, которые создает игрок при прыжке в воду и из воды.

Как мне создать водную поверхность с постоянными волнами и брызгами?

Я программирую в XNA. Я попробовал это сам, но я не мог заставить фоновые синусоидальные волны хорошо работать вместе с динамическими волнами.

Я не спрашиваю, как разработчики New Super Mario Bros сделали это точно - просто интересует, как воссоздать подобный эффект.

Ягода
источник

Ответы:

147

Я попробовал это.

Брызги (пружины)

Как упоминается в этом уроке , поверхность воды похожа на проволоку: если вы потянете за какую-то точку проволоки, точки рядом с этой точкой также будут смещены вниз. Все точки также возвращаются к базовой линии.

Это в основном много вертикальных пружин рядом друг с другом, которые также тянут друг друга.

Я набросал это в Lua, используя 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
Кстати, для всех, кто интересуется: вместо того, чтобы оборачивать стороны воды, я решил использовать базовую линию для нормализации сторон. В противном случае, если вы создадите всплеск справа от воды, он также создаст волны слева от воды, что я считаю нереальным. Кроме того, так как я не заворачивал волны, фоновые волны очень быстро улетучивались. Поэтому я решил сделать эти графические эффекты только такими, как сказал Микаэль Хогстрём, чтобы фоновые волны не учитывались при расчете скорости и ускорения.
Берри
1
Просто хотел, чтобы ты знал. Мы говорили о сокращении «всплеска» с помощью оператора if. Сначала я не хотел этого делать. Но теперь я заметил, что это на самом деле работает отлично, так как фоновые волны будут препятствовать тому, чтобы поверхность была плоской.
Берри
4
Я преобразовал этот волновой код в JavaScript и поместил его в jsfiddle здесь: jsfiddle.net/phil_mcc/sXmpD/8
Фил МакКаллик,
11

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

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

Решение 1

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

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

где

  • расстояние наше расстояние
  • FactorA - это значение, которое означает, насколько быстрыми / плотными должны быть волны
  • Фаза - это фаза волны, нам нужно увеличивать ее со временем, чтобы получить анимированную волну.

Обратите внимание, что мы можем добавить столько терминов, сколько захотим (принцип суперпозиции).

профессионал

  • Это действительно быстро рассчитать
  • Легко реализовать

против

  • Для (простых) отражений на 1d поверхности нам нужно создать «призрачные» источники волн для имитации отражений, это сложнее на 2d поверхностях, и это является одним из ограничений этого простого подхода

Решение 2

профессионал

  • Это тоже просто
  • Это позволяет легко рассчитывать отражения
  • Он может быть легко расширен до 2-го или 3-го пространства

против

  • Может стать численно нестабильным, если значение сброса слишком велико
  • требует большей вычислительной мощности, чем решение 1 (но не так сильно, как решение 3 )

Решение 3

Теперь я ударился о жесткую стену, это самое сложное решение.

Я не реализовал это, но возможно решить этих монстров.

Здесь вы можете найти представление о его математике, это не просто, и существуют также дифференциальные уравнения для различных видов волн.

Вот неполный список с некоторыми дифференциальными уравнениями для решения более частных случаев (Solitons, Peakons, ...)

профессионал

  • Реалистичные волны

против

  • Для большинства игр не стоит усилий
  • Требует наибольшего времени расчета

Решение 4

Немного сложнее, чем решение 1, но не так сложно решение 3.

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

В игре sturmovik использовался этот подход, но я не нахожу ссылку на статью об этом.

профессионал

  • это проще чем 3
  • это получает хорошие результаты (для 2d)
  • это может выглядеть реалистично, если художники хорошо справляются со своей работой

против

  • трудно оживить
  • повторяющиеся узоры могут быть видны на горизонте
Quonux
источник
6

Чтобы добавить постоянные волны, добавьте пару синусоид после расчета динамики. Для простоты я бы сделал это смещение только графическим эффектом и не позволил бы ему влиять на саму динамику, но вы могли бы попробовать обе альтернативы и посмотреть, какая из них работает лучше всего.

Чтобы уменьшить "всплеск", я бы предложил изменить метод Splash (int index, float speed) так, чтобы он напрямую воздействовал не только на индекс, но и на некоторые близкие вершины, чтобы распространить эффект, но при этом иметь тот же " энергия». Количество затронутых вершин может зависеть от ширины вашего объекта. Вам, вероятно, нужно будет настроить эффект, прежде чем вы получите идеальный результат.

Чтобы текстурировать более глубокие части воды, вы можете либо сделать, как описано в статье, и просто сделать более глубокую часть «более синим», либо вы можете интерполировать между двумя текстурами в зависимости от глубины воды.

Микаэль Хогстрём
источник
Спасибо за ваш ответ. Я действительно надеялся, что кто-то еще попробовал это до меня и мог дать мне более конкретный ответ. Но ваши советы тоже очень ценятся. Я на самом деле очень занят, но как только у меня будет время, я попробую то, что вы упомянули, и поиграюсь с кодом еще немного.
Ягода
1
Хорошо, но если есть что-то конкретное, с чем вам нужна помощь, просто скажите об этом, и я посмотрю, могу ли я быть немного более сложным.
Микаэль Хогстрем
Большое спасибо! Просто я не очень хорошо рассчитал свой вопрос, так как на следующей неделе у меня экзаменационная неделя. После того, как я закончу свои экзамены, я определенно потрачу больше времени на код и, скорее всего, вернусь с более конкретными вопросами.
Берри