Bagaimana cara membuat air 2D dengan gelombang dinamis?

81

Super Mario Bros baru memiliki air 2D yang sangat keren yang ingin saya pelajari cara membuatnya.

Ini video yang menunjukkannya. Bagian ilustrasi:

Efek air Super Mario Bros baru

Benda-benda yang menghantam air menciptakan gelombang. Ada juga gelombang "latar belakang" yang konstan. Anda dapat melihat gelombang konstan tepat setelah 00:50 dalam video, saat kamera tidak bergerak.

Saya menganggap efek splash berfungsi seperti pada bagian pertama tutorial ini .

Namun, di NSMB air juga memiliki gelombang konstan di permukaan, dan percikan terlihat sangat berbeda. Perbedaan lain adalah bahwa dalam tutorial, jika Anda membuat splash, pertama-tama menciptakan "lubang" yang dalam di air di asal splash. Dalam super mario bros baru lubang ini tidak ada atau jauh lebih kecil. Saya mengacu pada cipratan yang dibuat pemain saat melompat masuk dan keluar dari air.

Bagaimana cara membuat permukaan air dengan gelombang dan cipratan yang konstan?

Saya pemrograman dalam XNA. Saya sudah mencoba ini sendiri, tetapi saya tidak bisa benar-benar membuat gelombang sinus latar belakang bekerja dengan baik bersama dengan gelombang dinamis.

Saya tidak bertanya bagaimana para pengembang New Super Mario Bros melakukan ini dengan tepat — hanya tertarik bagaimana menciptakan kembali efek seperti itu.

Berry
sumber

Jawaban:

147

Saya mencobanya.

Percikan (mata air)

Seperti yang disebutkan dalam tutorial itu , permukaan air seperti kawat: Jika Anda menarik beberapa titik kawat, titik di samping titik itu akan ditarik ke bawah juga. Semua poin juga ditarik kembali ke garis dasar.

Ini pada dasarnya banyak pegas vertikal di samping satu sama lain yang saling tarik juga.

Saya membuat sketsa di Lua menggunakan LÖVE dan mendapatkan ini:

animasi percikan

Tampak masuk akal. Oh Hooke , kau jenius yang tampan.

Jika Anda ingin bermain dengannya, ini adalah port JavaScript milik Phil ! Kode saya ada di akhir jawaban ini.

Gelombang latar belakang (susun sinus)

Bagi saya, gelombang latar belakang alami seperti sekumpulan gelombang sinus (dengan berbagai amplitudo, fase, dan panjang gelombang) yang dijumlahkan bersama-sama. Inilah yang tampak ketika saya menulisnya:

gelombang latar belakang yang dihasilkan oleh gangguan sinus

Pola interferensi terlihat cukup masuk akal.

Semua bersama Sekarang

Jadi itu masalah yang cukup sederhana untuk menyatukan gelombang percikan dan gelombang latar belakang:

gelombang latar belakang, dengan percikan

Ketika percikan terjadi, Anda dapat melihat lingkaran abu-abu kecil yang menunjukkan di mana gelombang latar asli akan berada.

Ini mirip sekali dengan video yang Anda tautkan , jadi saya akan menganggap ini sebagai percobaan yang sukses.

Inilah saya main.lua(satu-satunya file). Saya pikir itu cukup mudah dibaca.

-- 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
sumber
Jawaban bagus! Terima kasih banyak. Dan juga, terima kasih telah memperbaiki pertanyaan saya, saya bisa melihat bagaimana ini lebih jelas. Juga gif sangat membantu. Apakah Anda kebetulan tahu cara untuk mencegah lubang besar yang muncul saat membuat percikan juga? Bisa jadi Mikael Högström sudah menjawab ini dengan benar tetapi saya telah mencobanya bahkan sebelum memposting pertanyaan ini dan hasil saya adalah bahwa lubang itu berbentuk segitiga dan itu terlihat sangat tidak realistis.
Berry
Untuk memotong kedalaman "lubang percikan", Anda dapat membatasi amplitudo maksimum gelombang, yaitu seberapa jauh setiap titik dibiarkan menyimpang dari garis dasar.
Anko
3
BTW untuk siapa pun yang tertarik: Alih-alih membungkus sisi air, saya memilih untuk menggunakan garis dasar untuk menormalkan sisi. Kalau tidak, jika Anda membuat percikan di sebelah kanan air, itu juga akan membuat gelombang di sebelah kiri air, yang saya temukan tidak realistis. Juga, karena saya tidak membungkus ombak, gelombang latar akan menjadi sangat cepat. Karena itu saya memilih untuk membuat efek grafis saja, seperti yang dikatakan Mikael Högström, sehingga gelombang latar tidak akan dimasukkan dalam perhitungan untuk kecepatan dan akselerasi.
Berry
1
Hanya ingin kamu tahu. Kami telah berbicara tentang memotong "splash-hole" dengan pernyataan if. Awalnya saya enggan melakukannya. Tapi sekarang saya perhatikan bahwa itu benar-benar berfungsi dengan baik, karena gelombang latar akan mencegah permukaan menjadi rata.
Berry
4
Saya mengonversi kode gelombang ini ke JavaScript dan menaruhnya di jsfiddle di sini: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick
11

Untuk solusinya (secara matematis Anda dapat memecahkan masalah dengan menyelesaikan persamaan diferensial, tetapi saya yakin mereka tidak melakukannya dengan cara seperti itu) untuk membuat gelombang Anda memiliki 3 kemungkinan (tergantung pada seberapa rinci hal itu seharusnya didapat):

  1. Hitung gelombang dengan fungsi trigonometri (paling sederhana dan tercepat)
  2. Lakukan seperti yang Anko usulkan
  3. Memecahkan persamaan diferensial
  4. Gunakan pencarian tekstur

Solusi 1

Sangat sederhana, untuk setiap gelombang kita menghitung jarak (absolut) dari setiap titik permukaan ke sumber dan kita menghitung 'tinggi' dengan rumus

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

dimana

  • dist adalah jarak kita
  • FactorA adalah nilai yang berarti seberapa cepat / padatnya gelombang
  • Fase adalah Fase gelombang, kita perlu menambahkannya dengan waktu untuk mendapatkan gelombang animasi

Perhatikan bahwa kita dapat menambahkan sebanyak mungkin istilah secara bersamaan (prinsip superposisi).

Pro

  • Sangat cepat untuk menghitung
  • Mudah diimplementasikan

Kontra

  • Untuk refleksi (sederhana) pada Permukaan 1d kita perlu membuat sumber gelombang "hantu" untuk mensimulasikan refleksi, ini lebih rumit pada permukaan 2d dan itu adalah salah satu keterbatasan dari pendekatan sederhana ini

Solusi 2

Pro

  • Sederhana juga
  • Memungkinkan untuk menghitung refleksi dengan mudah
  • Itu dapat diperluas ke ruang 2d atau 3d dengan mudah

Kontra

  • Dapat menjadi tidak stabil secara numerik jika nilai dumping terlalu tinggi
  • membutuhkan daya kalkulasi lebih dari Solusi 1 (tetapi tidak begitu banyak seperti Solusi 3 )

Solusi 3

Sekarang saya menabrak dinding keras, ini adalah solusi paling rumit.

Saya tidak menerapkan yang satu ini tetapi mungkin untuk menyelesaikan monster-monster ini.

Di sini Anda dapat menemukan presentasi tentang matematika itu, tidak sederhana dan ada juga persamaan diferensial untuk berbagai jenis gelombang.

Berikut adalah daftar yang tidak lengkap dengan beberapa Persamaan diferensial untuk menyelesaikan lebih banyak kasus khusus (Soliton, Peakons, ...)

Pro

  • Gelombang realistis

Kontra

  • Untuk sebagian besar game tidak sepadan dengan usaha
  • Membutuhkan waktu perhitungan paling banyak

Solusi 4

Sedikit lebih rumit dari solusi 1 tetapi tidak terlalu rumit solusi 3.

Kami menggunakan tekstur yang telah dihitung dan menyatukannya, setelah itu kami menggunakan pemetaan perpindahan (sebenarnya metode untuk gelombang 2d tapi prinsipnya juga bisa bekerja untuk gelombang 1d)

Permainan sturmovik telah menggunakan pendekatan ini tetapi saya tidak menemukan tautan ke artikel tentang itu.

Pro

  • ini lebih sederhana dari 3
  • itu mendapatkan hasil yang terlihat bagus (untuk 2d)
  • itu bisa terlihat realistis jika para artis itu pekerjaan yang bagus

Kontra

  • sulit untuk hidup
  • pola yang berulang bisa terlihat di cakrawala
Quonux
sumber
6

Untuk menambahkan gelombang konstan tambahkan beberapa gelombang sinus setelah Anda menghitung dinamika. Untuk kesederhanaan, saya akan membuat perpindahan ini efek grafis saja dan tidak membiarkannya mempengaruhi dinamika itu sendiri tetapi Anda bisa mencoba kedua alternatif dan melihat mana yang terbaik.

Untuk membuat "splashhole" lebih kecil saya sarankan mengubah metode Splash (int index, float speed) sehingga secara langsung mempengaruhi tidak hanya indeks tetapi juga beberapa simpul dekat, sehingga untuk menyebarkan efek tetapi masih memiliki yang sama " energi". Jumlah simpul yang terpengaruh dapat bergantung pada seberapa lebar objek Anda. Anda mungkin harus mengubah banyak efek sebelum Anda mendapatkan hasil yang sempurna.

Untuk tekstur bagian dalam air, Anda bisa melakukan seperti yang dijelaskan dalam artikel dan hanya membuat bagian yang lebih dalam "lebih biru" atau Anda bisa menyisipkan di antara dua tekstur tergantung pada kedalaman air.

Mikael Högström
sumber
Terimakasih atas balasan anda. Saya benar-benar berharap bahwa orang lain telah mencoba ini sebelum saya dan dapat memberikan saya jawaban yang lebih spesifik. Tapi tips Anda, sangat dihargai juga. Saya sebenarnya sangat sibuk, tetapi begitu saya punya waktu untuk itu, saya akan mencoba hal-hal yang telah Anda sebutkan dan bermain-main dengan kode lagi.
Berry
1
Ok, tetapi jika ada sesuatu yang spesifik yang perlu Anda bantu, katakan saja dan saya akan melihat apakah saya bisa sedikit lebih rumit.
Mikael Högström
Terima kasih banyak! Hanya saja saya belum menghitung waktu pertanyaan saya dengan baik, karena saya memiliki minggu ujian minggu depan. Setelah menyelesaikan ujian, saya pasti akan menghabiskan lebih banyak waktu pada kode, dan kemungkinan besar akan kembali dengan pertanyaan yang lebih spesifik.
Berry