Compare commits

..

5 Commits

Author SHA1 Message Date
Vladislav Khorev
8f9a18d960 Merge branch 'main' into spark 2025-12-12 22:45:57 +03:00
Vlad
bc49540a95 add blend 2025-12-12 17:12:03 +06:00
Vlad
d2605d9108 moved drawEffect() to SparkEmitter 2025-12-12 16:58:23 +06:00
Vlad
115cbbb7fa fix spark 2025-12-11 20:09:41 +06:00
Vlad
b855cff0e6 added sparks on the back of the spaceship 2025-12-08 19:28:47 +06:00
7 changed files with 793 additions and 409 deletions

View File

@ -436,6 +436,8 @@ add_executable(space-game001
OpenGlExtensions.h OpenGlExtensions.h
Utils.cpp Utils.cpp
Utils.h Utils.h
SparkEmitter.cpp
SparkEmitter.h
) )
# Установка проекта по умолчанию для Visual Studio # Установка проекта по умолчанию для Visual Studio

129
Game.cpp
View File

@ -1,4 +1,4 @@
#include "Game.h" #include "Game.h"
#include "AnimatedModel.h" #include "AnimatedModel.h"
#include "BoneAnimatedModel.h" #include "BoneAnimatedModel.h"
#include "Utils.h" #include "Utils.h"
@ -19,11 +19,11 @@ namespace ZL
Vector4f generateRandomQuaternion(std::mt19937& gen) Vector4f generateRandomQuaternion(std::mt19937& gen)
{ {
// Распределение для генерации случайных координат кватерниона // Ðàñïðåäåëåíèå äëÿ ãåíåðàöèè ñëó÷àéíûõ êîîðäèíàò êâàòåðíèîíà
std::normal_distribution<> distrib(0.0, 1.0); std::normal_distribution<> distrib(0.0, 1.0);
// Генерируем четыре случайных числа из нормального распределения N(0, 1). // Ãåíåðèðóåì ÷åòûðå ñëó÷àéíûõ ÷èñëà èç íîðìàëüíîãî ðàñïðåäåëåíèÿ N(0, 1).
// Нормализация этого вектора дает равномерное распределение по 4D-сфере (т.е. кватернион единичной длины). // Íîðìàëèçàöèÿ ýòîãî âåêòîðà äàåò ðàâíîìåðíîå ðàñïðåäåëåíèå ïî 4D-ñôåðå (ò.å. êâàòåðíèîí åäèíè÷íîé äëèíû).
Vector4f randomQuat = { Vector4f randomQuat = {
(float)distrib(gen), (float)distrib(gen),
(float)distrib(gen), (float)distrib(gen),
@ -35,25 +35,25 @@ namespace ZL
} }
// --- Основная функция генерации --- // --- Îñíîâíàÿ ôóíêöèÿ ãåíåðàöèè ---
std::vector<BoxCoords> generateRandomBoxCoords(int N) std::vector<BoxCoords> generateRandomBoxCoords(int N)
{ {
// Константы // Êîíñòàíòû
const float MIN_DISTANCE = 3.0f; const float MIN_DISTANCE = 3.0f;
const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; // Работаем с квадратом расстояния const float MIN_DISTANCE_SQUARED = MIN_DISTANCE * MIN_DISTANCE; // Ðàáîòàåì ñ êâàäðàòîì ðàññòîÿíèÿ
const float MIN_COORD = -100.0f; const float MIN_COORD = -100.0f;
const float MAX_COORD = 100.0f; const float MAX_COORD = 100.0f;
const int MAX_ATTEMPTS = 1000; // Ограничение на количество попыток, чтобы избежать бесконечного цикла const int MAX_ATTEMPTS = 1000; // Îãðàíè÷åíèå íà êîëè÷åñòâî ïîïûòîê, ÷òîáû èçáåæàòü áåñêîíå÷íîãî öèêëà
std::vector<BoxCoords> boxCoordsArr; std::vector<BoxCoords> boxCoordsArr;
boxCoordsArr.reserve(N); // Резервируем память boxCoordsArr.reserve(N); // Ðåçåðâèðóåì ïàìÿòü
// 1. Инициализация генератора псевдослучайных чисел // 1. Èíèöèàëèçàöèÿ ãåíåðàòîðà ïñåâäîñëó÷àéíûõ ÷èñåë
// Используем Mersenne Twister (mt19937) как высококачественный генератор // Èñïîëüçóåì Mersenne Twister (mt19937) êàê âûñîêîêà÷åñòâåííûé ãåíåðàòîð
std::random_device rd; std::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
// 2. Определение равномерного распределения для координат [MIN_COORD, MAX_COORD] // 2. Îïðåäåëåíèå ðàâíîìåðíîãî ðàñïðåäåëåíèÿ äëÿ êîîðäèíàò [MIN_COORD, MAX_COORD]
std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD); std::uniform_real_distribution<> distrib(MIN_COORD, MAX_COORD);
int generatedCount = 0; int generatedCount = 0;
@ -63,53 +63,53 @@ namespace ZL
bool accepted = false; bool accepted = false;
int attempts = 0; int attempts = 0;
// Попытка найти подходящие координаты // Ïîïûòêà íàéòè ïîäõîäÿùèå êîîðäèíàòû
while (!accepted && attempts < MAX_ATTEMPTS) while (!accepted && attempts < MAX_ATTEMPTS)
{ {
// Генерируем новые случайные координаты // Ãåíåðèðóåì íîâûå ñëó÷àéíûå êîîðäèíàòû
Vector3f newPos( Vector3f newPos(
(float)distrib(gen), (float)distrib(gen),
(float)distrib(gen), (float)distrib(gen),
(float)distrib(gen) (float)distrib(gen)
); );
// Проверка расстояния до всех уже существующих объектов // Ïðîâåðêà ðàññòîÿíèÿ äî âñåõ óæå ñóùåñòâóþùèõ îáúåêòîâ
accepted = true; // Предполагаем, что подходит, пока не доказано обратное accepted = true; // Ïðåäïîëàãàåì, ÷òî ïîäõîäèò, ïîêà íå äîêàçàíî îáðàòíîå
for (const auto& existingBox : boxCoordsArr) for (const auto& existingBox : boxCoordsArr)
{ {
// Расчет вектора разности // Ðàñ÷åò âåêòîðà ðàçíîñòè
Vector3f diff = newPos - existingBox.pos; Vector3f diff = newPos - existingBox.pos;
// Расчет квадрата расстояния // Ðàñ÷åò êâàäðàòà ðàññòîÿíèÿ
float distanceSquared = diff.squaredNorm(); float distanceSquared = diff.squaredNorm();
// Если квадрат расстояния меньше квадрата минимального расстояния // Åñëè êâàäðàò ðàññòîÿíèÿ ìåíüøå êâàäðàòà ìèíèìàëüíîãî ðàññòîÿíèÿ
if (distanceSquared < MIN_DISTANCE_SQUARED) if (distanceSquared < MIN_DISTANCE_SQUARED)
{ {
accepted = false; // Отклоняем, слишком близко accepted = false; // Îòêëîíÿåì, ñëèøêîì áëèçêî
break; // Нет смысла проверять дальше, если одно нарушение найдено break; // Íåò ñìûñëà ïðîâåðÿòü äàëüøå, åñëè îäíî íàðóøåíèå íàéäåíî
} }
} }
if (accepted) if (accepted)
{ {
// 2. Генерируем случайный кватернион // 2. Ãåíåðèðóåì ñëó÷àéíûé êâàòåðíèîí
Vector4f randomQuat = generateRandomQuaternion(gen); Vector4f randomQuat = generateRandomQuaternion(gen);
// 3. Преобразуем его в матрицу вращения // 3. Ïðåîáðàçóåì åãî â ìàòðèöó âðàùåíèÿ
Matrix3f randomMatrix = QuatToMatrix(randomQuat); Matrix3f randomMatrix = QuatToMatrix(randomQuat);
// 4. Добавляем объект с новой случайной матрицей // 4. Äîáàâëÿåì îáúåêò ñ íîâîé ñëó÷àéíîé ìàòðèöåé
boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix }); boxCoordsArr.emplace_back(BoxCoords{ newPos, randomMatrix });
generatedCount++; generatedCount++;
} }
attempts++; attempts++;
} }
// Если превышено максимальное количество попыток, выходим из цикла, // Åñëè ïðåâûøåíî ìàêñèìàëüíîå êîëè÷åñòâî ïîïûòîê, âûõîäèì èç öèêëà,
// чтобы избежать зависания, если N слишком велико или диапазон слишком мал. // ÷òîáû èçáåæàòü çàâèñàíèÿ, åñëè N ñëèøêîì âåëèêî èëè äèàïàçîí ñëèøêîì ìàë.
if (!accepted) { if (!accepted) {
std::cerr << "Предупреждение: Не удалось сгенерировать " << N << " объектов. Сгенерировано: " << generatedCount << std::endl; std::cerr << "Ïðåäóïðåæäåíèå: Íå óäàëîñü ñãåíåðèðîâàòü " << N << " îáúåêòîâ. Ñãåíåðèðîâàíî: " << generatedCount << std::endl;
break; break;
} }
} }
@ -117,15 +117,20 @@ namespace ZL
return boxCoordsArr; return boxCoordsArr;
} }
Game::Game() Game::Game()
: window(nullptr) : window(nullptr)
, glContext(nullptr) , glContext(nullptr)
, newTickCount(0) , newTickCount(0)
, lastTickCount(0) , lastTickCount(0)
{ {
} std::vector<Vector3f> emissionPoints = {
Vector3f{-2.1f, 0.9f, 5.0f},
Vector3f{2.1f, 0.9f, 5.0f}
};
sparkEmitter = SparkEmitter(emissionPoints, 100.0f);
}
Game::~Game() { Game::~Game() {
if (glContext) { if (glContext) {
SDL_GL_DeleteContext(glContext); SDL_GL_DeleteContext(glContext);
} }
@ -133,9 +138,9 @@ Game::~Game() {
SDL_DestroyWindow(window); SDL_DestroyWindow(window);
} }
SDL_Quit(); SDL_Quit();
} }
void Game::setup() { void Game::setup() {
glContext = SDL_GL_CreateContext(ZL::Environment::window); glContext = SDL_GL_CreateContext(ZL::Environment::window);
ZL::BindOpenGlFunctions(); ZL::BindOpenGlFunctions();
@ -193,12 +198,16 @@ void Game::setup() {
boxRenderArr[i].RefreshVBO(); boxRenderArr[i].RefreshVBO();
} }
sparkTexture = std::make_unique<Texture>(CreateTextureDataFromPng("./resources/spark.png", CONST_ZIP_FILE));
sparkEmitter.setTexture(sparkTexture);
renderer.InitOpenGL(); renderer.InitOpenGL();
} }
void Game::drawCubemap() void Game::drawCubemap()
{ {
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env"; static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
@ -229,9 +238,10 @@ void Game::drawCubemap()
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
CheckGlError(); CheckGlError();
} }
void Game::drawShip()
{ void Game::drawShip()
{
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env"; static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
@ -255,6 +265,8 @@ void Game::drawShip()
glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID()); glBindTexture(GL_TEXTURE_2D, spaceshipTexture->getTexID());
renderer.DrawVertexRenderStruct(spaceship); renderer.DrawVertexRenderStruct(spaceship);
sparkEmitter.draw(renderer, Environment::zoom, Environment::width, Environment::height);
renderer.PopMatrix(); renderer.PopMatrix();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.DisableVertexAttribArray(vPositionName); renderer.DisableVertexAttribArray(vPositionName);
@ -262,10 +274,10 @@ void Game::drawShip()
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
CheckGlError(); CheckGlError();
} }
void Game::drawBoxes() void Game::drawBoxes()
{ {
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env"; static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
@ -303,9 +315,9 @@ void Game::drawBoxes()
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
CheckGlError(); CheckGlError();
} }
void Game::drawScene() { void Game::drawScene() {
static const std::string defaultShaderName = "default"; static const std::string defaultShaderName = "default";
static const std::string envShaderName = "env"; static const std::string envShaderName = "env";
static const std::string vPositionName = "vPosition"; static const std::string vPositionName = "vPosition";
@ -324,9 +336,9 @@ void Game::drawScene() {
drawBoxes(); drawBoxes();
CheckGlError(); CheckGlError();
} }
void Game::processTickCount() { void Game::processTickCount() {
if (lastTickCount == 0) { if (lastTickCount == 0) {
lastTickCount = SDL_GetTicks64(); lastTickCount = SDL_GetTicks64();
@ -339,6 +351,7 @@ void Game::processTickCount() {
CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount; CONST_MAX_TIME_INTERVAL : newTickCount - lastTickCount;
//gameObjects.updateScene(delta); //gameObjects.updateScene(delta);
sparkEmitter.update(static_cast<float>(delta));
if (Environment::tapDownHold) { if (Environment::tapDownHold) {
@ -375,7 +388,7 @@ void Game::processTickCount() {
if (fabs(Environment::shipVelocity) > 0.01f) if (fabs(Environment::shipVelocity) > 0.01f)
{ {
Vector3f velocityDirection = { 0,0, -Environment::shipVelocity*delta / 1000.f }; Vector3f velocityDirection = { 0,0, -Environment::shipVelocity * delta / 1000.f };
Vector3f velocityDirectionAdjusted = MultMatrixVector(Environment::shipMatrix, velocityDirection); Vector3f velocityDirectionAdjusted = MultMatrixVector(Environment::shipMatrix, velocityDirection);
Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted; Environment::shipPosition = Environment::shipPosition + velocityDirectionAdjusted;
@ -383,9 +396,9 @@ void Game::processTickCount() {
lastTickCount = newTickCount; lastTickCount = newTickCount;
} }
} }
void Game::render() { void Game::render() {
SDL_GL_MakeCurrent(ZL::Environment::window, glContext); SDL_GL_MakeCurrent(ZL::Environment::window, glContext);
ZL::CheckGlError(); ZL::CheckGlError();
@ -396,8 +409,8 @@ void Game::render() {
processTickCount(); processTickCount();
SDL_GL_SwapWindow(ZL::Environment::window); SDL_GL_SwapWindow(ZL::Environment::window);
} }
void Game::update() { void Game::update() {
SDL_Event event; SDL_Event event;
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) { if (event.type == SDL_QUIT) {
@ -405,23 +418,23 @@ void Game::update() {
} }
else if (event.type == SDL_MOUSEBUTTONDOWN) { else if (event.type == SDL_MOUSEBUTTONDOWN) {
// 1. Обработка нажатия кнопки мыши // 1. Îáðàáîòêà íàæàòèÿ êíîïêè ìûøè
Environment::tapDownHold = true; Environment::tapDownHold = true;
// Координаты начального нажатия // Êîîðäèíàòû íà÷àëüíîãî íàæàòèÿ
Environment::tapDownStartPos.v[0] = event.button.x; Environment::tapDownStartPos.v[0] = event.button.x;
Environment::tapDownStartPos.v[1] = event.button.y; Environment::tapDownStartPos.v[1] = event.button.y;
// Начальная позиция также становится текущей // Íà÷àëüíàÿ ïîçèöèÿ òàêæå ñòàíîâèòñÿ òåêóùåé
Environment::tapDownCurrentPos.v[0] = event.button.x; Environment::tapDownCurrentPos.v[0] = event.button.x;
Environment::tapDownCurrentPos.v[1] = event.button.y; Environment::tapDownCurrentPos.v[1] = event.button.y;
} }
else if (event.type == SDL_MOUSEBUTTONUP) { else if (event.type == SDL_MOUSEBUTTONUP) {
// 2. Обработка отпускания кнопки мыши // 2. Îáðàáîòêà îòïóñêàíèÿ êíîïêè ìûøè
Environment::tapDownHold = false; Environment::tapDownHold = false;
} }
else if (event.type == SDL_MOUSEMOTION) { else if (event.type == SDL_MOUSEMOTION) {
// 3. Обработка перемещения мыши // 3. Îáðàáîòêà ïåðåìåùåíèÿ ìûøè
if (Environment::tapDownHold) { if (Environment::tapDownHold) {
// Обновление текущей позиции, если кнопка удерживается // Îáíîâëåíèå òåêóùåé ïîçèöèè, åñëè êíîïêà óäåðæèâàåòñÿ
Environment::tapDownCurrentPos.v[0] = event.motion.x; Environment::tapDownCurrentPos.v[0] = event.motion.x;
Environment::tapDownCurrentPos.v[1] = event.motion.y; Environment::tapDownCurrentPos.v[1] = event.motion.y;
} }
@ -451,6 +464,6 @@ void Game::update() {
} }
} }
render(); render();
} }
} // namespace ZL } // namespace ZL

13
Game.h
View File

@ -1,9 +1,10 @@
#pragma once #pragma once
#include "OpenGlExtensions.h" #include "OpenGlExtensions.h"
#include "Renderer.h" #include "Renderer.h"
#include "Environment.h" #include "Environment.h"
#include "TextureManager.h" #include "TextureManager.h"
#include "SparkEmitter.h"
namespace ZL { namespace ZL {
@ -15,8 +16,8 @@ namespace ZL {
}; };
class Game { class Game {
public: public:
Game(); Game();
~Game(); ~Game();
@ -26,7 +27,7 @@ public:
bool shouldExit() const { return Environment::exitGameLoop; } bool shouldExit() const { return Environment::exitGameLoop; }
private: private:
void processTickCount(); void processTickCount();
void drawScene(); void drawScene();
void drawCubemap(); void drawCubemap();
@ -43,6 +44,7 @@ private:
static const size_t CONST_TIMER_INTERVAL = 10; static const size_t CONST_TIMER_INTERVAL = 10;
static const size_t CONST_MAX_TIME_INTERVAL = 1000; static const size_t CONST_MAX_TIME_INTERVAL = 1000;
std::shared_ptr<Texture> sparkTexture;
std::shared_ptr<Texture> spaceshipTexture; std::shared_ptr<Texture> spaceshipTexture;
std::shared_ptr<Texture> cubemapTexture; std::shared_ptr<Texture> cubemapTexture;
VertexDataStruct spaceshipBase; VertexDataStruct spaceshipBase;
@ -56,6 +58,7 @@ private:
std::vector<BoxCoords> boxCoordsArr; std::vector<BoxCoords> boxCoordsArr;
std::vector<VertexRenderStruct> boxRenderArr; std::vector<VertexRenderStruct> boxRenderArr;
}; SparkEmitter sparkEmitter;
};
} // namespace ZL } // namespace ZL

297
SparkEmitter.cpp Normal file
View File

@ -0,0 +1,297 @@
#include "SparkEmitter.h"
#include <random>
#include <cmath>
#include "OpenGlExtensions.h"
namespace ZL {
SparkEmitter::SparkEmitter()
: emissionRate(100.0f), isActive(true), drawDataDirty(true), maxParticles(200) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions, float rate)
: emissionPoints(positions), emissionRate(rate), isActive(true),
drawDataDirty(true), maxParticles(positions.size() * 100) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
SparkEmitter::SparkEmitter(const std::vector<Vector3f>& positions,
std::shared_ptr<Texture> tex,
float rate)
: emissionPoints(positions), texture(tex), emissionRate(rate),
isActive(true), drawDataDirty(true), maxParticles(positions.size() * 100) {
particles.resize(maxParticles);
drawPositions.reserve(maxParticles * 6);
drawTexCoords.reserve(maxParticles * 6);
lastEmissionTime = std::chrono::steady_clock::now();
sparkQuad.data = VertexDataStruct();
}
void SparkEmitter::setTexture(std::shared_ptr<Texture> tex) {
texture = tex;
}
void SparkEmitter::prepareDrawData() {
if (!drawDataDirty) return;
drawPositions.clear();
drawTexCoords.clear();
if (getActiveParticleCount() == 0) {
drawDataDirty = false;
return;
}
std::vector<std::pair<const SparkParticle*, float>> sortedParticles;
sortedParticles.reserve(getActiveParticleCount());
for (const auto& particle : particles) {
if (particle.active) {
sortedParticles.push_back({ &particle, particle.position.v[2] });
}
}
std::sort(sortedParticles.begin(), sortedParticles.end(),
[](const auto& a, const auto& b) {
return a.second > b.second;
});
for (const auto& [particlePtr, depth] : sortedParticles) {
const auto& particle = *particlePtr;
Vector3f pos = particle.position;
float size = 0.04f * particle.scale;
drawPositions.push_back({ pos.v[0] - size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ pos.v[0] - size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 1.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ pos.v[0] - size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 0.0f, 0.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] + size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 1.0f });
drawPositions.push_back({ pos.v[0] + size, pos.v[1] - size, pos.v[2] });
drawTexCoords.push_back({ 1.0f, 0.0f });
}
drawDataDirty = false;
}
void SparkEmitter::draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight) {
if (getActiveParticleCount() == 0) {
return;
}
if (!texture) {
return;
}
prepareDrawData();
if (drawPositions.empty()) {
return;
}
sparkQuad.data.PositionData = drawPositions;
sparkQuad.data.TexCoordData = drawTexCoords;
sparkQuad.RefreshVBO();
static const std::string defaultShaderName = "default";
static const std::string vPositionName = "vPosition";
static const std::string vTexCoordName = "vTexCoord";
static const std::string textureUniformName = "Texture";
renderer.shaderManager.PushShader(defaultShaderName);
renderer.RenderUniform1i(textureUniformName, 0);
renderer.EnableVertexAttribArray(vPositionName);
renderer.EnableVertexAttribArray(vTexCoordName);
float aspectRatio = static_cast<float>(screenWidth) / static_cast<float>(screenHeight);
renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5, aspectRatio, 1, 1000);
glBindTexture(GL_TEXTURE_2D, texture->getTexID());
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE);// Аддитивное смешивание для эффекта свечения
renderer.PushMatrix();
renderer.LoadIdentity();
renderer.TranslateMatrix({ 0, 0, -1.0f * zoom });
renderer.DrawVertexRenderStruct(sparkQuad);
renderer.PopMatrix();
renderer.PopProjectionMatrix();
glDisable(GL_BLEND);
renderer.DisableVertexAttribArray(vPositionName);
renderer.DisableVertexAttribArray(vTexCoordName);
renderer.shaderManager.PopShader();
}
void SparkEmitter::update(float deltaTimeMs) {
auto currentTime = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
currentTime - lastEmissionTime).count();
if (isActive && elapsed >= emissionRate) {
emit();
lastEmissionTime = currentTime;
drawDataDirty = true;
}
bool anyChanged = false;
for (auto& particle : particles) {
if (particle.active) {
Vector3f oldPosition = particle.position;
float oldScale = particle.scale;
particle.position.v[0] += particle.velocity.v[0] * deltaTimeMs / 1000.0f;
particle.position.v[1] += particle.velocity.v[1] * deltaTimeMs / 1000.0f;
particle.position.v[2] += particle.velocity.v[2] * deltaTimeMs / 1000.0f;
particle.lifeTime += deltaTimeMs;
if (particle.lifeTime >= particle.maxLifeTime) {
particle.active = false;
anyChanged = true;
}
else {
float lifeRatio = particle.lifeTime / particle.maxLifeTime;
particle.scale = 1.0f - lifeRatio * 0.8f;
if (oldPosition.v[0] != particle.position.v[0] ||
oldPosition.v[1] != particle.position.v[1] ||
oldPosition.v[2] != particle.position.v[2] ||
oldScale != particle.scale) {
anyChanged = true;
}
}
}
}
if (anyChanged) {
drawDataDirty = true;
}
}
void SparkEmitter::emit() {
if (emissionPoints.empty()) return;
bool emitted = false;
for (int i = 0; i < emissionPoints.size(); ++i) {
bool particleFound = false;
for (auto& particle : particles) {
if (!particle.active) {
initParticle(particle, i);
particle.active = true;
particle.lifeTime = 0;
particle.position = emissionPoints[i];
particle.emitterIndex = i;
particleFound = true;
emitted = true;
break;
}
}
if (!particleFound && !particles.empty()) {
size_t oldestIndex = 0;
float maxLifeTime = 0;
for (size_t j = 0; j < particles.size(); ++j) {
if (particles[j].lifeTime > maxLifeTime) {
maxLifeTime = particles[j].lifeTime;
oldestIndex = j;
}
}
initParticle(particles[oldestIndex], i);
particles[oldestIndex].active = true;
particles[oldestIndex].lifeTime = 0;
particles[oldestIndex].position = emissionPoints[i];
particles[oldestIndex].emitterIndex = i;
emitted = true;
}
}
if (emitted) {
drawDataDirty = true;
}
}
void SparkEmitter::setEmissionPoints(const std::vector<Vector3f>& positions) {
emissionPoints = positions;
maxParticles = positions.size() * 100;
particles.resize(maxParticles);
drawDataDirty = true;
}
void SparkEmitter::initParticle(SparkParticle& particle, int emitterIndex) {
particle.velocity = getRandomVelocity(emitterIndex);
particle.scale = 1.0f;
particle.maxLifeTime = 800.0f + (rand() % 400);
particle.emitterIndex = emitterIndex;
}
Vector3f SparkEmitter::getRandomVelocity(int emitterIndex) {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<> angleDist(0, 2 * M_PI);
static std::uniform_real_distribution<> speedDist(0.5f, 2.0f);
static std::uniform_real_distribution<> zSpeedDist(1.0f, 3.0f);
float angle = angleDist(gen);
float speed = speedDist(gen);
float zSpeed = zSpeedDist(gen);
if (emitterIndex == 0) {
return Vector3f{
cosf(angle) * speed - 0.3f,
sinf(angle) * speed,
zSpeed
};
}
else {
return Vector3f{
cosf(angle) * speed + 0.3f,
sinf(angle) * speed,
zSpeed
};
}
}
const std::vector<SparkParticle>& SparkEmitter::getParticles() const {
return particles;
}
size_t SparkEmitter::getActiveParticleCount() const {
size_t count = 0;
for (const auto& particle : particles) {
if (particle.active) {
count++;
}
}
return count;
}
} // namespace ZL

66
SparkEmitter.h Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#include "ZLMath.h"
#include "Renderer.h"
#include "TextureManager.h"
#include <vector>
#include <chrono>
namespace ZL {
struct SparkParticle {
Vector3f position;
Vector3f velocity;
float scale;
float lifeTime;
float maxLifeTime;
bool active;
int emitterIndex;
SparkParticle() : position({ 0,0,0 }), velocity({ 0,0,0 }), scale(1.0f),
lifeTime(0), maxLifeTime(1000.0f), active(false), emitterIndex(0) {
}
};
class SparkEmitter {
private:
std::vector<SparkParticle> particles;
std::vector<Vector3f> emissionPoints;
std::chrono::steady_clock::time_point lastEmissionTime;
float emissionRate;
bool isActive;
std::vector<Vector3f> drawPositions;
std::vector<Vector2f> drawTexCoords;
bool drawDataDirty;
VertexRenderStruct sparkQuad;
std::shared_ptr<Texture> texture;
int maxParticles;
void prepareDrawData();
public:
SparkEmitter();
SparkEmitter(const std::vector<Vector3f>& positions, float rate = 100.0f);
SparkEmitter(const std::vector<Vector3f>& positions,
std::shared_ptr<Texture> tex,
float rate = 100.0f);
void setEmissionPoints(const std::vector<Vector3f>& positions);
void setTexture(std::shared_ptr<Texture> tex);
void update(float deltaTimeMs);
void emit();
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
const std::vector<SparkParticle>& getParticles() const;
size_t getActiveParticleCount() const;
private:
void initParticle(SparkParticle& particle, int emitterIndex);
Vector3f getRandomVelocity(int emitterIndex);
};
} // namespace ZL

BIN
resources/spark.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -5,8 +5,8 @@ varying vec2 texCoord;
void main() void main()
{ {
vec4 color = texture2D(Texture,texCoord).rgba; vec4 color = texture2D(Texture,texCoord).rgba;
gl_FragColor = vec4(color.rgb*0.9 + vec3(0.1, 0.1, 0.1), 1.0); //gl_FragColor = vec4(color.rgb*0.9 + vec3(0.1, 0.1, 0.1), 1.0);
//gl_FragColor = color; gl_FragColor = color;
} }