1 /** @file rend_particle.cpp  Particle effect rendering.
2  *
3  * @authors Copyright © 2003-2017 Jaakko Keränen <jaakko.keranen@iki.fi>
4  * @authors Copyright © 2006-2015 Daniel Swanson <danij@dengine.net>
5  *
6  * @par License
7  * GPL: http://www.gnu.org/licenses/gpl.html
8  *
9  * <small>This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by the
11  * Free Software Foundation; either version 2 of the License, or (at your
12  * option) any later version. This program is distributed in the hope that it
13  * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
14  * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
15  * Public License for more details. You should have received a copy of the GNU
16  * General Public License along with this program; if not, write to the Free
17  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18  * 02110-1301 USA</small>
19  */
20 
21 #include "de_base.h"
22 #include "render/rend_particle.h"
23 
24 #include "gl/gl_main.h"
25 #include "gl/gl_texmanager.h"
26 #include "gl/texturecontent.h"
27 
28 #include "world/map.h"
29 #include "world/p_players.h"
30 #include "BspLeaf"
31 #include "ConvexSubspace"
32 #include "Line"
33 #include "Plane"
34 #include "client/clientsubsector.h"
35 
36 #include "resource/image.h"
37 
38 #include "render/r_main.h"
39 #include "render/viewports.h"
40 #include "render/rend_main.h"
41 #include "render/rend_model.h"
42 #include "render/vissprite.h"
43 
44 #include "clientapp.h"
45 #include "misc/r_util.h"
46 #include "sys_system.h"  // novideo
47 
48 #include <doomsday/console/var.h>
49 #include <doomsday/filesys/fs_main.h>
50 #include <de/concurrency.h>
51 #include <de/vector1.h>
52 #include <de/Folder>
53 #include <de/GLInfo>
54 #include <de/ImageFile>
55 #include <cstdlib>
56 
57 using namespace de;
58 using namespace world;
59 
60 // Point + custom textures.
61 #define NUM_TEX_NAMES (MAX_PTC_TEXTURES)
62 
63 static DGLuint pointTex, ptctexname[MAX_PTC_TEXTURES];
64 
65 static bool hasPoints, hasLines, hasModels, hasNoBlend, hasAdditive;
66 static bool hasPointTexs[NUM_TEX_NAMES];
67 
68 struct OrderedParticle
69 {
70     Generator const *generator;
71     dint particleId;
72     dfloat distance;
73 };
74 static OrderedParticle *order;
75 static size_t orderSize;
76 
77 static size_t numParts;
78 
79 /*
80  * Console variables:
81  */
82 dbyte useParticles = true;
83 static dint maxParticles;           ///< @c 0= Unlimited.
84 static dint particleNearLimit;
85 static dfloat particleDiffuse = 4;
86 
pointDist(fixed_t const c[3])87 static dfloat pointDist(fixed_t const c[3])
88 {
89     viewdata_t const *viewData = &viewPlayer->viewport();
90     dfloat dist = ((viewData->current.origin.y - FIX2FLT(c[1])) * -viewData->viewSin)
91                 - ((viewData->current.origin.x - FIX2FLT(c[0])) * viewData->viewCos);
92 
93     return de::abs(dist);  // Always return positive.
94 }
95 
tryFindImage(String name)96 static Path tryFindImage(String name)
97 {
98     //
99     // First look for a colorkeyed version.
100     //
101     try
102     {
103         String foundPath = App_FileSystem().findPath(de::Uri("Textures", name + "-ck"),
104                                                      RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC));
105         // Ensure the path is absolute.
106         return App_BasePath() / foundPath;
107     }
108     catch(FS1::NotFoundError const&)
109     {}  // Ignore this error.
110 
111     //
112     // Look for the regular version.
113     //
114     try
115     {
116         String foundPath = App_FileSystem().findPath(de::Uri("Textures", name),
117                                                      RLF_DEFAULT, App_ResourceClass(RC_GRAPHIC));
118         // Ensure the path is absolute.
119         return App_BasePath() / foundPath;
120     }
121     catch(FS1::NotFoundError const&)
122     {}  // Ignore this error.
123 
124     return Path();  // Not found.
125 }
126 
127 // Try to load the texture.
loadParticleTexture(duint particleTex)128 static dbyte loadParticleTexture(duint particleTex)
129 {
130     DENG2_ASSERT(particleTex < MAX_PTC_TEXTURES);
131 
132     image_t image;
133 
134     try
135     {
136         // First check if there is a texture asset for this particle.
137         String const assetId = QStringLiteral("texture.particle.%1").arg(particleTex, 2, 10, QChar('0'));
138         if (App::assetExists(assetId))
139         {
140             auto asset = App::asset(assetId);
141 
142             ImageFile const &img = App::rootFolder().locate<ImageFile const>
143                     (asset.absolutePath(QStringLiteral("path")));
144 
145             Image_InitFromImage(image, img.image());
146         }
147         else
148         {
149             // Fallback: look in the Textures scheme.
150             auto particleImageName = String("Particle%1").arg(particleTex, 2, 10, QChar('0'));
151             Path foundPath = tryFindImage(particleImageName);
152             if (foundPath.isEmpty())
153                 return 0;
154 
155             if (!GL_LoadImage(image, foundPath.toUtf8().constData()))
156             {
157                 LOG_RES_WARNING("Failed to load \"%s\"") << NativePath(foundPath).pretty();
158                 return 0;
159             }
160         }
161     }
162     catch (Error const &er)
163     {
164         LOG_RES_ERROR("Failed to load texture for particle %i: %s")
165                 << particleTex << er.asText();
166         return 0;
167     }
168 
169     // If 8-bit with no alpha, generate alpha automatically.
170     if (image.pixelSize == 1)
171         Image_ConvertToAlpha(image, true);
172 
173     // Create a new texture and upload the image.
174     ptctexname[particleTex] = GL_NewTextureWithParams(
175         image.pixelSize == 4 ? DGL_RGBA :
176         image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 : DGL_RGB,
177         image.size.x, image.size.y, image.pixels,
178         TXCF_NO_COMPRESSION);
179 
180     // Free the buffer.
181     Image_ClearPixelData(image);
182     return 2; // External
183 }
184 
Rend_ParticleLoadSystemTextures()185 void Rend_ParticleLoadSystemTextures()
186 {
187     if(novideo) return;
188 
189     if(!pointTex)
190     {
191         // Load the default "zeroth" texture (a blurred point).
192         /// @todo Create a logical Texture in the "System" scheme for this.
193         image_t image;
194         if(GL_LoadExtImage(image, "Zeroth", LGM_WHITE_ALPHA))
195         {
196             // Loaded successfully and converted accordingly.
197             // Upload the image to GL.
198             pointTex = GL_NewTextureWithParams(
199                 ( image.pixelSize == 2 ? DGL_LUMINANCE_PLUS_A8 :
200                   image.pixelSize == 3 ? DGL_RGB :
201                   image.pixelSize == 4 ? DGL_RGBA : DGL_LUMINANCE ),
202                 image.size.x, image.size.y, image.pixels,
203                 ( TXCF_MIPMAP | TXCF_NO_COMPRESSION ),
204                 0, glmode[mipmapping], GL_LINEAR, 0 /*no anisotropy*/, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE);
205 
206             DENG2_ASSERT(pointTex != 0);
207         }
208         Image_ClearPixelData(image);
209     }
210 }
211 
Rend_ParticleLoadExtraTextures()212 void Rend_ParticleLoadExtraTextures()
213 {
214     if(novideo) return;
215 
216     Rend_ParticleReleaseExtraTextures();
217     if(!App_GameLoaded()) return;
218 
219     QList<dint> loaded;
220     for(dint i = 0; i < MAX_PTC_TEXTURES; ++i)
221     {
222         if(loadParticleTexture(i))
223         {
224             loaded.append(i);
225         }
226     }
227 
228     if(!loaded.isEmpty())
229     {
230         LOG_RES_NOTE("Loaded textures for particle IDs: %s") << Rangei::contiguousRangesAsText(loaded);
231     }
232 }
233 
Rend_ParticleReleaseSystemTextures()234 void Rend_ParticleReleaseSystemTextures()
235 {
236     if(novideo) return;
237 
238     Deferred_glDeleteTextures(1, (GLuint const *) &pointTex);
239     pointTex = 0;
240 }
241 
Rend_ParticleReleaseExtraTextures()242 void Rend_ParticleReleaseExtraTextures()
243 {
244     if(novideo) return;
245 
246     Deferred_glDeleteTextures(NUM_TEX_NAMES, (GLuint const *) ptctexname);
247     de::zap(ptctexname);
248 }
249 
250 /**
251  * Sorts in descending order.
252  */
comparePOrder(void const * a,void const * b)253 static dint comparePOrder(void const *a, void const *b)
254 {
255     auto const &ptA = *(OrderedParticle const *) a;
256     auto const &ptB = *(OrderedParticle const *) b;
257 
258     if(ptA.distance > ptB.distance) return -1;
259     if(ptA.distance < ptB.distance) return 1;
260     return 0;  // Highly unlikely (but possible).
261 }
262 
263 /**
264  * Allocate more memory for the particle ordering buffer, if necessary.
265  */
expandOrderBuffer(size_t max)266 static void expandOrderBuffer(size_t max)
267 {
268     size_t currentSize = orderSize;
269 
270     if(!orderSize)
271     {
272         orderSize = MAX_OF(max, 256);
273     }
274     else
275     {
276         while(max > orderSize)
277         {
278             orderSize *= 2;
279         }
280     }
281 
282     if(orderSize > currentSize)
283     {
284         order = (OrderedParticle *) Z_Realloc(order, sizeof(OrderedParticle) * orderSize, PU_APPSTATIC);
285     }
286 }
287 
288 /**
289  * Determines whether the given particle is potentially visible for the current viewer.
290  */
particlePVisible(ParticleInfo const & pinfo)291 static bool particlePVisible(ParticleInfo const &pinfo)
292 {
293     // Never if it has already expired.
294     if(pinfo.stage < 0) return false;
295 
296     // Never if the origin lies outside the map.
297     if(!pinfo.bspLeaf || !pinfo.bspLeaf->hasSubspace())
298         return false;
299 
300     // Potentially, if the subspace at the origin is visible.
301     return R_ViewerSubspaceIsVisible(pinfo.bspLeaf->subspace());
302 }
303 
304 /**
305  * @return  @c true if there are particles to be drawn.
306  */
listVisibleParticles(world::Map & map)307 static dint listVisibleParticles(world::Map &map)
308 {
309     ::hasPoints = ::hasModels = ::hasLines = false;
310     ::hasAdditive = ::hasNoBlend = false;
311     de::zap(::hasPointTexs);
312 
313     // Count the total number of particles used by generators marked 'visible'.
314     ::numParts = 0;
315     map.forAllGenerators([] (Generator &gen)
316     {
317         if(R_ViewerGeneratorIsVisible(gen))
318         {
319             ::numParts += gen.activeParticleCount();
320         }
321         return LoopContinue;
322     });
323     if(!::numParts) return false;
324 
325     // Allocate the particle depth sort buffer.
326     expandOrderBuffer(::numParts);
327 
328     // Populate the particle sort buffer and determine what type(s) of
329     // particle (model/point/line/etc...) we'll need to draw.
330     size_t numVisibleParts = 0;
331     map.forAllGenerators([&numVisibleParts] (Generator &gen)
332     {
333         if(!R_ViewerGeneratorIsVisible(gen)) return LoopContinue;  // Skip.
334 
335         for(dint i = 0; i < gen.count; ++i)
336         {
337             ParticleInfo const &pinfo = gen.particleInfo()[i];
338 
339             if(!particlePVisible(pinfo)) continue;  // Skip.
340 
341             // Skip particles too far from, or near to, the viewer.
342             dfloat const dist = de::max(pointDist(pinfo.origin), 1.f);
343             if(gen.def->maxDist != 0 && dist > gen.def->maxDist) continue;
344             if(dist < dfloat( ::particleNearLimit )) continue;
345 
346             // This particle is visible. Add it to the sort buffer.
347             OrderedParticle *slot = &::order[numVisibleParts++];
348             slot->generator  = &gen;
349             slot->particleId = i;
350             slot->distance   = dist;
351 
352             // Determine what type of particle this is, as this will affect how
353             // we go order our render passes and manipulate the render state.
354             dint const psType = gen.stages[pinfo.stage].type;
355             if(psType == PTC_POINT)
356             {
357                 ::hasPoints = true;
358             }
359             else if(psType == PTC_LINE)
360             {
361                 ::hasLines = true;
362             }
363             else if(psType >= PTC_TEXTURE && psType < PTC_TEXTURE + MAX_PTC_TEXTURES)
364             {
365                 if(::ptctexname[psType - PTC_TEXTURE])
366                 {
367                     ::hasPointTexs[psType - PTC_TEXTURE] = true;
368                 }
369                 else
370                 {
371                     ::hasPoints = true;
372                 }
373             }
374             else if(psType >= PTC_MODEL && psType < PTC_MODEL + MAX_PTC_MODELS)
375             {
376                 ::hasModels = true;
377             }
378 
379             if(gen.blendmode() == BM_ADD)
380             {
381                 ::hasAdditive = true;
382             }
383             else
384             {
385                 ::hasNoBlend = true;
386             }
387         }
388         return LoopContinue;
389     });
390 
391     // No visible particles?
392     if(!numVisibleParts) return false;
393 
394     // This is the real number of possibly visible particles.
395     ::numParts = numVisibleParts;
396 
397     // Sort the order list back->front. A quicksort is fast enough.
398     qsort(::order, ::numParts, sizeof(OrderedParticle), comparePOrder);
399 
400     return true;
401 }
402 
setupModelParamsForParticle(vissprite_t & spr,ParticleInfo const * pinfo,GeneratorParticleStage const * st,ded_ptcstage_t const * dst,Vector3f const & origin,dfloat dist,dfloat size,dfloat mark,dfloat alpha)403 static void setupModelParamsForParticle(vissprite_t &spr, ParticleInfo const *pinfo,
404     GeneratorParticleStage const *st, ded_ptcstage_t const *dst, Vector3f const &origin,
405     dfloat dist, dfloat size, dfloat mark, dfloat alpha)
406 {
407     drawmodelparams_t &parm = *VS_MODEL(&spr);
408 
409     spr.pose.origin     = Vector3d(origin.xz(), spr.pose.topZ = origin.y);
410     spr.pose.distance   = dist;
411     spr.pose.extraScale = size;  // Extra scaling factor.
412 
413     parm.mf = &ClientApp::resources().modelDef(dst->model);
414     parm.alwaysInterpolate = true;
415 
416     dint frame;
417     if(dst->endFrame < 0)
418     {
419         frame = dst->frame;
420         parm.inter = 0;
421     }
422     else
423     {
424         frame = dst->frame + (dst->endFrame - dst->frame) * mark;
425         parm.inter = M_CycleIntoRange(mark * (dst->endFrame - dst->frame), 1);
426     }
427 
428     ClientApp::resources().setModelDefFrame(*parm.mf, frame);
429     // Set the correct orientation for the particle.
430     if(parm.mf->testSubFlag(0, MFF_MOVEMENT_YAW))
431     {
432         spr.pose.yaw = R_MovementXYYaw(FIX2FLT(pinfo->mov[0]), FIX2FLT(pinfo->mov[1]));
433     }
434     else
435     {
436         spr.pose.yaw = pinfo->yaw / 32768.0f * 180;
437     }
438 
439     if(parm.mf->testSubFlag(0, MFF_MOVEMENT_PITCH))
440     {
441         spr.pose.pitch = R_MovementXYZPitch(FIX2FLT(pinfo->mov[0]), FIX2FLT(pinfo->mov[1]), FIX2FLT(pinfo->mov[2]));
442     }
443     else
444     {
445         spr.pose.pitch = pinfo->pitch / 32768.0f * 180;
446     }
447 
448     spr.light.ambientColor.w = alpha;
449 
450     if(st->flags.testFlag(GeneratorParticleStage::Bright) || levelFullBright)
451     {
452         spr.light.ambientColor.x = spr.light.ambientColor.y = spr.light.ambientColor.z = 1;
453         spr.light.vLightListIdx = 0;
454     }
455     else
456     {
457         world::Map &map = pinfo->bspLeaf->subspace().sector().map();
458 
459 #if 0
460         if(useBias && map.hasLightGrid())
461         {
462             Vector4f color = map.lightGrid().evaluate(spr.pose.origin);
463             // Apply light range compression.
464             for(dint i = 0; i < 3; ++i)
465             {
466                 color[i] += Rend_LightAdaptationDelta(color[i]);
467             }
468             spr.light.ambientColor.x = color.x;
469             spr.light.ambientColor.y = color.y;
470             spr.light.ambientColor.z = color.z;
471         }
472         else
473 #endif
474         {
475             Vector4f const color = pinfo->bspLeaf->subspace().subsector().as<world::ClientSubsector>()
476                                        .lightSourceColorfIntensity();
477 
478             dfloat lightLevel = color.w;
479 
480             // Apply distance attenuation.
481             lightLevel = Rend_AttenuateLightLevel(spr.pose.distance, lightLevel);
482 
483             // Add extra light.
484             lightLevel += Rend_ExtraLightDelta();
485 
486             // The last step is to compress the resultant light value by
487             // the global lighting function.
488             Rend_ApplyLightAdaptation(lightLevel);
489 
490             // Determine the final ambientColor.
491             for(dint i = 0; i < 3; ++i)
492             {
493                 spr.light.ambientColor[i] = lightLevel * color[i];
494             }
495         }
496         Rend_ApplyTorchLight(spr.light.ambientColor, spr.pose.distance);
497 
498         spr.light.vLightListIdx =
499                 Rend_CollectAffectingLights(spr.pose.origin, spr.light.ambientColor,
500                                             map.bspLeafAt(spr.pose.origin).subspacePtr());
501     }
502 }
503 
504 /**
505  * Calculate a unit vector parallel to @a line.
506  *
507  * @todo No longer needed (Surface has tangent space vectors).
508  *
509  * @param unitVect  Unit vector is written here.
510  */
lineUnitVector(Line const & line)511 static Vector2f lineUnitVector(Line const &line)
512 {
513     ddouble len = M_ApproxDistance(line.direction().x, line.direction().y);
514     if(len)
515     {
516         return line.direction() / len;
517     }
518     return Vector2f();
519 }
520 
drawParticles(dint rtype,bool withBlend)521 static void drawParticles(dint rtype, bool withBlend)
522 {
523     DENG2_ASSERT_IN_RENDER_THREAD();
524     DENG_ASSERT_GL_CONTEXT_ACTIVE();
525 
526     viewdata_t const *viewData = &viewPlayer->viewport();
527     Vector3f const leftoff     = viewData->upVec + viewData->sideVec;
528     Vector3f const rightoff    = viewData->upVec - viewData->sideVec;
529 
530     // Should we use a texture?
531     DGLuint tex = 0;
532     if (rtype == PTC_POINT
533        || (rtype >= PTC_TEXTURE && rtype < PTC_TEXTURE + MAX_PTC_TEXTURES))
534     {
535         if (renderTextures)
536         {
537             if (rtype == PTC_POINT || 0 == ptctexname[rtype - PTC_TEXTURE])
538                 tex = pointTex;
539             else
540                 tex = ptctexname[rtype - PTC_TEXTURE];
541         }
542     }
543 
544     dglprimtype_t primType = DGL_QUADS;
545     if (rtype == PTC_MODEL)
546     {
547         DGL_Enable(DGL_DEPTH_WRITE);
548         DGL_Enable(DGL_DEPTH_TEST);
549     }
550     else if (tex != 0)
551     {
552         DGL_Disable(DGL_DEPTH_WRITE);
553         DGL_DepthFunc(DGL_LEQUAL);
554         DGL_CullFace(DGL_NONE);
555 
556         GL_BindTextureUnmanaged(tex, gl::ClampToEdge, gl::ClampToEdge);
557         DGL_Enable(DGL_TEXTURE_2D);
558 
559         DGL_Begin(primType = DGL_QUADS);
560     }
561     else
562     {
563         DGL_Begin(primType = DGL_LINES);
564     }
565 
566     // How many particles will be drawn?
567     size_t i = 0;
568     if (maxParticles)
569     {
570         i = numParts - (unsigned) maxParticles;
571     }
572 
573     blendmode_t mode = BM_NORMAL, newMode;
574     for (; i < numParts; ++i)
575     {
576         OrderedParticle const *slot = &order[i];
577         Generator const *gen        = slot->generator;
578         ParticleInfo const &pinfo   = gen->particleInfo()[slot->particleId];
579 
580         GeneratorParticleStage const *st = &gen->stages[pinfo.stage];
581         ded_ptcstage_t const *stDef      = &gen->def->stages[pinfo.stage];
582 
583         dshort stageType = st->type;
584         if (stageType >= PTC_TEXTURE && stageType < PTC_TEXTURE + MAX_PTC_TEXTURES &&
585             0 == ptctexname[stageType - PTC_TEXTURE])
586         {
587             stageType = PTC_POINT;
588         }
589 
590         // Only render one type of particles.
591         if ((rtype == PTC_MODEL && stDef->model < 0) ||
592             (rtype != PTC_MODEL && stageType != rtype))
593         {
594             continue;
595         }
596 
597         if (rtype >= PTC_TEXTURE && rtype < PTC_TEXTURE + MAX_PTC_TEXTURES &&
598             0 == ptctexname[rtype - PTC_TEXTURE])
599             continue;
600 
601         if ((gen->blendmode() != BM_ADD) == withBlend)
602             continue;
603 
604         if (rtype != PTC_MODEL && !withBlend)
605         {
606             // We may need to change the blending mode.
607             newMode = gen->blendmode();
608 
609             if(newMode != mode)
610             {
611                 DGL_End();
612                 GL_BlendMode(mode = newMode);
613                 DGL_Begin(primType);
614             }
615         }
616 
617         // Is there a next stage for this particle?
618         ded_ptcstage_t const *nextStDef;
619         if (pinfo.stage >= gen->def->stages.size() - 1 ||
620             !gen->stages[pinfo.stage + 1].type)
621         {
622             // There is no "next stage". Use the current one.
623             nextStDef = &gen->def->stages[pinfo.stage];
624         }
625         else
626         {
627             nextStDef = &gen->def->stages[pinfo.stage + 1];
628         }
629 
630         // Where is intermark?
631         dfloat const inter = 1 - dfloat( pinfo.tics ) / stDef->tics;
632 
633         // Calculate size and color.
634         dfloat size = de::lerp(    stDef->particleRadius(slot->particleId),
635                                nextStDef->particleRadius(slot->particleId), inter);
636 
637         // Infinitely small?
638         if(!size) continue;
639 
640         Vector4f color = de::lerp(Vector4f(stDef->color), Vector4f(nextStDef->color), inter);
641 
642         if (!st->flags.testFlag(GeneratorParticleStage::Bright) && !levelFullBright)
643         {
644             // This is a simplified version of sectorlight (no distance attenuation or
645             // range compression).
646             if (world::ConvexSubspace *subspace = pinfo.bspLeaf->subspacePtr())
647             {
648                 dfloat const intensity = subspace->subsector().as<world::ClientSubsector>()
649                                             .lightSourceIntensity();
650                 color *= Vector4f(intensity, intensity, intensity, 1);
651             }
652         }
653 
654         dfloat const maxDist = gen->def->maxDist;
655         dfloat const dist    = order[i].distance;
656 
657         // Far diffuse?
658         if(maxDist)
659         {
660             if(dist > maxDist * .75f)
661             {
662                 color.w *= 1 - (dist - maxDist * .75f) / (maxDist * .25f);
663             }
664         }
665         // Near diffuse?
666         if(particleDiffuse > 0)
667         {
668             if(dist < particleDiffuse * size)
669             {
670                 color.w -= 1 - dist / (particleDiffuse * size);
671             }
672         }
673 
674         // Fully transparent?
675         if(color.w <= 0)
676             continue;
677 
678         DGL_Color4f(color.x, color.y, color.z, color.w);
679 
680         bool const nearWall = (pinfo.contact && !pinfo.mov[0] && !pinfo.mov[1]);
681 
682         bool nearPlane = false;
683         if (world::ConvexSubspace *space = pinfo.bspLeaf->subspacePtr())
684         {
685             auto &subsec = space->subsector().as<world::ClientSubsector>();
686             if (   FLT2FIX(subsec.  visFloor().heightSmoothed()) + 2 * FRACUNIT >= pinfo.origin[2]
687                 || FLT2FIX(subsec.visCeiling().heightSmoothed()) - 2 * FRACUNIT <= pinfo.origin[2])
688             {
689                 nearPlane = true;
690             }
691         }
692 
693         bool flatOnPlane = false, flatOnWall = false;
694         if(stageType == PTC_POINT ||
695            (stageType >= PTC_TEXTURE && stageType < PTC_TEXTURE + MAX_PTC_TEXTURES))
696         {
697             if(st->flags.testFlag(GeneratorParticleStage::PlaneFlat) && nearPlane)
698                 flatOnPlane = true;
699             if(st->flags.testFlag(GeneratorParticleStage::WallFlat) && nearWall)
700                 flatOnWall = true;
701         }
702 
703         Vector3f center = gen->particleOrigin(pinfo).xzy();
704 
705         if(!flatOnPlane && !flatOnWall)
706         {
707             Vector3f offset(frameTimePos, nearPlane ? 0 : frameTimePos, frameTimePos);
708             center += offset * gen->particleMomentum(pinfo).xzy();
709         }
710 
711         // Model particles are rendered using the normal model rendering routine.
712         if(rtype == PTC_MODEL && stDef->model >= 0)
713         {
714             vissprite_t temp;
715             setupModelParamsForParticle(temp, &pinfo, st, stDef, center, dist, size, inter, color.w);
716             Rend_DrawModel(temp);
717             continue;
718         }
719 
720         // The vertices, please.
721         if(tex != 0)
722         {
723             // Should the particle be flat against a plane?
724             if(flatOnPlane)
725             {
726                 DGL_TexCoord2f(0, 0, 0);
727                 DGL_Vertex3f(center.x - size, center.y, center.z - size);
728 
729                 DGL_TexCoord2f(0, 1, 0);
730                 DGL_Vertex3f(center.x + size, center.y, center.z - size);
731 
732                 DGL_TexCoord2f(0, 1, 1);
733                 DGL_Vertex3f(center.x + size, center.y, center.z + size);
734 
735                 DGL_TexCoord2f(0, 0, 1);
736                 DGL_Vertex3f(center.x - size, center.y, center.z + size);
737             }
738             // Flat against a wall, then?
739             else if(flatOnWall)
740             {
741                 DENG2_ASSERT(pinfo.contact);
742                 Line const &contact = *pinfo.contact;
743 
744                 // There will be a slight approximation on the XY plane since
745                 // the particles aren't that accurate when it comes to wall
746                 // collisions.
747 
748                 // Calculate a new center point (project onto the wall).
749                 vec2d_t origin;
750                 V2d_Set(origin, FIX2FLT(pinfo.origin[0]), FIX2FLT(pinfo.origin[1]));
751 
752                 vec2d_t projected;
753                 V2d_ProjectOnLine(projected, origin,
754                                   contact.from().origin().data().baseAs<ddouble>(),
755                                   contact.direction().data().baseAs<ddouble>());
756 
757                 // Move away from the wall to avoid the worst Z-fighting.
758                 ddouble const gap = -1;  // 1 map unit.
759                 ddouble diff[2], dist;
760                 V2d_Subtract(diff, projected, origin);
761                 if((dist = V2d_Length(diff)) != 0)
762                 {
763                     projected[0] += diff[0] / dist * gap;
764                     projected[1] += diff[1] / dist * gap;
765                 }
766 
767                 Vector2f unitVec = lineUnitVector(*pinfo.contact);
768 
769                 DGL_TexCoord2f(0, 0, 0);
770                 DGL_Vertex3f(projected[0] - size * unitVec.x, center.y - size,
771                            projected[1] - size * unitVec.y);
772 
773                 DGL_TexCoord2f(0, 1, 0);
774                 DGL_Vertex3f(projected[0] - size * unitVec.x, center.y + size,
775                            projected[1] - size * unitVec.y);
776 
777                 DGL_TexCoord2f(0, 1, 1);
778                 DGL_Vertex3f(projected[0] + size * unitVec.x, center.y + size,
779                            projected[1] + size * unitVec.y);
780 
781                 DGL_TexCoord2f(0, 0, 1);
782                 DGL_Vertex3f(projected[0] + size * unitVec.x, center.y - size,
783                            projected[1] + size * unitVec.y);
784             }
785             else
786             {
787                 DGL_TexCoord2f(0, 0, 0);
788                 DGL_Vertex3f(center.x + size * leftoff.x,
789                            center.y + size * leftoff.y / 1.2f,
790                            center.z + size * leftoff.z);
791 
792                 DGL_TexCoord2f(0, 1, 0);
793                 DGL_Vertex3f(center.x + size * rightoff.x,
794                            center.y + size * rightoff.y / 1.2f,
795                            center.z + size * rightoff.z);
796 
797                 DGL_TexCoord2f(0, 1, 1);
798                 DGL_Vertex3f(center.x - size * leftoff.x,
799                            center.y - size * leftoff.y / 1.2f,
800                            center.z - size * leftoff.z);
801 
802                 DGL_TexCoord2f(0, 0, 1);
803                 DGL_Vertex3f(center.x - size * rightoff.x,
804                            center.y - size * rightoff.y / 1.2f,
805                            center.z - size * rightoff.z);
806             }
807         }
808         else  // It's a line.
809         {
810             DGL_Vertex3f(center.x, center.y, center.z);
811             DGL_Vertex3f(center.x - FIX2FLT(pinfo.mov[0]),
812                          center.y - FIX2FLT(pinfo.mov[2]),
813                          center.z - FIX2FLT(pinfo.mov[1]));
814         }
815     }
816 
817     if(rtype != PTC_MODEL)
818     {
819         DGL_End();
820 
821         if(tex != 0)
822         {
823             DGL_CullFace(DGL_BACK);
824             DGL_Enable(DGL_DEPTH_WRITE);
825             DGL_DepthFunc(DGL_LESS);
826             DGL_Disable(DGL_TEXTURE_2D);
827         }
828     }
829 
830     if(!withBlend)
831     {
832         // We may have rendered subtractive stuff.
833         GL_BlendMode(BM_NORMAL);
834     }
835 }
836 
renderPass(bool useBlending)837 static void renderPass(bool useBlending)
838 {
839     DENG2_ASSERT(!Sys_GLCheckError());
840 
841     // Set blending mode.
842     if(useBlending)
843     {
844         GL_BlendMode(BM_ADD);
845     }
846 
847     if(hasModels)
848     {
849         drawParticles(PTC_MODEL, useBlending);
850     }
851 
852     if(hasLines)
853     {
854         drawParticles(PTC_LINE, useBlending);
855     }
856 
857     if(hasPoints)
858     {
859         drawParticles(PTC_POINT, useBlending);
860     }
861 
862     for(dint i = 0; i < NUM_TEX_NAMES; ++i)
863     {
864         if(hasPointTexs[i])
865         {
866             drawParticles(PTC_TEXTURE + i, useBlending);
867         }
868     }
869 
870     // Restore blending mode.
871     if(useBlending)
872     {
873         GL_BlendMode(BM_NORMAL);
874     }
875 
876     DENG2_ASSERT(!Sys_GLCheckError());
877 }
878 
Rend_RenderParticles(world::Map & map)879 void Rend_RenderParticles(world::Map &map)
880 {
881     if(!useParticles) return;
882 
883     // No visible particles at all?
884     if(!listVisibleParticles(map)) return;
885 
886     // Render all the visible particles.
887     if(hasNoBlend)
888     {
889         renderPass(false);
890     }
891 
892     if(hasAdditive)
893     {
894         // A second pass with additive blending.
895         // This makes the additive particles 'glow' through all other
896         // particles.
897         renderPass(true);
898     }
899 }
900 
Rend_ParticleRegister()901 void Rend_ParticleRegister()
902 {
903     C_VAR_BYTE ("rend-particle",                   &useParticles,      0,              0, 1);
904     C_VAR_INT  ("rend-particle-max",               &maxParticles,      CVF_NO_MAX,     0, 0);
905     C_VAR_FLOAT("rend-particle-diffuse",           &particleDiffuse,   CVF_NO_MAX,     0, 0);
906     C_VAR_INT  ("rend-particle-visible-near",      &particleNearLimit, CVF_NO_MAX,     0, 0);
907 }
908