반응형

마이크를 통해 입력된 음성 데이터를 스피커로 출력해 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import sounddevice as sd
import numpy as np
 
def callback(indata, outdata, frames, time, status):
    volume_norm = np.linalg.norm(indata)    
    print("Volume: " + '='*(int(volume_norm)) + ' '*(79-(int(volume_norm))) + '\r', end = '')
    
    # indata를 outdata에 넣으면 마이크로 넘어온 데이터가 스피커로 출력된다.
    outdata[:] = indata
 
try:
    with sd.Stream(callback=callback):
        input("Press Enter to quit.\n\n")
except KeyboardInterrupt:
    print("exit.")
 

 

 

코드를 실행하면 마이크에 입력된 음성이 스피커로 출력된다.

 

python-sounddevice

PyAudio Examples

 

반응형
Posted by J-sean
:
반응형

프로젝트 세팅 창을 열려고 하면 작동을 멈추는 버그가 발생할 수 있다.

 

Project Settings... 를 클릭하면 그 다음부터 아무 동작도 하지 않는다.

 

프로젝트 폴더 - .godot 폴더를 삭제한다.

 

다른 문제가 발생할지 모르니 프로젝트 폴더는 미리 백업해 둔다.

작업중인 프로젝트 폴더의 .godot 폴더를 삭제하고 다시 실행한다. 그러면 Godot 엔진이 .godot 폴더를 다시 빌드하고 문제가 해결된다.

 

반응형
Posted by J-sean
:
반응형

아이들이 컴퓨터로 영상을 보거나 게임할 때 습관적으로 소리를 너무 크게 해 놓는 경우가 많다. 소리를 줄이라고 말해도 그때 뿐, 다시 크게 해 놓곤 한다. 시스템 볼륨에 제한을 두고 일정 수준 이상으로 올라가면 자동으로 내리는 프로그램(Turn It Down)을 만들어 보자.

 

※ 참고

2021.11.21 - [C#] - C# Media Player Mp3

2022.01.06 - [C#] - C# AudioSwitcher System Audio/Sound Volume Control - 시스템 오디오/사운드 볼륨 컨트롤 1

2023.10.28 - [C#] - C# Sound Meter 사운드 미터

 

위 링크의 내용들을 참고해 코드를 작성한다.

 

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
using System;
using System.Runtime.InteropServices;
using System.Windows.Media;
 
using AudioSwitcher.AudioApi.CoreAudio;
using NAudio.CoreAudioApi;
 
namespace ConsoleApp1
{
    class Program
    {
        [DllImport("kernel32.dll")]
        static extern IntPtr GetConsoleWindow();
 
        [DllImport("user32.dll")]
        static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
 
        // Usage: filename [maxVol(0~100)] [playTime(sec)]
 
        static void Main(string[] args)
        {
            const int SW_HIDE = 0// 창 숨기기
            IntPtr handle = GetConsoleWindow();
            ShowWindow(handle, SW_HIDE);
            
            MediaPlayer MP = new MediaPlayer();
            MP.Open(new Uri("turndown.mp3", UriKind.Relative));
 
            int maxVol;
            if (args.Length > 0)
                maxVol = int.Parse(args[0]);
            else
                maxVol = 20;
 
            int playTime;
            if (MP.NaturalDuration.HasTimeSpan)
                playTime = MP.NaturalDuration.TimeSpan.Milliseconds;
            else if (args.Length > 1)
                playTime = int.Parse(args[1]) * 1000;
            else
                playTime = 6000;
                // 미디어 파일 시간 정보가 없다면 적당한 값을 넣는다.
 
            MMDeviceEnumerator enumerator = new MMDeviceEnumerator();
            MMDevice device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console);
            // MMDeviceEnumerator를 CoreAudioController 보다 먼저 초기화해야 한다.
 
            CoreAudioDevice defaultPlaybackDevice = new CoreAudioController().DefaultPlaybackDevice;
            
            // defaultPlaybackDevice.Volume: 시스템 볼륨(0~100)
            // device.AudioMeterInformation.MasterPeakValue: 시스템 오디오 값(0~1)
            while (true)
            {
                float masterPeakValue = device.AudioMeterInformation.MasterPeakValue;
 
                if (defaultPlaybackDevice.Volume > maxVol && masterPeakValue > 0.3f)
                    // 볼륨이 maxVol 보다 크고 오디오 값이 0.3보다 크다면..
                {                    
                    MP.Play();
                    System.Threading.Thread.Sleep(playTime); // 메세지 재생 대기
                    MP.Stop();
 
                    defaultPlaybackDevice.Volume = maxVol; // 볼륨 감소
                }
 
                System.Threading.Thread.Sleep(1000); // 1초 마다 확인
            }
        }
    }
}
 

 

 

TurnItDown.zip
1.04MB

위 압축 파일에는 실행에 필요한 모든 파일과 설치 배치파일이 들어 있다.

 

압축을 풀고 install.bat를 실행한다.

 

C드라이브 turnitdown 폴더에 프로그램이 복사된다.

 

Startup 폴더에 run.bat 파일이 복사된다.

 

 

아이들 컴퓨터에 이렇게 세팅해 놓고 재부팅하면 프로그램이 자동으로 시작된다. 물론 어떤 UI도 없기 때문에 아이들은 이 프로그램이 실행되었다는걸 인식할 수 없다. 초기 조건은 시스템 볼륨 20 이상이고 오디오로 출력되는 소리 값이 0.3 이상이면 지정한 MP3 파일이 플레이되고 볼륨이 20으로 줄어든다.

 

ex) 시스템 볼륨을 70으로 해 놓고 유튜브 영상을 보면 볼륨을 줄인다는 MP3 메세지가 플레이되고 볼륨이 20으로 줄어든다.

 

플레이되는 MP3 메세지는 C:\turnitdown 폴더의 turndown.mp3인데, 원하는 메세지를 녹음해서 바꿀 수 있다.

설치시 제공되는 MP3파일에는 내 조카(준석이)를 위한 메세지가 녹음되어 있다.

 

메세지를 바꾸거나 볼륨 제한 조건을 바꾸고 싶다면 run.bat 파일을 수정한다.

예를들어 볼륨 제한을 35로 바꾸고, 메세지가 녹음된 MP3파일이 8초로 바뀌었다면 run.bat 파일에서 위 그림과 같이 20 6으로 된 부분을 35 8로 바꾼다.

 

반응형
Posted by J-sean
:

C# Sound Meter 사운드 미터

C# 2023. 10. 28. 20:10 |
반응형

Sound Meter를 표시해 보자.

 

NAudio 패키지를 설치한다.

 

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
using System;
using System.Text;
using NAudio.CoreAudioApi;
 
namespace ConsoleApp1
{    class Program
    {
        static void Main()
        {
            MMDeviceEnumerator enumerator = new MMDeviceEnumerator();
            MMDevice device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Console);
            
            while (true)
            {
                //Console.Write("\r{0}", device.AudioMeterInformation.MasterPeakValue);
                //System.Threading.Thread.Sleep(100);
 
                System.Threading.Thread.Sleep(100);
 
                float volume = device.AudioMeterInformation.MasterPeakValue;
                int scale = (int)Math.Floor(volume * 79);
 
                StringBuilder sb = new StringBuilder();
 
                Console.Write("\r");
                sb.Append("Value: " + scale + " ");
 
                if (scale < 1)
                {
                    sb.Append(' '79);
                    Console.Write(sb.ToString());
                    continue;
                }
                
                sb.Append('=', scale);
                sb.Append(' '79 - scale);
                Console.Write(sb.ToString());
            }
        }
    }
}
 

 

 

코드를 작성하고 실행한다.

 

시스템 오디오에서 나는 소리값이 표시된다.

 

※ 참고

Displaying a Volume Meter using NAudio

2023.10.26 - [Python] - Python Core Audio Windows Library 파이썬 코어 오디오 라이브러리

2022.01.06 - [C#] - C# AudioSwitcher System Audio/Sound Volume Control - 시스템 오디오/사운드 볼륨 컨트롤 1

2022.01.07 - [C#] - C# AudioSwitcher System Audio/Sound Volume Control - 시스템 오디오/사운드 볼륨 컨트롤 2

 

반응형
Posted by J-sean
:
반응형

pycaw를 이용해 시스템 오디오 볼륨을 설정해 보자.

 

pycaw를 설치한다.

 

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
from ctypes import cast, POINTER
from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
from time import sleep
 
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
volume = cast(interface, POINTER(IAudioEndpointVolume))
 
# Control volume
#volume.SetMasterVolumeLevel(-0.0, None) # Max(100%)
#volume.SetMasterVolumeLevel(-3.3, None) # 80%
#volume.SetMasterVolumeLevel(-10.3, None) # 50%
#volume.SetMasterVolumeLevel(-23.6, None) # 20%
#volume.SetMasterVolumeLevel(-64.0, None) # Min(0%)
 
while(True):
    current = volume.GetMasterVolumeLevel()    
    print("Current Volume:", current)
 
    if (current > -23.6):
        current -= 1
        volume.SetMasterVolumeLevel(current, None)
 
    sleep(1)
 

 

 

위와 같이 코드를 작성하고 실행한다.

 

100% 었던 볼륨이 20% 이하로 내려간다.

 

실행시 콘솔 화면이 뜨지 않게 하려면 파일 확장명을 .pyw로 바꾼다.

 

※ 참고

pycaw

2022.01.06 - [C#] - C# AudioSwitcher System Audio/Sound Volume Control - 시스템 오디오/사운드 볼륨 컨트롤 1

2022.01.07 - [C#] - C# AudioSwitcher System Audio/Sound Volume Control - 시스템 오디오/사운드 볼륨 컨트롤 2

2023.10.28 - [C#] - C# Sound Meter 사운드 미터

 

반응형
Posted by J-sean
:
반응형

베지어 스플라인 곡선을 그려보자.

 

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
using Godot;
 
public partial class test : Node2D
{    
    private Curve2D pos;
    
    private Vector2 position0;
    private Vector2 position1;
    private Vector2 position2;
    private Vector2 position3;
    private Vector2 position4;
 
    private Vector2 control0;
    private Vector2 control1;
 
    public override void _Ready()
    {
        pos = new Curve2D();
 
        position0 = new Vector2(200200);
        position1 = new Vector2(400400);
        position2 = new Vector2(600200);
        position3 = new Vector2(800400);
        position4 = new Vector2(1000200);
 
        control0 = new Vector2(-1000);
        control1 = new Vector2(1000);
 
        BezierSpline(position0, position1, position2, position3,
            position4, control0, control1);
    }
 
    public override void _Draw()
    {
        //DrawPolyline(pos.GetBakedPoints(), Colors.Red, 5);
        DrawPolyline(pos.Tessellate(), Colors.Red, 5);
    }
 
    private void BezierSpline(Vector2 p0, Vector2 p1, Vector2 p2,
        Vector2 p3, Vector2 p4, Vector2 c0, Vector2 c1)
    {
        pos.AddPoint(p0, c0, c1);
        pos.AddPoint(p1, c0, c1);
        pos.AddPoint(p2, c0, c1);
        pos.AddPoint(p3, c0, c1);
        pos.AddPoint(p4, c0, c1);
    }
}
 

 

 

위와 같이 코드를 작성하고 실행한다.

_Draw()는 처음 한 번만 호출된다. 스플라인이 변경되거나 _Draw()를 다시 호출할 필요가 있다면 QueueRedraw()를 사용한다.

 

각 포인트에서 컨트롤이 적용된 스플라인이 표시된다.

 

※ 참고

Custom drawing in 2D

Beziers, curves, and paths

About Spline Interpolation

 

반응형
Posted by J-sean
:
반응형

 

2023.10.14 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 1. 준비

 

위 링크를 참고해 준비가 끝난 씬에 Polygon2D를 이용해 물 모양을 추가해 보자.

 

WaterBody에 Polygon2D를 추가한다.

 

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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 int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        // 물의 깊이
        bottom = (int)GlobalPosition.Y + depth;
        // 물의 바닥 위치
        water_polygon = GetNode<Polygon2D>("Polygon2D");
        // Polygon2D 노드를 가져온다.
 
        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];
                }
            }
        }
 
        draw_waterbody();
        // 물 그리는 함수 호출.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        foreach (spring item in springs)
        {
            surface_points.Add(item.Position);
        }
        // 모든 스프링의 위치를 리스트에 추가한다.
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        // 폴리곤을 사각형으로 만들기 위해 리스트 마지막에 사각형 밑변을 구성할 포인트를
        // 두 개 추가한다.
 
        water_polygon.Polygon = surface_points.ToArray();
        // 폴리곤을 구성하는 포인트 배열을 지정한다.
    }
}
 

 

 

WaterBody.cs 파일은 위와 같이 수정한다.

 

게임을 실행하고 엔터키를 누르면 폴리곤이 그려지고 스프링과 함께 물결처럼 운동한다.

 

좀 더 부드러운 물결을 만들어 보자.

 

WaterBody 씬에서 Polygon2D - Inspector - Visibility - Show Behind Parent 옵션을 체크한다.

 

 

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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 int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
    public Curve2D wave_position;
    // 부드러운 물결 곡선의 포인트를 저장할 Curve2D 변수
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        bottom = (int)GlobalPosition.Y + depth;
        water_polygon = GetNode<Polygon2D>("Polygon2D");
 
        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);
        }
 
        wave_position = new Curve2D();
        // 물결 곡선 Curve2D 초기화
    }
 
    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];
                }
            }
        }
 
        draw_waterbody();
        draw_wave();
        // 부드러운 물결 그리기 함수 호출.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        foreach (spring item in springs)
        {
            surface_points.Add(item.Position);
        }
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        
        water_polygon.Polygon = surface_points.ToArray();
    }
 
    public override void _Draw()
    {
        DrawPolyline(wave_position.Tessellate(), Colors.Blue, 5);
        // 물결 곡선을 그린다.
    }
 
    public void draw_wave()
    {
        wave_position.ClearPoints();
        // 매번 곡선의 포인트를 초기화 한다.
 
        Vector2 control0 = new Vector2(-500);
        Vector2 control1 = new Vector2(500);
        // 수평 컨트롤
 
        foreach (spring item in springs)
        {
            wave_position.AddPoint(item.Position, control0, control1);
            // 스프링 위치와 컨트롤을 곡선 포인트로 추가한다.
        }
 
        QueueRedraw();
        // _Draw() 호출
    }
}
 

 

 

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

 

spring 씬에서 Sprite2D - Inspector - Visibility - Visible 옵션을 해제한다.

 

Main 씬에서 실행한다.

 

폴리곤 위에 파란 물결이 부드러운 곡선으로 표시된다.

 

 

하지만 자세히 보면 폴리곤은 여전히 거칠다.

 

폴리곤을 부드럽게 바꿔보자.

 

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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 int depth;
    public int target_height;
    public int bottom;
    public PackedScene scene;
    public Polygon2D water_polygon;
    public Curve2D wave_position;
 
    public override void _Ready()
    {
        k = 0.015f;
        d = 0.05f;
        spread = 0.0002f;
        passes = 8;
        depth = 1000;
        bottom = (int)GlobalPosition.Y + depth;
        water_polygon = GetNode<Polygon2D>("Polygon2D");
 
        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);
        }
 
        wave_position = new Curve2D();
    }
 
    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];
                }
            }
        }
 
        draw_wave();
        draw_waterbody();
        // draw_wave()가 먼저 호출되어야 한다.
    }
 
    public void splash(int index, float speed)
    {
        if(index >= 0 && index < springs.Count)
        {
            springs[index].velocity += speed;
        }
    }
 
    public void draw_waterbody()
    {
        List<Vector2> surface_points = new List<Vector2>();
        //foreach (spring item in springs)
        //{
        //    surface_points.Add(item.Position);
        //}
        Vector2[] baked_points = wave_position.GetBakedPoints();
        for (int i = 0; i < baked_points.Length; i++)
        {
            surface_points.Add(baked_points[i]);
        }
        // wave_position으로 만든 곡선의 포인트를 폴리곤 포인트로 사용하기 위해
        // 위와 같이 수정한다.
 
        int first_index = 0;
        int last_index = surface_points.Count - 1;
 
        surface_points.Add(new Vector2(surface_points[last_index].X, bottom));
        surface_points.Add(new Vector2(surface_points[first_index].X, bottom));
        
        water_polygon.Polygon = surface_points.ToArray();
    }
 
    public override void _Draw()
    {
        DrawPolyline(wave_position.Tessellate(), Colors.Blue, 5);
    }
 
    public void draw_wave()
    {
        wave_position.ClearPoints();
 
        Vector2 control0 = new Vector2(-500);
        Vector2 control1 = new Vector2(500);
 
        foreach (spring item in springs)
        {
            wave_position.AddPoint(item.Position, control0, control1);
        }
 
        QueueRedraw();
    }
}
 

 

 

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

 

폴리곤도 부드럽게 바뀌었다.

 

2024.02.11 - [Godot] - [Godot] 2D Splash Dynamic Wave 자연스러운 물결 파동 3. 응용

 

※ 참고

Beziers, curves, and paths

Make a Splash With Dynamic 2D Water Effects

 

반응형
Posted by J-sean
:
반응형

자연스러운 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
: