ダイナミックウェーブで2D水を作成するにはどうすればよいですか?


81

新しいスーパーマリオブラザーズには本当にクールな2D水がありますので、作成方法を学びたいです。

これを示すビデオがあります。例示的な部分:

新しいスーパーマリオブラザーズの水の効果

水に当たるものは波を作ります。また、一定の「背景」波もあります。カメラが動いていないビデオの00:50直後の一定の波をよく見ることができます。

このチュートリアルの最初の部分のように、スプラッシュ効果が機能すると仮定します。

ただし、NSMBでは、水も表面に一定の波を持ち、水しぶきは非常に異なって見えます。もう1つの違いは、チュートリアルでは、スプラッシュを作成すると、最初にスプラッシュの起点で水に深い「穴」が作成されることです。新しいスーパーマリオブラザーズでは、この穴は存在しないか、はるかに小さくなっています。私は、プレイヤーが水に飛び込んだり飛び出したときに生じる水しぶきについて言及しています。

一定の波と水しぶきで水面を作成するにはどうすればよいですか?

XNAでプログラミングしています。私は自分でこれを試しましたが、背景の正弦波を動的な波とうまく連携させることはできませんでした。

私はNew Super Mario Brosの開発者がこれを正確にどのように行ったかを尋ねているのではなく、そのような効果を再現する方法に興味があるだけです。

回答:


147

私はそれを試してみました。

スプラッシュ(スプリング)

そのチュートリアルで言及しているように、水面はワイヤーのようなものです。ワイヤーのあるポイントを引っ張ると、そのポイントの隣のポイントも引き下げられます。すべてのポイントもベースラインに引き付けられます。

基本的に、互いに引っ張るのは、隣り合ったたくさんの垂直スプリングです。

LÖVEを使用してLuaでそれをスケッチし、これを得ました:

スプラッシュのアニメーション

もっともらしい。ああフック、あなたはハンサムな天才。

試してみたい場合は、Philのご厚意による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が既にこの権利に答えていたのかもしれませんが、この質問を投稿する前から試してみたところ、穴が三角形になり、非常に非現実的に見えました。
ベリー

「スプラッシュホール」の深さを切り詰めるために、波の最大振幅、つまり、ベースラインからの任意のポイントの逸脱を制限できます。
アンコ

3
ところで、興味のある人なら誰でも:水の側面をラップする代わりに、ベースラインを使用して側面を正規化することにしました。それ以外の場合、水の右側にスプラッシュを作成すると、水の左側にも波が作成されますが、これは非現実的です。また、私は波をラップしなかったので、背景波は非常に速く平らになります。そのため、速度と加速度の計算に背景波が含まれないように、MikaelHögströmが言ったように、それらをグラフィック効果のみにすることを選択しました。
ベリー

1
あなたに知らせたかっただけです。if文で「スプラッシュホール」を切り捨てることについて説明しました。最初はそうすることに消極的でした。しかし、今では、背景波が表面の平坦化を妨げるため、実際に完全に機能することに気付きました。
ベリー

4
このwaveコードをJavaScriptに変換し、jsfiddleに配置しました:jsfiddle.net/phil_mcc/sXmpD/8
Phil McCullick

11

波を作成する解決策(数学的に言えば、微分方程式の解法で問題を解決できますが、そうしないと確信しています)には、3つの可能性があります(取得する方法に応じて):

  1. 三角関数を使用して波を計算します(最も単純で最速)
  2. アンコが提案したようにやってください
  3. 微分方程式を解く
  4. テクスチャルックアップを使用する

解決策1

本当に簡単です。各波について、表面の各点からソースまでの(絶対)距離を計算し、次の式で「高さ」を計算します。

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

どこ

  • distは私たちの距離です
  • FactorAは、波の速さ/密度を示す値です
  • フェーズは波のフェーズです。アニメーション化された波を取得するには、時間とともに増分する必要があります

好きなだけ用語を追加できることに注意してください(重ね合わせの原理)。

プロ

  • 計算が本当に速い
  • 実装が簡単です

コントラ

  • 1d表面での(単純な)反射の場合、反射をシミュレートするために「ゴースト」波源を作成する必要があります。これは2d表面でより複雑であり、この単純なアプローチの制限の1つです。

解決策2

プロ

  • そのシンプルさも
  • 反射を簡単に計算できます
  • 比較的簡単に2Dまたは3D空間に拡張できます

コントラ

  • ダンプ値が高すぎる場合、数値的に不安定になる可能性があります
  • ソリューション1よりも多くの計算能力が必要です(ただし、ソリューション3とは異なります)

解決策3

今、私は難しい壁にぶつかった、これは最も複雑なソリューションです。

私はこれを実装しませんでしたが、これらのモンスターを解決することは可能です。

ここでは、その数学についてのプレゼンテーションを見つけることができますが、単純ではなく、さまざまな種類の波の微分方程式も存在します。

以下は、より特殊なケース(ソリトン、ピークン、...)を解決するためのいくつかの微分方程式を含む完全はないリストです。

プロ

  • リアルな波

コントラ

  • 努力する価値がないほとんどのゲーム
  • 計算時間が最も長い

解決策4

ソリューション1より少し複雑です、ソリューション3ほど複雑ではありません。

事前に計算されたテクスチャを使用し、それらをブレンドします。その後、ディスプレイスメントマッピングを使用します(実際には2D波の方法ですが、原理は1D波にも機能します)

ゲームsturmovikはこのアプローチを使用しましたが、それに関する記事へのリンクが見つかりません。

プロ

  • 3より簡単です
  • 見栄えの良い結果が得られます(2日間)
  • アーティストが素晴らしい仕事をすれば、現実的に見える

コントラ

  • アニメーション化が難しい
  • 繰り返しパターンが地平線上に見える可能性があります

6

一定の波を追加するには、ダイナミクスを計算した後にいくつかの正弦波を追加します。簡単にするために、このディスプレイスメントをグラフィック効果のみにし、ダイナミクス自体には影響を与えませんが、両方の代替案を試して、どちらが最適かを確認できます。

「スプラッシュホール」を小さくするには、メソッドSplash(int index、float speed)を変更することをお勧めします。これにより、インデックスだけでなく、近くの頂点のいくつかにも直接影響を与え、効果を広げながらも同じ「エネルギー」。影響を受ける頂点の数は、オブジェクトの幅によって異なります。おそらく、完璧な結果を得る前に、多くのエフェクトを微調整する必要があります。

水のより深い部分にテクスチャを付けるには、記事で説明されているようにして、より深い部分を「より青く」するか、水深に応じて2つのテクスチャ間を補間します。


お返事ありがとうございます。私は実際に他の誰かが私の前にこれを試し、より具体的な答えをくれることを望んでいました。しかし、あなたのヒントも非常に高く評価されています。私は実際に非常に忙しいですが、時間があるとすぐに、あなたが言及したことを試し、コードでさらに遊んでいきます。
ベリー

1
わかりましたが、何か助けが必要な場合は、その旨を伝えてください。もう少し詳しく説明できるかどうかを確認します。
ミカエルヘグストローム

どうもありがとうございました!来週は試験の週があるので、質問のタイミングがよくなかっただけです。試験を終えた後、私は間違いなくコードにより多くの時間を費やし、ほとんどの場合、より具体的な質問で戻ってきます。
ベリー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.