ฉันจะสร้างน้ำ 2D ด้วยคลื่นแบบไดนามิกได้อย่างไร


81

New Super Mario Brosมีน้ำ 2D ที่ยอดเยี่ยมจริงๆที่ฉันต้องการเรียนรู้วิธีการสร้าง

นี่คือวิดีโอที่แสดง ส่วนที่เป็นตัวอย่าง:

New เอฟเฟกต์น้ำ Super Mario Bros

สิ่งที่กระทบกับน้ำสร้างคลื่น นอกจากนี้ยังมีคลื่น "พื้นหลัง" ที่คงที่ คุณสามารถดูคลื่นคงที่ได้หลังจากวิดีโอ 00:50 เมื่อกล้องไม่เคลื่อนไหว

ฉันคิดว่าเอฟเฟกต์สแปลชทำงานเหมือนในส่วนแรกของบทช่วยสอนนี้

อย่างไรก็ตามในNSMBน้ำยังมีคลื่นคงที่บนพื้นผิวและกระเด็นดูแตกต่างกันมาก ข้อแตกต่างอีกอย่างก็คือในบทช่วยสอนถ้าคุณสร้างสแปลชมันจะสร้าง "รู" ที่ลึกลงไปในน้ำที่จุดกำเนิดของสาด ใน super mario bros ใหม่หลุมนี้หายไปหรือเล็กกว่ามาก ฉันหมายถึงสาดที่ผู้เล่นสร้างขึ้นเมื่อกระโดดเข้าและออกจากน้ำ

ฉันจะสร้างผิวน้ำที่มีคลื่นคงที่และกระเด็นได้อย่างไร

ฉันกำลังเขียนโปรแกรมใน XNA ฉันลองตัวเองแล้ว แต่ฉันไม่สามารถทำให้คลื่นไซน์แบ็คกราวน์ทำงานร่วมกับคลื่นแบบไดนามิกได้

ฉันไม่ได้ถามว่านักพัฒนาของNew Super Mario Brosทำสิ่งนี้ได้อย่างไร - เพียงแค่สนใจที่จะสร้างเอฟเฟกต์แบบใหม่

คำตอบ:


147

ฉันลองแล้ว

กระเด็น (สปริง)

ตามที่ระบุไว้ในบทช่วยสอนพื้นผิวของน้ำก็เหมือนลวด: ถ้าคุณดึงที่จุดใดจุดหนึ่งจุดที่อยู่ติดกับจุดนั้นจะถูกดึงลงมาด้วย คะแนนทั้งหมดจะถูกดึงดูดกลับไปที่พื้นฐาน

มันเป็นน้ำพุแนวตั้งจำนวนมากที่อยู่ติดกันซึ่งดึงเข้าหากันด้วย

ฉันวาดภาพนั้นใน Lua โดยใช้LÖVEและรับสิ่งนี้:

ภาพเคลื่อนไหวของการสาด

ดูน่าเชื่อถือ โอ้ฮุกคุณหล่ออัจฉริยะ

ถ้าคุณต้องการที่จะเล่นกับมันนี่คือพอร์ตจาวาสคริปต์มารยาทของฟิล ! รหัสของฉันอยู่ท้ายคำตอบนี้

คลื่นพื้นหลัง (ไซน์เรียงซ้อน)

คลื่นพื้นหลังตามธรรมชาติดูเหมือนจะเป็นคลื่นไซน์ (กับแอมพลิจูดเฟสและความยาวคลื่นที่แตกต่างกัน) ทั้งหมดรวมเข้าด้วยกัน นี่คือสิ่งที่ดูเหมือนว่าเมื่อฉันเขียนมัน:

คลื่นพื้นหลังที่เกิดจากการรบกวนไซน์

รูปแบบการรบกวนดูน่าเชื่อถือมาก

ด้วยกันตอนนี้

ดังนั้นจึงเป็นเรื่องง่ายที่จะรวมคลื่นสาดและคลื่นพื้นหลังเข้าด้วยกัน:

คลื่นพื้นหลังพร้อมกระเด็น

เมื่อมีการกระเด็นเกิดขึ้นคุณจะเห็นวงกลมสีเทาเล็ก ๆ แสดงตำแหน่งที่คลื่นพื้นหลังดั้งเดิมจะปรากฏ

ดูเหมือนว่าวิดีโอที่คุณเชื่อมโยงดังนั้นฉันจึงคิดว่าการทดลองนี้ประสบความสำเร็จ

นี่คือไฟล์ของฉัน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

คำตอบที่ดี! ขอบคุณมาก. และขอขอบคุณสำหรับการแก้ไขคำถามของฉันฉันสามารถเห็นได้ว่าสิ่งนี้ชัดเจนยิ่งขึ้น gifs ยังมีประโยชน์มาก คุณบังเอิญรู้วิธีป้องกันหลุมขนาดใหญ่ที่โผล่ออกมาเมื่อสร้างสาดด้วยหรือไม่? อาจเป็นได้ว่า Mikael Högströmตอบคำถามนี้ไปแล้ว แต่ฉันได้ลองก่อนที่จะโพสต์คำถามนี้และผลลัพธ์ของฉันก็คือหลุมกลายเป็นรูปสามเหลี่ยมและดูไม่สมจริงมาก
Berry

ในการตัดทอนความลึกของ "หลุมสแปลช" คุณสามารถปิดแอมพลิจูดได้สูงสุดของคลื่นนั่นคือจุดที่อนุญาตให้หลงทางจากจุดเริ่มต้นไปไกลแค่ไหน
Anko

3
BTW สำหรับทุกคนที่สนใจ: แทนที่จะห่อด้านข้างของน้ำฉันเลือกที่จะใช้พื้นฐานเพื่อทำให้ปกติด้านข้าง มิฉะนั้นถ้าคุณสร้างสาดที่ด้านขวาของน้ำก็จะสร้างคลื่นที่ด้านซ้ายของน้ำซึ่งฉันพบว่าไม่สมจริง นอกจากนี้เนื่องจากฉันไม่ได้พันคลื่นคลื่นพื้นหลังก็จะแบนอย่างรวดเร็ว ดังนั้นฉันเลือกที่จะทำให้เอฟเฟ็กต์กราฟิกเหล่านั้นเท่านั้นเช่น Mikael Högströmกล่าวเพื่อไม่ให้คลื่นพื้นหลังรวมอยู่ในการคำนวณความเร็วและความเร่ง
Berry

1
แค่อยากให้คุณรู้ เราได้พูดคุยเกี่ยวกับการตัดทอน "สแปลชหลุม" ด้วยคำสั่ง if ตอนแรกฉันลังเลที่จะทำเช่นนั้น แต่ตอนนี้ฉันสังเกตเห็นว่ามันใช้งานได้จริงอย่างสมบูรณ์แบบเนื่องจากคลื่นพื้นหลังจะป้องกันไม่ให้พื้นผิวเรียบ
Berry

4
ฉันแปลงรหัสคลื่นนี้เป็น JavaScript และวางบน jsfiddle ที่นี่: jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick

11

สำหรับการแก้ปัญหา (การพูดทางคณิตศาสตร์คุณสามารถแก้ปัญหาด้วยการแก้สมการเชิงอนุพันธ์ แต่ฉันแน่ใจว่าพวกเขาไม่ได้ทำอย่างนั้น) ในการสร้างคลื่นที่คุณมีความเป็นไปได้ 3 แบบ (ขึ้นอยู่กับว่าจะได้รายละเอียดอย่างไร):

  1. คำนวณคลื่นด้วยฟังก์ชันตรีโกณมิติ (ง่ายที่สุดและเร็วที่สุด)
  2. ทำเหมือนที่ Anko เสนอ
  3. แก้สมการเชิงอนุพันธ์
  4. ใช้การค้นหาพื้นผิว

โซลูชันที่ 1

ง่ายมากสำหรับแต่ละคลื่นเราคำนวณระยะทาง (สัมบูรณ์) จากแต่ละจุดของพื้นผิวไปยังแหล่งกำเนิดและเราคำนวณ 'สูง' ด้วยสูตร

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

ที่ไหน

  • dist คือระยะทางของเรา
  • FactorA คือค่าซึ่งหมายถึงความเร็วที่คลื่นจะหนาแน่น
  • เฟสคือเฟสของคลื่นเราต้องเพิ่มขึ้นตามเวลาเพื่อให้ได้คลื่นแบบเคลื่อนไหว

โปรดทราบว่าเราสามารถเพิ่มคำหลาย ๆ คำพร้อมกันได้ตามต้องการ (หลักการซ้อนทับ)

มือโปร

  • มันเร็วมากในการคำนวณ
  • ใช้งานง่าย

ในทางตรงกันข้าม

  • สำหรับการสะท้อน (อย่างง่าย) บนพื้นผิว 1d เราจำเป็นต้องสร้างแหล่งกำเนิดคลื่น "ผี" เพื่อจำลองการสะท้อนนี่มีความซับซ้อนมากขึ้นที่พื้นผิว 2 มิติและเป็นหนึ่งในข้อ จำกัด ของวิธีการง่ายๆนี้

โซลูชันที่ 2

มือโปร

  • มันง่ายเกินไป
  • ช่วยให้สามารถคำนวณการสะท้อนกลับได้ง่าย
  • มันสามารถขยายไปยังพื้นที่ 2d หรือ 3d ได้อย่างง่ายดาย relativly

ในทางตรงกันข้าม

  • สามารถรับความไม่แน่นอนเชิงตัวเลขหากค่าการถ่ายโอนข้อมูลสูงเกินไป
  • ต้องการพลังการคำนวณมากกว่าโซลูชัน1 (แต่ไม่มากเหมือนโซลูชัน3 )

โซลูชัน 3

ตอนนี้ฉันชนกำแพงนี่เป็นทางออกที่ซับซ้อนที่สุด

ฉันไม่ได้ใช้อันนี้ แต่มันเป็นไปได้ที่จะแก้ปัญหาสัตว์ประหลาดเหล่านี้

ที่นี่คุณสามารถหางานนำเสนอเกี่ยวกับคณิตศาสตร์ของมันมันไม่ง่ายและยังมีสมการเชิงอนุพันธ์สำหรับคลื่นชนิดต่าง ๆ

นี่คือรายการที่ไม่สมบูรณ์พร้อมสมการเชิงอนุพันธ์เพื่อแก้ไขกรณีพิเศษเพิ่มเติม (Solitons, Peakons, ... )

มือโปร

  • คลื่นที่สมจริง

ในทางตรงกันข้าม

  • สำหรับเกมส่วนใหญ่ไม่คุ้มค่ากับความพยายาม
  • ต้องการเวลาในการคำนวณมากที่สุด

โซลูชันที่ 4

ซับซ้อนกว่าโซลูชัน 1 เล็กน้อย แต่ซับซ้อนกว่าโซลูชัน 3 เล็กน้อย

เราใช้พื้นผิว precalculated และผสมผสานเข้าด้วยกันหลังจากนั้นเราใช้การแมปราง (อันที่จริงวิธีการสำหรับคลื่น 2d แต่หลักการยังสามารถทำงานได้สำหรับคลื่น 1d)

เกม sturmovik ใช้วิธีนี้ แต่ฉันไม่พบลิงค์ไปยังบทความเกี่ยวกับมัน

มือโปร

  • มันง่ายกว่า 3
  • มันจะได้ผลลัพธ์ที่ดูดี (สำหรับ 2d)
  • มันดูสมจริงถ้าศิลปินเก่งงานยอดเยี่ยม

ในทางตรงกันข้าม

  • ยากที่จะเคลื่อนไหว
  • รูปแบบซ้ำ ๆ สามารถมองเห็นได้บนขอบฟ้า

6

ในการเพิ่มคลื่นคงที่ให้เพิ่มคลื่นไซน์สองสามอันหลังจากที่คุณคำนวณพลวัต เพื่อความเรียบง่ายฉันจะทำให้การกระจัดนี้เป็นเอฟเฟกต์กราฟิกเท่านั้นและไม่ปล่อยให้มันส่งผลกระทบต่อการเปลี่ยนแปลง แต่คุณสามารถลองทั้งทางเลือกและดูว่าอะไรดีที่สุด

หากต้องการทำให้ "splashhole" เล็กลงฉันแนะนำให้เปลี่ยนวิธีการ Splash (ดัชนี int, ความเร็วลอย) เพื่อให้มันส่งผลโดยตรงต่อดัชนีเท่านั้น พลังงาน". จำนวนจุดยอดที่ได้รับผลกระทบอาจขึ้นอยู่กับความกว้างของวัตถุ คุณอาจต้องปรับแต่งเอฟเฟกต์ให้มากก่อนที่จะได้ผลลัพธ์ที่สมบูรณ์แบบ

การทำพื้นผิวส่วนลึกของน้ำคุณสามารถทำตามที่อธิบายไว้ในบทความและทำส่วนลึก "สีน้ำเงินมากขึ้น" หรือคุณสามารถแก้ไขระหว่างพื้นผิวสองขึ้นอยู่กับความลึกของน้ำ


ขอบคุณสำหรับการตอบกลับของคุณ. จริง ๆ แล้วฉันหวังว่าจะมีคนอื่นลองใช้สิ่งนี้ต่อหน้าฉันและจะให้คำตอบที่เจาะจงยิ่งขึ้น แต่เคล็ดลับของคุณได้รับการชื่นชมอย่างมากเช่นกัน จริง ๆ แล้วฉันยุ่งมาก แต่ทันทีที่ฉันมีเวลาฉันจะลองสิ่งที่คุณพูดถึงและเล่นกับรหัสอีกต่อไป
Berry

1
ตกลง แต่ถ้ามีบางอย่างที่คุณต้องการความช่วยเหลือเพียงแค่พูดอย่างนั้นและฉันจะดูว่าฉันสามารถทำอย่างละเอียดมากขึ้นเล็กน้อย
Mikael Högström

ขอบคุณมาก! เป็นเพียงว่าฉันไม่ได้จับเวลาคำถามของฉันดีมากเนื่องจากฉันมีสัปดาห์สอบสัปดาห์หน้า หลังจากที่ฉันทำข้อสอบเสร็จฉันจะใช้เวลามากขึ้นในรหัสและอาจกลับมาพร้อมคำถามที่เฉพาะเจาะจงมากขึ้น
Berry
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.