반응형

자연스러운 2D 물결 파동을 만들어 보자.

 

우선 저항이 없는 완전 탄성 스프링 운동을 시뮬레이션 해 보자.

 

Node2D, Sprite2D를 이용해 스프링으로 사용할 씬을 구성하고 스크립트를 추가한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
    public float k;
    
    public override void _Ready()
    {
        target_height = Position.Y + 200;
        // 목표 위치
        k = 0.0015f;
        // 스프링 탄성 계수. 탄성 계수가 클수록 빠르게
        // 움직인다. 운동 거리와는 상관 없다.
    }
 
    public override void _PhysicsProcess(double delta)
    {
        water_update(k);        
    }
 
    public void water_update(float spring_constant)
    {
        height = Position.Y;
        float x = height - target_height;
        // 변위. 스프링 운동시 실제 이동 거리는 -x ~ +x 다.
 
        force = -spring_constant * x;
        // 훅의 법칙(Hooke's Law) F = -kx
        // 스프링의 복원력과 변형량 사이에는 비례관계가 성립한다.
        // F = ma, a = -kx/m
        // -k/m = c(상수), 가속도 a는 x에 비례한다.
 
        velocity += force;
        // 속도에 힘을 누적시킨다.
        // 이동거리(s) = 속도(v) X 시간(t)
        // 매 순간의 속도를 모두 더하면 총 이동거리를 알 수 있다.
        // 이 예에서는 아래 Position 값이 매 순간의 총 이동거리다.
 
        Position += new Vector2(0, velocity);
        // 스프링은 목표 높이(Position.Y + 200)를 중심으로 변위 x만큼
        // (-x ~ +x) 왕복 운동한다. 목표 높이가 스프링을 늘리거나
        // 압축하지 않은 평상시 위치다.
        // 예) 초기 위치(Position)의 Y값이 100 이면, 300(100+200)이
        // 평상시 위치고 100(300-200)에서 500(300+200)사이의 왕복
        // 운동을 한다.
    }
}
 

 

 

추가한 스크립트에 스프링 운동을 표현할 코드를 작성한다.

 

스프링을 사용할 씬을 구성한다. Node2D에 위에서 만든 스프링 씬을 추가한다.

 

프로그램을 실행하면 스프링 운동을 한다.

 

 

비탄성 스프링 운동을 시뮬레이션 해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
    public float k;
    public float d;
 
    public override void _Ready()
    {
        target_height = Position.Y + 200;
        // 목표 위치
        k = 0.0015f;
        // 스프링 탄성 계수. 탄성 계수가 클수록 빠르게
        // 움직인다. 운동 거리와는 상관 없다.
        d = 0.005f;
        // 저항 계수
    }
 
    public override void _PhysicsProcess(double delta)
    {
        water_update(k, d);        
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        // 변위. 스프링 운동시 실제 이동 거리는 -x ~ +x 다.
 
        float resist = -dampening * velocity;
        // 속도에 비례하는 저항값을 계산한다.
 
        force = -spring_constant * x + resist;
        // 훅의 법칙(Hooke's Law) F = -kx
        // 스프링의 복원력과 변형량 사이에는 비례관계가 성립한다.
        // F = ma, a = -kx/m
        // -k/m = c(상수), 가속도 a는 x에 비례한다.
        // 그리고 저항을 더해주면 힘이 점점 감소한다.
 
        velocity += force;
        // 속도에 힘을 누적시킨다.
        // 이동거리(s) = 속도(v) X 시간(t)
        // 매 순간의 속도를 모두 더하면 총 이동거리를 알 수 있다.
        // 이 예에서는 아래 Position 값이 매 순간의 총 이동거리다.
 
        Position += new Vector2(0, velocity);
        // 스프링은 목표 높이(Position.Y + 200)를 중심으로 변위 x만큼
        // (-x ~ +x) 왕복 운동한다. 목표 높이가 스프링을 늘리거나
        // 압축하지 않은 평상시 위치다.
        // 예) 초기 위치(Position)의 Y값이 100 이면, 300(100+200)이
        // 평상시 위치고 100(300-200)에서 500(300+200)사이의 왕복
        // 운동을 한다.
    }
}
 

 

 

저항이 추가된 코드를 작성한다.

 

저항이 적용돼 움직임이 점점 감소한다.

 

스프링을 여러개 만들고 서로 영향을 주고 받게 해 보자.

 

스프링 스프라이트를 적당한 크기로 조절하고 스크립트(spring.cs)는 아래와 같이 수정한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public void Initialize()
    {
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        // 매번 높이값을 업데이트 한다.
 
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
        
        velocity += force;
        Position += new Vector2(0, velocity);
    }
}
 

 

 

 

새로운 씬을 만들고 스프링을 추가한다. 스크립트(WaterBody.cs)를 추가하고 아래와 같이 작성한다.

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    // 파동이 옆으로 퍼지는 정도
    public int passes;
    // 파동의 영향력
    public List<spring> springs;
    // 파동을 시뮬레이션 할 스프링 리스트
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        springs= new List<spring>();
 
        foreach(spring item in GetChildren())
        {
            springs.Add(item);
            item.Initialize();
        }
        
        //splash(2, 10);
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(210);
        // 엔터키를 누르면 물결 효과가 진행된다.
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        // 각 스프링의 왼쪽 스프링과의 높이 차이 리스트
        List<float> right_deltas = new List<float>();
        // 각 스프링의 오른쪽 스프링과의 높이 차이 리스트
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
            // 모든 차이를 0으로 초기화.
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                    // 높이 차이와 파동 전달 정도(spread)를 곱하여 왼쪽 스프링 속도에 누적시킨다.
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                    // 높이 차이와 파동 전달 정도(spread)를 곱하여 오른쪽 스프링 속도에 누적시킨다.
                }
            }
        }        
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
        // index번 스프링에 속도를 지정하여 물에 돌을 던진 것 같은 효과를 준다.
    }
}
 

 

 

메인 씬에 WaterBody씬을 추가한다.

 

실행하고 엔터키를 누르면 가운데 스프링의 움직임이 다른 스프링에 전달된다.

 

 

이제 스프링을 스크립트에서 인스턴스화 하고 초기화 되도록 해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Godot;
 
public partial class spring : Node2D
{
    public float velocity;
    public float force;
    public float height;
    public float target_height;
 
    public void Initialize(int x_position)
    {
        Position = new Vector2(x_position, 100);
        // 인수로 받은 x 좌표와 y=100으로 위치를 정한다.
        height = Position.Y;
        target_height = Position.Y;
        velocity = 0;        
    }
 
    public void water_update(float spring_constant, float dampening)
    {
        height = Position.Y;
        float x = height - target_height;
        float resist = -dampening * velocity;
        force = -spring_constant * x + resist;
        
        velocity += force;
        Position += new Vector2(0, velocity);
    }
}
 

 

 

spring.cs 는 위와 같이 변경한다.

 

WaterBody 씬은 스프링 노드를 모두 삭제한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using Godot;
using System.Collections.Generic;
 
public partial class WaterBody : Node2D
{
    public float k;
    public float d;
    public float spread;
    public int passes;
    public List<spring> springs;
    public int spring_number;
    public int distance_between_springs;
    public PackedScene scene;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        springs= new List<spring>();
        spring_number = 10;
        // 스프링 갯수
        distance_between_springs = 100;
        // 스프링 사이 간격
        scene = ResourceLoader.Load<PackedScene>("res://spring.tscn");
        // 스프링 씬을 로드한다.
        for (int i = 0; i < spring_number; i++)
        {            
            Node2D s = scene.Instantiate<Node2D>();            
            AddChild(s);            
            springs.Add(s as spring);
            // 로드한 스프링 씬을 인스턴스화 하고 자식노드 및 리스트에 추가한다.
 
            int x_position = distance_between_springs * i + 100;
            (s as spring).Initialize(x_position);
            // 인스턴스된 스프링에 적당한 위치를 인수로 주고 초기화한다.
        }
    }
 
    public override void _PhysicsProcess(double delta)
    {
        if (Input.IsActionJustPressed("ui_accept"))
            splash(520);
 
        foreach (spring item in springs)
        {
            item.water_update(k, d);
        }
 
        List<float> left_deltas = new List<float>();
        List<float> right_deltas = new List<float>();
 
        for(int i = 0; i < springs.Count; i++)
        {
            left_deltas.Add(0.0f);
            right_deltas.Add(0.0f);
        }
 
        for (int p = 0; p < passes; p++)
        {
            for (int i = 0; i < springs.Count; i++)
            {
                if (i > 0)
                {
                    left_deltas[i] = spread * (springs[i].height - springs[i-1].height);
                    springs[i-1].velocity += left_deltas[i];
                }
                if (i < springs.Count - 1)
                {
                    right_deltas[i] = spread * (springs[i].height - springs[i+1].height);
                    springs[i+1].velocity += right_deltas[i];
                }
            }
        }        
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
}
 

 

 

WaterBody.cs 는 위와 같이 변경한다.

 

Main에서 플레이 버튼을 클릭한다.

 

 

엔터키를 누르면 6번째 스프링의 움직임이 양 옆으로 전달된다.

 

2023.10.18 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 2. 구현

 

※ 참고

Make a Splash With Dynamic 2D Water Effects

 

 

반응형
Posted by J-sean
: