如何使用动态波创建2D水?


81

新版《超级马里奥兄弟》拥有非常酷的2D水,我想学习如何创建。

这是显示它的视频。说明部分:

新的《超级马里奥兄弟》水效

碰到水的东西会产生波浪。还有恒定的“背景”波。当摄像机不移动时,您可以在视频中00:50之后的恒定波中看到效果很好。

我假定飞溅效果与本教程的第一部分相同。

但是,在NSMB中,水在表面上也具有恒定的波浪,并且飞溅看起来非常不同。另一个区别是,在本教程中,如果创建了一个飞溅,它首先会在飞溅的起点在水中创建一个深的“洞”。在新的超级马里奥兄弟中,这个孔不存在或较小。我指的是玩家从水中跳入跳出时所产生的飞溅。

如何创建不断波动和飞溅的水面?

我在XNA中编程。我自己尝试过,但是我无法真正使背景正弦波与动态波一起很好地工作。

我不是在问“ 新超级马里奥兄弟 ”的开发者是如何做到这一点的,只是对如何重现这种效果感兴趣。

Answers:


147

我尝试过这个。

飞溅(弹簧)

正如该教程所提到的,水的表面就像一根导线:如果您拉动导线的某个点,则该点旁边的点也会被下拉。所有点也都吸引回基线。

基本上,许多彼此相邻的垂直弹簧也相互拉动。

我使用LÖVE在Lua中绘制了草图,并得到了:

飞溅的动画

看起来很合理。哦,胡克,你这个英俊的天才。

如果你想用它玩,这里是一个JavaScript的端口的礼貌菲尔!我的代码在此答案的结尾。

背景波(堆积的正弦波)

自然背景波在我看来就像一堆正弦波(振幅,相位和波长不同)加在一起。这是我写时的样子:

正弦干扰产生的背景波

干扰模式看起来很合理。

现在都在一起了

因此,将飞溅波和背景波加在一起是一件非常简单的事情:

背景波,溅起

发生飞溅时,您会看到灰色的小圆圈,显示原始背景波的位置。

看起来很像您链接的视频,所以我认为这是一个成功的实验。

这是我的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

好答案!非常感谢你。而且,感谢您对我的问题的修改,我可以看到这更加清楚。gif图像也非常有帮助。您是否也偶然知道一种方法来防止在创建飞溅时出现大洞?可能是MikaelHögström已经回答了这个权利,但我甚至在发布此问题之前就已经尝试过,结果是该孔变成了三角形,看起来非常不现实。
贝里

要截断“飞溅孔”的深度,可以设置波浪的最大振幅,即允许任何点偏离基线多远。
Anko 2012年

3
顺便说一句,对任何感兴趣的人:我没有使用水的侧面,而是选择使用基线对侧面进行标准化。否则,如果您在水的右侧创建了一个飞溅,它也会在水的左侧创建波浪,我发现这是不现实的。另外,由于我没有包裹波浪,因此背景波浪会很快消失。因此,我选择仅使这些图形效果生效,就像MikaelHögström所说的那样,这样背景波就不会包含在速度和加速度的计算中。
贝里

1
只是想让你知道。我们已经讨论过用if语句截断“ splash-hole”。起初我不愿意这样做。但是现在我注意到它实际上可以完美工作,因为背景波将阻止表面变平。
贝里

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是一个值,表示波应达到的速度/密集程度
  • 相位是波形的相位,我们需要随着时间的推移将其递增以获取动画波形

请注意,我们可以根据需要将任意多个项加在一起(叠加原理)。

专业版

  • 它的计算速度非常快
  • 易于实施

相反

  • 对于一维曲面上的(简单)反射,我们需要创建“重影”波源来模拟反射,这在二维曲面上更为复杂,这是此简单方法的局限性之一

解决方案2

专业版

  • 它也很简单
  • 它可以轻松计算反射
  • 它可以相对轻松地扩展到2d或3d空间

相反

  • 如果倾销值太高,可能会导致数值不稳定
  • 比解决方案1需要更多的计算能力(但不像解决方案3那样多)

解决方案3

现在我遇到了困难,这是最复杂的解决方案。

我没有实现这一目标,但是有可能解决这些怪兽。

在这里,您可以找到有关它的数学的演示,它并不简单,并且还存在针对不同种类的波动的微分方程。

是一个不完整的列表,其中包含一些微分方程来解决更多特殊情况(孤子,Peakons等)

专业版

  • 现实波

相反

  • 对于大多数不值得付出的游戏
  • 需要最长时间

解决方案4

比解决方案1稍微复杂一点,解决方案3则没有那么复杂。

我们使用预先计算的纹理并将其融合在一起,然后使用位移映射(实际上是一种用于2d波浪的方法,但原理也可以用于1d波浪)

sturmovik游戏使用了这种方法,但是我找不到有关此文章的链接。

专业版

  • 比3更简单
  • 它获得漂亮的结果(2d)
  • 如果艺术家做得好,这看起来很现实

相反

  • 难以动画
  • 重复的图案可能在地平线上变得可见

6

要添加恒定波,请在计算完动态后添加几个正弦波。为简单起见,我将这种位移仅作为图形效果,而不是让它本身影响动力学,但是您可以尝试两种选择,然后看看哪种效果最好。

为了减小“ splashhole”,我建议更改Splash(int index,float speed)方法,以便它不仅直接影响索引,还影响一些接近的顶点,以便扩展效果,但仍然具有相同的效果。能源”。受影响的顶点数量可能取决于对象的宽度。在获得完美结果之前,您可能需要大量调整效果。

要对水的较深部分进行纹理化处理,您可以按照本文中的描述进行操作,而只是使较深的部分“更蓝”,或者可以根据水的深度在两个纹理之间进行插值。


谢谢您的回复。我实际上希望其他人在我之前尝试过此方法,并且可以给我更具体的答案。但是您的提示也非常感谢。我实际上很忙,但是只要有时间,我就会尝试您提到的内容并使用更多代码。
贝里

1
好的,但是如果您需要帮助的话,请这样说,我是否能做得更详细一点。
MikaelHögström'12

非常感谢你!只是由于我下周有考试周,所以我对问题的时机还不太好。完成考试后,我将最终花更多的时间在代码上,并且很可能会返回更具体的问题。
贝里2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.