반응형

파티클은 아니지만 차일드 윈도우로 화면에 내리는 눈을 만들어 보자.

 

// 차일드 윈도우가 100개 정도만 되어도 엄청 느려지고 잘 안된다.
// 그러다 그 이상되면 101번째 윈도우 생성시(정확히는 렌더러 생성시) 아래와 같은 Access Violation이 발생한다.
// Exception thrown at 0x00007FF8EE239D68 (nvd3dumx.dll) in SDLTest.exe: 0xC0000005 : Access violation reading location 0x0000000000000038.
// SDL3가 문제인지, 그래픽 드라이버가 문제인지... 정확히는 모르겠지만 아무래도 그래픽 드라이버 문제같다.

#define SDL_MAIN_USE_CALLBACKS 1
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <stdio.h>
#include <windows.h>

SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;

const int number_flakes = 10;
int screen_width;
int screen_height;

SDL_Window* subwindow[number_flakes]; // 서브 윈도우
SDL_Renderer* subrenderer[number_flakes]; // 서브 윈도우 렌더러

typedef struct position {
	float x, y;
	float vx, vy;
} s_position;
s_position subwindow_pos[number_flakes]; // 서브 윈도우 위치

// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture[number_flakes];
SDL_FRect rect;

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[])
{
	ShowWindow(GetConsoleWindow(), SW_HIDE);
	// 콘솔 창 숨김. Project - Property Pages - Linker - System - SubSystem - Windows로 바꿔서 컴파일해도 된다.

	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("Press ESC to Quit", 320, 32, SDL_WINDOW_MINIMIZED, &window, &renderer)) {
		SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
		return SDL_APP_FAILURE;
	}

	// 비트맵 로드
	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, 0x00, 0x00, 0x00));
	//SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
	rect = { 0, 0, (float)bmpSurface->w, (float)bmpSurface->h };

	// 현재 디스플레이 정보 가져오기(해상도, 주사율 등)
	const SDL_DisplayMode* displaymode = SDL_GetCurrentDisplayMode(SDL_GetPrimaryDisplay());
	screen_width = displaymode->w / 3;
	screen_height = displaymode->h / 3;

	for (int i = 0; i < number_flakes; i++) {
		subwindow_pos[i].x = (float)SDL_rand(screen_width);
		subwindow_pos[i].y = (float)SDL_rand(screen_height); // 서브 윈도우 위치 설정
		subwindow_pos[i].vx = SDL_randf() * 0.5f;
		subwindow_pos[i].vy = SDL_randf() + 0.8f;

		/*
		// 서브 윈도우 생성
		SDL_PropertiesID props = SDL_CreateProperties(); // Create Properties
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_TRANSPARENT_BOOLEAN, true);
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_BORDERLESS_BOOLEAN, true);
		SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_ALWAYS_ON_TOP_BOOLEAN, true);
		SDL_GetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_MENU_BOOLEAN, true);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_WIDTH_NUMBER, rect.w);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_HEIGHT_NUMBER, rect.h);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_X_NUMBER, subwindow_pos[i].x);
		SDL_SetNumberProperty(props, SDL_PROP_WINDOW_CREATE_Y_NUMBER, subwindow_pos[i].y);
		SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_PARENT_POINTER, window);

		subwindow[i] = SDL_CreateWindowWithProperties(props);
		if (!subwindow[i]) {
			SDL_Log("Couldn't create subwindow: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}

		subrenderer[i] = SDL_CreateRenderer(subwindow[i], NULL);
		if (!subrenderer[i]) {
			SDL_Log("Couldn't create subwindow renderer: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
		*/

		// 위 주석의 코드와 동일
		if (!SDL_CreateWindowAndRenderer("Sub Window", (int)rect.w, (int)rect.h, SDL_WINDOW_BORDERLESS | SDL_WINDOW_ALWAYS_ON_TOP | SDL_WINDOW_TRANSPARENT, &subwindow[i], &subrenderer[i])) {
			SDL_Log("Couldn't create subwindow and subrenderer: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
		SDL_SetWindowParent(subwindow[i], window); // 서브 윈도우 부모 설정		
		SDL_SetWindowPosition(subwindow[i], (int)subwindow_pos[i].x, (int)subwindow_pos[i].y);

		SDL_SetRenderDrawBlendMode(subrenderer[i], SDL_BLENDMODE_BLEND);
	}

	for (int i = 0; i < number_flakes; i++) {
		texture[i] = SDL_CreateTextureFromSurface(subrenderer[i], bmpSurface);
		if (!texture[i]) {
			SDL_Log("Couldn't create texture: %s", SDL_GetError());
			return SDL_APP_FAILURE;
		}
	}

	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)
{
	SDL_Delay(1000 / 60); // 60 FPS

	SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
	SDL_RenderClear(renderer);

	SDL_RenderPresent(renderer);

	for (int i = 0; i < number_flakes; i++) {
		SDL_SetRenderDrawColor(subrenderer[i], 0, 0, 0, SDL_ALPHA_TRANSPARENT);
		SDL_RenderClear(subrenderer[i]);
		SDL_RenderTexture(subrenderer[i], texture[i], NULL, &rect);
		SDL_RenderPresent(subrenderer[i]);

		// 서브 윈도우 위치 업데이트
		subwindow_pos[i].x += subwindow_pos[i].vx; // 오른쪽으로 이동
		subwindow_pos[i].y += subwindow_pos[i].vy; // 아래로 이동

		if (subwindow_pos[i].x > screen_width) {
			subwindow_pos[i].x = 0; // 화면 왼쪽으로 다시 이동
		}
		if (subwindow_pos[i].y > screen_height) {
			subwindow_pos[i].y = 0; // 화면 위로 다시 이동
		}
		SDL_SetWindowPosition(subwindow[i], (int)subwindow_pos[i].x, (int)subwindow_pos[i].y);
	}

	return SDL_APP_CONTINUE;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result)
{
	for (int i = 0; i < number_flakes; i++) {
		SDL_DestroyTexture(texture[i]);
		SDL_DestroyRenderer(subrenderer[i]);
		SDL_DestroyWindow(subwindow[i]);
	}

	SDL_DestroyRenderer(renderer);
	SDL_DestroyWindow(window);

	SDL_Quit();
}

 

 

 

하지만 위 예처럼 눈의 수 만큼 차일드 윈도우를 생성하는건 부담스럽다.

특별한 비활성화 투명 윈도우를 생성하는 방식으로 바꿔보자.

 

#define SDL_MAIN_USE_CALLBACKS 1
#include "SDL3/SDL.h"
#include "SDL3/SDL_main.h"
#include <stdio.h>

#include <windows.h>
#include <WinUser.h>

SDL_Window* window = NULL;
SDL_Renderer* renderer = NULL;

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

Uint64 start;
Uint64 end;
float fps;

typedef struct {
	float x, y;
	float vx, vy;
} s_particle;

const int number_particles = 2000;
s_particle particles[number_particles];

// 파티클 이미지 텍스쳐와 렉트
SDL_Texture* texture;
SDL_FRect rect;

void spawn_particle(s_particle* pt) {
	pt->x = (SDL_randf() * 2 - 1) * screen_width * 10;
	pt->y = 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->x += particle->vx;
		particle->y += particle->vy;
		// 눈이 화면 왼쪽에서도 바람에 날려 올 수 있도록 생존 범위 조정.
		if (particle->x > screen_width || particle->x < -screen_width || particle->y > screen_height)
			spawn_particle(particle);

		// 텍스쳐 그리기
		rect.x = particle->x - rect.w / 2;
		rect.y = particle->y - 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.

	// 디스플레이 스케일 정보 확인
	SDL_DisplayID display = SDL_GetPrimaryDisplay(); // Return the primary display
	SDL_Log("DisplayID: %u", display);
	float display_scale = SDL_GetDisplayContentScale(display);
	SDL_Log("Display Scale: %f", display_scale);
	SDL_Log("Original Display Size: (%d X %d)", screen_width, screen_height);

	// 스케일 적용
	screen_width = int(screen_width * display_scale);
	screen_height = int(screen_height * display_scale);

	SDL_Log("Scaled Display Size: (%d X %d)", screen_width, screen_height);

	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_PropertiesID props = SDL_GetWindowProperties(window);
	HWND hwnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
	// 윈도우 투명 및 클릭 무시 설정
	SetWindowLongPtr(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_LAYERED | WS_EX_TRANSPARENT);
	// 항상 위에 표시, 사이즈, 위치 변경, 활성화 금지 
	SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);

	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, 0x00, 0x00, 0x00));
	//SDL_SetSurfaceColorKey(bmpSurface, true, SDL_MapRGB(SDL_GetPixelFormatDetails(bmpSurface->format), NULL, 0x00, 0x00, 0x00));
	rect = { 0, 0, (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, 0, 0, 0, 128);
		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이 아닌 경우를 위해 screen_width, screen_height 등의 정보를 Windows API를 이용해 확인하고, 화면 배율(display_scale)을 적용해 정확한 스크린 사이즈를 결정한다. 화면 배율이 1이 아닌 경우 정확한 스크린 사이즈 확인은 아래 링크와 같이 하는게 더 낫다.

 

2025.06.02 - [SDL, raylib] - [SDL3] Display Information 디스플레이 정보 가져오기

 

살짝 어두운 배경으로 바뀌고 눈이 내린다. 동시에 다른 프로그램을 사용할 수 있다.

 

 

※ 참고

2025.04.18 - [SDL, raylib] - [SDL3] Particle 파티클 (Fireworks/Snow)

 

반응형
Posted by J-sean
: