--- /dev/null
+/*
+This file is part of the 3dengfx, realtime visualization system.
+
+Copyright (c) 2004, 2005 John Tsiombikas <nuclear@siggraph.org>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+*/
+
+#include <vector>
+#include <cmath>
+#include "3dengfx_config.h"
+#include "3denginefx.hpp"
+#include "texman.hpp"
+#include "psys.hpp"
+#include "common/config_parser.h"
+#include "common/err_msg.h"
+
+#ifdef SINGLE_PRECISION_MATH
+#define GL_SCALAR_TYPE GL_FLOAT
+#else
+#define GL_SCALAR_TYPE GL_DOUBLE
+#endif // SINGLE_PRECISION_MATH
+
+static scalar_t global_time;
+
+// just a trial and error constant to match point-sprite size with billboard size
+#define PSPRITE_BILLBOARD_RATIO 100
+
+// particle rendering state
+static bool use_psprites = true;
+static bool volatile_particles = false;
+
+Fuzzy::Fuzzy(scalar_t num, scalar_t range) {
+ this->num = num;
+ this->range = range;
+}
+
+scalar_t Fuzzy::operator()() const {
+ return range == 0.0 ? num : frand(range) + num - range / 2.0;
+}
+
+
+FuzzyVec3::FuzzyVec3(const Fuzzy &x, const Fuzzy &y, const Fuzzy &z) {
+ this->x = x;
+ this->y = y;
+ this->z = z;
+}
+
+Vector3 FuzzyVec3::operator()() const {
+ return Vector3(x(), y(), z());
+}
+
+
+Particle::Particle() {
+ friction = 1.0;
+ lifespan = 0;
+ birth_time = 0;
+}
+
+Particle::Particle(const Vector3 &pos, const Vector3 &vel, scalar_t friction, scalar_t lifespan) {
+ set_position(pos);
+ velocity = vel;
+ this->friction = friction;
+ this->lifespan = lifespan;
+ birth_time = global_time;
+}
+
+Particle::~Particle(){}
+
+bool Particle::alive() const {
+ return global_time - birth_time < lifespan;
+}
+
+void Particle::update(const Vector3 &ext_force) {
+ scalar_t time = global_time - birth_time;
+ if(time > lifespan) return;
+
+ velocity = (velocity + ext_force) * friction;
+ translate(velocity); // update position
+}
+
+void BillboardParticle::update(const Vector3 &ext_force) {
+ Particle::update(ext_force);
+
+ scalar_t time = global_time - birth_time;
+ if(time > lifespan) return;
+ scalar_t t = time / lifespan;
+
+ color = blend_colors(start_color, end_color, t);
+
+ size = size_start + (size_end - size_start) * t;
+
+ angle = rot * time + birth_angle;
+}
+
+/* NOTE:
+ * if we use point sprites, and the particles are not rotating, then the
+ * calling function has taken care to call glBegin() before calling this.
+ */
+void BillboardParticle::draw() const {
+ Matrix4x4 tex_rot;
+ if(volatile_particles) {
+ tex_rot.translate(Vector3(0.5, 0.5, 0.0));
+ tex_rot.rotate(Vector3(0.0, 0.0, angle));
+ tex_rot.translate(Vector3(-0.5, -0.5, 0.0));
+ set_matrix(XFORM_TEXTURE, tex_rot);
+ }
+
+ Vector3 pos = get_position();
+
+ if(use_psprites) {
+ if(volatile_particles) {
+ glPointSize(size);
+
+ glBegin(GL_POINTS);
+ glColor4f(color.r, color.g, color.b, color.a);
+ glVertex3f(pos.x, pos.y, pos.z);
+ glEnd();
+ } else {
+ glColor4f(color.r, color.g, color.b, color.a);
+ glVertex3f(pos.x, pos.y, pos.z);
+ }
+ } else { // don't use point sprites
+ Vertex v(pos, 0, 0, color);
+ draw_point(v, size / PSPRITE_BILLBOARD_RATIO);
+ }
+}
+
+
+
+ParticleSysParams::ParticleSysParams() {
+ psize_end = -1.0;
+ friction = 0.95;
+ billboard_tex = 0;
+ halo = 0;
+ rot = 0.0;
+ glob_rot = 0.0;
+ halo_rot = 0.0;
+ big_particles = false;
+ spawn_offset_curve = 0;
+ spawn_offset_curve_area = Fuzzy(0.5, 1.0);
+
+ src_blend = BLEND_SRC_ALPHA;
+ dest_blend = BLEND_ONE;
+}
+
+
+
+ParticleSystem::ParticleSystem(const char *fname) {
+ timeslice = 1.0 / 50.0;
+ SysCaps sys_caps = get_system_capabilities();
+ /* XXX: My Radeon Mobility 9000 supports point sprites but does not say so
+ * in the extension string, it only has point params there. So I changed this
+ * condition to && since probably if a card supports one, it will also support
+ * the other.
+ */
+ psprites_unsupported = !sys_caps.point_sprites && !sys_caps.point_params;
+
+ prev_update = -1.0;
+ fraction = 0.0;
+ ptype = PTYPE_BILLBOARD;
+
+ ready = true;
+
+ if(fname) {
+ if(!psys::load_particle_sys_params(fname, &psys_params)) {
+ error("Error loading particle file: %s", fname);
+ ready = false;
+ }
+ }
+}
+
+ParticleSystem::~ParticleSystem() {
+ reset();
+}
+
+void ParticleSystem::reset() {
+ prev_update = -1.0;
+ std::list<Particle*>::iterator iter = particles.begin();
+ while(iter != particles.end()) {
+ delete *iter++;
+ }
+ particles.clear();
+}
+
+void ParticleSystem::set_update_interval(scalar_t timeslice) {
+ this->timeslice = timeslice;
+}
+
+void ParticleSystem::set_params(const ParticleSysParams &psys_params) {
+ this->psys_params = psys_params;
+}
+
+ParticleSysParams *ParticleSystem::get_params() {
+ return &psys_params;
+}
+
+void ParticleSystem::set_particle_type(ParticleType ptype) {
+ this->ptype = ptype;
+}
+
+void ParticleSystem::update(const Vector3 &ext_force) {
+ if(!ready) return;
+
+ curr_time = global_time;
+ int updates_missed = (int)round((global_time - prev_update) / timeslice);
+
+ if(!updates_missed) return; // less than a timeslice has elapsed, nothing to do
+
+ PRS prs = get_prs((unsigned long)(global_time * 1000.0));
+ curr_pos = prs.position;
+ curr_halo_rot = psys_params.halo_rot * global_time;
+
+ curr_rot = fmod(psys_params.glob_rot * global_time, two_pi);
+
+ // spawn new particles
+ scalar_t spawn = psys_params.birth_rate() * (global_time - prev_update);
+ int spawn_count = (int)round(spawn);
+
+ // handle sub-timeslice spawning rates
+ fraction += spawn - round(spawn);
+ if(fraction > 1.0) {
+ fraction -= 1.0;
+ spawn_count++;
+ } else if(fraction < -1.0) {
+ fraction += 1.0;
+ spawn_count--;
+ }
+
+ Vector3 dp, pos;
+ if(prev_update < 0.0) {
+ prev_pos = curr_pos;
+ prev_update = curr_time;
+ return;
+ } else {
+ dp = (curr_pos - prev_pos) / (scalar_t)spawn_count;
+ pos = prev_pos;
+ }
+
+ scalar_t dt = (global_time - prev_update) / (scalar_t)spawn_count;
+ scalar_t t = prev_update;
+
+ for(int i=0; i<spawn_count; i++) {
+ Particle *particle;
+
+ switch(ptype) {
+ case PTYPE_BILLBOARD:
+ particle = new BillboardParticle;
+ {
+ curr_rot = fmod(psys_params.glob_rot * t, two_pi);
+
+ BillboardParticle *bbp = (BillboardParticle*)particle;
+ bbp->texture = psys_params.billboard_tex;
+ bbp->start_color = psys_params.start_color;
+ bbp->end_color = psys_params.end_color;
+ bbp->rot = psys_params.rot;
+ bbp->birth_angle = curr_rot;
+ }
+
+ break;
+
+ default:
+ error("Only billboarded particles implemented currently");
+ exit(-1);
+ break;
+ }
+
+ //PRS sub_prs = get_prs((unsigned long)(t * 1000.0));
+
+
+ Vector3 offset = psys_params.spawn_offset();
+ if(psys_params.spawn_offset_curve) {
+ float t = psys_params.spawn_offset_curve_area();
+ offset += (*psys_params.spawn_offset_curve)(t);
+ }
+ // XXX: correct this rotation to span the whole interval
+ particle->set_position(pos + offset.transformed(prs.rotation));
+ particle->set_rotation(prs.rotation);
+ particle->set_scaling(prs.scale);
+
+ particle->size_start = psys_params.psize();
+ if(psys_params.psize_end < 0.0) {
+ particle->size_end = particle->size_start;
+ } else {
+ particle->size_end = psys_params.psize_end;
+ }
+ particle->velocity = psys_params.shoot_dir().transformed(prs.rotation); // XXX: correct this rotation to span the interval
+ particle->friction = psys_params.friction;
+ particle->birth_time = t;
+ particle->lifespan = psys_params.lifespan();
+
+ particles.push_back(particle);
+
+ pos += dp;
+ t += dt;
+ }
+
+
+ // update particles
+
+ std::list<Particle*>::iterator iter = particles.begin();
+ while(iter != particles.end()) {
+ Particle *p = *iter;
+ int i = 0;
+ while(p->alive() && i++ < updates_missed) {
+ p->update(psys_params.gravity);
+ }
+
+ if(p->alive()) {
+ iter++;
+ } else {
+ delete *iter;
+ iter = particles.erase(iter);
+ }
+ }
+
+ prev_update = global_time;
+ prev_pos = curr_pos;
+}
+
+void ParticleSystem::draw() const {
+ if(!ready) return;
+
+ // use point sprites if the system supports them AND we don't need big particles
+ use_psprites = !psys_params.big_particles && !psprites_unsupported;
+
+ // particles are volatile if they rotate OR they fluctuate in size
+ volatile_particles = psys_params.rot > small_number || psys_params.psize.range > small_number;
+
+ set_matrix(XFORM_WORLD, Matrix4x4());
+ load_xform_matrices();
+
+ std::list<Particle*>::const_iterator iter = particles.begin();
+ if(iter != particles.end()) {
+
+ if(ptype == PTYPE_BILLBOARD) {
+ // ------ setup render state ------
+ set_lighting(false);
+ set_zwrite(false);
+ set_alpha_blending(true);
+ set_blend_func(psys_params.src_blend, psys_params.dest_blend);
+
+ if(psys_params.billboard_tex) {
+ enable_texture_unit(0);
+ disable_texture_unit(1);
+ set_texture(0, psys_params.billboard_tex);
+ set_texture_addressing(0, TEXADDR_CLAMP, TEXADDR_CLAMP);
+
+ if(use_psprites) {
+ set_point_sprites(true);
+ set_point_sprite_coords(0, true);
+ }
+
+ if(!volatile_particles) {
+ Matrix4x4 prot;
+ prot.translate(Vector3(0.5, 0.5, 0.0));
+ prot.rotate(Vector3(0.0, 0.0, curr_rot));
+ prot.translate(Vector3(-0.5, -0.5, 0.0));
+ set_matrix(XFORM_TEXTURE, prot);
+ }
+ }
+
+ set_texture_unit_color(0, TOP_MODULATE, TARG_TEXTURE, TARG_PREV);
+ set_texture_unit_alpha(0, TOP_MODULATE, TARG_TEXTURE, TARG_PREV);
+
+ if(use_psprites && !volatile_particles) {
+ glPointSize((*iter)->size);
+ glBegin(GL_POINTS);
+ }
+ }
+
+ // ------ render particles ------
+ while(iter != particles.end()) {
+ (*iter++)->draw();
+ }
+
+ if(ptype == PTYPE_BILLBOARD) {
+ // ------ restore render states -------
+ if(use_psprites) {
+ if(!volatile_particles) glEnd();
+ glPointSize(1.0);
+ }
+
+ if(psys_params.billboard_tex) {
+ if(use_psprites) {
+ set_point_sprites(true);
+ set_point_sprite_coords(0, true);
+ }
+ set_texture_addressing(0, TEXADDR_WRAP, TEXADDR_WRAP);
+ disable_texture_unit(0);
+
+ set_matrix(XFORM_TEXTURE, Matrix4x4::identity_matrix);
+ }
+
+ set_alpha_blending(false);
+ set_zwrite(true);
+ set_lighting(true);
+ }
+ }
+
+ // ------ render a halo around the emitter if we need to ------
+ if(psys_params.halo) {
+ // construct texture matrix for halo rotation
+ Matrix4x4 mat;
+ mat.translate(Vector3(0.5, 0.5, 0.0));
+ mat.rotate(Vector3(0, 0, curr_halo_rot));
+ mat.translate(Vector3(-0.5, -0.5, 0.0));
+ set_matrix(XFORM_TEXTURE, mat);
+
+ set_alpha_blending(true);
+ set_blend_func(BLEND_SRC_ALPHA, BLEND_ONE);
+ enable_texture_unit(0);
+ disable_texture_unit(1);
+ set_texture(0, psys_params.halo);
+ set_zwrite(false);
+ Vertex v(curr_pos, 0, 0, psys_params.halo_color);
+ draw_point(v, psys_params.halo_size() / PSPRITE_BILLBOARD_RATIO);
+ set_zwrite(true);
+ disable_texture_unit(0);
+ set_alpha_blending(false);
+
+ set_matrix(XFORM_TEXTURE, Matrix4x4::identity_matrix);
+ }
+}
+
+
+void psys::set_global_time(unsigned long msec) {
+ global_time = (scalar_t)msec / 1000.0;
+}
+
+
+static Vector3 get_vector(const char *str) {
+ char *buf = new char[strlen(str) + 1];
+ strcpy(buf, str);
+
+ Vector3 res;
+
+ char *beg = buf;
+ char *ptr;
+
+ res.x = atof(beg);
+ if(!(ptr = strchr(beg, ','))) {
+ delete [] buf;
+ return res;
+ }
+ *ptr = 0;
+ beg = ptr + 1;
+
+ res.y = atof(beg);
+ if(!(ptr = strchr(beg, ','))) {
+ delete [] buf;
+ return res;
+ }
+ *ptr = 0;
+ beg = ptr + 1;
+
+ res.z = atof(beg);
+
+ delete [] buf;
+ return res;
+}
+
+static Vector4 get_vector4(const char *str) {
+ char *buf = new char[strlen(str) + 1];
+ strcpy(buf, str);
+
+ Vector4 res;
+
+ char *beg = buf;
+ char *ptr;
+
+ res.x = atof(beg);
+ if(!(ptr = strchr(beg, ','))) {
+ delete [] buf;
+ return res;
+ }
+ *ptr = 0;
+ beg = ptr + 1;
+
+ res.y = atof(beg);
+ if(!(ptr = strchr(beg, ','))) {
+ delete [] buf;
+ return res;
+ }
+ *ptr = 0;
+ beg = ptr + 1;
+
+ res.z = atof(beg);
+ if(!(ptr = strchr(beg, ','))) {
+ delete [] buf;
+ return res;
+ }
+ *ptr = 0;
+ beg = ptr + 1;
+
+ res.w = atof(beg);
+
+ delete [] buf;
+ return res;
+}
+
+
+bool psys::load_particle_sys_params(const char *fname, ParticleSysParams *psp) {
+ Vector3 shoot, shoot_range;
+ Vector3 spawn_off, spawn_off_range;
+
+ set_parser_state(PS_AssignmentSymbol, ':');
+ set_parser_state(PS_CommentSymbol, '#');
+
+ if(load_config_file(fname) == -1) return false;
+
+ const ConfigOption *opt;
+ while((opt = get_next_option())) {
+
+ if(!strcmp(opt->option, "psize")) {
+ psp->psize.num = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "psize-r")) {
+ psp->psize.range = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "psize_end")) {
+ psp->psize_end = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "life")) {
+ psp->lifespan.num = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "life-r")) {
+ psp->lifespan.range = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "birth-rate")) {
+ psp->birth_rate.num = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "birth-rate-r")) {
+ psp->birth_rate.range = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "grav")) {
+ psp->gravity = get_vector(opt->str_value);
+
+ } else if(!strcmp(opt->option, "shoot")) {
+ shoot = get_vector(opt->str_value);
+
+ } else if(!strcmp(opt->option, "shoot-r")) {
+ shoot_range = get_vector(opt->str_value);
+
+ } else if(!strcmp(opt->option, "friction")) {
+ psp->friction = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "spawn_off")) {
+ spawn_off = get_vector(opt->str_value);
+
+ } else if(!strcmp(opt->option, "spawn_off-r")) {
+ spawn_off_range = get_vector(opt->str_value);
+
+ } else if(!strcmp(opt->option, "tex")) {
+ psp->billboard_tex = get_texture(opt->str_value);
+ if(!psp->billboard_tex) {
+ error("Could not load texture: \"%s\"", opt->str_value);
+ }
+
+ } else if(!strcmp(opt->option, "color")) {
+ Vector4 v = get_vector4(opt->str_value);
+ psp->start_color = psp->end_color = Color(v.x, v.y, v.z, v.w);
+
+ } else if(!strcmp(opt->option, "color_start")) {
+ Vector4 v = get_vector4(opt->str_value);
+ psp->start_color = Color(v.x, v.y, v.z, v.w);
+
+ } else if(!strcmp(opt->option, "color_end")) {
+ Vector4 v = get_vector4(opt->str_value);
+ psp->end_color = Color(v.x, v.y, v.z, v.w);
+
+ } else if(!strcmp(opt->option, "rot")) {
+ psp->rot = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "glob_rot")) {
+ psp->glob_rot = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "halo")) {
+ psp->halo = get_texture(opt->str_value);
+ if(!psp->halo) {
+ error("Could not load texture: \"%s\"", opt->str_value);
+ }
+
+ } else if(!strcmp(opt->option, "halo_color")) {
+ Vector4 v = get_vector4(opt->str_value);
+ psp->halo_color = Color(v.x, v.y, v.z, v.w);
+
+ } else if(!strcmp(opt->option, "halo_size")) {
+ psp->halo_size.num = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "halo_size-r")) {
+ psp->halo_size.range = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "halo_rot")) {
+ psp->halo_rot = opt->flt_value;
+
+ } else if(!strcmp(opt->option, "big_particles")) {
+ if(!strcmp(opt->str_value, "true")) {
+ psp->big_particles = true;
+ }
+
+ } else if(!strcmp(opt->option, "blend_src") || !strcmp(opt->option, "blend_dest")) {
+ BlendingFactor factor = (BlendingFactor)0xfbad;
+ if(!strcmp(opt->str_value, "0")) {
+ factor = BLEND_ZERO;
+ } else if(!strcmp(opt->str_value, "1")) {
+ factor = BLEND_ONE;
+ } else if(!strcmp(opt->str_value, "srcc")) {
+ factor = BLEND_SRC_COLOR;
+ } else if(!strcmp(opt->str_value, "srca")) {
+ factor = BLEND_SRC_ALPHA;
+ } else if(!strcmp(opt->str_value, "1-srcc")) {
+ factor = BLEND_ONE_MINUS_SRC_COLOR;
+ } else if(!strcmp(opt->str_value, "1-srca")) {
+ factor = BLEND_ONE_MINUS_SRC_ALPHA;
+ } else if(!strcmp(opt->str_value, "1-dstc")) {
+ factor = BLEND_ONE_MINUS_DST_COLOR;
+ } else {
+ error("psys: invalid blend specification: %s", opt->str_value);
+ }
+
+ if(factor != (BlendingFactor)0xfbad) {
+ if(!strcmp(opt->option, "blend_src")) {
+ psp->src_blend = factor;
+ } else {
+ psp->dest_blend = factor;
+ }
+ }
+ } else if(!strcmp(opt->option, "spawn_offset_curve")) {
+ if(!(psp->spawn_offset_curve = load_curve(opt->str_value))) {
+ error("psys: could not load spawn offset curve: %s", opt->str_value);
+ }
+ }
+ }
+
+ psp->shoot_dir = FuzzyVec3(Fuzzy(shoot.x, shoot_range.x), Fuzzy(shoot.y, shoot_range.y), Fuzzy(shoot.z, shoot_range.z));
+ psp->spawn_offset = FuzzyVec3(Fuzzy(spawn_off.x, spawn_off_range.x), Fuzzy(spawn_off.y, spawn_off_range.y), Fuzzy(spawn_off.z, spawn_off_range.z));
+
+ return true;
+}