Files
DoomRTX/neo/quake4/bse/BSE_SegmentRuntime.cpp
2026-05-09 22:10:40 -07:00

529 lines
13 KiB
C++

// Copyright (C) 2007 Id Software, Inc.
//
#include "precompiled.h"
#include "BSE_Envelope.h"
#include "BSE_Particle.h"
#include "BSE.h"
namespace {
rvParticle* AllocateParticleArray(int particleType, int count) {
if (count <= 0) {
return NULL;
}
switch (particleType) {
case PTYPE_LINE:
return static_cast<rvParticle*>(new rvLineParticle[count]);
case PTYPE_ORIENTED:
return static_cast<rvParticle*>(new rvOrientedParticle[count]);
case PTYPE_DECAL:
return static_cast<rvParticle*>(new rvDecalParticle[count]);
case PTYPE_MODEL:
return static_cast<rvParticle*>(new rvModelParticle[count]);
case PTYPE_LIGHT:
return static_cast<rvParticle*>(new rvLightParticle[count]);
case PTYPE_ELECTRICITY:
return static_cast<rvParticle*>(new rvElectricityParticle[count]);
case PTYPE_LINKED:
return static_cast<rvParticle*>(new rvLinkedParticle[count]);
case PTYPE_ORIENTEDLINKED:
return static_cast<rvParticle*>(new sdOrientedLinkedParticle[count]);
case PTYPE_DEBRIS:
return static_cast<rvParticle*>(new rvDebrisParticle[count]);
default:
return static_cast<rvParticle*>(new rvSpriteParticle[count]);
}
}
ID_INLINE void InsertByEndTime(rvParticle*& head, rvParticle* particle) {
rvParticle* prev = NULL;
rvParticle* cur = head;
while (cur && particle->GetEndTime() > cur->GetEndTime()) {
prev = cur;
cur = cur->GetNext();
}
particle->SetNext(cur);
if (prev) {
prev->SetNext(particle);
}
else {
head = particle;
}
}
ID_INLINE int GetSegmentParticleCap() {
return idMath::ClampInt(0, MAX_PARTICLES, bse_maxParticles.GetInteger());
}
ID_INLINE bool BSESpawnTraceEnabled() {
return cvarSystem && cvarSystem->GetCVarInteger("bse_frameCounters") >= 2;
}
ID_INLINE void BSE_BoundTriSurf(srfTriangles_t* tri) {
if (!tri) {
return;
}
if (tri->numVerts <= 0 || !tri->verts) {
tri->bounds.Clear();
return;
}
tri->bounds.Clear();
for (int i = 0; i < tri->numVerts; ++i) {
tri->bounds.AddPoint(tri->verts[i].xyz);
}
}
void BSESpawnTrace(const char* fmt, ...) {
static int spawnTraceCount = 0;
if (spawnTraceCount >= 256) {
return;
}
va_list ap;
va_start(ap, fmt);
common->VPrintf(fmt, ap);
va_end(ap);
++spawnTraceCount;
}
}
void rvSegment::PlayEffect(rvBSE* effect, rvSegmentTemplate* st, float depthOffset) {
if (!effect || !st || st->mNumEffects <= 0 || !game) {
return;
}
const int index = rvRandom::irand(0, st->mNumEffects - 1);
const rvDeclEffect* nested = st->mEffects[index];
if (!nested) {
return;
}
game->PlayEffect(
nested,
effect->GetCurrentOrigin(),
effect->GetCurrentAxis(),
false,
effect->GetHasEndOrigin() ? effect->GetCurrentEndOrigin() : vec3_origin,
false,
false,
EC_IGNORE,
vec4_one);
}
rvParticle* rvSegment::InitParticleArray(rvBSE* effect) {
if (!effect) {
return NULL;
}
if (mParticles) {
switch (mParticleType) {
case PTYPE_LINE:
delete[] static_cast<rvLineParticle*>(mParticles);
break;
case PTYPE_ORIENTED:
delete[] static_cast<rvOrientedParticle*>(mParticles);
break;
case PTYPE_DECAL:
delete[] static_cast<rvDecalParticle*>(mParticles);
break;
case PTYPE_MODEL:
delete[] static_cast<rvModelParticle*>(mParticles);
break;
case PTYPE_LIGHT:
delete[] static_cast<rvLightParticle*>(mParticles);
break;
case PTYPE_ELECTRICITY:
delete[] static_cast<rvElectricityParticle*>(mParticles);
break;
case PTYPE_LINKED:
delete[] static_cast<rvLinkedParticle*>(mParticles);
break;
case PTYPE_ORIENTEDLINKED:
delete[] static_cast<sdOrientedLinkedParticle*>(mParticles);
break;
case PTYPE_DEBRIS:
delete[] static_cast<rvDebrisParticle*>(mParticles);
break;
default:
delete[] static_cast<rvSpriteParticle*>(mParticles);
break;
}
}
mParticles = NULL;
mFreeHead = NULL;
mUsedHead = NULL;
const rvSegmentTemplate* st = GetSegmentTemplate();
const int requestedCount = effect->GetLooping() ? mLoopParticleCount : mParticleCount;
int particleCount = requestedCount;
const int particleCap = GetSegmentParticleCap();
if (particleCount > particleCap) {
common->Warning("^4BSE:^1 '%s' exceeded particle cap (%d > %d), clamping", effect->GetDeclName(), particleCount, particleCap);
}
particleCount = idMath::ClampInt(0, particleCap, particleCount);
if (particleCount <= 0) {
if (BSESpawnTraceEnabled()) {
BSESpawnTrace(
"BSE spawn init: decl=%s segType=%d pType=%d handle=%d particleCount=%d loopCount=%d (clamped<=0)\n",
effect ? effect->GetDeclName() : "<null>",
st ? st->GetType() : -1,
st ? st->GetParticleTemplate()->GetType() : -1,
mSegmentTemplateHandle,
mParticleCount,
mLoopParticleCount);
}
return NULL;
}
mParticles = AllocateParticleArray(mParticleType, particleCount);
if (!mParticles) {
return NULL;
}
for (int i = 0; i < particleCount - 1; ++i) {
rvParticle* cur = mParticles->GetArrayEntry(i);
rvParticle* next = mParticles->GetArrayEntry(i + 1);
cur->SetNext(next);
}
mParticles->GetArrayEntry(particleCount - 1)->SetNext(NULL);
mFreeHead = mParticles;
mUsedHead = NULL;
return mParticles;
}
rvParticle* rvSegment::GetFreeParticle(rvBSE* effect) {
rvSegmentTemplate* st = GetSegmentTemplate();
if (!st) {
return NULL;
}
// Decal/effect paths can request a spawn before explicit segment particle
// initialization runs. Lazily allocate from precomputed counts so authored
// spawn data (size/rotation/tint) is available instead of default fallbacks.
if (mParticles == NULL) {
InitParticleArray(effect);
}
if (mParticles == NULL) {
return NULL;
}
rvParticle* particle = NULL;
if (st->GetTemporary()) {
particle = mParticles;
if (particle) {
particle->SetNext(NULL);
}
}
else {
particle = mFreeHead;
if (!particle) {
return NULL;
}
mFreeHead = particle->GetNext();
particle->SetNext(NULL);
}
return particle;
}
rvParticle* rvSegment::SpawnParticle(rvBSE* effect, rvSegmentTemplate* st, float birthTime, const idVec3& initOffset, const idMat3& initAxis) {
if (!effect || !st) {
return NULL;
}
rvParticle* particle = GetFreeParticle(effect);
if (!particle) {
if (BSESpawnTraceEnabled()) {
BSESpawnTrace(
"BSE spawn miss: decl=%s segType=%d handle=%d birth=%.4f freeHead=null particleCount=%d loopCount=%d\n",
effect ? effect->GetDeclName() : "<null>",
st ? st->GetType() : -1,
mSegmentTemplateHandle,
birthTime,
mParticleCount,
mLoopParticleCount);
}
return NULL;
}
const float fraction = birthTime - effect->GetStartTime();
particle->FinishSpawn(effect, this, birthTime, fraction, initOffset, initAxis);
BSE_AddSpawned(1);
if (BSESpawnTraceEnabled()) {
BSESpawnTrace(
"BSE spawn ok: decl=%s segType=%d pType=%d handle=%d birth=%.4f end=%.4f dur=%.4f fraction=%.4f\n",
effect ? effect->GetDeclName() : "<null>",
st ? st->GetType() : -1,
st ? st->GetParticleTemplate()->GetType() : -1,
mSegmentTemplateHandle,
birthTime,
particle->GetEndTime(),
particle->GetDuration(),
fraction);
}
const int type = st->mParticleTemplate.GetType();
const bool linked = (type == PTYPE_LINKED || type == PTYPE_ORIENTEDLINKED);
if (st->GetTemporary()) {
mUsedHead = particle;
particle->SetNext(NULL);
}
else if (linked || !st->GetComplexParticle()) {
InsertByEndTime(mUsedHead, particle);
}
else {
particle->SetNext(mUsedHead);
mUsedHead = particle;
}
return particle;
}
void rvSegment::SpawnParticles(rvBSE* effect, rvSegmentTemplate* st, float birthTime, int count) {
if (!effect || !st || count <= 0) {
return;
}
for (int i = 0; i < count; ++i) {
rvParticle* particle = GetFreeParticle(effect);
if (!particle) {
if (BSESpawnTraceEnabled()) {
BSESpawnTrace(
"BSE spawn batch miss: decl=%s segType=%d handle=%d birth=%.4f idx=%d count=%d freeHead=null particleCount=%d loopCount=%d\n",
effect ? effect->GetDeclName() : "<null>",
st ? st->GetType() : -1,
mSegmentTemplateHandle,
birthTime,
i,
count,
mParticleCount,
mLoopParticleCount);
}
break;
}
float fraction = 0.0f;
if (count > 0) {
fraction = static_cast<float>(i) / static_cast<float>(count);
}
particle->FinishSpawn(effect, this, birthTime, fraction, vec3_origin, mat3_identity);
BSE_AddSpawned(1);
if (BSESpawnTraceEnabled()) {
BSESpawnTrace(
"BSE spawn batch ok: decl=%s segType=%d pType=%d handle=%d birth=%.4f idx=%d count=%d end=%.4f dur=%.4f fraction=%.4f\n",
effect ? effect->GetDeclName() : "<null>",
st ? st->GetType() : -1,
st ? st->GetParticleTemplate()->GetType() : -1,
mSegmentTemplateHandle,
birthTime,
i,
count,
particle->GetEndTime(),
particle->GetDuration(),
fraction);
}
const int type = st->mParticleTemplate.GetType();
const bool linked = (type == PTYPE_LINKED || type == PTYPE_ORIENTEDLINKED);
if (st->GetTemporary()) {
mUsedHead = particle;
particle->SetNext(NULL);
}
else if (linked || !st->GetComplexParticle()) {
InsertByEndTime(mUsedHead, particle);
}
else {
particle->SetNext(mUsedHead);
mUsedHead = particle;
}
}
}
void rvSegment::UpdateSimpleParticles(float time) {
rvSegmentTemplate* st = GetSegmentTemplate();
if (!st) {
mActiveCount = 0;
return;
}
rvParticle* prev = NULL;
rvParticle* p = mUsedHead;
mActiveCount = 0;
while (p) {
rvParticle* next = p->GetNext();
if (p->GetEndTime() - BSE_TIME_EPSILON > time) {
mActiveCount += p->Update(&st->mParticleTemplate, time);
prev = p;
}
else {
if (prev) {
prev->SetNext(next);
}
else {
mUsedHead = next;
}
p->SetNext(mFreeHead);
p->Destroy();
mFreeHead = p;
}
p = next;
}
}
void rvSegment::UpdateGenericParticles(rvBSE* effect, rvSegmentTemplate* st, float time) {
if (!effect || !st) {
mActiveCount = 0;
return;
}
const bool infinite = st->GetInfiniteDuration();
const bool smoker = st->GetSmoker();
rvParticle* prev = NULL;
rvParticle* p = mUsedHead;
mActiveCount = 0;
while (p) {
rvParticle* next = p->GetNext();
bool remove = false;
if (infinite) {
p->RunPhysics(effect, st, time);
remove = effect->GetStopped();
}
else {
if (p->GetEndTime() - BSE_TIME_EPSILON <= time) {
p->CheckTimeoutEffect(effect, st, time);
remove = true;
}
else {
remove = p->RunPhysics(effect, st, time);
}
}
if (effect->GetStopped() && (p->GetFlags() & PTFLAG_PERSIST) == 0) {
remove = true;
}
if (smoker && st->mTrailSegmentIndex >= 0) {
rvSegment* trail = effect->GetTrailSegment(st->mTrailSegmentIndex);
if (trail) {
p->EmitSmokeParticles(effect, trail, &st->mParticleTemplate, time);
}
}
if (remove) {
if (prev) {
prev->SetNext(next);
}
else {
mUsedHead = next;
}
p->SetNext(mFreeHead);
p->Destroy();
mFreeHead = p;
}
else {
mActiveCount += p->Update(st->GetParticleTemplate(), time);
prev = p;
}
p = next;
}
}
void rvSegment::AddToParticleCount(rvBSE* effect, int count, int loopCount, float duration) {
rvSegmentTemplate* st = GetSegmentTemplate();
if (!st) {
return;
}
if (duration < st->mParticleTemplate.GetMaxDuration()) {
duration = st->mParticleTemplate.GetMaxDuration();
}
const float countDuration = duration + 0.016f;
if (mSecondsPerParticle.y > BSE_TIME_EPSILON) {
const int mult = static_cast<int>(ceilf(countDuration / mSecondsPerParticle.y)) + 1;
mLoopParticleCount += loopCount * mult;
mParticleCount += count * mult;
}
else {
mLoopParticleCount += loopCount;
mParticleCount += count;
}
const int particleCap = GetSegmentParticleCap();
mParticleCount = idMath::ClampInt(0, particleCap, mParticleCount);
mLoopParticleCount = idMath::ClampInt(0, particleCap, mLoopParticleCount);
}
void rvSegment::CalcTrailCounts(rvBSE* effect, rvSegmentTemplate* st, rvParticleTemplate* pt, float duration) {
if (!effect || !st || !pt) {
return;
}
if (st->mTrailSegmentIndex >= 0) {
rvSegment* trail = effect->GetTrailSegment(st->mTrailSegmentIndex);
if (trail) {
trail->AddToParticleCount(effect, mParticleCount, mLoopParticleCount, duration);
}
}
}
void rvSegment::RenderMotion(rvBSE* effect, const renderEffect_s* owner, idRenderModel* model, rvParticleTemplate* pt, float time) {
if (!effect || !owner || !model || !pt || mSurfaceIndex < 0) {
return;
}
if (mSurfaceIndex + 1 >= model->NumSurfaces()) {
return;
}
srfTriangles_t* tri = model->Surface(mSurfaceIndex + 1)->geometry;
if (!tri) {
return;
}
for (rvParticle* p = mUsedHead; p; p = p->GetNext()) {
p->RenderMotion(effect, pt, tri, owner, time);
}
BSE_BoundTriSurf(tri);
}
void rvSegment::Sort(const idVec3& eyePos) {
if (!mUsedHead || !mUsedHead->GetNext()) {
return;
}
rvParticle* sorted = NULL;
rvParticle* particle = mUsedHead;
while (particle) {
rvParticle* next = particle->GetNext();
const float dist = particle->UpdateViewDist(eyePos);
if (!sorted || dist > sorted->UpdateViewDist(eyePos)) {
particle->SetNext(sorted);
sorted = particle;
}
else {
rvParticle* insertAfter = sorted;
while (insertAfter->GetNext()) {
rvParticle* compare = insertAfter->GetNext();
if (dist > compare->UpdateViewDist(eyePos)) {
break;
}
insertAfter = compare;
}
particle->SetNext(insertAfter->GetNext());
insertAfter->SetNext(particle);
}
particle = next;
}
mUsedHead = sorted;
}