// renderparticles.cpp #include "console.hh" /* conoutf */ #include "main.hh" // initing, minimized, loadprogress #include "renderparticles.hh" #include "octaedit.hh" // editmode #include "octarender.hh" #include "pvs.hh" #include "rendergl.hh" #include "rendertext.hh" #include "renderva.hh" #include "shader.hh" #include "stain.hh" #include "texture.hh" #include "water.hh" #include "world.hh" #include #if 0 void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15); void particle_splash(int type, int num, int fade, const vec &p, int color = 0xFFFFFF, float size = 1.0f, int radius = 150, int gravity = 2); void particle_trail(int type, int fade, const vec &from, const vec &to, int color = 0xFFFFFF, float size = 1.0f, int gravity = 20); void particle_text(const vec &s, const char *t, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0); void particle_icon(const vec &s, int ix, int iy, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0); void particle_meter(const vec &s, float val, int type, int fade = 1, int color = 0xFFFFFF, int color2 = 0xFFFFF, float size = 2.0f); void particle_flare(const vec &p, const vec &dest, int fade, int type, int color = 0xFFFFFF, float size = 0.28f, physent *owner = NULL); void particle_fireball(const vec &dest, float max, int type, int fade = -1, int color = 0xFFFFFF, float size = 4.0f); void removetrackedparticles(physent *owner = NULL); #endif enum { PART_BLOOD = 0, PART_WATER, PART_SMOKE, PART_STEAM, PART_FLAME, PART_STREAK, PART_RAIL_TRAIL, PART_PULSE_SIDE, PART_PULSE_FRONT, PART_LIGHTNING, PART_EXPLOSION, PART_PULSE_BURST, PART_SPARK, PART_EDIT, PART_SNOW, PART_RAIL_MUZZLE_FLASH, PART_PULSE_MUZZLE_FLASH, PART_HUD_ICON, PART_HUD_ICON_GREY, PART_TEXT, PART_METER, PART_METER_VS, PART_LENS_FLARE }; Shader *particleshader = NULL, *particlenotextureshader = NULL, *particlesoftshader = NULL, *particletextshader = NULL; VARP(particlelayers, 0, 1, 1); FVARP(particlebright, 0, 2, 100); VARP(particlesize, 20, 100, 500); VARP(softparticles, 0, 1, 1); VARP(softparticleblend, 1, 8, 64); // Check canemitparticles() to limit the rate that paricles can be emitted for models/sparklies // Automatically stops particles being emitted when paused or in reflective drawing VARP(emitmillis, 1, 17, 1000); static int lastemitframe = 0, emitoffset = 0; static bool canemit = false, regenemitters = false, canstep = false; static bool canemitparticles() { return canemit || emitoffset; } VARP(showparticles, 0, 1, 1); VAR(cullparticles, 0, 1, 1); VAR(replayparticles, 0, 1, 1); VARN(seedparticles, seedmillis, 0, 3000, 10000); VAR(dbgpcull, 0, 0, 1); VAR(dbgpseed, 0, 0, 1); struct particleemitter { extentity *ent; vec bbmin, bbmax; vec center; float radius; ivec cullmin, cullmax; int maxfade, lastemit, lastcull; particleemitter(extentity *ent) : ent(ent), bbmin(ent->o), bbmax(ent->o), maxfade(-1), lastemit(0), lastcull(0) {} void finalize() { center = vec(bbmin).add(bbmax).mul(0.5f); radius = bbmin.dist(bbmax)/2; cullmin = ivec::floor(bbmin); cullmax = ivec::ceil(bbmax); if(dbgpseed) conoutf(CON_DEBUG, "radius: %f, maxfade: %d", radius, maxfade); } void extendbb(const vec &o, float size = 0) { bbmin.x = min(bbmin.x, o.x - size); bbmin.y = min(bbmin.y, o.y - size); bbmin.z = min(bbmin.z, o.z - size); bbmax.x = max(bbmax.x, o.x + size); bbmax.y = max(bbmax.y, o.y + size); bbmax.z = max(bbmax.z, o.z + size); } void extendbb(float z, float size = 0) { bbmin.z = min(bbmin.z, z - size); bbmax.z = max(bbmax.z, z + size); } }; static vector emitters; static particleemitter *seedemitter = NULL; void clearparticleemitters() { emitters.setsize(0); regenemitters = true; } static void addparticleemitters() { emitters.setsize(0); const vector &ents = entities::getents(); loopv(ents) { extentity &e = *ents[i]; if(e.type != ET_PARTICLES) continue; emitters.add(particleemitter(&e)); } regenemitters = false; } enum { PT_PART = 0, PT_TAPE, PT_TRAIL, PT_TEXT, PT_TEXTUP, PT_METER, PT_METERVS, PT_FIREBALL, PT_LIGHTNING, PT_FLARE, PT_MOD = 1<<8, PT_RND4 = 1<<9, PT_LERP = 1<<10, // use very sparingly - order of blending issues PT_TRACK = 1<<11, PT_BRIGHT = 1<<12, PT_SOFT = 1<<13, PT_HFLIP = 1<<14, PT_VFLIP = 1<<15, PT_ROT = 1<<16, PT_CULL = 1<<17, PT_FEW = 1<<18, PT_ICON = 1<<19, PT_NOTEX = 1<<20, PT_SHADER = 1<<21, PT_NOLAYER = 1<<22, PT_COLLIDE = 1<<23, PT_FLIP = PT_HFLIP | PT_VFLIP | PT_ROT }; const char *partnames[] = { "part", "tape", "trail", "text", "textup", "meter", "metervs", "fireball", "lightning", "flare" }; struct particle { vec o, d; int gravity, fade, millis; bvec color; uchar flags; float size; union { const char *text; float val; physent *owner; struct { uchar color2[3]; uchar progress; }; }; }; struct partvert { vec pos; bvec4 color; vec2 tc; }; #define COLLIDERADIUS 8.0f #define COLLIDEERROR 1.0f struct partrenderer { Texture *tex; const char *texname; int texclamp; uint type; int stain; string info; partrenderer(const char *texname, int texclamp, int type, int stain = -1) : tex(NULL), texname(texname), texclamp(texclamp), type(type), stain(stain) { } partrenderer(int type, int stain = -1) : tex(NULL), texname(NULL), texclamp(0), type(type), stain(stain) { } virtual ~partrenderer() { } virtual void init(int n) { } virtual void reset() = 0; virtual void resettracked(physent *owner) { } virtual particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity = 0) = 0; virtual void update() { } virtual void render() = 0; virtual bool haswork() = 0; virtual int count() = 0; //for debug virtual void cleanup() {} virtual void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) { } virtual void preload() { if(texname && !tex) tex = textureload(texname, texclamp); } //blend = 0 => remove it void calc(particle *p, int &blend, int &ts, vec &o, vec &d, bool step = true) { o = p->o; d = p->d; //if(type&PT_TRACK && p->owner) game::particletrack(p->owner, o, d); if(p->fade <= 5) { ts = 1; blend = 255; } else { ts = lastmillis-p->millis; blend = max(255 - (ts<<8)/p->fade, 0); if(p->gravity) { if(ts > p->fade) ts = p->fade; float t = ts; o.add(vec(d).mul(t/5000.0f)); o.z -= t*t/(2.0f * 5000.0f * p->gravity); } if(type&PT_COLLIDE && o.z < p->val && step) { if(stain >= 0) { vec surface; float floorz = rayfloor(vec(o.x, o.y, p->val), surface, RAY_CLIPMAT, COLLIDERADIUS); float collidez = floorz<0 ? o.z-COLLIDERADIUS : p->val - floorz; if(o.z >= collidez+COLLIDEERROR) p->val = collidez+COLLIDEERROR; else { addstain(stain, vec(o.x, o.y, collidez), vec(p->o).sub(o).normalize(), 2*p->size, p->color, type&PT_RND4 ? (p->flags>>5)&3 : 0); blend = 0; } } else blend = 0; } } } void debuginfo() { formatstring(info, "%d\t%s(", count(), partnames[type&0xFF]); if(type&PT_LERP) concatstring(info, "l,"); if(type&PT_MOD) concatstring(info, "m,"); if(type&PT_RND4) concatstring(info, "r,"); if(type&PT_TRACK) concatstring(info, "t,"); if(type&PT_FLIP) concatstring(info, "f,"); if(type&PT_COLLIDE) concatstring(info, "c,"); int len = strlen(info); info[len-1] = info[len-1] == ',' ? ')' : '\0'; if(texname) { const char *title = strrchr(texname, '/'); if(title) concformatstring(info, ": %s", title+1); } } }; struct listparticle : particle { listparticle *next; }; VARP(outlinemeters, 0, 0, 1); struct listrenderer : partrenderer { static listparticle *parempty; listparticle *list; listrenderer(const char *texname, int texclamp, int type, int stain = -1) : partrenderer(texname, texclamp, type, stain), list(NULL) { } listrenderer(int type, int stain = -1) : partrenderer(type, stain), list(NULL) { } virtual ~listrenderer() { } virtual void killpart(listparticle *p) { } void reset() { if(!list) return; listparticle *p = list; for(;;) { killpart(p); if(p->next) p = p->next; else break; } p->next = parempty; parempty = list; list = NULL; } void resettracked(physent *owner) { if(!(type&PT_TRACK)) return; for(listparticle **prev = &list, *cur = list; cur; cur = *prev) { if(!owner || cur->owner==owner) { *prev = cur->next; cur->next = parempty; parempty = cur; } else prev = &cur->next; } } particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) { if(!parempty) { listparticle *ps = new listparticle[256]; loopi(255) ps[i].next = &ps[i+1]; ps[255].next = parempty; parempty = ps; } listparticle *p = parempty; parempty = p->next; p->next = list; list = p; p->o = o; p->d = d; p->gravity = gravity; p->fade = fade; p->millis = lastmillis + emitoffset; p->color = bvec::hexcolor(color); p->size = size; p->owner = NULL; p->flags = 0; return p; } int count() { int num = 0; listparticle *lp; for(lp = list; lp; lp = lp->next) num++; return num; } bool haswork() { return (list != NULL); } virtual void startrender() = 0; virtual void endrender() = 0; virtual void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) = 0; bool renderpart(listparticle *p) { vec o, d; int blend, ts; calc(p, blend, ts, o, d, canstep); if(blend <= 0) return false; renderpart(p, o, d, blend, ts); return p->fade > 5; } void render() { startrender(); if(tex) glBindTexture(GL_TEXTURE_2D, tex->id); if(canstep) for(listparticle **prev = &list, *p = list; p; p = *prev) { if(renderpart(p)) prev = &p->next; else { // remove *prev = p->next; p->next = parempty; killpart(p); parempty = p; } } else for(listparticle *p = list; p; p = p->next) renderpart(p); endrender(); } }; listparticle *listrenderer::parempty = NULL; struct meterrenderer : listrenderer { meterrenderer(int type) : listrenderer(type|PT_NOTEX|PT_LERP|PT_NOLAYER) {} void startrender() { glDisable(GL_BLEND); gle::defvertex(); } void endrender() { glEnable(GL_BLEND); } void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) { int basetype = type&0xFF; float scale = FONTH*p->size/80.0f, right = 8, left = p->progress/100.0f*right; matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o); m.scale(scale); m.translate(-right/2.0f, 0, 0); if(outlinemeters) { gle::colorf(0, 0.8f, 0); gle::begin(GL_TRIANGLE_STRIP); loopk(10) { const vec2 &sc = sincos360[k*(180/(10-1))]; float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x; gle::attrib(m.transform(vec2(-c, s))); gle::attrib(m.transform(vec2(right + c, s))); } gle::end(); } if(basetype==PT_METERVS) gle::colorub(p->color2[0], p->color2[1], p->color2[2]); else gle::colorf(0, 0, 0); gle::begin(GL_TRIANGLE_STRIP); loopk(10) { const vec2 &sc = sincos360[k*(180/(10-1))]; float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x; gle::attrib(m.transform(vec2(left + c, s))); gle::attrib(m.transform(vec2(right + c, s))); } gle::end(); if(outlinemeters) { gle::colorf(0, 0.8f, 0); gle::begin(GL_TRIANGLE_FAN); loopk(10) { const vec2 &sc = sincos360[k*(180/(10-1))]; float c = (0.5f + 0.1f)*sc.y, s = 0.5f - (0.5f + 0.1f)*sc.x; gle::attrib(m.transform(vec2(left + c, s))); } gle::end(); } gle::color(p->color); gle::begin(GL_TRIANGLE_STRIP); loopk(10) { const vec2 &sc = sincos360[k*(180/(10-1))]; float c = 0.5f*sc.y, s = 0.5f - 0.5f*sc.x; gle::attrib(m.transform(vec2(-c, s))); gle::attrib(m.transform(vec2(left + c, s))); } gle::end(); } }; static meterrenderer meters(PT_METER), metervs(PT_METERVS); struct textrenderer : listrenderer { textrenderer(int type = 0) : listrenderer(type|PT_TEXT|PT_LERP|PT_SHADER|PT_NOLAYER) {} void startrender() { textshader = particletextshader; pushfont(); setfont("default_outline"); } void endrender() { textshader = NULL; popfont(); } void killpart(listparticle *p) { if(p->text && p->flags&1) delete[] p->text; } void renderpart(listparticle *p, const vec &o, const vec &d, int blend, int ts) { float scale = p->size/80.0f, xoff = -text_width(p->text)/2, yoff = 0; if((type&0xFF)==PT_TEXTUP) { xoff += detrnd((size_t)p, 100)-50; yoff -= detrnd((size_t)p, 101); } matrix4x3 m(camright, vec(camup).neg(), vec(camdir).neg(), o); m.scale(scale); m.translate(xoff, yoff, 50); textmatrix = &m; draw_text(p->text, 0, 0, p->color.r, p->color.g, p->color.b, blend); textmatrix = NULL; } }; static textrenderer texts; template static inline void modifyblend(const vec &o, int &blend) { blend = min(blend<<2, 255); } template<> inline void modifyblend(const vec &o, int &blend) { } template static inline void genpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs) { vec udir = vec(camup).sub(camright).mul(size); vec vdir = vec(camup).add(camright).mul(size); vs[0].pos = vec(o.x + udir.x, o.y + udir.y, o.z + udir.z); vs[1].pos = vec(o.x + vdir.x, o.y + vdir.y, o.z + vdir.z); vs[2].pos = vec(o.x - udir.x, o.y - udir.y, o.z - udir.z); vs[3].pos = vec(o.x - vdir.x, o.y - vdir.y, o.z - vdir.z); } template<> inline void genpos(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs) { vec dir1 = vec(d).sub(o), dir2 = vec(d).sub(camera1->o), c; c.cross(dir2, dir1).normalize().mul(size); vs[0].pos = vec(d.x-c.x, d.y-c.y, d.z-c.z); vs[1].pos = vec(o.x-c.x, o.y-c.y, o.z-c.z); vs[2].pos = vec(o.x+c.x, o.y+c.y, o.z+c.z); vs[3].pos = vec(d.x+c.x, d.y+c.y, d.z+c.z); } template<> inline void genpos(const vec &o, const vec &d, float size, int ts, int grav, partvert *vs) { vec e = d; if(grav) e.z -= float(ts)/grav; e.div(-75.0f).add(o); genpos(o, e, size, ts, grav, vs); } template static inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot) { genpos(o, d, size, grav, ts, vs); } #define ROTCOEFFS(n) { \ vec2(-1, 1).rotate_around_z(n*2*M_PI/32.0f), \ vec2( 1, 1).rotate_around_z(n*2*M_PI/32.0f), \ vec2( 1, -1).rotate_around_z(n*2*M_PI/32.0f), \ vec2(-1, -1).rotate_around_z(n*2*M_PI/32.0f) \ } static const vec2 rotcoeffs[32][4] = { ROTCOEFFS(0), ROTCOEFFS(1), ROTCOEFFS(2), ROTCOEFFS(3), ROTCOEFFS(4), ROTCOEFFS(5), ROTCOEFFS(6), ROTCOEFFS(7), ROTCOEFFS(8), ROTCOEFFS(9), ROTCOEFFS(10), ROTCOEFFS(11), ROTCOEFFS(12), ROTCOEFFS(13), ROTCOEFFS(14), ROTCOEFFS(15), ROTCOEFFS(16), ROTCOEFFS(17), ROTCOEFFS(18), ROTCOEFFS(19), ROTCOEFFS(20), ROTCOEFFS(21), ROTCOEFFS(22), ROTCOEFFS(7), ROTCOEFFS(24), ROTCOEFFS(25), ROTCOEFFS(26), ROTCOEFFS(27), ROTCOEFFS(28), ROTCOEFFS(29), ROTCOEFFS(30), ROTCOEFFS(31), }; template<> inline void genrotpos(const vec &o, const vec &d, float size, int grav, int ts, partvert *vs, int rot) { const vec2 *coeffs = rotcoeffs[rot]; vs[0].pos = vec(o).madd(camright, coeffs[0].x*size).madd(camup, coeffs[0].y*size); vs[1].pos = vec(o).madd(camright, coeffs[1].x*size).madd(camup, coeffs[1].y*size); vs[2].pos = vec(o).madd(camright, coeffs[2].x*size).madd(camup, coeffs[2].y*size); vs[3].pos = vec(o).madd(camright, coeffs[3].x*size).madd(camup, coeffs[3].y*size); } template static inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) { if(grav) { float t = fade; vec end = vec(o).madd(d, t/5000.0f); end.z -= t*t/(2.0f * 5000.0f * grav); pe.extendbb(end, size); float tpeak = d.z*grav; if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size); } } template<> inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) { pe.extendbb(d, size); } template<> inline void seedpos(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int grav) { vec e = d; if(grav) e.z -= float(fade)/grav; e.div(-75.0f).add(o); pe.extendbb(e, size); } template struct varenderer : partrenderer { partvert *verts; particle *parts; int maxparts, numparts, lastupdate, rndmask; GLuint vbo; varenderer(const char *texname, int type, int stain = -1) : partrenderer(texname, 3, type, stain), verts(NULL), parts(NULL), maxparts(0), numparts(0), lastupdate(-1), rndmask(0), vbo(0) { if(type & PT_HFLIP) rndmask |= 0x01; if(type & PT_VFLIP) rndmask |= 0x02; if(type & PT_ROT) rndmask |= 0x1F<<2; if(type & PT_RND4) rndmask |= 0x03<<5; } void cleanup() { if(vbo) { glDeleteBuffers_(1, &vbo); vbo = 0; } } void init(int n) { DELETEA(parts); DELETEA(verts); parts = new particle[n]; verts = new partvert[n*4]; maxparts = n; numparts = 0; lastupdate = -1; } void reset() { numparts = 0; lastupdate = -1; } void resettracked(physent *owner) { if(!(type&PT_TRACK)) return; loopi(numparts) { particle *p = parts+i; if(!owner || (p->owner == owner)) p->fade = -1; } lastupdate = -1; } int count() { return numparts; } bool haswork() { return (numparts > 0); } particle *addpart(const vec &o, const vec &d, int fade, int color, float size, int gravity) { particle *p = parts + (numparts < maxparts ? numparts++ : rnd(maxparts)); //next free slot, or kill a random kitten p->o = o; p->d = d; p->gravity = gravity; p->fade = fade; p->millis = lastmillis + emitoffset; p->color = bvec::hexcolor(color); p->size = size; p->owner = NULL; p->flags = 0x80 | (rndmask ? rnd(0x80) & rndmask : 0); lastupdate = -1; return p; } void seedemitter(particleemitter &pe, const vec &o, const vec &d, int fade, float size, int gravity) { pe.maxfade = max(pe.maxfade, fade); size *= SQRT2; pe.extendbb(o, size); seedpos(pe, o, d, fade, size, gravity); if(!gravity) return; vec end(o); float t = fade; end.add(vec(d).mul(t/5000.0f)); end.z -= t*t/(2.0f * 5000.0f * gravity); pe.extendbb(end, size); float tpeak = d.z*gravity; if(tpeak > 0 && tpeak < fade) pe.extendbb(o.z + 1.5f*d.z*tpeak/5000.0f, size); } void genverts(particle *p, partvert *vs, bool regen) { vec o, d; int blend, ts; calc(p, blend, ts, o, d); if(blend <= 1 || p->fade <= 5) p->fade = -1; //mark to remove on next pass (i.e. after render) modifyblend(o, blend); if(regen) { p->flags &= ~0x80; #define SETTEXCOORDS(u1c, u2c, v1c, v2c, body) \ { \ float u1 = u1c, u2 = u2c, v1 = v1c, v2 = v2c; \ body; \ vs[0].tc = vec2(u1, v1); \ vs[1].tc = vec2(u2, v1); \ vs[2].tc = vec2(u2, v2); \ vs[3].tc = vec2(u1, v2); \ } if(type&PT_RND4) { float tx = 0.5f*((p->flags>>5)&1), ty = 0.5f*((p->flags>>6)&1); SETTEXCOORDS(tx, tx + 0.5f, ty, ty + 0.5f, { if(p->flags&0x01) swap(u1, u2); if(p->flags&0x02) swap(v1, v2); }); } else if(type&PT_ICON) { float tx = 0.25f*(p->flags&3), ty = 0.25f*((p->flags>>2)&3); SETTEXCOORDS(tx, tx + 0.25f, ty, ty + 0.25f, {}); } else SETTEXCOORDS(0, 1, 0, 1, {}); #define SETCOLOR(r, g, b, a) \ do { \ bvec4 col(r, g, b, a); \ loopi(4) vs[i].color = col; \ } while(0) #define SETMODCOLOR SETCOLOR((p->color.r*blend)>>8, (p->color.g*blend)>>8, (p->color.b*blend)>>8, 255) if(type&PT_MOD) SETMODCOLOR; else SETCOLOR(p->color.r, p->color.g, p->color.b, blend); } else if(type&PT_MOD) SETMODCOLOR; else loopi(4) vs[i].color.a = blend; if(type&PT_ROT) genrotpos(o, d, p->size, ts, p->gravity, vs, (p->flags>>2)&0x1F); else genpos(o, d, p->size, ts, p->gravity, vs); } void genverts() { loopi(numparts) { particle *p = &parts[i]; partvert *vs = &verts[i*4]; if(p->fade < 0) { do { --numparts; if(numparts <= i) return; } while(parts[numparts].fade < 0); *p = parts[numparts]; genverts(p, vs, true); } else genverts(p, vs, (p->flags&0x80)!=0); } } void genvbo() { if(lastmillis == lastupdate && vbo) return; lastupdate = lastmillis; genverts(); if(!vbo) glGenBuffers_(1, &vbo); gle::bindvbo(vbo); glBufferData_(GL_ARRAY_BUFFER, maxparts*4*sizeof(partvert), NULL, GL_STREAM_DRAW); glBufferSubData_(GL_ARRAY_BUFFER, 0, numparts*4*sizeof(partvert), verts); gle::clearvbo(); } void render() { genvbo(); glBindTexture(GL_TEXTURE_2D, tex->id); gle::bindvbo(vbo); const partvert *ptr = 0; gle::vertexpointer(sizeof(partvert), ptr->pos.v); gle::texcoord0pointer(sizeof(partvert), ptr->tc.v); gle::colorpointer(sizeof(partvert), ptr->color.v); gle::enablevertex(); gle::enabletexcoord0(); gle::enablecolor(); gle::enablequads(); gle::drawquads(0, numparts); gle::disablequads(); gle::disablevertex(); gle::disabletexcoord0(); gle::disablecolor(); gle::clearvbo(); } }; typedef varenderer quadrenderer; typedef varenderer taperenderer; typedef varenderer trailrenderer; #include "explosion.hh" #include "lensflare.hh" #include "lightning.hh" struct softquadrenderer : quadrenderer { softquadrenderer(const char *texname, int type, int stain = -1) : quadrenderer(texname, type|PT_SOFT, stain) { } }; static partrenderer *parts[] = { new quadrenderer("media/particle/blood.png", PT_PART|PT_FLIP|PT_MOD|PT_RND4|PT_COLLIDE, STAIN_BLOOD), // blood spats (note: rgb is inverted) new trailrenderer("media/particle/base.png", PT_TRAIL|PT_LERP), // water, entity new quadrenderer("media/particle/smoke.png", PT_PART|PT_FLIP|PT_LERP), // smoke new quadrenderer("media/particle/steam.png", PT_PART|PT_FLIP), // steam new quadrenderer("media/particle/flames.png", PT_PART|PT_HFLIP|PT_RND4|PT_BRIGHT), // flame new taperenderer("media/particle/flare.png", PT_TAPE|PT_BRIGHT), // streak new taperenderer("media/particle/rail_trail.png", PT_TAPE|PT_FEW|PT_BRIGHT), // rail trail new taperenderer("media/particle/pulse_side.png", PT_TAPE|PT_FEW|PT_BRIGHT), // pulse side new quadrenderer("media/particle/pulse_front.png", PT_PART|PT_FLIP|PT_FEW|PT_BRIGHT), // pulse front &lightnings, // lightning &fireballs, // explosion fireball &pulsebursts, // pulse burst new quadrenderer("media/particle/spark.png", PT_PART|PT_FLIP|PT_BRIGHT), // sparks new quadrenderer("media/particle/base.png", PT_PART|PT_FLIP|PT_BRIGHT), // edit mode entities new quadrenderer("media/particle/snow.png", PT_PART|PT_FLIP|PT_RND4|PT_COLLIDE), // colliding snow new quadrenderer("media/particle/rail_muzzle.png", PT_PART|PT_FEW|PT_FLIP|PT_BRIGHT|PT_TRACK), // rail muzzle flash new quadrenderer("media/particle/pulse_muzzle.png", PT_PART|PT_FEW|PT_FLIP|PT_BRIGHT|PT_TRACK), // pulse muzzle flash new quadrenderer("media/interface/hud/items.png", PT_PART|PT_FEW|PT_ICON), // hud icon new quadrenderer("media/interface/hud/items.png", PT_PART|PT_FEW|PT_ICON), // grey hud icon &texts, // text &meters, // meter &metervs, // meter vs. &flares // lens flares - must be done last }; VARFP(maxparticles, 10, 4000, 10000, initparticles()); VARFP(fewparticles, 10, 100, 10000, initparticles()); void initparticles() { if(initing) return; if(!particleshader) particleshader = lookupshaderbyname("particle"); if(!particlenotextureshader) particlenotextureshader = lookupshaderbyname("particlenotexture"); if(!particlesoftshader) particlesoftshader = lookupshaderbyname("particlesoft"); if(!particletextshader) particletextshader = lookupshaderbyname("particletext"); loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->init(parts[i]->type&PT_FEW ? min(fewparticles, maxparticles) : maxparticles); loopi(sizeof(parts)/sizeof(parts[0])) { loadprogress = float(i+1)/(sizeof(parts)/sizeof(parts[0])); parts[i]->preload(); } loadprogress = 0; } void clearparticles() { loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->reset(); clearparticleemitters(); } void cleanupparticles() { loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->cleanup(); } #if 0 static void removetrackedparticles(physent *owner) { loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->resettracked(owner); } #endif VARN(debugparticles, dbgparts, 0, 0, 1); void debugparticles() { if(!dbgparts) return; int n = sizeof(parts)/sizeof(parts[0]); pushhudmatrix(); hudmatrix.ortho(0, FONTH*n*2*vieww/float(viewh), FONTH*n*2, 0, -1, 1); // squeeze into top-left corner flushhudmatrix(); loopi(n) draw_text(parts[i]->info, FONTH, (i+n/2)*FONTH); pophudmatrix(); } void renderparticles(int layer) { canstep = layer != PL_UNDER; //want to debug BEFORE the lastpass render (that would delete particles) if(dbgparts && (layer == PL_ALL || layer == PL_UNDER)) loopi(sizeof(parts)/sizeof(parts[0])) parts[i]->debuginfo(); bool rendered = false; uint lastflags = PT_LERP|PT_SHADER, flagmask = PT_LERP|PT_MOD|PT_BRIGHT|PT_NOTEX|PT_SOFT|PT_SHADER, excludemask = layer == PL_ALL ? ~0 : (layer != PL_NOLAYER ? PT_NOLAYER : 0); loopi(sizeof(parts)/sizeof(parts[0])) { partrenderer *p = parts[i]; if((p->type&PT_NOLAYER) == excludemask || !p->haswork()) continue; if(!rendered) { rendered = true; glDepthMask(GL_FALSE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glActiveTexture_(GL_TEXTURE2); if(msaalight) glBindTexture(GL_TEXTURE_2D_MULTISAMPLE, msdepthtex); else glBindTexture(GL_TEXTURE_RECTANGLE, gdepthtex); glActiveTexture_(GL_TEXTURE0); } uint flags = p->type & flagmask, changedbits = flags ^ lastflags; if(changedbits) { if(changedbits&PT_LERP) { if(flags&PT_LERP) resetfogcolor(); else zerofogcolor(); } if(changedbits&(PT_LERP|PT_MOD)) { if(flags&PT_LERP) glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); else if(flags&PT_MOD) glBlendFunc(GL_ZERO, GL_ONE_MINUS_SRC_COLOR); else glBlendFunc(GL_SRC_ALPHA, GL_ONE); } if(!(flags&PT_SHADER)) { if(changedbits&(PT_LERP|PT_SOFT|PT_NOTEX|PT_SHADER)) { if(flags&PT_SOFT && softparticles) { particlesoftshader->set(); LOCALPARAMF(softparams, -1.0f/softparticleblend, 0, 0); } else if(flags&PT_NOTEX) particlenotextureshader->set(); else particleshader->set(); } if(changedbits&(PT_MOD|PT_BRIGHT|PT_SOFT|PT_NOTEX|PT_SHADER)) { float colorscale = flags&PT_MOD ? 1 : ldrscale; if(flags&PT_BRIGHT) colorscale *= particlebright; LOCALPARAMF(colorscale, colorscale, colorscale, colorscale, 1); } } lastflags = flags; } p->render(); } if(rendered) { if(lastflags&(PT_LERP|PT_MOD)) glBlendFunc(GL_SRC_ALPHA, GL_ONE); if(!(lastflags&PT_LERP)) resetfogcolor(); glDisable(GL_BLEND); glDepthMask(GL_TRUE); } } static int addedparticles = 0; static inline particle *newparticle(const vec &o, const vec &d, int fade, int type, int color, float size, int gravity = 0) { static particle dummy; if(seedemitter) { parts[type]->seedemitter(*seedemitter, o, d, fade, size, gravity); return &dummy; } if(fade + emitoffset < 0) return &dummy; addedparticles++; return parts[type]->addpart(o, d, fade, color, size, gravity); } VARP(maxparticledistance, 256, 1024, 4096); static void splash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity) { if(camera1->o.dist(p) > maxparticledistance && !seedemitter) return; float collidez = parts[type]->type&PT_COLLIDE ? p.z - raycube(p, vec(0, 0, -1), COLLIDERADIUS, RAY_CLIPMAT) + (parts[type]->stain >= 0 ? COLLIDEERROR : 0) : -1; int fmin = 1; int fmax = fade*3; loopi(num) { int x, y, z; do { x = rnd(radius*2)-radius; y = rnd(radius*2)-radius; z = rnd(radius*2)-radius; } while(x*x+y*y+z*z>radius*radius); vec tmp = vec((float)x, (float)y, (float)z); int f = (num < 10) ? (fmin + rnd(fmax)) : (fmax - (i*(fmax-fmin))/(num-1)); //help deallocater by using fade distribution rather than random newparticle(p, tmp, f, type, color, size, gravity)->val = collidez; } } static void regularsplash(int type, int color, int radius, int num, int fade, const vec &p, float size, int gravity, int delay = 0) { if(!canemitparticles() || (delay > 0 && rnd(delay) != 0)) return; splash(type, color, radius, num, fade, p, size, gravity); } static void regular_particle_splash(int type, int num, int fade, const vec &p, int color = 0xFFFFFF, float size = 1.0f, int radius = 150, int gravity = 2, int delay = 0); static void particle_textcopy(const vec &s, const char *t, int type, int fade = 2000, int color = 0xFFFFFF, float size = 2.0f, int gravity = 0); static bool canaddparticles() { return !minimized; } static void regular_particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity, int delay) { if(!canaddparticles()) return; regularsplash(type, color, radius, num, fade, p, size, gravity, delay); } VARP(particletext, 0, 1, 1); VARP(maxparticletextdistance, 0, 128, 10000); static void particle_textcopy(const vec &s, const char *t, int type, int fade, int color, float size, int gravity) { if(!canaddparticles()) return; if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return; particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); p->text = newstring(t); p->flags = 1; } //dir = 0..6 where 0=up static inline vec offsetvec(vec o, int dir, int dist) { vec v = vec(o); v[(2+dir)%3] += (dir>2)?(-dist):dist; return v; } //converts a 16bit color to 24bit static inline int colorfromattr(int attr) { return (((attr&0xF)<<4) | ((attr&0xF0)<<8) | ((attr&0xF00)<<12)) + 0x0F0F0F; } #if 0 void particle_splash(int type, int num, int fade, const vec &p, int color, float size, int radius, int gravity) { if(!canaddparticles()) return; splash(type, color, radius, num, fade, p, size, gravity); } VARP(maxtrail, 1, 500, 10000); void particle_trail(int type, int fade, const vec &s, const vec &e, int color, float size, int gravity) { if(!canaddparticles()) return; vec v; float d = e.dist(s, v); int steps = clamp(int(d*2), 1, maxtrail); v.div(steps); vec p = s; loopi(steps) { p.add(v); vec tmp = vec(float(rnd(11)-5), float(rnd(11)-5), float(rnd(11)-5)); newparticle(p, tmp, rnd(fade)+fade, type, color, size, gravity); } } void particle_text(const vec &s, const char *t, int type, int fade, int color, float size, int gravity) { if(!canaddparticles()) return; if(!particletext || camera1->o.dist(s) > maxparticletextdistance) return; particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); p->text = t; } void particle_icon(const vec &s, int ix, int iy, int type, int fade, int color, float size, int gravity) { if(!canaddparticles()) return; particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size, gravity); p->flags |= ix | (iy<<2); } void particle_meter(const vec &s, float val, int type, int fade, int color, int color2, float size) { if(!canaddparticles()) return; particle *p = newparticle(s, vec(0, 0, 1), fade, type, color, size); p->color2[0] = color2>>16; p->color2[1] = (color2>>8)&0xFF; p->color2[2] = color2&0xFF; p->progress = clamp(int(val*100), 0, 100); } void particle_flare(const vec &p, const vec &dest, int fade, int type, int color, float size, physent *owner) { if(!canaddparticles()) return; newparticle(p, dest, fade, type, color, size)->owner = owner; } void particle_fireball(const vec &dest, float maxsize, int type, int fade, int color, float size) { if(!canaddparticles()) return; float growth = maxsize - size; if(fade < 0) fade = int(growth*20); newparticle(dest, vec(0, 0, 1), fade, type, color, size)->val = growth; } #endif /* 0 */ /* Experiments in shapes... * dir: (where dir%3 is similar to offsetvec with 0=up) * 0..2 circle * 3.. 5 cylinder shell * 6..11 cone shell * 12..14 plane volume * 15..20 line volume, i.e. wall * 21 sphere * 24..26 flat plane * +32 to inverse direction */ static void regularshape(int type, int radius, int color, int dir, int num, int fade, const vec &p, float size, int gravity, int vel = 200) { if(!canemitparticles()) return; int basetype = parts[type]->type&0xFF; bool flare = (basetype == PT_TAPE) || (basetype == PT_LIGHTNING), inv = (dir&0x20)!=0, taper = (dir&0x40)!=0 && !seedemitter; dir &= 0x1F; loopi(num) { vec to, from; if(dir < 12) { const vec2 &sc = sincos360[rnd(360)]; to[dir%3] = sc.y*radius; to[(dir+1)%3] = sc.x*radius; to[(dir+2)%3] = 0.0; to.add(p); if(dir < 3) //circle from = p; else if(dir < 6) //cylinder { from = to; to[(dir+2)%3] += radius; from[(dir+2)%3] -= radius; } else //cone { from = p; to[(dir+2)%3] += (dir < 9)?radius:(-radius); } } else if(dir < 15) //plane { to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0; to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0; to[(dir+2)%3] = radius; to.add(p); from = to; from[(dir+2)%3] -= 2*radius; } else if(dir < 21) //line { if(dir < 18) { to[dir%3] = float(rnd(radius<<4)-(radius<<3))/8.0; to[(dir+1)%3] = 0.0; } else { to[dir%3] = 0.0; to[(dir+1)%3] = float(rnd(radius<<4)-(radius<<3))/8.0; } to[(dir+2)%3] = 0.0; to.add(p); from = to; to[(dir+2)%3] += radius; } else if(dir < 24) //sphere { to = vec(2*M_PI*float(rnd(1000))/1000.0, M_PI*float(rnd(1000)-500)/1000.0).mul(radius); to.add(p); from = p; } else if(dir < 27) // flat plane { to[dir%3] = float(rndscale(2*radius)-radius); to[(dir+1)%3] = float(rndscale(2*radius)-radius); to[(dir+2)%3] = 0.0; to.add(p); from = to; } else from = to = p; if(inv) swap(from, to); if(taper) { float dist = clamp(from.dist2(camera1->o)/maxparticledistance, 0.0f, 1.0f); if(dist > 0.2f) { dist = 1 - (dist - 0.2f)/0.8f; if(rnd(0x10000) > dist*dist*0xFFFF) continue; } } if(flare) newparticle(from, to, rnd(fade*3)+1, type, color, size, gravity); else { vec d = vec(to).sub(from).rescale(vel); //velocity particle *n = newparticle(from, d, rnd(fade*3)+1, type, color, size, gravity); if(parts[type]->type&PT_COLLIDE) n->val = from.z - raycube(from, vec(0, 0, -1), parts[type]->stain >= 0 ? COLLIDERADIUS : max(from.z, 0.0f), RAY_CLIPMAT) + (parts[type]->stain >= 0 ? COLLIDEERROR : 0); } } } static void regularflame(int type, const vec &p, float radius, float height, int color, int density = 3, float scale = 2.0f, float speed = 200.0f, float fade = 600.0f, int gravity = -15) { if(!canemitparticles()) return; float size = scale * min(radius, height); vec v(0, 0, min(1.0f, height)*speed); loopi(density) { vec s = p; s.x += rndscale(radius*2.0f)-radius; s.y += rndscale(radius*2.0f)-radius; newparticle(s, v, rnd(max(int(fade*height), 1))+1, type, color, size, gravity); } } #if 0 void regular_particle_flame(int type, const vec &p, float radius, float height, int color, int density, float scale, float speed, float fade, int gravity) { if(!canaddparticles()) return; regularflame(type, p, radius, height, color, density, scale, speed, fade, gravity); } #endif /* 0 */ static void makeparticles(entity &e) { switch(e.attr1) { case 0: //fire and smoke - - 0 values default to compat for old maps { float radius = e.attr2 ? float(e.attr2)/100.0f : 1.5f, height = e.attr3 ? float(e.attr3)/100.0f : radius/3; regularflame(PART_FLAME, e.o, radius, height, e.attr4 ? colorfromattr(e.attr4) : 0x903020, 3, 2.0f); regularflame(PART_SMOKE, vec(e.o.x, e.o.y, e.o.z + 4.0f*min(radius, height)), radius, height, 0x303020, 1, 4.0f, 100.0f, 2000.0f, -20); break; } case 1: //steam vent - regularsplash(PART_STEAM, 0x897661, 50, 1, 200, offsetvec(e.o, e.attr2, rnd(10)), 2.4f, -20); break; case 2: //water fountain - { int color; if(e.attr3 > 0) color = colorfromattr(e.attr3); else { int mat = MAT_WATER + clamp(-e.attr3, 0, 3); color = getwaterfallcolour(mat).tohexcolor(); if(!color) color = getwatercolour(mat).tohexcolor(); } regularsplash(PART_WATER, color, 150, 4, 200, offsetvec(e.o, e.attr2, rnd(10)), 0.6f, 2); break; } case 3: //fire ball - newparticle(e.o, vec(0, 0, 1), 1, PART_EXPLOSION, colorfromattr(e.attr3), 4.0f)->val = 1+e.attr2; break; case 4: //tape - case 7: //lightning case 9: //steam case 10: //water case 13: //snow { static const int typemap[] = { PART_STREAK, -1, -1, PART_LIGHTNING, -1, PART_STEAM, PART_WATER, -1, -1, PART_SNOW }; static const float sizemap[] = { 0.28f, 0.0f, 0.0f, 1.0f, 0.0f, 2.4f, 0.60f, 0.0f, 0.0f, 0.5f }; static const int gravmap[] = { 0, 0, 0, 0, 0, -20, 2, 0, 0, 20 }; int type = typemap[e.attr1-4]; float size = sizemap[e.attr1-4]; int gravity = gravmap[e.attr1-4]; if(e.attr2 >= 256) regularshape(type, max(1+e.attr3, 1), colorfromattr(e.attr4), e.attr2-256, 5, e.attr5 > 0 ? min(int(e.attr5), 10000) : 200, e.o, size, gravity); else newparticle(e.o, offsetvec(e.o, e.attr2, max(1+e.attr3, 0)), 1, type, colorfromattr(e.attr4), size, gravity); break; } case 5: //meter, metervs - case 6: { particle *p = newparticle(e.o, vec(0, 0, 1), 1, e.attr1==5 ? PART_METER : PART_METER_VS, colorfromattr(e.attr3), 2.0f); int color2 = colorfromattr(e.attr4); p->color2[0] = color2>>16; p->color2[1] = (color2>>8)&0xFF; p->color2[2] = color2&0xFF; p->progress = clamp(int(e.attr2), 0, 100); break; } case 11: // flame - radius=100, height=100 is the classic size regularflame(PART_FLAME, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 3, 2.0f); break; case 12: // smoke plume regularflame(PART_SMOKE, e.o, float(e.attr2)/100.0f, float(e.attr3)/100.0f, colorfromattr(e.attr4), 1, 4.0f, 100.0f, 2000.0f, -20); break; case 32: //lens flares - plain/sparkle/sun/sparklesun case 33: case 34: case 35: flares.addflare(e.o, e.attr2, e.attr3, e.attr4, (e.attr1&0x02)!=0, (e.attr1&0x01)!=0); break; default: if(!editmode) { defformatstring(ds, "particles %d?", e.attr1); particle_textcopy(e.o, ds, PART_TEXT, 1, 0x6496FF, 2.0f); } break; } } bool printparticles(extentity &e, char *buf, int len) { switch(e.attr1) { case 0: case 4: case 7: case 8: case 9: case 10: case 11: case 12: case 13: nformatstring(buf, len, "%s %d %d %d 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); return true; case 3: nformatstring(buf, len, "%s %d %d 0x%.3hX %d %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); return true; case 5: case 6: nformatstring(buf, len, "%s %d %d 0x%.3hX 0x%.3hX %d", entities::entname(e.type), e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); return true; } return false; } void seedparticles() { renderprogress(0, "seeding particles"); addparticleemitters(); canemit = true; loopv(emitters) { particleemitter &pe = emitters[i]; extentity &e = *pe.ent; seedemitter = &pe; for(int millis = 0; millis < seedmillis; millis += min(emitmillis, seedmillis/10)) makeparticles(e); seedemitter = NULL; pe.lastemit = -seedmillis; pe.finalize(); } } void updateparticles() { if(regenemitters) addparticleemitters(); if(minimized) { canemit = false; return; } if(lastmillis - lastemitframe >= emitmillis) { canemit = true; lastemitframe = lastmillis - (lastmillis%emitmillis); } else canemit = false; loopi(sizeof(parts)/sizeof(parts[0])) { parts[i]->update(); } if(!editmode || showparticles) { int emitted = 0, replayed = 0; addedparticles = 0; loopv(emitters) { particleemitter &pe = emitters[i]; extentity &e = *pe.ent; if(e.o.dist(camera1->o) > maxparticledistance) { pe.lastemit = lastmillis; continue; } if(cullparticles && pe.maxfade >= 0) { if(isfoggedsphere(pe.radius, pe.center)) { pe.lastcull = lastmillis; continue; } if(pvsoccluded(pe.cullmin, pe.cullmax)) { pe.lastcull = lastmillis; continue; } } makeparticles(e); emitted++; if(replayparticles && pe.maxfade > 5 && pe.lastcull > pe.lastemit) { for(emitoffset = max(pe.lastemit + emitmillis - lastmillis, -pe.maxfade); emitoffset < 0; emitoffset += emitmillis) { makeparticles(e); replayed++; } emitoffset = 0; } pe.lastemit = lastmillis; } if(dbgpcull && (canemit || replayed) && addedparticles) conoutf(CON_DEBUG, "%d emitters, %d particles", emitted, addedparticles); } if(editmode) // show sparkly thingies for map entities in edit mode { const vector &ents = entities::getents(); // note: order matters in this case as particles of the same type are drawn in the reverse order that they are added loopv(entgroup) { entity &e = *ents[entgroup[i]]; particle_textcopy(e.o, entities::entname(e.type), PART_TEXT, 1, 0xFF4B19, 2.0f); } loopv(ents) { entity &e = *ents[i]; if(e.type==ET_EMPTY) continue; particle_textcopy(e.o, entities::entname(e.type), PART_TEXT, 1, 0x1EC850, 2.0f); regular_particle_splash(PART_EDIT, 2, 40, e.o, 0x3232FF, 0.32f*particlesize/100.0f); } } }