Làm cách nào để tạo nước 2D bằng sóng động?


81

Super Mario Bros mới có nước 2D thực sự tuyệt vời mà tôi muốn tìm hiểu cách tạo.

Đây là một video cho thấy nó. Một phần minh họa:

Hiệu ứng nước Super Mario Bros mới

Những thứ đập vào mặt nước tạo ra sóng. Ngoài ra còn có sóng "nền" không đổi. Bạn có thể có được cái nhìn tốt về các sóng không đổi chỉ sau 00:50 trong video, khi máy ảnh không di chuyển.

Tôi giả sử các hiệu ứng giật gân hoạt động như trong phần đầu tiên của hướng dẫn này .

Tuy nhiên, trong NSMB , nước cũng có sóng không đổi trên bề mặt và các vết bắn trông rất khác nhau. Một sự khác biệt nữa là trong hướng dẫn, nếu bạn tạo ra một văng nước, trước tiên, nó sẽ tạo ra một "lỗ hổng" sâu trong nước ở điểm gốc của văng. Trong siêu mario mới, lỗ này không có hoặc nhỏ hơn nhiều. Tôi đang đề cập đến những cú bắn mà người chơi tạo ra khi nhảy xuống nước.

Làm thế nào để tôi tạo ra một mặt nước với sóng và bắn liên tục?

Tôi đang lập trình trong XNA. Tôi đã thử điều này bản thân mình, nhưng tôi thực sự không thể làm cho các sóng hình sin hoạt động tốt cùng với các sóng động.

Tôi không hỏi làm thế nào các nhà phát triển của New Super Mario Bros đã làm điều này chính xác là chỉ quan tâm đến cách tái tạo một hiệu ứng như thế này.

Câu trả lời:


147

Tôi đã thử nó.

Tách nước (lò xo)

Như hướng dẫn đó đề cập, bề mặt của nước giống như một sợi dây: Nếu bạn kéo vào một điểm nào đó của dây, các điểm bên cạnh điểm đó cũng sẽ bị kéo xuống. Tất cả các điểm cũng được thu hút trở lại một đường cơ sở.

Về cơ bản, nó có rất nhiều lò xo thẳng đứng cạnh nhau cũng kéo theo nhau.

Tôi đã phác thảo rằng trong Lua bằng cách sử dụng LÖVE và nhận được điều này:

hoạt hình của một giật gân

Có vẻ hợp lý. Oh Hooke , bạn thiên tài đẹp trai.

Nếu bạn muốn chơi với nó, đây là một cổng JavaScript lịch sự của Phil ! Mã của tôi là ở cuối câu trả lời này.

Sóng nền (xếp chồng lên nhau)

Sóng nền tự nhiên đối với tôi giống như một loạt các sóng hình sin (với biên độ, pha và bước sóng khác nhau) tất cả được tổng hợp lại với nhau. Đây là những gì trông giống như khi tôi viết nó:

sóng nền được tạo ra bởi nhiễu hình sin

Các mô hình giao thoa trông khá hợp lý.

Bây giờ tất cả cùng nhau

Vì vậy, đây là một vấn đề khá đơn giản để kết hợp các sóng giật gân và sóng nền:

sóng nền, với bắn

Khi xảy ra hiện tượng bắn, bạn có thể thấy các vòng tròn nhỏ màu xám hiển thị vị trí sóng nền ban đầu.

Nó trông rất giống video mà bạn đã liên kết , vì vậy tôi coi đây là một thử nghiệm thành công.

Đây là của tôi main.lua(tập tin duy nhất). Tôi nghĩ nó khá dễ đọc.

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

Câu trả lời chính xác! Cảm ơn rât nhiều. Và cũng, cảm ơn vì đã xem lại câu hỏi của tôi, tôi có thể thấy điều này rõ ràng hơn như thế nào. Ngoài ra các gifs rất hữu ích. Bạn có tình cờ biết một cách để ngăn chặn lỗ hổng lớn xuất hiện khi tạo ra một văng quá không? Có thể là Mikael Högström đã trả lời đúng nhưng tôi đã thử ngay cả trước khi đăng câu hỏi này và kết quả của tôi là cái lỗ có hình tam giác và trông rất phi thực tế.
Berry

Để cắt bớt độ sâu của "lỗ giật gân", bạn có thể giới hạn biên độ cực đại của sóng, tức là bất kỳ điểm nào được phép đi lạc từ đường cơ sở.
Anko

3
BTW cho bất cứ ai quan tâm: Thay vì quấn hai bên mặt nước, tôi chọn sử dụng đường cơ sở để bình thường hóa các mặt. Mặt khác, nếu bạn tạo ra một văng ở bên phải của nước, nó cũng sẽ tạo ra sóng ở bên trái của nước, điều mà tôi thấy không thực tế. Ngoài ra, vì tôi không quấn sóng, sóng nền sẽ bị xẹp rất nhanh. Do đó, tôi đã chọn làm cho những hiệu ứng đồ họa đó chỉ như Mikael Högström nói, để sóng nền sẽ không được đưa vào các tính toán cho tốc độ và gia tốc.
Berry

1
Chỉ là muốn cho bạn biết. Chúng tôi đã nói về việc cắt ngắn "lỗ hổng" bằng một câu lệnh if. Lúc đầu tôi miễn cưỡng làm như vậy. Nhưng bây giờ tôi đã nhận thấy rằng nó thực sự hoạt động hoàn hảo, vì sóng nền sẽ ngăn không cho bề mặt phẳng.
Berry

4
Tôi đã chuyển đổi mã sóng này thành JavaScript và đưa nó lên jsfiddle tại đây: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick

11

Đối với giải pháp (nói một cách toán học, bạn có thể giải quyết vấn đề bằng cách giải phương trình vi phân, nhưng tôi chắc chắn rằng họ không làm theo cách đó) trong việc tạo sóng bạn có 3 khả năng (tùy thuộc vào mức độ chi tiết của nó):

  1. Tính toán các sóng với các hàm lượng giác (đơn giản nhất và nhanh nhất)
  2. Làm như Anko đã đề xuất
  3. Giải phương trình vi phân
  4. Sử dụng tra cứu kết cấu

Giải pháp 1

Thực sự đơn giản, với mỗi sóng, chúng tôi tính khoảng cách (tuyệt đối) từ mỗi điểm trên bề mặt đến nguồn và chúng tôi tính toán 'độ cao' với công thức

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

Ở đâu

  • khoảng cách là khoảng cách của chúng tôi
  • FactorA là một giá trị có nghĩa là sóng phải nhanh / đậm đặc như thế nào
  • Pha là Pha của sóng, chúng ta cần tăng dần theo thời gian để có được sóng hoạt hình

Lưu ý rằng chúng ta có thể thêm bao nhiêu thuật ngữ cùng nhau tùy thích (nguyên tắc chồng chất).

Chuyên nghiệp

  • Nó rất nhanh để tính toán
  • Dễ thực hiện

Contra

  • Đối với các phản xạ (đơn giản) trên Bề mặt 1d, chúng ta cần tạo các nguồn sóng "ma" để mô phỏng các phản xạ, điều này phức tạp hơn ở các bề mặt 2d và đó là một trong những hạn chế của phương pháp đơn giản này

Giải pháp 2

Chuyên nghiệp

  • Nó cũng đơn giản
  • Nó cho phép tính toán phản xạ dễ dàng
  • Nó có thể được mở rộng đến không gian 2d hoặc 3d một cách dễ dàng

Contra

  • Có thể không ổn định về số lượng nếu giá trị bán phá giá quá cao
  • cần nhiều năng lực tính toán hơn Giải pháp 1 (nhưng không nhiều như Giải pháp 3 )

Giải pháp 3

Bây giờ tôi nhấn một bức tường cứng, đây là giải pháp phức tạp nhất.

Tôi đã không thực hiện điều này nhưng có thể giải quyết những con quái vật này.

Ở đây bạn có thể tìm thấy một bài trình bày về toán học của nó, nó không đơn giản và cũng tồn tại các phương trình vi phân cho các loại sóng khác nhau.

Dưới đây là một danh sách không đầy đủ với một số phương trình vi phân để giải quyết các trường hợp đặc biệt hơn (Solitons, Peakons, ...)

Chuyên nghiệp

  • Sóng thực tế

Contra

  • Đối với hầu hết các trò chơi không đáng nỗ lực
  • Cần nhiều thời gian tính toán nhất

Giải pháp 4

Một chút phức tạp hơn giải pháp 1 nhưng không quá phức tạp một giải pháp 3.

Chúng tôi sử dụng các kết cấu được tính toán trước và trộn chúng lại với nhau, sau đó chúng tôi sử dụng ánh xạ dịch chuyển (thực ra là một phương pháp cho sóng 2d nhưng nguyên tắc cũng có thể hoạt động cho sóng 1d)

Trò chơi sturmovik đã sử dụng phương pháp này nhưng tôi không tìm thấy liên kết đến bài viết về nó.

Chuyên nghiệp

  • nó đơn giản hơn 3
  • nó nhận được kết quả tốt (2d)
  • nó có thể trông thực tế nếu các nghệ sĩ làm tốt công việc

Contra

  • khó hoạt hình
  • mô hình lặp đi lặp lại có thể nhìn thấy trên đường chân trời

6

Để thêm sóng không đổi, thêm một vài sóng hình sin sau khi bạn đã tính toán động lực học. Để đơn giản, tôi sẽ làm cho sự dịch chuyển này chỉ có hiệu ứng đồ họa và không để nó ảnh hưởng đến động lực học nhưng bạn có thể thử cả hai phương án và xem cái nào hoạt động tốt nhất.

Để làm cho "lỗ hổng" nhỏ hơn, tôi khuyên bạn nên thay đổi phương thức Splash (int index, float speed) để nó ảnh hưởng trực tiếp đến không chỉ chỉ số mà cả một số đỉnh gần, để lan rộng hiệu ứng mà vẫn có cùng " năng lượng". Số lượng đỉnh bị ảnh hưởng có thể phụ thuộc vào độ rộng của đối tượng của bạn. Có lẽ bạn sẽ cần phải điều chỉnh hiệu ứng rất nhiều trước khi bạn có một kết quả hoàn hảo.

Để kết cấu các phần sâu hơn của nước, bạn có thể làm như được mô tả trong bài viết và chỉ cần làm cho phần sâu hơn "xanh hơn" hoặc bạn có thể nội suy giữa hai kết cấu tùy thuộc vào độ sâu của nước.


Cảm ơn bạn đã trả lời của bạn. Tôi đã thực sự hy vọng rằng ai đó đã thử điều này trước tôi và có thể cho tôi một câu trả lời cụ thể hơn. Nhưng lời khuyên của bạn, cũng được đánh giá rất cao. Tôi thực sự rất bận rộn, nhưng ngay khi tôi có thời gian cho nó, tôi sẽ thử những thứ bạn đã đề cập và chơi xung quanh với một số mã nữa.
Berry

1
Ok, nhưng nếu có điều gì đó cụ thể mà bạn cần trợ giúp, chỉ cần nói như vậy và tôi sẽ xem liệu tôi có thể phức tạp hơn một chút không.
Mikael Högström

Cảm ơn rât nhiều! Chỉ là tôi đã không trả lời đúng câu hỏi của mình, vì tôi có một tuần thi vào tuần tới. Sau khi tôi hoàn thành bài kiểm tra của mình, tôi chắc chắn sẽ dành nhiều thời gian hơn cho mã, và rất có thể sẽ trở lại với các câu hỏi cụ thể hơn.
Berry
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.