323 lines
11 KiB
C++
Executable File
323 lines
11 KiB
C++
Executable File
/*
|
|
* Created by Brett on 30/03/23.
|
|
* Licensed under GNU General Public License V3.0
|
|
* See LICENSE file for license detail
|
|
*/
|
|
|
|
#ifndef ASSIGN3_PARTICLE_SYSTEM_H
|
|
#define ASSIGN3_PARTICLE_SYSTEM_H
|
|
|
|
#include <texture.h>
|
|
#include <unordered_map>
|
|
#include <blt/std/random.h>
|
|
#include <vector>
|
|
#include <queue>
|
|
#include <algorithm>
|
|
#include <util.h>
|
|
#include <sstream>
|
|
#include <config.h>
|
|
|
|
struct particle_t {
|
|
vec pos, dir;
|
|
float speed;
|
|
float age;
|
|
};
|
|
|
|
class particle_system {
|
|
private:
|
|
std::unordered_map<unsigned int, std::vector<particle_t*>> particles;
|
|
blt::vec3 position;
|
|
blt::vec3 direction;
|
|
float spread;
|
|
int pps;
|
|
// current number of particles spawned in the last second
|
|
int current_pps = 0;
|
|
// delta to keep track of pps relative to the framerate
|
|
double delta = 0;
|
|
enum status_t {
|
|
CONTINUOUS = 0, MANUAL = 1, SINGLE = 2
|
|
};
|
|
enum spray_status_t {
|
|
DEFAULT = 0, LOW = 1, HIGH = 2
|
|
};
|
|
|
|
status_t status = CONTINUOUS;
|
|
spray_status_t sprayMode = DEFAULT;
|
|
const float sprays[3] = {1, 5, 10};
|
|
|
|
bool applyFriction = true;
|
|
bool randomizeTexture = false;
|
|
|
|
blt::random<float> DIRECTION_RANDOMIZER{-1, 1};
|
|
blt::random<float> TEXTURE_RANDOMIZER{0, 9};
|
|
|
|
/*
|
|
*
|
|
* spawns a new particle in the world using the particle system's origin and direction
|
|
*
|
|
*/
|
|
void spawnParticle() {
|
|
auto* p = new particle_t;
|
|
|
|
constexpr float SPEED_FACTOR = 25;
|
|
|
|
unsigned int textureID = 0;
|
|
|
|
if (randomizeTexture)
|
|
textureID = (int) round(TEXTURE_RANDOMIZER.get());
|
|
|
|
p->pos = conv(position);
|
|
auto offset = blt::vec3{DIRECTION_RANDOMIZER.get(), 0, DIRECTION_RANDOMIZER.get()};
|
|
p->dir = conv(direction * SPEED_FACTOR + offset.normalize() * spread * sprays[sprayMode]);
|
|
p->speed = 1.0;
|
|
p->age = 0;
|
|
|
|
particles[textureID].push_back(p);
|
|
}
|
|
|
|
/*
|
|
*
|
|
* returns the relative distance to between the particle pos and the provided pos
|
|
*
|
|
*/
|
|
inline static double distance(const particle_t* p, const blt::vec3& pos){
|
|
const auto ppos = p->pos;
|
|
auto dx = ppos.x - pos.x();
|
|
auto dy = ppos.y - pos.y();
|
|
auto dz = ppos.z - pos.z();
|
|
return dx * dx + dy * dy + dz * dz;
|
|
}
|
|
|
|
unsigned int quad = 0;
|
|
|
|
const float s = 0.5;
|
|
const float PARTICLE_LIFETIME = 25;
|
|
public:
|
|
particle_system(
|
|
const blt::vec3& position, const blt::vec3& direction, float spread, int pps
|
|
): position(position), direction(direction), spread(spread), pps(pps) {
|
|
#ifndef EXTRAS
|
|
quad = glGenLists(1);
|
|
glNewList(quad, GL_COMPILE);
|
|
glBegin(GL_QUADS);
|
|
glTexCoord2f(1, 0);
|
|
glVertex3f(-s, s, 0);
|
|
glTexCoord2f(1, 1);
|
|
glVertex3f(-s, -s, 0);
|
|
glTexCoord2f(0, 1);
|
|
glVertex3f(s, -s, 0);
|
|
glTexCoord2f(0, 0);
|
|
glVertex3f(s, s, 0);
|
|
glEnd();
|
|
glEndList();
|
|
#endif
|
|
}
|
|
|
|
void update(camera& cam, float bnx, float bnz, float bpx, float bpz) {
|
|
// used to keep updates correct without being dependent on FPS
|
|
auto frameDeltaSeconds = ((double) getDelta() / 1000000000.0);
|
|
const auto gravity_vec = vec{0, -GRAVITY, 0};
|
|
|
|
for (auto& pair : particles) {
|
|
std::queue<particle_t*> deleteList;
|
|
for (auto& particle : pair.second) {
|
|
auto ppos = particle->pos;
|
|
particle->pos = ppos + particle->dir * particle->speed * frameDeltaSeconds;
|
|
particle->age += (float) frameDeltaSeconds;
|
|
|
|
float BOUNCE_FACTOR = 0.75;
|
|
|
|
// bounce particle
|
|
if (particle->pos.y < 0 && particle->pos.x > bnx && particle->pos.x < bpx &&
|
|
particle->pos.z > bnz && particle->pos.z < bpz) {
|
|
// NOTE: I am not deleting stationary particles because I think they look cool sitting on the floor.
|
|
if (!applyFriction)
|
|
BOUNCE_FACTOR = 1.0;
|
|
particle->dir.y = -particle->dir.y * BOUNCE_FACTOR;
|
|
particle->pos = ppos;
|
|
}
|
|
|
|
// remove particles outside the bounds of the world
|
|
if (particle->pos.y < -50 || particle->age > PARTICLE_LIFETIME) {
|
|
deleteList.push(particle);
|
|
continue;
|
|
}
|
|
|
|
particle->dir = particle->dir + gravity_vec * frameDeltaSeconds;
|
|
}
|
|
// std::erase_if would be better here (c++20)
|
|
while (!deleteList.empty()) {
|
|
auto*& particle = deleteList.front();
|
|
|
|
auto itr = std::find(pair.second.begin(), pair.second.end(), particle);
|
|
if (itr != pair.second.end())
|
|
pair.second.erase(itr);
|
|
|
|
delete (particle);
|
|
|
|
deleteList.pop();
|
|
}
|
|
}
|
|
int particlesToSpawn = (int) (round(pps * frameDeltaSeconds));
|
|
|
|
if (current_pps + particlesToSpawn > pps)
|
|
particlesToSpawn = pps - current_pps;
|
|
|
|
if (status == CONTINUOUS) {
|
|
for (int i = 0; i < particlesToSpawn; i++)
|
|
spawnParticle();
|
|
} else if (status == MANUAL) {
|
|
if (cam.isKeyPressed('f'))
|
|
for (int i = 0; i < particlesToSpawn; i++)
|
|
spawnParticle();
|
|
}
|
|
|
|
delta += frameDeltaSeconds;
|
|
if (delta > 1.0) {
|
|
current_pps = 0;
|
|
delta = 0;
|
|
}
|
|
}
|
|
|
|
inline static void applyBillboard() {
|
|
// i was going to merge the extra renderer into the particle system, but I have ran out of time as outlined.
|
|
#ifndef EXTRAS
|
|
GLfloat m[16];
|
|
glMatrixMode(GL_MODELVIEW);
|
|
glGetFloatv(GL_MODELVIEW_MATRIX, m);
|
|
// undo all rotations (and scaling!) by setting rotation part to the identity matrix
|
|
for (int i = 0; i < 3; i++) {
|
|
for (int j = 0; j < 3; j++) {
|
|
if (i == j)
|
|
m[i * 4 + j] = 1.0;
|
|
else
|
|
m[i * 4 + j] = 0.0;
|
|
}
|
|
}
|
|
|
|
glLoadMatrixf(m);
|
|
#endif
|
|
}
|
|
|
|
void render(camera& cam, texture** textures) {
|
|
#ifndef EXTRAS
|
|
glMatrixMode(GL_MODELVIEW);
|
|
// by batching particles by texture we save a little driver overhead
|
|
for (auto& pair : particles) {
|
|
//glBindTexture(GL_TEXTURE_2D, );
|
|
textures[pair.first]->bind();
|
|
auto& particles_vec = pair.second;
|
|
|
|
// since we have enabled transparency, we need to sort the particles based on the distance to the camera.
|
|
// TODO: this does not account for bill-boarding / rotation!
|
|
// TODO: std::sort is likely a quick-sort derivative, insertion sort would be better as particles are always* mostly* sorted!
|
|
std::sort(particles_vec.begin(), particles_vec.end(), [&](const particle_t* p1, const particle_t* p2) {
|
|
auto d1 = distance(p1, cam.getPosition());
|
|
auto d2 = distance(p2, cam.getPosition());
|
|
return d1 > d2;
|
|
});
|
|
|
|
for (auto& p : particles_vec) {
|
|
glPushMatrix();
|
|
glTranslatef(p->pos.x, p->pos.y, p->pos.z);
|
|
|
|
// due to the inefficiency of getting the modelview and updating it to billboard,
|
|
// this will cause a considerable slow down.
|
|
applyBillboard();
|
|
|
|
glCallList(quad);
|
|
|
|
glPopMatrix();
|
|
}
|
|
}
|
|
|
|
auto err = glGetError();
|
|
if (err != 0)
|
|
std::cout << "GL Error: " << err << "\n";
|
|
{
|
|
std::stringstream str;
|
|
str << WINDOW_TITLE;
|
|
str << " | Particles: ";
|
|
size_t count = 0;
|
|
for (const auto& pair : particles)
|
|
count += pair.second.size();
|
|
str << count;
|
|
str << " FPS: ";
|
|
str << 1000000000.0 / (double)getDelta();
|
|
str << " | Status: ";
|
|
str << (status == CONTINUOUS ? "Continuous" : status == MANUAL ? "Manual" : "Single");
|
|
str << " | Friction: ";
|
|
str << (applyFriction ? "True" : "False");
|
|
str << " | Spray: ";
|
|
str << (sprayMode == DEFAULT ? "Default" : sprayMode == LOW ? "Low" : "High");
|
|
str << " | Randomize: ";
|
|
str << (randomizeTexture ? "True" : "False");
|
|
glutSetWindowTitle(str.str().c_str());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void randomizeSpeed(float n) {
|
|
blt::random<float> SPEED_RANDOMIZER{1, n};
|
|
|
|
for (auto& pair : particles)
|
|
for (auto& p : pair.second)
|
|
p->speed = SPEED_RANDOMIZER.get();
|
|
}
|
|
|
|
void setFriction(bool f){
|
|
applyFriction = f;
|
|
}
|
|
|
|
[[nodiscard]] bool getFriction() const {
|
|
return applyFriction;
|
|
}
|
|
|
|
void toggleSpray(){
|
|
int currentSpray = sprayMode;
|
|
// increment and bound the spray pattern
|
|
currentSpray++;
|
|
if (currentSpray > 2)
|
|
currentSpray = 0;
|
|
sprayMode = static_cast<spray_status_t>(currentSpray);
|
|
}
|
|
|
|
void toggleTexRandomizer() {
|
|
randomizeTexture = !randomizeTexture;
|
|
}
|
|
|
|
void changeFiringMode(int mode){
|
|
// wrap
|
|
if (mode < 0)
|
|
mode = SINGLE;
|
|
if (mode > 2)
|
|
mode = CONTINUOUS;
|
|
// convert
|
|
status = static_cast<status_t>(mode);
|
|
}
|
|
|
|
int getFiringMode(){
|
|
return status;
|
|
}
|
|
|
|
void singleFire(){
|
|
BLT_TRACE("Hello!");
|
|
if (status == SINGLE)
|
|
spawnParticle();
|
|
}
|
|
|
|
void reset(){
|
|
particles.clear();
|
|
}
|
|
|
|
~particle_system() {
|
|
glDeleteLists(quad, 1);
|
|
for (const auto& pair : particles) {
|
|
for (auto& particle : pair.second)
|
|
delete (particle);
|
|
}
|
|
}
|
|
};
|
|
|
|
#endif //ASSIGN3_PARTICLE_SYSTEM_H
|