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