동적 파도로 2D 물을 만들려면 어떻게해야합니까?


81

새로운 슈퍼 마리오 브라더스 는 정말 멋진 2D 물을 만드는 법을 배우고 싶습니다.

여기에 비디오 가 있습니다. 예시적인 부분 :

새로운 슈퍼 마리오 브라더스 물 효과

물에 부딪 치는 것들은 파도를 만듭니다. 일정한 "배경"파도 있습니다. 카메라가 움직이지 않을 때 비디오에서 00:50 직후의 일정한 파도를 잘 볼 수 있습니다.

스플래시 효과 가이 튜토리얼 의 첫 부분에서와 같이 작동한다고 가정합니다 .

그러나 NSMB 에서 물은 표면에 일정한 파도를 가지고 있으며 스플래시는 매우 다르게 보입니다. 또 다른 차이점은 자습서에서 스플래시를 만들면 먼저 스플래시의 원점에서 물에 깊은 "구멍"을 만듭니다. 새로운 슈퍼 마리오 형제에서는이 구멍이 없거나 훨씬 작습니다. 나는 물 속으로 뛰어 들거나 뛰어 내릴 때 플레이어가 생성하는 스플래시를 언급하고 있습니다.

일정한 파도와 물이 튀는 수면을 어떻게 만들 수 있습니까?

XNA로 프로그래밍 중입니다. 나는 이것을 직접 시도했지만 배경 사인파가 동적 파도와 잘 작동 하지는 못했습니다 .

뉴 슈퍼 마리오 브라더스 의 개발자들이 어떻게 정확하게이 작업을 했는지 묻지 않고 그대로 효과를 재현하는 방법에 관심이 있습니다.

답변:


147

나는 그것을 시도했다.

밝아진 (봄)

이 튜토리얼에서 언급 했듯이 수면은 와이어와 같습니다. 와이어의 특정 지점을 당기면 해당 지점 옆의 지점도 아래로 당겨집니다. 모든 포인트는 다시 기준으로 끌어 당겨집니다.

기본적으로 서로 옆에 많은 수직 스프링이 있습니다.

LÖVE를 사용하여 Lua에서 스케치하고 이것을 얻었습니다.

스플래시의 애니메이션

그럴듯 해 보인다. 오 , 잘 생긴 천재.

당신이 그것으로 재생하려면, 여기입니다 자바 스크립트 포트 의 호의 ! 내 코드는이 답변의 끝에 있습니다.

배경 파 (적층 사인)

자연의 배경 파는 서로 다른 진폭, 위상 및 파장을 가진 여러 사인파처럼 보입니다. 내가 썼을 때의 모습은 다음과 같습니다.

사인 간섭에 의해 생성 된 배경 파

간섭 패턴은 그럴듯 해 보입니다.

이제 모두 함께

따라서 스플래시 파도와 배경 파도를 합치는 것은 매우 간단한 문제입니다.

밝아진와 배경 파도

스플래시가 발생하면 원래 배경 파의 위치를 ​​나타내는 작은 회색 원이 표시됩니다.

연결 한 동영상 과 매우 비슷해 보이 므로 성공적인 실험이라고 생각합니다.

여기 내 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는 매우 유용합니다. 스플래시를 만들 때 나오는 큰 구멍을 막는 방법을 우연히 알고 있습니까? Mikael Högström이 이미이 답변에 대답했을 수도 있지만이 질문을 게시하기 전에 시도했지만 구멍이 삼각형 모양이되어 매우 비현실적으로 보였습니다.
Berry

"스 프레쉬 홀"의 깊이를 자르기 위해, 웨이브의 최대 진폭, 즉 어떤 점이 기준선으로부터 빗 나갈 수 있는지를 제한 할 수 있습니다.
Anko

3
관심있는 사람을위한 BTW : 물의 측면을 감싸는 대신, 기준선을 사용하여 측면을 정규화하기로했습니다. 그렇지 않으면 물 오른쪽에 스플래시를 만들면 물 왼쪽에 파도가 생겨 비현실적입니다. 또한 파도를 감싸지 않았기 때문에 배경 파가 매우 빠르게 평평해질 것입니다. 따라서 Mikael Högström이 말한 것처럼 그래픽 효과 만 선택하여 배경 파가 속도 및 가속도 계산에 포함되지 않도록 선택했습니다.
Berry

1
알려 드리고 싶었습니다. 우리는 if 문으로 "스 프레쉬 홀"을 자르는 것에 대해 이야기했습니다. 처음에는 그렇게하기를 꺼려했습니다. 그러나 이제는 배경 파가 표면이 평평하지 못하게하기 때문에 실제로 완벽하게 작동한다는 것을 알았습니다.
Berry

4
이 웨이브 코드를 JavaScript로 변환하여 여기 jsfiddle
Phil McCullick

11

파동을 생성하는 솔루션 (수학적으로 말하면 미분 방정식의 해결로 문제를 해결할 수는 있지만 그렇게하지 않는 것이 확실합니다)의 경우 3 가지 가능성이 있습니다 (세부 사항에 따라 다름).

  1. 삼각 함수를 사용하여 파도를 계산하십시오 (가장 간단하고 빠름).
  2. Anko가 제안한 것처럼 수행
  3. 미분 방정식 풀기
  4. 텍스처 조회 사용

해결책 1

정말 간단합니다. 각 파도에 대해 표면의 각 점에서 소스까지의 (절대) 거리를 계산하고 공식을 사용하여 '높이'를 계산합니다.

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

어디

  • 거리는 우리의 거리입니다
  • FactorA는 파도의 속도 / 밀도를 의미하는 값입니다.
  • 페이즈는 웨이브의 페이즈입니다. 애니메이션 웨이브를 얻으려면 시간에 따라 증가시켜야합니다

원하는만큼 많은 용어를 추가 할 수 있습니다 (중첩 원리).

찬성

  • 계산이 정말 빠릅니다
  • 구현하기 쉽다

콘트라

  • 1d 표면에서의 (간단한) 반사의 경우, 반사를 시뮬레이션하기 위해 "고스트"파 소스를 생성해야합니다. 이는 2d 표면에서 더 복잡하며이 간단한 접근 방식의 한계 중 하나입니다

해결책 2

찬성

  • 그것도 간단
  • 반사를 쉽게 계산할 수 있습니다
  • 2D 또는 3D 공간으로 비교적 쉽게 확장 가능

콘트라

  • 덤핑 값이 너무 높으면 수치 적으로 불안정해질 수 있습니다
  • 솔루션 1 보다 더 많은 계산 능력이 필요합니다 (그러나 솔루션 3 만큼 그리 많지는 않습니다 )

해결책 3

이제 나는 딱딱한 벽에 부딪쳤다. 이것이 가장 복잡한 해결책이다.

나는 이것을 구현하지 않았지만이 괴물을 해결할 수 있습니다.

여기서 당신은 그것의 수학에 대한 프리젠 테이션을 찾을 수 있습니다. 간단하지는 않으며 다른 종류의 파동에 대한 미분 방정식도 있습니다.

다음 은 좀 더 특별한 경우를 해결하기위한 몇 가지 미분 방정식이있는 전체 목록이 아닙니다 (Solitons, Peakons 등).

찬성

  • 현실적인 파도

콘트라

  • 노력할 가치가없는 대부분의 게임
  • 가장 많은 계산 시간이 필요합니다

해결책 4

솔루션 1보다 약간 복잡 하지만 솔루션 3은 그렇게 복잡하지 않습니다.

우리는 미리 계산 된 텍스처를 사용하고 그것들을 함께 섞습니다. 그 후에 변위 매핑을 사용합니다 (실제로는 2D 파에 대한 방법이지만 원리는 1D 파에 대해서도 가능합니다)

게임 sturmovik 은이 접근법을 사용했지만 관련 기사에 대한 링크를 찾지 못했습니다.

찬성

  • 3보다 더 간단합니다
  • 좋은 결과를 얻습니다 (2d)
  • 예술가들이 좋은 일을하면 현실적으로 보일 수 있습니다

콘트라

  • 애니메이션하기 어려운
  • 수평선에서 반복되는 패턴이 보일 수 있음

6

일정한 파도를 추가하려면 역학을 계산 한 후 몇 개의 사인파를 추가하십시오. 간단하게하기 위해이 변위를 그래픽 효과로만 만들고 역학 자체에 영향을 미치지 않도록했지만 두 가지 대안을 모두 시도하고 어느 것이 가장 적합한 지 확인할 수 있습니다.

"스플래시 홀"을 더 작게 만들기 위해 Splash (int index, float speed) 메소드를 변경하여 인덱스뿐만 아니라 가까운 정점에 직접 영향을 미치면서 효과를 확산 시키지만 여전히 동일한 " 에너지". 영향을받는 정점의 수는 객체의 넓이에 따라 달라질 수 있습니다. 완벽한 결과를 얻기 전에 효과를 많이 조정해야 할 수도 있습니다.

물의 더 깊은 부분을 텍스처링하려면 기사에 설명 된대로 수행하고 더 깊은 부분을 "파란색"으로 만들거나 물의 깊이에 따라 두 개의 텍스처 사이를 보간 할 수 있습니다.


당신의 답변에 감사드립니다. 나는 실제로 다른 사람이 내 앞에서 이것을 시도하고 더 구체적인 대답을 줄 수 있기를 바랐습니다. 그러나 귀하의 팁도 대단히 감사합니다. 나는 실제로 매우 바쁩니다. 그러나 시간을 내 자마자, 당신이 언급 한 것들을 시도하고 코드를 더 가지고 놀 것입니다.
Berry

1
하지만 도움이 필요한 특정 사항이 있으면 그렇게 말하면 좀 더 정교해질 수 있습니다.
Mikael Högström

대단히 감사합니다! 다음 주에 시험 주가 있기 때문에 질문 시간을 잘 맞추지 못한 것입니다. 시험을 마친 후에는 코드에 더 많은 시간을 할애 할 것이며,보다 구체적인 질문으로 돌아올 것입니다.
Berry
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.