¿Cómo creo agua 2D con ondas dinámicas?

81

New Super Mario Bros tiene agua 2D realmente genial que me gustaría aprender a crear.

Aquí hay un video que lo muestra. Una parte ilustrativa:

Nuevos efectos de agua de Super Mario Bros

Las cosas que golpean el agua crean olas. También hay ondas constantes de "fondo". Puedes ver bien las ondas constantes justo después de las 00:50 en el video, cuando la cámara no se mueve.

Supongo que los efectos de bienvenida funcionan como en la primera parte de este tutorial .

Sin embargo, en NSMB el agua también tiene ondas constantes en la superficie, y las salpicaduras se ven muy diferentes. Otra diferencia es que en el tutorial, si crea una salpicadura, primero crea un "agujero" profundo en el agua en el origen de la salpicadura. En el nuevo Super Mario Bros, este agujero está ausente o es mucho más pequeño. Me refiero a las salpicaduras que crea el jugador al saltar dentro y fuera del agua.

¿Cómo creo una superficie de agua con olas y salpicaduras constantes?

Estoy programando en XNA. Lo intenté yo mismo, pero no pude lograr que las ondas sinusoidales de fondo funcionen bien junto con las ondas dinámicas.

No estoy preguntando cómo los desarrolladores de New Super Mario Bros hicieron exactamente esto, solo me interesó cómo recrear un efecto como este.

Baya
fuente

Respuestas:

147

Lo intenté.

Salpicaduras (resortes)

Como se menciona en el tutorial , la superficie del agua es como un cable: si tira de algún punto del cable, los puntos al lado de ese punto también se tirarán hacia abajo. Todos los puntos también son atraídos de vuelta a una línea de base.

Básicamente, son muchos resortes verticales uno al lado del otro los que se tiran unos a otros también.

Dibujé eso en Lua usando LÖVE y obtuve esto:

animación de un chapoteo

Parece plausible Oh Hooke , genio guapo.

Si quieres jugar con él, ¡aquí hay un puerto JavaScript cortesía de Phil ! Mi código está al final de esta respuesta.

Ondas de fondo (senos apilados)

Las ondas de fondo natural me parecen un conjunto de ondas sinusoidales (con diferentes amplitudes, fases y longitudes de onda), todas sumadas. Esto es lo que parecía cuando lo escribí:

ondas de fondo producidas por interferencia sinusoidal

Los patrones de interferencia parecen bastante plausibles.

Todos juntos ahora

Entonces, es bastante simple sumar las ondas de bienvenida y las ondas de fondo:

ondas de fondo, con salpicaduras

Cuando se producen salpicaduras, puede ver pequeños círculos grises que muestran dónde estaría la onda de fondo original.

Se parece mucho al video que vinculó , por lo que lo consideraría un experimento exitoso.

Aquí está mi main.lua(el único archivo). Creo que es bastante legible.

-- 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
fuente
¡Gran respuesta! Muchas gracias. Y también, gracias por revisar mi pregunta, puedo ver cómo esto es más claro. También los gifs son muy útiles. ¿Conoce por casualidad una forma de evitar el gran agujero que emerge al crear una salpicadura también? Podría ser que Mikael Högström ya respondió esto correctamente, pero lo intenté incluso antes de publicar esta pregunta y mi resultado fue que el agujero se convirtió en una forma triangular y parecía muy poco realista.
Berry
Para truncar la profundidad del "agujero de salpicadura", puede limitar la amplitud máxima de la onda, es decir, qué tan lejos se puede desviar cualquier punto de la línea de base.
Anko
3
Por cierto para cualquier persona interesada: en lugar de envolver los lados del agua, elegí usar la línea de base para normalizar los lados. De lo contrario, si crea una salpicadura a la derecha del agua, también crearía olas a la izquierda del agua, lo que me parece poco realista. Además, como no envolví las olas, las ondas de fondo se aplastarían muy rápidamente. Por lo tanto, elegí hacer que esos fueran solo un efecto gráfico, como dijo Mikael Högström, para que las ondas de fondo no se incluyeran en los cálculos de velocidad y aceleración.
Berry
1
Sólo quería hacerte saber. Hemos hablado sobre truncar el "splash-hole" con una declaración if. Al principio era reacio a hacerlo. Pero ahora he notado que realmente funciona perfectamente, ya que las ondas de fondo evitarán que la superficie sea plana.
Berry
44
Convertí este código de onda a JavaScript y lo puse en jsfiddle aquí: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick
11

Para la solución (matemáticamente hablando, puede resolver el problema con la resolución de ecuaciones diferenciales, pero estoy seguro de que no lo hacen de esa manera) de crear ondas, tiene 3 posibilidades (dependiendo de qué tan detallado debe ser):

  1. Calcule las ondas con las funciones trigonométricas (la más simple y la más rápida)
  2. Hazlo como Anko ha propuesto
  3. Resolver las ecuaciones diferenciales.
  4. Usar búsquedas de textura

Solución 1

Realmente simple, para cada onda calculamos la distancia (absoluta) desde cada punto de la superficie hasta la fuente y calculamos la 'altura' con la fórmula

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

dónde

  • dist es nuestra distancia
  • El factor A es un valor que significa qué tan rápido / denso deberían ser las ondas
  • La fase es la fase de la ola, necesitamos incrementarla con el tiempo para obtener una onda animada.

Tenga en cuenta que podemos agregar tantos términos como queramos (principio de superposición).

Pro

  • Es realmente rápido de calcular
  • Es fácil de implementar

Contra

  • Para los reflejos (simples) en una superficie 1d necesitamos crear fuentes de ondas "fantasmas" para simular reflejos, esto es más complicado en las superficies 2d y es una de las limitaciones de este enfoque simple

Solución 2

Pro

  • También es simple
  • Permite calcular fácilmente los reflejos
  • Se puede extender al espacio 2d o 3d relativamente fácilmente

Contra

  • Puede ser numéricamente inestable si el valor de descarga es demasiado alto
  • necesita más potencia de cálculo que la Solución 1 (pero no tanto como la Solución 3 )

Solución 3

Ahora golpeé una pared dura, esta es la solución más complicada.

No implementé este, pero es posible resolver estos monstruos.

Aquí puede encontrar una presentación sobre la matemática de la misma, no es simple y también existen ecuaciones diferenciales para diferentes tipos de ondas.

Aquí hay una lista no completa con algunas ecuaciones diferenciales para resolver casos más especiales (Solitones, Picos, ...)

Pro

  • Olas realistas

Contra

  • Para la mayoría de los juegos no vale la pena
  • Necesita la mayor cantidad de tiempo de cálculo

Solución 4

Un poco más complicado que la solución 1, pero no una solución tan complicada 3.

Usamos texturas precalculadas y las combinamos, luego usamos el mapeo de desplazamiento (en realidad es un método para ondas 2D pero el principio también puede funcionar para ondas 1D)

El juego sturmovik ha utilizado este enfoque, pero no encuentro el enlace al artículo al respecto.

Pro

  • es más simple que 3
  • obtiene buenos resultados (para 2d)
  • puede parecer realista si los artistas hacen un buen trabajo

Contra

  • difícil de animar
  • patrones repetidos podrían hacerse visibles en el horizonte
Quonux
fuente
6

Para agregar ondas constantes, agregue un par de ondas sinusoidales después de haber calculado la dinámica. Por simplicidad, haría de este desplazamiento un efecto gráfico solamente y no dejaría que afectara la dinámica en sí, pero podría probar ambas alternativas y ver cuál funciona mejor.

Para hacer el "splashhole" más pequeño, sugeriría alterar el método Splash (int index, float speed) para que afecte directamente no solo el índice sino también algunos de los vértices cercanos, para extender el efecto pero seguir teniendo el mismo " energía". El número de vértices afectados podría depender de qué tan ancho sea su objeto. Probablemente necesites modificar mucho el efecto antes de obtener un resultado perfecto.

Para texturizar las partes más profundas del agua, puede hacer lo que se describe en el artículo y simplemente hacer que la parte más profunda sea "más azul" o puede interpolar entre dos texturas dependiendo de la profundidad del agua.

Mikael Högström
fuente
Gracias por su respuesta. En realidad esperaba que alguien más lo hubiera intentado antes y pudiera darme una respuesta más específica. Pero tus consejos también son muy apreciados. En realidad estoy muy ocupado, pero tan pronto como tenga tiempo para hacerlo, intentaré las cosas que mencionaste y jugaré un poco más con el código.
Berry
1
Ok, pero si hay algo específico con lo que necesitas ayuda, solo dilo y veré si puedo ser un poco más elaborado.
Mikael Högström
¡Muchas gracias! Es solo que no he cronometrado mi pregunta muy bien, ya que tengo un examen la semana que viene. Después de terminar mis exámenes, definitivamente pasaré más tiempo en el código, y probablemente regresaré con preguntas más específicas.
Berry