Como crio água 2D com ondas dinâmicas?

81

O novo Super Mario Bros tem água 2D muito legal que eu gostaria de aprender a criar.

Aqui está um vídeo mostrando isso. Uma parte ilustrativa:

Novos efeitos da água de Super Mario Bros

As coisas que atingem a água criam ondas. Também existem ondas constantes de "fundo". Você pode dar uma boa olhada nas ondas constantes logo após 00:50 no vídeo, quando a câmera não está se movendo.

Presumo que os efeitos de respingo funcionem como na primeira parte deste tutorial .

No entanto, no NSMB, a água também tem ondas constantes na superfície e os salpicos parecem muito diferentes. Outra diferença é que, no tutorial, se você cria um respingo, ele primeiro cria um "buraco" profundo na água na origem do respingo. No novo super mario bros, esse buraco está ausente ou muito menor. Refiro-me aos salpicos que o jogador cria ao pular dentro e fora da água.

Como crio uma superfície de água com ondas e salpicos constantes?

Estou programando em XNA. Eu mesmo tentei isso, mas não consegui realmente fazer com que as ondas senoidais de fundo funcionassem bem juntas com as ondas dinâmicas.

Não estou perguntando como os desenvolvedores do New Super Mario Bros fizeram exatamente isso - apenas interessados ​​em recriar um efeito como esse.

baga
fonte

Respostas:

147

Eu tentei.

Salpicos (molas)

Como o tutorial menciona, a superfície da água é como um fio: se você puxar algum ponto do fio, os pontos próximos a esse ponto também serão puxados para baixo. Todos os pontos também são atraídos de volta à linha de base.

Basicamente, são muitas as molas verticais próximas uma da outra que também se puxam.

Eu esbocei isso em Lua usando LÖVE e consegui isso:

animação de um splash

Parece plausível. Oh Hooke , seu gênio bonito.

Se você quiser brincar com ele, aqui está uma porta JavaScript cortesia de Phil ! Meu código está no final desta resposta.

Ondas de fundo (seno empilhado)

Ondas naturais de fundo parecem-me com um monte de ondas senoidais (com diferentes amplitudes, fases e comprimentos de onda), todas somadas. Aqui está o que parecia quando eu escrevi:

ondas de fundo produzidas por interferência senoidal

Os padrões de interferência parecem bastante plausíveis.

Todos juntos agora

Portanto, é uma questão bastante simples reunir as ondas de respingo e as ondas de fundo:

ondas de fundo, com salpicos

Quando os salpicos acontecem, é possível ver pequenos círculos cinza mostrando onde estaria a onda de fundo original.

Parece muito com o vídeo que você vinculou , então considero este um experimento bem-sucedido.

Aqui está o meu main.lua(o único arquivo). Eu acho que é bem legível.

-- 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
fonte
Ótima resposta! Muito obrigado. E também, obrigado por revisar minha pergunta, posso ver como isso é mais claro. Também os gifs são muito úteis. Por acaso, você conhece uma maneira de evitar o grande buraco que surge ao criar um respingo também? Pode ser que Mikael Högström já tenha respondido corretamente, mas eu já havia tentado isso antes de postar essa pergunta, e meu resultado foi que o buraco se tornou de forma triangular e parecia muito irreal.
Berry
Para truncar a profundidade do "furo de respingo", você pode limitar a amplitude máxima da onda, ou seja, até que ponto qualquer ponto pode se afastar da linha de base.
Anko
3
BTW para qualquer pessoa interessada: em vez de envolver os lados da água, optei por usar a linha de base para normalizar os lados. Caso contrário, se você criar um respingo à direita da água, também criaria ondas à esquerda da água, o que eu achei irreal. Além disso, como eu não envolvi as ondas, as ondas de fundo se achatavam muito rapidamente. Por isso, optei por tornar esses efeitos apenas gráficos, como Mikael Högström disse, para que as ondas de fundo não fossem incluídas nos cálculos de velocidade e aceleração.
Berry
1
Só queria que você soubesse. Nós falamos sobre truncar o "splash-hole" com uma declaração if. No começo, relutei em fazê-lo. Mas agora notei que ele realmente funciona perfeitamente, pois as ondas de fundo impedem que a superfície seja plana.
Berry
4
Eu converti esse código de onda para JavaScript e o coloquei no jsfiddle aqui: jsfiddle.net/phil_mcc/sXmpD/8 #
Phil McCullick
11

Para a solução (matematicamente falando, você pode resolver o problema com a resolução de equações diferenciais, mas tenho certeza de que não o fazem dessa maneira) de criar ondas, você tem três possibilidades (dependendo do detalhamento):

  1. Calcule as ondas com as funções trigonométricas (mais simples e mais rápidas)
  2. Faça como Anko propôs
  3. Resolva as equações diferenciais
  4. Use pesquisas de textura

Solução 1

Realmente simples, para cada onda calculamos a distância (absoluta) de cada ponto da superfície até a fonte e calculamos a 'altura' com a fórmula

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

Onde

  • dist é a nossa distância
  • O fator A é um valor que significa quão rápido / denso as ondas devem ser
  • Phase é a fase da onda, precisamos incrementá-la com tempo para obter uma onda animada

Observe que podemos adicionar tantos termos quanto quisermos (princípio de superposição).

Pró

  • É muito rápido calcular
  • É fácil de implementar

Contra

  • Para reflexões (simples) em uma superfície 1d, precisamos criar fontes de ondas "fantasmas" para simular reflexões, isso é mais complicado em superfícies 2d e é uma das limitações dessa abordagem simples

Solução 2

Pró

  • É simples também
  • Permite calcular reflexões facilmente
  • Pode ser estendido para o espaço 2D ou 3D facilmente

Contra

  • Pode ser instável numericamente se o valor do dumping for muito alto
  • precisa de mais poder de cálculo que a solução 1 (mas não tanto quanto a solução 3 )

Solução 3

Agora eu bati em uma parede dura, esta é a solução mais complicada.

Eu não implementei este, mas é possível resolver esses monstros.

Aqui você pode encontrar uma apresentação sobre a matemática, não é simples e também existem equações diferenciais para diferentes tipos de ondas.

Aqui está uma lista não completa com algumas equações diferenciais para resolver casos mais especiais (Solitons, Peakons, ...)

Pró

  • Ondas realistas

Contra

  • Para a maioria dos jogos que não valem o esforço
  • Precisa de mais tempo de cálculo

Solução 4

Um pouco mais complicado que a solução 1, mas não tão complicada 3.

Usamos texturas pré-calculadas e as misturamos, depois usamos o mapeamento de deslocamento (na verdade, um método para ondas 2D, mas o princípio também pode funcionar para ondas 2D)

O jogo sturmovik usou essa abordagem, mas não encontro o link para o artigo sobre ela.

Pró

  • é mais simples que 3
  • obtém bons resultados (para 2D)
  • pode parecer realista se os artistas fizerem um ótimo trabalho

Contra

  • difícil de animar
  • padrões repetidos podem ficar visíveis no horizonte
Quonux
fonte
6

Para adicionar ondas constantes, adicione duas ondas senoidais depois de calcular a dinâmica. Por uma questão de simplicidade, tornaria esse deslocamento apenas um efeito gráfico e não o deixaria afetar a dinâmica, mas você poderia tentar as duas alternativas e ver qual funcionaria melhor.

Para tornar o "splashhole" menor, sugiro alterar o método Splash (índice int, velocidade de flutuação) para que ele afete diretamente não apenas o índice, mas também alguns dos vértices próximos, para espalhar o efeito, mas ainda ter o mesmo " energia". O número de vértices afetados pode depender da largura do seu objeto. Você provavelmente precisará ajustar muito o efeito antes de obter um resultado perfeito.

Para texturizar as partes mais profundas da água, você pode fazer como descrito no artigo e apenas tornar a parte mais profunda "mais azul" ou interpolar entre duas texturas, dependendo da profundidade da água.

Mikael Högström
fonte
Obrigado por sua resposta. Na verdade, eu esperava que alguém tivesse tentado isso antes de mim e me desse uma resposta mais específica. Mas suas dicas também são muito apreciadas. Na verdade, estou muito ocupado, mas assim que tiver tempo, tentarei as coisas que você mencionou e brinque mais com o código.
Berry
1
Ok, mas se houver algo específico em que você precise de ajuda, basta dizer e vou ver se posso ser um pouco mais elaborado.
Mikael Högström
Muito obrigado! Só que não cronometrei minha pergunta muito bem, já que tenho uma semana de exames na próxima semana. Depois de terminar meus exames, definitivamente gastarei mais tempo no código e provavelmente retornarei com perguntas mais específicas.
Berry