/* Copyright (C) 1996-1997 Id Software, Inc. 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 included (GNU.txt) 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 "quakedef.h" #define NUMVERTEXNORMALS 162 float r_avertexnormals[NUMVERTEXNORMALS][3] = { #include "anorms.h" }; #include "shader.h" #include "com_mesh.h" //FIXME: we're likely going to want to thread the building routine at some point. //the alias mesh stuff will need some rework as it uses statics inside. #define DESCSPERSHADER 8 typedef struct { int x, y, z; //rebuilt if changed int key; model_t *loadingmodel; //needs rebuilding, but wait till this is loaded. struct { shader_t *shader; mesh_t mesh; mesh_t *pmesh; vbo_t vbo; } soups[64]; size_t numsoups; } cluttersector_t; static cluttersector_t cluttersector[3*3*3]; cvar_t r_clutter_density = CVARD("r_clutter_density", "0", "Scaler for clutter counts. 0 disables clutter completely.\nClutter requires shaders with 'fte_clutter MODEL SPACING SCALEMIN SCALEMAX ZOFS ANGLEMIN ANGLEMAX' terms"); cvar_t r_clutter_distance = CVARD("r_clutter_distance", "1024", "Distance at which clutter will become invisible."); //should be used by various shaders to fade it out by here void R_Clutter_Init(void) { Cvar_Register(&r_clutter_density, "Ground Clutter"); Cvar_Register(&r_clutter_distance, "Ground Clutter"); } typedef struct { model_t *loadingmodel; struct clutter_build_ctx_soup_s { shader_t *shader; vecV_t *coord; vec2_t *texcoord; vec4_t *colour; vec3_t *normal; vec3_t *sdir; vec3_t *tdir; index_t *idx; size_t numverts; size_t numidx; size_t maxverts; size_t maxidx; } soups[64]; unsigned int numsoups; float area[DESCSPERSHADER]; //here so it can overflow, so large values with small surfaces actually does something. not evenly perhaps, but not much else we can do unsigned int x, y, z, w; } clutter_build_ctx_t; //to make things repeatable so that people can depend upon placement. unsigned int R_Clutter_Random(clutter_build_ctx_t *ctx) { //ripped from wikipedia (originally called xorshift128) unsigned int t = ctx->x ^ (ctx->x << 11); ctx->x = ctx->y; ctx->y = ctx->z; ctx->z = ctx->w; return ctx->w = ctx->w ^ (ctx->w >> 19) ^ t ^ (t >> 8); } float R_Clutter_FRandom(clutter_build_ctx_t *ctx) { unsigned int r = R_Clutter_Random(ctx); return (r & 0xffffff) / (float)0xffffff; } static void R_Clutter_Insert_Soup(clutter_build_ctx_t *ctx, shader_t *shader, vecV_t *fte_restrict coord, vec2_t *fte_restrict texcoord, vec3_t *fte_restrict normal, vec3_t *fte_restrict sdir, vec3_t *fte_restrict tdir, vec4_t *fte_restrict colours, size_t numverts, index_t *fte_restrict index, size_t numidx, float scale, vec3_t origin, vec3_t axis[]) { vec3_t diffuse, ambient, ldir; float dot; struct clutter_build_ctx_soup_s *soup = NULL; size_t i; shader_t *os = shader; shader = R_RegisterShader(va("clutter#replace=%s", os->name), SUF_NONE, "{\n" "program defaultsprite#MASK=0.666\n" // "surfaceparm nodlight\n" "surfaceparm noshadows\n" // "cull disable\n" "{\n" "map $diffuse\n" "rgbgen vertex\n" "alphagen vertex\n" // "alphafunc ge128\n" "}\n" "}\n" ); *shader->defaulttextures = *os->defaulttextures; for (i = 0, soup = ctx->soups; i < ctx->numsoups; i++, soup++) { if (soup->shader == shader) if (soup->numverts + numverts <= MAX_INDICIES) break; } if (i == ctx->numsoups) { if (i == sizeof(ctx->soups)/sizeof(ctx->soups[0])) return; //too many different shaders or something soup->shader = shader; ctx->numsoups++; } //inject the indicies if (soup->numidx + numidx > soup->maxidx) { soup->maxidx = (soup->numidx + numidx) * 2; soup->idx = BZ_Realloc(soup->idx, sizeof(*soup->idx) * soup->maxidx); } for (i = 0; i < numidx; i++) soup->idx[soup->numidx++] = soup->numverts+*index++; cl.worldmodel->funcs.LightPointValues(cl.worldmodel, origin, diffuse, ambient, ldir); VectorScale(ambient, 1/255.0, ambient); VectorScale(diffuse, 1/255.0, diffuse); //inject the verts if (soup->numverts + numverts > soup->maxverts) { soup->maxverts = (soup->numverts + numverts) * 2; soup->coord = BZ_Realloc(soup->coord, sizeof(*soup->coord) * soup->maxverts); soup->texcoord = BZ_Realloc(soup->texcoord, sizeof(*soup->texcoord) * soup->maxverts); soup->colour = BZ_Realloc(soup->colour, sizeof(*soup->colour) * soup->maxverts); soup->normal = BZ_Realloc(soup->normal, sizeof(*soup->normal) * soup->maxverts); soup->sdir = BZ_Realloc(soup->sdir, sizeof(*soup->sdir) * soup->maxverts); soup->tdir = BZ_Realloc(soup->tdir, sizeof(*soup->tdir) * soup->maxverts); } for (i = 0; i < numverts; i++) { VectorMA(origin, scale*coord[i][0], axis[0], soup->coord[soup->numverts]); VectorMA(soup->coord[soup->numverts], scale*coord[i][1], axis[1], soup->coord[soup->numverts]); VectorMA(soup->coord[soup->numverts], scale*coord[i][2], axis[2], soup->coord[soup->numverts]); Vector2Copy(texcoord[i], soup->texcoord[soup->numverts]); VectorMA(vec3_origin, normal[i][0], axis[0], soup->normal[soup->numverts]); VectorMA(soup->normal[soup->numverts], normal[i][1], axis[1], soup->normal[soup->numverts]); VectorMA(soup->normal[soup->numverts], normal[i][2], axis[2], soup->normal[soup->numverts]); VectorMA(vec3_origin, sdir[i][0], axis[0], soup->sdir[soup->numverts]); VectorMA(soup->sdir[soup->numverts], sdir[i][1], axis[1], soup->sdir[soup->numverts]); VectorMA(soup->sdir[soup->numverts], sdir[i][2], axis[2], soup->sdir[soup->numverts]); VectorMA(vec3_origin, tdir[i][0], axis[0], soup->tdir[soup->numverts]); VectorMA(soup->tdir[soup->numverts], tdir[i][1], axis[1], soup->tdir[soup->numverts]); VectorMA(soup->tdir[soup->numverts], tdir[i][2], axis[2], soup->tdir[soup->numverts]); // VectorCopy(ambient, soup->colour[soup->numverts]); dot = DotProduct(ldir, soup->normal[soup->numverts]); if (dot < 0) dot = 0; VectorMA(ambient, dot, diffuse, soup->colour[soup->numverts]); if (colours) //most model formats don't have vertex colours soup->colour[soup->numverts][3] = colours[i][3]; else soup->colour[soup->numverts][3] = 1; soup->numverts++; } } static void R_Clutter_Insert_Mesh(clutter_build_ctx_t *ctx, model_t *mod, float scale, vec3_t origin, vec3_t axis[3]) { mesh_t mesh; galiasinfo_t *inf; unsigned int surfnum = 0; entity_t re; unsigned int randanim = R_Clutter_Random(ctx); unsigned int randskin = R_Clutter_Random(ctx); if (!mod) return; if (mod->type == mod_alias) { //fill in the parts of the entity_t that Alias_GAliasBuildMesh needs. memset(&re, 0, sizeof(re)); re.framestate.g[FS_REG].lerpweight[0] = 1; re.model = mod; inf = (galiasinfo_t*)Mod_Extradata (mod); while(inf) { galiasskin_t *skins = inf->ofsskins; re.framestate.g[FS_REG].frame[0] = randanim%inf->numanimations; if (skins->numframes) { unsigned int frame = randskin%skins->numframes; Alias_GAliasBuildMesh(&mesh, NULL, inf, surfnum, &re, false); surfnum++; //fixme: if shares verts, rewind the verts and don't add more somehow, while being careful with shaders R_Clutter_Insert_Soup(ctx, skins->frame[frame].shader, mesh.xyz_array, mesh.st_array, mesh.normals_array, mesh.snormals_array, mesh.tnormals_array, mesh.colors4f_array[0], mesh.numvertexes, mesh.indexes, mesh.numindexes, scale, origin, axis); } inf = inf->nextsurf; } Alias_FlushCache(); //it got built using an entity on the stack, make sure other stuff doesn't get hurt. } } static void R_Clutter_Insert(void *vctx, vec3_t *fte_restrict points, size_t numtris, shader_t *surface) { struct shader_clutter_s *clut; unsigned int obj; clutter_build_ctx_t *ctx = vctx; model_t *mod[DESCSPERSHADER]; if (!surface || !surface->clutter) return; //nothing to do. //avoid returning on error, so the randomization is dependable when content is still loading. for (clut = surface->clutter, obj = 0; clut && obj <= DESCSPERSHADER; clut = clut->next, obj++) { mod[obj] = Mod_ForName(clut->modelname, MLV_WARN); if (mod[obj]->loadstate == MLS_LOADING) { if (!ctx->loadingmodel) ctx->loadingmodel = mod[obj]; mod[obj] = NULL; } else if (mod[obj]->type != mod_alias) mod[obj] = NULL; } while(numtris-->0) { vec3_t xd; vec3_t yd; vec3_t zd; vec3_t norm; vec3_t axis[3]; vec3_t org, dir; float dot; float triarea; // vec3_t discard; // unsigned int subimage; vec_t xm, ym, zm, s; VectorSubtract(points[1], points[0], xd); VectorSubtract(points[2], points[0], yd); VectorSubtract(points[2], points[1], zd); CrossProduct(yd, xd, norm); VectorNormalize(norm); if (norm[2] >= 0.7) { //determine area of triangle xm = Length(xd); ym = Length(yd); zm = Length(zd); s = (xm+ym+zm)/2; triarea = sqrt(s*(s-xm)*(s-ym)*(s-zm)); for (clut = surface->clutter, obj = 0; clut && obj <= DESCSPERSHADER; clut = clut->next, obj++) { float spacing = clut->spacing / r_clutter_density.value; if (spacing < 1) spacing = 1; ctx->area[obj] += triarea; while (ctx->area[obj] >= spacing) { float scale = clut->scalemin + R_Clutter_FRandom(ctx) * (clut->scalemax-clut->scalemin); ctx->area[obj] -= spacing; //pick a random spot xm = R_Clutter_FRandom(ctx)*R_Clutter_FRandom(ctx); ym = R_Clutter_FRandom(ctx) * (1-xm); VectorMA(points[0], xm, xd, org); VectorMA(org, ym, yd, org); //randomize the direction dot = clut->anglemin + R_Clutter_FRandom(ctx) * (clut->anglemax-clut->anglemin); dir[0] = cos(dot); dir[1] = sin(dot); dir[2] = 0; //figure out various directions dot = -DotProduct(dir, norm); VectorMA(dir, dot, norm, dir); VectorNormalize(dir); VectorCopy(norm, axis[2]); CrossProduct(axis[2], dir, axis[1]); CrossProduct(axis[1], axis[2], axis[0]); VectorMA(org, clut->zofs*scale, axis[2], org); R_Clutter_Insert_Mesh(ctx, mod[obj], scale, org, axis); /* VectorMA(org, r_clutter_size.value/2, dir, vertcoord[numverts]); VectorMA(org, -(r_clutter_size.value/2), dir, vertcoord[numverts+1]); VectorMA(vertcoord[numverts], r_clutter_height.value, norm, vertcoord[numverts+2]); VectorMA(vertcoord[numverts+1], r_clutter_height.value, norm, vertcoord[numverts+3]); subimage = R_Clutter_Random(ctx); Vector2Set(texcoord[numverts], subimage%r_clutter_atlaswidth.ival, (subimage/r_clutter_atlaswidth.ival)%r_clutter_atlasheight.ival); texcoord[numverts][0] *= 1/r_clutter_atlaswidth.value; texcoord[numverts][1] *= 1/r_clutter_atlasheight.value; Vector2Set(texcoord[numverts+1], texcoord[numverts][0]+(1/r_clutter_atlaswidth.value), texcoord[numverts][1]); Vector2Set(texcoord[numverts+2], texcoord[numverts][0] , texcoord[numverts][1]); Vector2Set(texcoord[numverts+3], texcoord[numverts][0]+(1/r_clutter_atlaswidth.value), texcoord[numverts][1]); texcoord[numverts+0][1]+=(1/r_clutter_atlasheight.value); texcoord[numverts+1][1]+=(1/r_clutter_atlasheight.value); Vector4Set(colours[numverts+0], 1, 1, 1, 1); VectorMA(org, 1/8.0, norm, org);//push away from the surface to avoid precision issues with lighting on slopes cl.worldmodel->funcs.LightPointValues(cl.worldmodel, org, colours[numverts+0], discard, discard); VectorScale(colours[numverts+0], 1/512.0, colours[numverts+0]); Vector4Copy(colours[numverts+0], colours[numverts+1]); Vector4Copy(colours[numverts+0], colours[numverts+2]); Vector4Copy(colours[numverts+1], colours[numverts+3]); indexes[numidx+0] = numverts+0; indexes[numidx+1] = numverts+2; indexes[numidx+2] = numverts+1; indexes[numidx+3] = numverts+2; indexes[numidx+4] = numverts+3; indexes[numidx+5] = numverts+1; numverts += 4; numidx += 6; */ } } } points += 3; } } void R_Clutter_Emit(batch_t **batches) { const float cluttersize = r_clutter_distance.value; int vx, vy, vz; int x, y, z, key, i, j; cluttersector_t *sect; batch_t *b; qboolean rebuildlimit = false; if (!cl.worldmodel || cl.worldmodel->loadstate != MLS_LOADED || r_clutter_density.value <= 0 || (r_refdef.flags & RDF_NOWORLDMODEL)) return; if (qrenderer != QR_OPENGL && qrenderer != QR_VULKAN) //vbo only! return; //rebuild if any of the cvars changes. key = r_clutter_density.modified + r_clutter_distance.modified; vx = floor((r_refdef.vieworg[0] / cluttersize)); vy = floor((r_refdef.vieworg[1] / cluttersize)); vz = floor((r_refdef.vieworg[2] / cluttersize)); for (z = vz-1; z <= vz+1; z++) for (y = vy-1; y <= vy+1; y++) for (x = vx-1; x <= vx+1; x++) { int ix = x%3; int iy = y%3; int iz = z%3; if (ix < 0) ix += 3; if (iy < 0) iy += 3; if (iz < 0) iz += 3; sect = &cluttersector[ix + (iy*3) + (iz*3*3)]; if (sect->loadingmodel && sect->loadingmodel->loadstate != MLS_LOADING) { sect->loadingmodel = NULL; sect->key-=1; //rebuild even if failed, this covers multiple models. } if (sect->x != x || sect->y != y || sect->z != z || sect->key != key) { vbobctx_t vctx; clutter_build_ctx_t cctx; vec3_t org = {x*cluttersize+(cluttersize/2),y*cluttersize+(cluttersize/2),z*cluttersize+(cluttersize/2)}; vec3_t down = {0, 0, -1}; vec3_t forward = {1, 0, 0}; vec3_t right = {0, 1, 0}; if (r_refdef.recurse) //FIXME continue; if (rebuildlimit) continue; rebuildlimit = true; sect->x = x; sect->y = y; sect->z = z; sect->key = key; //make sure any old state is gone for (i = 0; i < sect->numsoups; i++) { BE_VBO_Destroy(§->soups[i].vbo.coord, sect->soups[i].vbo.vbomem); BE_VBO_Destroy(§->soups[i].vbo.indicies, sect->soups[i].vbo.ebomem); } sect->numsoups = 0; memset(&cctx, 0, sizeof(cctx)); cctx.x = x; cctx.y = y; cctx.z = z; cctx.w = (sect-cluttersector)+1; Mod_ClipDecal(cl.worldmodel, org, down, forward, right, cluttersize, 0, 0, R_Clutter_Insert, &cctx); sect->loadingmodel = cctx.loadingmodel; for (i = 0; i < cctx.numsoups; i++) { if (cctx.soups[i].numverts) { sect->soups[sect->numsoups].shader = cctx.soups[i].shader; sect->soups[sect->numsoups].pmesh = §->soups[sect->numsoups].mesh; BE_VBO_Begin(&vctx, (sizeof(cctx.soups[i].coord[0]) + sizeof(cctx.soups[i].texcoord[0]) + sizeof(cctx.soups[i].colour[0]) + 3*sizeof(vec3_t))*cctx.soups[i].numverts); BE_VBO_Data(&vctx, cctx.soups[i].coord, sizeof(cctx.soups[i].coord[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.coord); BE_VBO_Data(&vctx, cctx.soups[i].texcoord, sizeof(cctx.soups[i].texcoord[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.texcoord); BE_VBO_Data(&vctx, cctx.soups[i].colour, sizeof(cctx.soups[i].colour[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.colours[0]); BE_VBO_Data(&vctx, cctx.soups[i].normal, sizeof(cctx.soups[i].normal[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.normals); BE_VBO_Data(&vctx, cctx.soups[i].sdir, sizeof(cctx.soups[i].sdir[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.svector); BE_VBO_Data(&vctx, cctx.soups[i].tdir, sizeof(cctx.soups[i].tdir[0])*cctx.soups[i].numverts, §->soups[sect->numsoups].vbo.tvector); BE_VBO_Finish(&vctx, cctx.soups[i].idx, sizeof(cctx.soups[i].idx[0])*cctx.soups[i].numidx, §->soups[sect->numsoups].vbo.indicies, §->soups[sect->numsoups].vbo.vbomem, §->soups[sect->numsoups].vbo.ebomem); sect->soups[sect->numsoups].vbo.colours_bytes = false; sect->soups[sect->numsoups].mesh.numindexes = sect->soups[sect->numsoups].vbo.indexcount = cctx.soups[i].numidx; sect->soups[sect->numsoups].mesh.numvertexes = sect->soups[sect->numsoups].vbo.vertcount = cctx.soups[i].numverts; sect->numsoups++; } BZ_Free(cctx.soups[i].coord); BZ_Free(cctx.soups[i].texcoord); BZ_Free(cctx.soups[i].colour); BZ_Free(cctx.soups[i].normal); BZ_Free(cctx.soups[i].sdir); BZ_Free(cctx.soups[i].tdir); BZ_Free(cctx.soups[i].idx); } } //emit a batch if we have grassy surfaces in this block for (i = 0; i < sect->numsoups; i++) { b = BE_GetTempBatch(); if (!b) return; memset(b, 0, sizeof(*b)); for (j = 0; j < MAXRLIGHTMAPS; j++) b->lightmap[j] = -1; b->ent = &r_worldentity; b->meshes = 1; b->mesh = §->soups[i].pmesh; b->vbo = §->soups[i].vbo; b->shader = sect->soups[i].shader; b->next = batches[b->shader->sort]; batches[b->shader->sort] = b; } } } void R_Clutter_Purge(void) { size_t i, j; cluttersector_t *sect; if (!qrenderer) return; for (i = 0; i < sizeof(cluttersector)/sizeof(cluttersector[0]); i++) { sect = &cluttersector[i]; for (j = 0; j < sect->numsoups; j++) { BE_VBO_Destroy(§->soups[j].vbo.coord, sect->soups[j].vbo.vbomem); BE_VBO_Destroy(§->soups[j].vbo.indicies, sect->soups[j].vbo.ebomem); } memset(sect, 0, sizeof(*sect)); } } static void QDECL R_Rockettrail_Callback(struct cvar_s *var, char *oldvalue) { int i; model_t *mod; extern model_t *mod_known; extern int mod_numknown; if (cls.state == ca_disconnected) return; // don't bother parsing while disconnected for (i=0 , mod=mod_known ; iloadstate == MLS_LOADED) if (mod->flags & MF_ROCKET) P_LoadedModel(mod); } } static void QDECL R_Grenadetrail_Callback(struct cvar_s *var, char *oldvalue) { int i; model_t *mod; extern model_t *mod_known; extern int mod_numknown; if (cls.state == ca_disconnected) return; // don't bother parsing while disconnected for (i=0 , mod=mod_known ; iloadstate == MLS_LOADED) if (mod->flags & MF_GRENADE) P_LoadedModel(mod); } } extern particleengine_t pe_null; #ifdef PSET_CLASSIC extern particleengine_t pe_classic; #endif particleengine_t pe_darkplaces; particleengine_t pe_qmb; #ifdef PSET_SCRIPT extern particleengine_t pe_script; #endif particleengine_t *particlesystem[] = { #ifdef PSET_SCRIPT &pe_script, #endif &pe_darkplaces, &pe_qmb, #ifdef PSET_CLASSIC &pe_classic, #endif &pe_null, NULL, }; static void QDECL R_ParticleSystem_Callback(struct cvar_s *var, char *oldvalue) { int i; if (pe) { CL_ClearTEntParticleState(); CL_ClearLerpEntsParticleState(); #ifdef Q2CLIENT CLQ2_ClearParticleState(); #endif pe->ShutdownParticles(); } if (!qrenderer) { pe = &pe_null; } else { pe = NULL; for (i = 0; particlesystem[i]; i++) { if ( (particlesystem[i]->name1 && !stricmp(var->string, particlesystem[i]->name1)) || (particlesystem[i]->name2 && !stricmp(var->string, particlesystem[i]->name2))) { pe = particlesystem[i]; break; } if (!pe) if (particlesystem[i]->name1) pe = particlesystem[i]; } } if (!pe) Sys_Error("No particle system available. Please recompile."); if (!pe->InitParticles()) { Con_Printf("Particlesystem %s failed to init\n", pe->name1); pe = &pe_null; pe->InitParticles(); } pe->ClearParticles(); CL_RegisterParticles(); } cvar_t r_decal_noperpendicular = CVARD("r_decal_noperpendicular", "1", "When enabled, decals will not be generated on planes at a steep angle from clipped decal orientation."); cvar_t r_rockettrail = CVARFC("r_rockettrail", "1", CVAR_SEMICHEAT, R_Rockettrail_Callback); cvar_t r_grenadetrail = CVARFC("r_grenadetrail", "1", CVAR_SEMICHEAT, R_Grenadetrail_Callback); #ifdef NOLEGACY cvar_t r_particlesystem = CVARFC("r_particlesystem", "script", CVAR_SEMICHEAT|CVAR_ARCHIVE|CVAR_NOSET, R_ParticleSystem_Callback); cvar_t r_particledesc = CVARAF("r_particledesc", "", "r_particlesdesc", CVAR_SEMICHEAT|CVAR_ARCHIVE); #else cvar_t r_particlesystem = CVARFC("r_particlesystem", IFMINIMAL("classic", "script"), CVAR_SEMICHEAT|CVAR_ARCHIVE, R_ParticleSystem_Callback); cvar_t r_particledesc = CVARAF("r_particledesc", "classic", "r_particlesdesc", CVAR_SEMICHEAT|CVAR_ARCHIVE); #endif extern cvar_t r_bouncysparks; extern cvar_t r_part_rain; extern cvar_t r_bloodstains; extern cvar_t gl_part_flame; cvar_t r_part_rain_quantity = CVARF("r_part_rain_quantity", "1", CVAR_ARCHIVE); cvar_t r_particle_tracelimit = CVARFD("r_particle_tracelimit", "0x7fffffff", CVAR_ARCHIVE, "Number of traces to allow per frame for particle physics."); cvar_t r_part_sparks = CVAR("r_part_sparks", "1"); cvar_t r_part_sparks_trifan = CVAR("r_part_sparks_trifan", "1"); cvar_t r_part_sparks_textured = CVAR("r_part_sparks_textured", "1"); cvar_t r_part_beams = CVAR("r_part_beams", "1"); cvar_t r_part_contentswitch = CVARFD("r_part_contentswitch", "1", CVAR_ARCHIVE, "Enable particle effects to change based on content (ex. water)."); cvar_t r_part_density = CVARF("r_part_density", "1", CVAR_ARCHIVE); cvar_t r_part_classic_expgrav = CVARFD("r_part_classic_expgrav", "10", CVAR_ARCHIVE, "Scaler for how fast classic explosion particles should accelerate due to gravity. 1 for like vanilla, 10 for like zquake."); cvar_t r_part_classic_opaque = CVARFD("r_part_classic_opaque", "0", CVAR_ARCHIVE, "Disables transparency on classic particles, for the oldskool look."); cvar_t r_part_maxparticles = CVAR("r_part_maxparticles", "8192"); cvar_t r_part_maxdecals = CVAR("r_part_maxdecals", "8192"); particleengine_t *pe; void P_ParticleEffect_f(void); static void P_ParticleEffectAlias_f(void); void P_InitParticleSystem(void) { char *particlecvargroupname = "Particle effects"; Cvar_Register(&r_decal_noperpendicular, particlecvargroupname); //decals might actually be used for more than just particles, but oh well. Cvar_Register(&r_particlesystem, particlecvargroupname); //particles Cvar_Register(&r_particledesc, particlecvargroupname); Cvar_Register(&r_bouncysparks, particlecvargroupname); Cvar_Register(&r_part_rain, particlecvargroupname); Cvar_Register(&r_part_rain_quantity, particlecvargroupname); Cvar_Register(&r_particle_tracelimit, particlecvargroupname); Cvar_Register(&r_part_maxparticles, particlecvargroupname); Cvar_Register(&r_part_maxdecals, particlecvargroupname); Cvar_Register(&r_part_sparks, particlecvargroupname); Cvar_Register(&r_part_sparks_trifan, particlecvargroupname); Cvar_Register(&r_part_sparks_textured, particlecvargroupname); Cvar_Register(&r_part_beams, particlecvargroupname); Cvar_Register(&r_part_contentswitch, particlecvargroupname); Cvar_Register(&r_part_density, particlecvargroupname); Cvar_Register(&r_part_classic_expgrav, particlecvargroupname); Cvar_Register(&r_part_classic_opaque, particlecvargroupname); Cvar_Register (&gl_part_flame, particlecvargroupname); Cvar_Register (&r_rockettrail, particlecvargroupname); Cvar_Register (&r_grenadetrail, particlecvargroupname); //always registered to suck up stray r_part commands even when the scripted system is not active. #ifdef PSET_SCRIPT Cmd_AddCommand("r_part", P_ParticleEffect_f); #endif Cmd_AddCommand("r_partredirect", P_ParticleEffectAlias_f); R_Clutter_Init(); } static struct partalias_s { struct partalias_s *next; const char *from; const char *to; } *partaliaslist; static void P_ParticleEffectAlias_f(void) { struct partalias_s **link, *l; char *from = Cmd_Argv(1); char *to = Cmd_Argv(2); //user wants to list all if (!*from) { for (l = partaliaslist; l; l = l->next) { Con_Printf("%s -> %s\n", l->from, l->to); } return; } //unlink the current value for (link = &partaliaslist; (l=*link); link = &(*link)->next) { if (!Q_strcasecmp(l->from, from)) { //they didn't specify a to, so just print out this one effect without removing it. if (Cmd_Argc() == 2) { Con_Printf("particle %s is currently remapped to %s\n", l->from, l->to); return; } *link = l->next; Z_Free(l); break; } } //create a new entry. if (*to && Q_strcasecmp(from, to)) { l = Z_Malloc(sizeof(*l) + strlen(from) + strlen(to) + 2); l->from = (char*)(l + 1); strcpy((char*)l->from, from); l->to = l->from + strlen(l->from)+1; strcpy((char*)l->to, to); l->next = partaliaslist; partaliaslist = l; } CL_RegisterParticles(); } int P_FindParticleType(const char *efname) { struct partalias_s *l; int recurselimit = 5; if (!pe) return P_INVALID; for (l = partaliaslist; l; ) { if (!Q_strcasecmp(l->from, efname)) { efname = l->to; if (recurselimit --> 0) l = partaliaslist; else return P_INVALID; } else l = l->next; } return pe->FindParticleType(efname); } void P_Shutdown(void) { if (pe) { CL_ClearTEntParticleState(); CL_ClearLerpEntsParticleState(); #ifdef Q2CLIENT CLQ2_ClearParticleState(); #endif pe->ShutdownParticles(); } pe = NULL; R_Clutter_Purge(); } //traces against renderable entities //0 says hit nothing. //1 says hit world //>1 says hit some entity entity_t *TraceLineR (vec3_t start, vec3_t end, vec3_t impact, vec3_t normal) { trace_t trace; float len, bestlen; int i, j; vec3_t delta, ts, te; entity_t *pe; entity_t *result=NULL; // vec3_t axis[3]; vec3_t movemins, movemaxs; memset (&trace, 0, sizeof(trace)); VectorSubtract(end, start, delta); bestlen = Length(delta); VectorCopy (end, impact); for (i = 0; i < 3; i++) { if (start[i] > end[i]) { movemins[i] = end[i]; movemaxs[i] = start[i]; } else { movemins[i] = start[i]; movemaxs[i] = end[i]; } } for (i=-1 ; irtype != RT_MODEL || pe->shaderRGBAf[3] < 1 || (pe->flags & (RF_ADDITIVE|RF_NODEPTHTEST|RF_TRANSLUCENT|RF_EXTERNALMODEL))) continue; if (pe->model && pe->model->funcs.NativeTrace && pe->model->loadstate == MLS_LOADED) { //try to trivially reject the mesh. float ext = 0; float t; for (j = 0; j < 3; j++) { t = fabs(pe->model->maxs[j]); ext = max(ext, t); t = fabs(pe->model->mins[j]); ext = max(ext, t); } if ( movemins[0] > pe->origin[0]+ext || movemins[1] > pe->origin[1]+ext || movemins[2] > pe->origin[2]+ext || movemaxs[0] < pe->origin[0]-ext || movemaxs[1] < pe->origin[1]-ext || movemaxs[2] < pe->origin[2]-ext ) continue; VectorSubtract(start, pe->origin, ts); VectorSubtract(end, pe->origin, te); pe->model->funcs.NativeTrace(pe->model, 0, pe->framestate.g[FS_REG].frame[pe->framestate.g[FS_REG].lerpweight[1] > pe->framestate.g[FS_REG].lerpweight[0]], pe->axis, ts, te, vec3_origin, vec3_origin, false, MASK_WORLDSOLID, &trace); if (trace.fraction<1) { VectorSubtract(trace.endpos, ts, delta); len = DotProduct(delta,delta); if (len < bestlen) { bestlen = len; if (normal) VectorCopy (trace.plane.normal, normal); VectorAdd (pe->origin, trace.endpos, impact); } result = pe; } /*if (trace.startsolid) { VectorNormalize(delta); if (normal) { normal[0] = -delta[0]; normal[1] = -delta[1]; normal[2] = -delta[2]; } VectorCopy (end, impact); return NULL; }*/ } } return result; } //traces against networked entities only. //0 says hit nothing. //1 says hit world //>1 says hit some entity float CL_TraceLine (vec3_t start, vec3_t end, vec3_t impact, vec3_t normal, int *ent) { trace_t trace; float bestfrac=1; int i; vec3_t ts, te; physent_t *pe; int result=0; vec3_t axis[3]; memset (&trace, 0, sizeof(trace)); VectorCopy (end, impact); if (normal) VectorClear(normal); for (i=0 ; i < pmove.numphysent ; i++) { pe = &pmove.physents[i]; if (pe->nonsolid) continue; if (pe->model && pe->model->loadstate == MLS_LOADED) { VectorSubtract(start, pe->origin, ts); VectorSubtract(end, pe->origin, te); if (pe->angles[0] || pe->angles[1] || pe->angles[2]) { AngleVectors(pe->angles, axis[0], axis[1], axis[2]); VectorNegate(axis[1], axis[1]); pe->model->funcs.NativeTrace(pe->model, 0, 0, axis, ts, te, vec3_origin, vec3_origin, false, MASK_WORLDSOLID, &trace); } else pe->model->funcs.NativeTrace(pe->model, 0, 0, NULL, ts, te, vec3_origin, vec3_origin, false, MASK_WORLDSOLID, &trace); if (trace.fraction<1) { if (bestfrac > trace.fraction) { bestfrac = trace.fraction; if (normal) VectorCopy (trace.plane.normal, normal); VectorAdd (pe->origin, trace.endpos, impact); } result = pe->info; } if (trace.startsolid) { if (normal) { VectorSubtract(start, end, normal); VectorNormalize(normal); } VectorCopy (end, impact); //hit nothing if (ent) *ent = 0; return 1; } } } if (ent) *ent = result; return bestfrac; } //handy utility... void P_EmitEffect (vec3_t pos, int type, trailstate_t **tsk) { if (cl.paused) return; pe->RunParticleEffectState(pos, NULL, ((host_frametime>0.1)?0.1:host_frametime), type, tsk); } // P_SelectableTrail: given default/opposite effects, model pointer, and a user selection cvar // changes model to the appropriate trail effect and default trail index static void P_SelectableTrail(int *trailid, int *trailpalidx, cvar_t *selection, int mdleffect, int mdlcidx, int oppeffect, int oppcidx) { int select = (int)(selection->value); switch (select) { case 0: // check for string, otherwise no trail if (selection->string[0] == '0') { *trailid = P_INVALID; *trailpalidx = -1; break; } else { int effect = P_FindParticleType(selection->string); if (effect >= 0) { *trailid = effect; *trailpalidx = mdlcidx; break; } } // fall through to default (so semicheat will work properly) case 1: // default model effect default: *trailid = mdleffect; *trailpalidx = mdlcidx; break; case 2: // opposite effect *trailid = oppeffect; *trailpalidx= oppcidx; break; case 3: // alt rocket effect *trailid = P_FindParticleType("TR_ALTROCKET"); *trailpalidx = 107; break; case 4: // gib *trailid = P_FindParticleType("TR_BLOOD"); *trailpalidx = 70; break; case 5: // zombie gib *trailid = P_FindParticleType("TR_SLIGHTBLOOD"); *trailpalidx = 70; break; case 6: // Scrag tracer *trailid = P_FindParticleType("TR_WIZSPIKE"); *trailpalidx = 60; break; case 7: // Knight tracer *trailid = P_FindParticleType("TR_KNIGHTSPIKE"); *trailpalidx = 238; break; case 8: // Vore tracer *trailid = P_FindParticleType("TR_VORESPIKE"); *trailpalidx = 154; break; case 9: // rail trail *trailid = P_FindParticleType("TE_RAILTRAIL"); *trailpalidx = 15; break; } } //figure out which particle trail to use for the given model, filling in its values as required. void P_DefaultTrail (unsigned int entityeffects, unsigned int modelflags, int *trailid, int *trailpalidx) { // TODO: EF_BRIGHTFIELD should probably be handled in here somewhere // TODO: make trail default color into RGB values instead of indexes if (!pe) return; if (entityeffects & EF_BRIGHTFIELD) { *trailid = P_FindParticleType("EF_BRIGHTFIELD"); *trailpalidx = 70; } else if (entityeffects & DPEF_FLAME) { *trailid = P_FindParticleType("EF_FLAME"); *trailpalidx = 70; } else if (entityeffects & DPEF_STARDUST) { *trailid = P_FindParticleType("EF_STARDUST"); *trailpalidx = 70; } else if (modelflags & MF_ROCKET) P_SelectableTrail(trailid, trailpalidx, &r_rockettrail, P_FindParticleType("TR_ROCKET"), 109, P_FindParticleType("TR_GRENADE"), 6); else if (modelflags & MF_GRENADE) P_SelectableTrail(trailid, trailpalidx, &r_grenadetrail, P_FindParticleType("TR_GRENADE"), 6, P_FindParticleType("TR_ROCKET"), 109); else if (modelflags & MF_GIB) { *trailid = P_FindParticleType("TR_BLOOD"); *trailpalidx = 70; } else if (modelflags & MF_TRACER) { *trailid = P_FindParticleType("TR_WIZSPIKE"); *trailpalidx = 60; } else if (modelflags & MF_ZOMGIB) { *trailid = P_FindParticleType("TR_SLIGHTBLOOD"); *trailpalidx = 70; } else if (modelflags & MF_TRACER2) { *trailid = P_FindParticleType("TR_KNIGHTSPIKE"); *trailpalidx = 238; } else if (modelflags & MF_TRACER3) { *trailid = P_FindParticleType("TR_VORESPIKE"); *trailpalidx = 154; } #ifdef HEXEN2 else if (modelflags & MFH2_BLOODSHOT) //these are the hexen2 ones. { *trailid = P_FindParticleType("tr_bloodshot"); *trailpalidx = 136; } else if (modelflags & MFH2_FIREBALL) { *trailid = P_FindParticleType("tr_fireball"); *trailpalidx = 424; } else if (modelflags & MFH2_ACIDBALL) { *trailid = P_FindParticleType("tr_acidball"); *trailpalidx = 440; } else if (modelflags & MFH2_ICE) { *trailid = P_FindParticleType("tr_ice"); *trailpalidx = 408; } else if (modelflags & MFH2_SPIT) { *trailid = P_FindParticleType("tr_spit"); *trailpalidx = 260; } else if (modelflags & MFH2_SPELL) { *trailid = P_FindParticleType("tr_spell"); *trailpalidx = 260; } else if (modelflags & MFH2_VORP_MISSILE) { *trailid = P_FindParticleType("tr_vorpmissile"); *trailpalidx = 302; } else if (modelflags & MFH2_SET_STAFF) { *trailid = P_FindParticleType("tr_setstaff"); *trailpalidx = 424; } else if (modelflags & MFH2_MAGICMISSILE) { *trailid = P_FindParticleType("tr_magicmissile"); *trailpalidx = 149; } else if (modelflags & MFH2_BONESHARD) { *trailid = P_FindParticleType("tr_boneshard"); *trailpalidx = 384; } else if (modelflags & MFH2_SCARAB) { *trailid = P_FindParticleType("tr_scarab"); *trailpalidx = 254; } else if (modelflags & MFH2_ROCKET) { //spiders *trailid = P_FindParticleType("TR_GREENBLOOD"); *trailpalidx = 70; //fixme } #endif else { *trailid = P_INVALID; *trailpalidx = -1; } }