SDL, raylib

[SDL3] Particle 파티클 (Fireworks/Snow)

J-sean 2025. 4. 18. 20:50
반응형

픽셀 파티클 시스템을 만들어 보자.

 

더보기

 

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
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
 
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
 
int screen_width = 640;
int screen_height = 480;
 
float speed = 0.001f;
Uint64 start;
Uint64 end;
float fps = 3000;
 
typedef struct {
    float x, y;
    float vx, vy;
    int life;
} s_particle;
 
const int number_particles = 100;
s_particle particles[number_particles];
 
void spawn_particle(s_particle* pt) {
    pt->= SDL_randf() * screen_width;
    pt->= SDL_randf() * screen_height;
    pt->vx = (SDL_randf() * 2 - 1* speed;
    pt->vy = (SDL_randf() * 2 - 1* speed;
    // '* 2 - 1' 의 의미: 음의 방향으로도 움직이기 위해서.
    pt->life = (int)fps + SDL_rand((int)fps * 2);
}
 
void draw_particle(s_particle pt[]) {
    for (int i = 0; i < number_particles; i++) {
        s_particle* particle = &pt[i];
        particle->+= particle->vx;
        particle->+= particle->vy;
        particle->life--;
 
        if (particle->life <= 0)
            spawn_particle(particle);
 
        SDL_SetRenderDrawColor(renderer, 02550, SDL_ALPHA_OPAQUE);
        SDL_RenderPoint(renderer, particle->x, particle->y);
    }
}
 
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    SDL_SetAppMetadata("Example""1.0""sean");
 
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    if (!SDL_CreateWindowAndRenderer("examples/renderer/clear", screen_width, screen_height, 0&window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    for (int i = 0; i < number_particles; i++) {
        spawn_particle(&particles[i]);
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    switch (event->type) {
    case SDL_EVENT_QUIT:
        return SDL_APP_SUCCESS;
    case SDL_EVENT_KEY_DOWN:
        printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
        if (event->key.key == SDLK_ESCAPE)
            return SDL_APP_SUCCESS;
        break;
    default:
        break;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppIterate(void* appstate)
{
    // FPS 계산. 사용하지는 않음.
    start = SDL_GetPerformanceCounter();
 
    {
        SDL_SetRenderDrawColor(renderer, 000, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
 
    end = SDL_GetPerformanceCounter();
    fps = 1.0f / ((end - start) / (float)SDL_GetPerformanceFrequency());
 
    return SDL_APP_CONTINUE;
}
 
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
 

 

 

위 코드에서는 FPS를 설정하지 않고 시스템의 CPU 속도에 맞게 speed 변수를 적당히 조절했다. FPS 설정이 필요하면 아래 링크를 참고한다.

2025.04.22 - [C, C++] - [SDL] Framerate Per Second FPS

 

파티클들이 사방으로 움직인다.

 

이번엔 불꽃놀이 파티클을 만들어 보자.

 

더보기

 

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
#define SDL_MAIN_USE_CALLBACKS 1
 
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
 
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
 
int screen_width = 640;
int screen_height = 480;
 
float speed = 3.0f;
float fps = 60;
 
typedef struct {
    float x, y;
    float vx, vy;
    SDL_Color color;
    int life;
    bool exist;
} s_particle;
 
const int number_particles = 1000;
s_particle particles[number_particles];
SDL_Point spawn_point;
 
void spawn_particle(s_particle* pt) {
    pt->exist = true;
    pt->= spawn_point.x;
    pt->= spawn_point.y;
 
    float x = (SDL_randf() * 2 - 1);
    float y = (SDL_randf() * 2 - 1);
    float n = SDL_sqrtf(x * x + y * y);
    // 벡터 정규화. 불꽃이 원형으로 퍼져나가기 위해.
    // '* 2 - 1' 의 의미: 음의 방향으로도 움직이기 위해.
    pt->vx = (x / n) * speed * SDL_randf();
    pt->vy = (y / n) * speed * SDL_randf();
 
    pt->color.r = SDL_rand(256);
    pt->color.g = SDL_rand(256);
    pt->color.b = SDL_rand(256);
    pt->life = (int)fps + SDL_rand((int)fps * 0.5f);
}
 
void draw_particle(s_particle pt[]) {
    for (int i = 0; i < number_particles; i++) {
        s_particle* particle = &pt[i];
        if (particle->exist) {
            particle->+= particle->vx;
            particle->+= particle->vy;
            particle->life--;
 
            if (particle->life <= 0) {
                particle->exist = false;
                continue;
            }
            SDL_SetRenderDrawColor(renderer, particle->color.r, particle->color.g, particle->color.b, SDL_ALPHA_OPAQUE);
            SDL_RenderPoint(renderer, particle->x, particle->y);
        }
    }
}
 
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    SDL_SetAppMetadata("Example""1.0""sean");
 
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    if (!SDL_CreateWindowAndRenderer("examples/renderer/clear", screen_width, screen_height, 0&window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    switch (event->type) {
    case SDL_EVENT_QUIT:
        return SDL_APP_SUCCESS;
    case SDL_EVENT_MOUSE_BUTTON_DOWN:
        if (event->button.button == 1) { // 왼쪽 버튼 클릭. 오른쪽 버튼은 3.
            printf("x: %f, y: %f\n", event->button.x, event->button.y);
            spawn_point.x = event->button.x;
            spawn_point.y = event->button.y;
            for (int i = 0; i < number_particles; i++)
                spawn_particle(&particles[i]);
        }
        break;
    case SDL_EVENT_KEY_DOWN:
        printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
        if (event->key.key == SDLK_ESCAPE)
            return SDL_APP_SUCCESS;
        break;
    default:
        break;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppIterate(void* appstate)
{
    SDL_Delay(1000 / fps);
 
    {
        SDL_SetRenderDrawColor(renderer, 000, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
 
    return SDL_APP_CONTINUE;
}
 
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
 

 

 

마우스 클릭 지점에서 간단한 불꽃놀이 파티클이 생성된다.

 

이번엔 픽셀이 아닌 비트맵 이미지로 파티클 시스템을 만들어 보자.

snow.bmp
0.00MB

 

snow1.bmp
0.00MB

 

 

더보기

 

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
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
 
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
 
int screen_width = 640;
int screen_height = 480;
 
float speed = 0.01f;
Uint64 start;
Uint64 end;
float fps = 3000;
 
typedef struct {
    float x, y;
    float vx, vy;
    int life;
} s_particle;
 
const int number_particles = 200;
s_particle particles[number_particles];
 
// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture;
SDL_FRect rect;
 
void spawn_particle(s_particle* pt) {
    pt->= SDL_randf() * screen_width;
    pt->= SDL_randf() * screen_height;
    pt->vx = (SDL_randf() * 2 - 1* speed;
    pt->vy = (SDL_randf() * 2 - 1* speed;
    // '* 2 - 1' 의 의미: 음의 방향으로도 움직이기 위해서.
    pt->life = (int)fps + SDL_rand((int)fps * 2);
}
 
void draw_particle(s_particle pt[]) {
    for (int i = 0; i < number_particles; i++) {
        s_particle* particle = &pt[i];
        particle->+= particle->vx;
        particle->+= particle->vy;
        particle->life--;
 
        if (particle->life <= 0)
            spawn_particle(particle);
 
        // 텍스쳐 그리기
        rect.x = particle->- rect.w / 2;
        rect.y = particle->- rect.h / 2;
        SDL_RenderTexture(renderer, texture, NULL&rect);
 
        //SDL_SetRenderDrawColor(renderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
        //SDL_RenderPoint(renderer, particle->x, particle->y);
    }
}
 
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    SDL_SetAppMetadata("Example""1.0""sean");
 
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    if (!SDL_CreateWindowAndRenderer("example", screen_width, screen_height, 0&window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    for (int i = 0; i < number_particles; i++) {
        spawn_particle(&particles[i]);
    }
 
    // 비트맵 로드
    SDL_Surface* bmpSurface = SDL_LoadBMP("snow.bmp");
    if (!bmpSurface) {
        SDL_Log("Couldn't load BMP: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    // 컬러키(투명) 설정
    SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapSurfaceRGB(bmpSurface, 0x000x000x00));
    //SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
    rect = { 00, (float)bmpSurface->w, (float)bmpSurface->h };
    texture = SDL_CreateTextureFromSurface(renderer, bmpSurface);
 
    SDL_DestroySurface(bmpSurface);
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    switch (event->type) {
    case SDL_EVENT_QUIT:
        return SDL_APP_SUCCESS;
    case SDL_EVENT_KEY_DOWN:
        printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
        if (event->key.key == SDLK_ESCAPE)
            return SDL_APP_SUCCESS;
        break;
    default:
        break;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppIterate(void* appstate)
{
    // FPS 계산. 사용하지는 않음.
    start = SDL_GetPerformanceCounter();
 
    {
        SDL_SetRenderDrawColor(renderer, 000, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
 
    end = SDL_GetPerformanceCounter();
    fps = 1.0f / ((end - start) / (float)SDL_GetPerformanceFrequency());
 
    return SDL_APP_CONTINUE;
}
 
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(texture);
 
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
 

 

 

알파채널이 없는 비트맵이라 조금 어색하지만 눈이 사방으로 흩날린다.

 

 

이번엔 진짜 눈처럼 내리는 환경을 만들어 보자.

 

더보기

 

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
#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
 
SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;
 
int screen_width = 640;
int screen_height = 480;
 
Uint64 start;
Uint64 end;
float fps;
 
typedef struct {
    float x, y;
    float vx, vy;
} s_particle;
 
const int number_particles = 400;
s_particle particles[number_particles];
 
// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture;
SDL_FRect rect;
 
void spawn_particle(s_particle* pt) {
    pt->= (SDL_randf() * 2 - 1* screen_width * 10;
    pt->= SDL_randf() * screen_height * -2;
    // 넓은 범위에서 눈이 생성되게 해서 실행 초기에 눈이 쏟아져 내리는걸 방지.
    pt->vx = SDL_randf() * 0.5f; // 눈은 오른쪽으로만 흩날린다.
    pt->vy = SDL_randf() + 0.8f;
}
 
void draw_particle(s_particle pt[]) {
    for (int i = 0; i < number_particles; i++) {
        s_particle* particle = &pt[i];
        particle->+= particle->vx;
        particle->+= particle->vy;
        // 눈이 화면 왼쪽에서도 바람에 날려 올 수 있도록 생존 범위 조정.
        if (particle->> screen_width || particle->< -screen_width || particle->> screen_height)
            spawn_particle(particle);
 
        // 텍스쳐 그리기
        rect.x = particle->- rect.w / 2;
        rect.y = particle->- rect.h / 2;
        SDL_RenderTexture(renderer, texture, NULL&rect);
 
        //SDL_SetRenderDrawColor(renderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
        //SDL_RenderPoint(renderer, particle->x, particle->y);
    }
}
 
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    SDL_SetAppMetadata("Example""1.0""sean");
 
    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    SDL_SetHint(SDL_HINT_RENDER_VSYNC, "1");
    // A variable controlling whether updates to the SDL screen surface should be synchronized
    // with the vertical refresh, to avoid tearing.
 
    if (!SDL_CreateWindowAndRenderer("example", screen_width, screen_height, 0&window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    for (int i = 0; i < number_particles; i++) {
        spawn_particle(&particles[i]);
    }
 
    // 비트맵 로드
    SDL_Surface* bmpSurface = SDL_LoadBMP("snow.bmp");
    if (!bmpSurface) {
        SDL_Log("Couldn't load BMP: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    // 컬러키(투명) 설정
    SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapSurfaceRGB(bmpSurface, 0x000x000x00));
    //SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
    rect = { 00, (float)bmpSurface->w, (float)bmpSurface->h };
    texture = SDL_CreateTextureFromSurface(renderer, bmpSurface);
 
    SDL_DestroySurface(bmpSurface);
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppEvent(void* appstate, SDL_Event* event)
{
    switch (event->type) {
    case SDL_EVENT_QUIT:
        return SDL_APP_SUCCESS;
    case SDL_EVENT_KEY_DOWN:
        printf("Key pressed: %s\n", SDL_GetKeyName(event->key.key));
        if (event->key.key == SDLK_ESCAPE)
            return SDL_APP_SUCCESS;
        break;
    default:
        break;
    }
 
    return SDL_APP_CONTINUE;
}
 
SDL_AppResult SDL_AppIterate(void* appstate)
{
    // FPS 계산. 사용하지는 않음.
    start = SDL_GetPerformanceCounter();
 
    {
        SDL_SetRenderDrawColor(renderer, 000, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
 
    end = SDL_GetPerformanceCounter();
    fps = 1.0f / ((end - start) / (float)SDL_GetPerformanceFrequency());
 
    return SDL_APP_CONTINUE;
}
 
void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
    SDL_DestroyTexture(texture);
 
    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
}
 

 

 

실제 프로그램을 실행하면 훨씬 자연스럽게 눈이 내린다.

 

이번엔 배경이 투명한 윈도우를 만들어 보자.

 

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
...
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    ...
    if (!SDL_CreateWindowAndRenderer("example", screen_width, screen_height, SDL_WINDOW_TRANSPARENT | SDL_WINDOW_BORDERLESS, &window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
    ...
}
...
SDL_AppResult SDL_AppIterate(void* appstate)
{
    start = SDL_GetPerformanceCounter();
 
    {
        //SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
    ...
}
...
 

 

윈도우 생성시 SDL_WINDOW_TRANSPARENT |  SDL_WINDOW_BOARDERLESS 옵선을 주고 SDL_SetRenderDrawColor() 호출을 삭제하면 투명한 배경의 윈도우를 만들 수 있다.

 

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
...
SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
    ...    
    if (!SDL_CreateWindowAndRenderer("example", screen_width, screen_height, SDL_WINDOW_TRANSPARENT | SDL_WINDOW_BORDERLESS | SDL_WINDOW_FULLSCREEN, &window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }
 
    SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
    ...
}
...
SDL_AppResult SDL_AppIterate(void* appstate)
{
    start = SDL_GetPerformanceCounter();
 
    {
        SDL_SetRenderDrawColor(renderer, 0000);
        SDL_RenderClear(renderer);
 
        draw_particle(particles);
 
        SDL_RenderPresent(renderer);
    }
    ...
}
...
 

 

아니면 위 코드와 같이 바꿔도 투명한 배경의 윈도우를 만들 수 있다. 위 코드의 경우 SDL_SetRendererDrawColor()의 r, g, b, a 값을 바꾸면 반투명하면서 색이 있는 배경의 윈도우를 만들 수 있다.

예) SDL_SetRendererDrawColor(renderer, 0, 0, 0, 128); // 약간 어두운 투명 배경

   SDL_SetRendererDrawColor(renderer, 128, 0, 0, 128); // 약간 붉은 투명 배경

 

윈도우의 배경이 투명해서 데스크탑에서 눈이 내리는것 처럼 보인다.

 

만약 전체 화면으로 만들고 싶다면 아래 내용을 추가(변경)하면 된다.

1. #include <Windows.h>

2. int screen_width =  GetSystemMetrics(SM_CXSCREEN);
   int screen_height = GetSystemMetrics(SM_CYSCREEN);

3. SDL_CreateWindowAndRenderer("example", screen_width, screen_height, SDL_WINDOW_TRANSPARENT | SDL_WINDOW_BORDERLESS | SDL_WINDOW_FULLSCREEN, &window, &renderer)

 

반응형