Фейерверк OpenGL

Опубликовано 30.12.2009 в 11:37 в разделе ,

Не за горами Новый 2010 год, замечательный, любимый всеми поколениями праздник. И само собой, такой праздник никогда не обходится без красивейших салютов, озаряющих небо над городом. Год назад на новый 2009 год в Саранске на центральной площади был просто превосходный салют. Вдохновлённый этим прекрасным зрелищем, я попытался хоть как-то воспроизвести его на своём компьютере. Получилось весьма неплохо, ну а об остальном судить уже читателю …

Фейерверк на OpenGL

Фейерверк на OpenGL

Скачать саму программу и исходники можно по ссылкам в конце статьи.

Пожалуй, это временно будет та самая редкая статья, где я не буду расписывать всех тонкостей работы программы, просто потому что успел их подзабыть, и ограничусь лишь кратким обзором функций и того, зачем они нужны.

Сам проект был написан достаточно давно, поэтому для меня выглядит несколько странным. Всё реализовано без использования классов, целиком на процедурном программировании.
Центральной частью программы, как всегда, является файл main.cpp. В нём содержатся функции инициализации и прорисовки OpenGL-сцены и отработка Windows-команд, которые я уже описывал в далёком прошлом в статье Создание ландшафта по карте высот (OpenGL) кратко и ёмко. Более подробное описание всегда можно найти у NeHe. Вся программа целиком использует крупную структуру данных, называемую контекстом.

struct _global_gl_context {
	// Window
	int		win_x, win_y;		// Window Position
	int		win_cx, win_cy;		// Window Center Position
	int		screen_x, screen_y;	// Window Size
	// Keys
	int		keys[256];			// Keys State
	int		keyp[256];			// Keys Pressed
	// Mouse
	float	mx, my;				// mouse x/y
	float	mmx, mmy;			// mouse move x/y
};

Эта структура данных позволяет обращаться к некоторым глобально полезным данным о текущем состоянии приложения, таким как размеры окна отрисовки, положение мыши и состояние клавиш. Ссылка на глобальный контекст имеется в контексте каждого отдельного модуля.

Для работы с «игровым миром» мы используем ещё два модуля — walker и world.

Модуль walker отвечает за перемещение наблюдателя. Он описывается достаточно просто, в коде вы всё можете разобрать, или задать мне вопрос сразу же под этой статьёй. В данной программе это вещица совершенно бесполезная, но всё равно, как минимум, можно «взглянуть на мир» по-другому.

Кстати, о «мире». Если взглянуть на код, то сразу же возникает мечта меня убить. Весь мир отрисовывается одним единственным квадратиком «снега», столкновения к границами не предусмотрены, высота везде равна нулю. Для чего нужны были три дополнительных функции (кроме отрисовки, инициализации и удаления), я за давностью лет не помню. Но мир в нашем случае не более чем присказка. А сказка будет далее.

Основой программы является модуль XPSM — Extensible Particles System Machine (англ. «расширяемая машина частиц»). Он отвечает собственно за отрисовку всех частиц, в нём же эти частицы и описываются. Рассмотрим его поближе.

Центром модуля является файл xpsm_main, в задачи которого входит инициализация контекста, выделение памяти и … собственно обработка создания частиц.

int xpsm_process () {

	// Generator Rules Go Here
	if (xpsm_context.timer < 0) {
		xpsm_context.timer = 20 + (rand() % 35);
		int type = rand() % XPSM_TYPES_ROCKET;
		xpsm_particle_add (&xpsm_context, xpsm_rockets[type], 0, 0);
	}
	--xpsm_context.timer;

	xpsm_particle_process (&xpsm_context);

	return 0;

}

Так как в нашем случае создаётся «фейерверк», то мы добавляем объект типа «ракета» и запускаем его в полёт.

Обработка частиц описана в xpsm_process. Здесь мы выделили три базовых функции — создание новой частицы (xpsm_particle_add), удаление частицы и собственно обработка всех существующих в «мире» частиц. Первые две функции не более чем осуществляют работу с памятью и вызов функций инициализации и уничтожения от каждой из частиц. А вот на обработке частицы стоит заострить своё внимание.

Прежде всего, рассмотрим саму структуру каждой из частиц.

Частица — это элементарный объект материального мира игры. Для неё характерны такие действия, как зарождение, жизнь и смерть. В каждом из этих действий она может изменять своё текущее состояние и порождать новые частицы. Так как мы описываем частицы материальных объектов, таких как дым, туман или фейерверк, то для них будет характерен также размер, вес и сопротивление воздуха.

Каждая частица может принадлежать к одному из базовых типов.

struct _particle_type {
	int			type;
	void		(*create)	(pcont_p, particle_p, particle_p, void*);
	void		(*die)		(pcont_p, particle_p, void*);
	void		(*draw)		(pcont_p, particle_p, void*);
	float		color[4];
	unsigned long	texture;
	char		texname[100];
};

Структура типа описывает, прежде всего, список функций, используемых для создания, отрисовки и уничтожения частицы. Эти функции получают на вход контекст системы частиц, собственно частицу и любой указатель на всё что угодно — вдруг пригодится?

struct _particle {
	bool		active;			// activity flag
	float		x, y, z;		// position
	float		sx, sy, sz;		// moving vectors
	float		size;			// size
	float		mass;			// particle mass (0 - none)
	float		air;			// air resistance (0 - none)
	int		life;			// lifetime (on 0 dies)
	ptype_p		type;			// type identifier
};

Сама частица описывается следующими характеристиками:

  1. Позиция — координаты частицы в материальном мире.
  2. Вектор движения — проекция скорости частицы на оси координат.
  3. Размер — абстрактная величина, используемая при отрисовке.
  4. Масса — восприимчивость частицы с «силе гравитации».
  5. Сопротивление воздуха — трение, действующее на частицу в процессе движения.
  6. Жизнь — постоянно уменьшающееся время жизни частицы, при достижении нуля наступает смерть.

Все эти характеристики используются в обработчике частиц.

void xpsm_particle_process (pcont_p context) {

	particle_p particle;

	xpsm_particle_reset (context);

	// Main Cycle - Reading Particles One After Another
	while (particle = xpsm_particle_next(context)) {

		// Movement Processing
		particle->x += particle->sx;
		particle->y += particle->sy;
		particle->z += particle->sz;

		// Air Resistance Processing
		particle->sx *= (1.0f - particle->air);
		particle->sy *= (1.0f - particle->air);
		particle->sz *= (1.0f - particle->air);

		// Gravity Processing
		particle->sy -= (XPSM_GRAVITY * particle->mass);

		// Life Processing
		particle->life -= 1;
		if (particle->life <= 0) {
			particle->type->die (context, particle, 0);
		} else {
			particle->type->draw (context, particle, 0);
		}

	}

}

Что может быть проще, чем обработать все частицы?.. Разумеется, только не обрабатывать их совсем!
Мы изменяем позицию частицы на величину вектора движения, сопротивляемся воздуху, поддаёмся гравитации и следим за жизненными силами. Всё! Ничего лишнего больше не требуется!
В идеале, конечно, стоило бы добавить ещё какие-то силы, действующие на частицу, например тот же ветер. Описать их можно как раз здесь же.

Собственно, описание действующих частиц происходит в файле xpsm_patricles. Здесь мы описываем все функции обработки каждой конкретной частицы.

Для нашего фейерверка нам потребуется несколько «ракет» — они являются частицами, порождающими сами фейерверки. В xpsm_particles.h мы определяем их список, который затем используем в xpsm_process для создания нового взрыва.

const int xpsm_rockets [XPSM_TYPES_ROCKET] = {
	XPSM_T_SMPL_R,
	XPSM_T_EXPL_R,
	XPSM_T_DBL_R,
	XPSM_T_HRT_R1,
	XPSM_T_HRT_R2,
	XPSM_T_GNJ_R
};

Описание всех частиц выглядит намного внушительнее …

const ptype_t xpsm_types [XPSM_TYPES_COUNT] = {
	{XPSM_T_SMOKE,		xpsm_smoke_create,		xpsm_null_die,		xpsm_smoke_draw,	{0.4f, 0.3f, 0.2f, 1.0f},	0,	""},
	{XPSM_T_EXPLODE,	xpsm_explode_create,	xpsm_null_die,		xpsm_draw_randot,	{0.6f, 0.5f, 0.4f, 1.0f},	0,	""},
	{XPSM_T_SMPL_R,		xpsm_rocket_create,		xpsm_t_smpl_r_die,	xpsm_rocket_draw,	{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_SMPL_E,		xpsm_t_smpl_e_create,	xpsm_null_die,		xpsm_draw_line,		{1.0f, 0.3f, 0.3f, 1.0f},	0,	""},
	{XPSM_T_EXPL_R,		xpsm_rocket_create,		xpsm_t_expl_r_die,	xpsm_rocket_draw,	{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_EXPL_R,		xpsm_t_expl_e_create,	xpsm_explode_die,	xpsm_draw_line,		{1.0f, 0.4f, 0.4f, 1.0f},	0,	""},
	{XPSM_T_DBL_R,		xpsm_rocket_create,		xpsm_t_dbl_r_die,	xpsm_rocket_draw,	{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_DBL_E1,		xpsm_t_dbl_e1_create,	xpsm_null_die,		xpsm_draw_line,		{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_DBL_E2,		xpsm_t_dbl_e2_create,	xpsm_null_die,		xpsm_draw_line,		{1.0f, 0.4f, 0.4f, 1.0f},	0,	""},
	{XPSM_T_HRT_R1,		xpsm_rocket_create,		xpsm_t_hrt_r1_die,	xpsm_rocket_draw,	{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_HRT_R2,		xpsm_rocket_create,		xpsm_t_hrt_r2_die,	xpsm_rocket_draw,	{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_HRT_E1,		xpsm_t_hrt_e1_create,	xpsm_null_die,		xpsm_draw_line,		{1.0f, 0.4f, 0.4f, 1.0f},	0,	""},
	{XPSM_T_HRT_E2,		xpsm_t_hrt_e2_create,	xpsm_null_die,		xpsm_draw_line,		{1.0f, 1.0f, 1.0f, 1.0f},	0,	""},
	{XPSM_T_HRT_E3,		xpsm_t_hrt_e1_create,	xpsm_explode_die,	xpsm_draw_line,		{1.0f, 0.4f, 0.4f, 1.0f},	0,	""},
	{XPSM_T_GNJ_R,		xpsm_rocket_create,		xpsm_t_gnj_r_die,	xpsm_rocket_draw,	{0.0f, 1.0f, 0.0f, 1.0f},	0,	""},
	{XPSM_T_GNJ_E,		xpsm_t_gnj_e_create,	xpsm_explode_die,	xpsm_draw_line,		{0.4f, 1.0f, 0.4f, 1.0f},	0,	""},
};

Если присмотреться, у нас имеются частицы стандартных дыма и взрыва, которые мы постоянно творим в процессе полёта ракеты или искрения огня. Остальные частицы содержат в себе ряд фейерверков. Это простой фейерверк, разрывная ракета, фейерверк с двойным кольцом взрыва, два «сердечных» фейерверка и фейерверк-каннабола. Описание каждой из ракет — штука сложная, и я обязательно им займусь, но немножко позже. А сейчас скачайте программку и попробуйте изучить всё сами. Я с радостью отвечу на любой вопрос.

Скачать:

  1. Исполняемый файл — собственно программку с фейерверками — rar, 396kb
  2. Исходные коды программы — zip, 459kb

Надеюсь, кому-нибудь такое пригодится в целях обучения)

Создано и скомпилировано в Visual Studio 2008, лицензия ФЭТ МГУ им. Н.П.Огарёва,  г.Саранск.