Files
2026-05-09 22:10:40 -07:00

2037 lines
62 KiB
C++

// Copyright (C) 2007 Id Software, Inc.
//
#include "precompiled.h"
#include "BSE_Envelope.h"
#include "BSE_Particle.h"
#include "BSE_SpawnDomains.h"
#include "BSE.h"
#include <stddef.h>
#include <stdint.h>
#include <string.h>
struct SElecWork {
srfTriangles_t* tri;
idVec3* coords;
int coordCount;
idVec3 length;
idVec3 forward;
idVec3 viewPos;
idVec4 tint;
float size;
float alpha;
float step;
float fraction;
};
namespace {
ID_INLINE int BSE_GetFrameCounterMode() {
return cvarSystem ? cvarSystem->GetCVarInteger("bse_frameCounters") : 0;
}
ID_INLINE float Clamp01(float x) {
return idMath::ClampFloat(0.0f, 1.0f, x);
}
ID_INLINE byte ToByte(float x) {
return static_cast<byte>(idMath::ClampInt(0, 255, idMath::FtoiFast(Clamp01(x) * 255.0f)));
}
ID_INLINE dword PackColorLocal(const idVec4& color) {
const dword r = static_cast<dword>(ToByte(color[0]));
const dword g = static_cast<dword>(ToByte(color[1])) << 8;
const dword b = static_cast<dword>(ToByte(color[2])) << 16;
const dword a = static_cast<dword>(ToByte(color[3])) << 24;
return r | g | b | a;
}
ID_INLINE void UnpackColor(dword rgba, byte out[4]) {
out[0] = static_cast<byte>(rgba & 0xFF);
out[1] = static_cast<byte>((rgba >> 8) & 0xFF);
out[2] = static_cast<byte>((rgba >> 16) & 0xFF);
out[3] = static_cast<byte>((rgba >> 24) & 0xFF);
}
ID_INLINE void SetDrawVert(idDrawVert& vert, const idVec3& xyz, float s, float t, dword rgba) {
byte color[4];
UnpackColor(rgba, color);
vert.Clear();
vert.xyz = xyz;
vert.st[0] = s;
vert.st[1] = t;
vert.normal.Set(1.0f, 0.0f, 0.0f);
vert.tangents[0].Set(0.0f, 1.0f, 0.0f);
vert.tangents[1].Set(0.0f, 0.0f, 1.0f);
vert.color[0] = color[0];
vert.color[1] = color[1];
vert.color[2] = color[2];
vert.color[3] = color[3];
}
ID_INLINE bool HasTriCapacity(const srfTriangles_t* tri, int addVerts, int addIndexes) {
if (!tri || addVerts < 0 || addIndexes < 0) {
return false;
}
if (tri->numAllocedVerts > 0 && tri->numVerts + addVerts > tri->numAllocedVerts) {
return false;
}
if (tri->numAllocedIndices > 0 && tri->numIndexes + addIndexes > tri->numAllocedIndices) {
return false;
}
return true;
}
ID_INLINE void AppendQuad(srfTriangles_t* tri, const idVec3& p0, const idVec3& p1, const idVec3& p2, const idVec3& p3, dword rgba) {
const int base = tri->numVerts;
SetDrawVert(tri->verts[base + 0], p0, 0.0f, 0.0f, rgba);
SetDrawVert(tri->verts[base + 1], p1, 1.0f, 0.0f, rgba);
SetDrawVert(tri->verts[base + 2], p2, 1.0f, 1.0f, rgba);
SetDrawVert(tri->verts[base + 3], p3, 0.0f, 1.0f, rgba);
const int indexBase = tri->numIndexes;
tri->indexes[indexBase + 0] = base + 0;
tri->indexes[indexBase + 1] = base + 1;
tri->indexes[indexBase + 2] = base + 2;
tri->indexes[indexBase + 3] = base + 0;
tri->indexes[indexBase + 4] = base + 2;
tri->indexes[indexBase + 5] = base + 3;
tri->numVerts += 4;
tri->numIndexes += 6;
}
ID_INLINE idVec3 WorldFromLocal(const rvBSE* effect, const idVec3& local) {
if (!effect) {
return local;
}
return effect->GetCurrentOrigin() + effect->GetCurrentAxis() * local;
}
ID_INLINE bool IsScalarDomain(const rvParticleParms* parms) {
if (!parms) {
return false;
}
return (parms->mSpawnType & 0x3) == 1;
}
ID_INLINE void BuildPerpBasis(const idVec3& forward, idVec3& right, idVec3& up) {
if (idMath::Fabs(forward.x) < 0.99f) {
right = idVec3(0.0f, 0.0f, 1.0f).Cross(forward);
}
else {
right = idVec3(0.0f, 1.0f, 0.0f).Cross(forward);
}
if (right.LengthSqr() > 1e-8f) {
right.NormalizeFast();
}
else {
right.Set(0.0f, 1.0f, 0.0f);
}
up = forward.Cross(right);
if (up.LengthSqr() > 1e-8f) {
up.NormalizeFast();
}
else {
up.Set(0.0f, 0.0f, 1.0f);
}
}
ID_INLINE idMat3 BuildInitToCurrentAxis(const rvBSE* effect, const idMat3& initAxis) {
if (!effect) {
return initAxis;
}
// Vanilla converts unlocked particle axes with matrix-divide semantics.
return initAxis / effect->GetCurrentAxis();
}
ID_INLINE idMat3 BuildCurrentToInitAxis(const rvBSE* effect, const idMat3& initAxis) {
if (!effect) {
return initAxis;
}
// Inverse mapping of BuildInitToCurrentAxis, used when persisting
// current-frame results back into the particle's original init frame.
return effect->GetCurrentAxis() / initAxis;
}
ID_INLINE void BSETraceRenderDrop(const char* typeName, const rvParticle* particle, float time, const char* reason, float value0 = 0.0f, float value1 = 0.0f) {
static int bseRenderDropTraceCount = 0;
if (!particle || BSE_GetFrameCounterMode() < 2 || bseRenderDropTraceCount >= 256) {
return;
}
const float duration = particle->GetDuration();
const float endTime = particle->GetEndTime();
const float startTime = endTime - duration;
common->Printf(
"BSE render drop %d: type=%s reason=%s time=%.4f start=%.4f end=%.4f dur=%.4f v0=%.4f v1=%.4f\n",
bseRenderDropTraceCount,
typeName ? typeName : "<null>",
reason ? reason : "<null>",
time,
startTime,
endTime,
duration,
value0,
value1);
++bseRenderDropTraceCount;
}
}
// ---------------------------------------------------------------------------
// attenuation helpers
// ---------------------------------------------------------------------------
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms1& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms2& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms3& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms1Particle& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms2Particle& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvParticle::Attenuate(float atten, rvParticleParms& parms, rvEnvParms3Particle& result) {
if ((parms.mFlags & PPFLAG_ATTENUATE) == 0) {
return;
}
if (parms.mFlags & PPFLAG_INV_ATTENUATE) {
atten = 1.0f - atten;
}
result.Scale(atten);
}
void rvLineParticle::HandleTiling(rvParticleTemplate* pt) {
if (!pt || !GetTiled()) {
return;
}
const float* len = GetInitLength();
if (!len) {
mTextureScale = 1.0f;
return;
}
const idVec3 length(len[0], len[1], len[2]);
mTextureScale = Max(0.001f, length.LengthFast() / Max(0.001f, pt->GetTiling()));
}
void rvLinkedParticle::HandleTiling(rvParticleTemplate* pt) {
if (!pt || !GetTiled()) {
return;
}
mTextureScale = Max(0.001f, pt->GetTiling());
}
// ---------------------------------------------------------------------------
// array helpers
// ---------------------------------------------------------------------------
#define DEFINE_ARRAY_ENTRY(TYPE) \
rvParticle* TYPE::GetArrayEntry(int i) const { \
return (i < 0) ? NULL : const_cast<TYPE*>(this) + i; \
}
#define DEFINE_ARRAY_INDEX(TYPE) \
int TYPE::GetArrayIndex(rvParticle* p) const { \
if (!p) { return -1; } \
const ptrdiff_t diff = reinterpret_cast<const byte*>(p) - reinterpret_cast<const byte*>(this); \
return static_cast<int>(diff / sizeof(TYPE)); \
}
DEFINE_ARRAY_ENTRY(rvSpriteParticle)
DEFINE_ARRAY_INDEX(rvSpriteParticle)
DEFINE_ARRAY_ENTRY(rvLineParticle)
DEFINE_ARRAY_INDEX(rvLineParticle)
DEFINE_ARRAY_ENTRY(rvOrientedParticle)
DEFINE_ARRAY_INDEX(rvOrientedParticle)
DEFINE_ARRAY_ENTRY(rvElectricityParticle)
DEFINE_ARRAY_INDEX(rvElectricityParticle)
DEFINE_ARRAY_ENTRY(rvDecalParticle)
DEFINE_ARRAY_INDEX(rvDecalParticle)
DEFINE_ARRAY_ENTRY(rvModelParticle)
DEFINE_ARRAY_INDEX(rvModelParticle)
DEFINE_ARRAY_ENTRY(rvLightParticle)
DEFINE_ARRAY_INDEX(rvLightParticle)
DEFINE_ARRAY_ENTRY(rvLinkedParticle)
DEFINE_ARRAY_INDEX(rvLinkedParticle)
DEFINE_ARRAY_ENTRY(rvDebrisParticle)
DEFINE_ARRAY_INDEX(rvDebrisParticle)
// ---------------------------------------------------------------------------
// spawning helpers
// ---------------------------------------------------------------------------
void rvParticle::SetOriginUsingEndOrigin(rvBSE* effect, rvParticleTemplate* pt, idVec3* normal, idVec3* centre) {
if (!effect || !pt || !pt->mpSpawnPosition) {
mInitPos.Zero();
return;
}
// Match vanilla end-origin spawn behavior:
// 1) seed/randomize once, 2) force fraction in X and resample.
// Domains with linearSpacing consume that pre-seeded X value.
pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, NULL, NULL);
mInitPos.x = mFraction;
pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, normal, centre);
if (!effect->GetHasEndOrigin()) {
return;
}
const idVec3 endLocal = effect->GetCurrentAxisTransposed() * (effect->GetCurrentEndOrigin() - effect->GetCurrentOrigin());
idVec3 forward = endLocal;
const float endLenSqr = forward.LengthSqr();
if (endLenSqr <= 1e-8f) {
return;
}
forward.NormalizeFast();
idVec3 right;
idVec3 up;
BuildPerpBasis(forward, right, up);
const bool linearSpacing = (pt->mpSpawnPosition->mFlags & PPFLAG_LINEARSPACING) != 0;
const float t = linearSpacing ? Clamp01(mFraction) : Clamp01(mInitPos.x);
const int spawnShape = pt->mpSpawnPosition->mSpawnType & ~0x3;
// Spiral domains authored with `useEndOrigin linearSpacing` expect their
// lateral offset to advance around the beam as spacing advances.
const float range = pt->mpSpawnPosition->mRange;
const bool spiralTwist = linearSpacing && (spawnShape == SPF_SPIRAL_0) && (idMath::Fabs(range) > BSE_TIME_EPSILON);
float twistS = 0.0f;
float twistC = 1.0f;
if (spiralTwist) {
const float endLength = idMath::Sqrt(endLenSqr);
const float twist = idMath::TWO_PI * ((t * endLength) / range);
idMath::SinCos(twist, twistS, twistC);
}
idVec3 local = mInitPos;
if (spiralTwist) {
const float y = local.y * twistC - local.z * twistS;
const float z = local.y * twistS + local.z * twistC;
local.y = y;
local.z = z;
}
mInitPos = endLocal * t + forward * local.x + right * local.y + up * local.z;
if (normal) {
idVec3 localNormal = *normal;
if (spiralTwist) {
const float y = localNormal.y * twistC - localNormal.z * twistS;
const float z = localNormal.y * twistS + localNormal.z * twistC;
localNormal.y = y;
localNormal.z = z;
}
*normal = forward * localNormal.x + right * localNormal.y + up * localNormal.z;
if (normal->LengthSqr() > 1e-8f) {
normal->NormalizeFast();
}
else {
*normal = forward;
}
}
}
void rvParticle::HandleEndOrigin(rvBSE* effect, rvParticleTemplate* pt, idVec3* normal, idVec3* centre) {
if (!pt || !pt->mpSpawnPosition) {
mInitPos.Zero();
return;
}
// Preserve per-particle spawn fraction for end-origin domains.
mInitPos.x = mFraction;
if (effect && effect->GetHasEndOrigin() && (pt->mpSpawnPosition->mFlags & PPFLAG_USEENDORIGIN)) {
SetOriginUsingEndOrigin(effect, pt, normal, centre);
return;
}
pt->mpSpawnPosition->Spawn(mInitPos.ToFloatPtr(), *pt->mpSpawnPosition, normal, centre);
}
void rvParticle::SetLengthUsingEndOrigin(rvBSE* effect, rvParticleParms& parms, float* length) {
if (!length) {
return;
}
parms.Spawn(length, parms, NULL, NULL);
if (!effect || !effect->GetHasEndOrigin()) {
return;
}
const idVec3 endLocal = effect->GetCurrentAxisTransposed() * (effect->GetCurrentEndOrigin() - effect->GetCurrentOrigin());
idVec3 forward = endLocal;
if (forward.LengthSqr() <= 1e-8f) {
return;
}
forward.NormalizeFast();
idVec3 right;
idVec3 up;
BuildPerpBasis(forward, right, up);
// `useEndOrigin` lengths are authored as offsets around the baseline
// vector from origin to end-origin.
const idVec3 out = endLocal + forward * length[0] + right * length[1] + up * length[2];
length[0] = out.x;
length[1] = out.y;
length[2] = out.z;
}
void rvParticle::HandleEndLength(rvBSE* effect, rvParticleTemplate* pt, rvParticleParms& parms, float* length) {
if ((parms.mFlags & PPFLAG_USEENDORIGIN) != 0) {
SetLengthUsingEndOrigin(effect, parms, length);
}
else {
parms.Spawn(length, parms, NULL, NULL);
}
}
void rvParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
if (!effect || !segment) {
return;
}
rvSegmentTemplate* st = segment->GetSegmentTemplate();
if (!st) {
return;
}
rvParticleTemplate* pt = st->GetParticleTemplate();
if (!pt) {
return;
}
mNext = NULL;
mMotionStartTime = birthTime;
mLastTrailTime = birthTime;
mFlags = pt->GetFlags();
mStartTime = birthTime;
mFraction = fraction;
mTextureScale = 1.0f;
mTextureOffset = 0.0f;
mInitEffectPos = vec3_origin;
mInitAxis = mat3_identity;
mTrailRepeat = pt->GetTrailRepeat();
const bool generatedOriginNormal = pt->GetGeneratedOriginNormal();
const bool generatedNormal = pt->GetGeneratedNormal();
const bool transformByNormal = generatedOriginNormal || generatedNormal;
const bool flipNormal = pt->GetFlippedNormal();
idVec3 normal(1.0f, 0.0f, 0.0f);
if (generatedOriginNormal) {
HandleEndOrigin(effect, pt, &normal, NULL);
}
else if (generatedNormal) {
idVec3 centre = pt->mCentre;
HandleEndOrigin(effect, pt, &normal, &centre);
}
else {
HandleEndOrigin(effect, pt, NULL, NULL);
if (pt->GetCalculatedNormal() && pt->mpSpawnDirection) {
pt->mpSpawnDirection->Spawn(normal.ToFloatPtr(), *pt->mpSpawnDirection, NULL, NULL);
}
}
SetLocked(st->GetLocked());
// Legacy particle flag bit 0x80000 is set for trail-child segments in
// segment-template finish and controls use of parent-supplied init transform.
const bool transformParent = (pt->GetFlags() & PTFLAG_LINKED) != 0;
if (GetLocked()) {
mInitEffectPos = vec3_origin;
mInitAxis = initAxis;
}
else {
mInitEffectPos = effect->GetCurrentOrigin();
mInitAxis = effect->GetCurrentAxis();
// Match vanilla spawn timing: compensate for owner interpolation so
// particles start in the frame-accurate local position at birthTime.
mInitPos -= mInitAxis * effect->GetInterpolatedOffset(birthTime);
}
if (pt->mpSpawnOffset && pt->mpSpawnOffset->mSpawnType != SPF_NONE_0) {
SetHasOffset(true);
}
if (pt->GetTiled()) {
SetFlag(true, PTFLAG_TILED);
}
if (pt->GetGeneratedLine()) {
SetFlag(true, PTFLAG_GENERATED_LINE);
}
idVec3 direction = normal;
if (direction.LengthSqr() > 1e-6f) {
direction.NormalizeFast();
}
else {
direction.Set(1.0f, 0.0f, 0.0f);
}
if (pt->mpSpawnVelocity) {
pt->mpSpawnVelocity->Spawn(mVelocity.ToFloatPtr(), *pt->mpSpawnVelocity, NULL, NULL);
if (IsScalarDomain(pt->mpSpawnVelocity)) {
if (!transformByNormal) {
mVelocity = direction * mVelocity.x;
}
}
if (transformParent) {
mVelocity = initAxis * mVelocity;
}
}
else {
mVelocity.Zero();
}
if (pt->mpSpawnAcceleration) {
pt->mpSpawnAcceleration->Spawn(mAcceleration.ToFloatPtr(), *pt->mpSpawnAcceleration, NULL, NULL);
if (IsScalarDomain(pt->mpSpawnAcceleration)) {
if (!transformByNormal) {
mAcceleration = direction * mAcceleration.x;
}
}
}
else {
mAcceleration.Zero();
}
if (transformByNormal) {
if (normal.LengthSqr() > 1e-8f) {
normal.NormalizeFast();
}
const idMat3 normalAxis = normal.ToMat3();
mVelocity = normalAxis * mVelocity;
mAcceleration = normalAxis * mAcceleration;
}
if (flipNormal) {
mVelocity = -mVelocity;
}
if (normal.LengthSqr() <= 1e-8f) {
normal = mVelocity;
if (normal.LengthSqr() > 1e-8f) {
normal.NormalizeFast();
}
}
if (pt->mpSpawnFriction) {
float frictionParms[3] = { 0.0f, 0.0f, 0.0f };
pt->mpSpawnFriction->Spawn(frictionParms, *pt->mpSpawnFriction, NULL, NULL);
mFriction = Max(0.0f, frictionParms[0]);
}
else {
mFriction = 0.0f;
}
if (pt->GetParentVelocity()) {
mVelocity += effect->GetCurrentVelocity();
}
if (transformParent) {
mInitPos += initOffset;
}
const float duration = idMath::ClampFloat(BSE_TIME_EPSILON, BSE_MAX_DURATION, pt->GetDuration());
mEndTime = mStartTime + duration;
if (pt->mpSpawnTint) {
pt->mpSpawnTint->Spawn(mTintEnv.GetStart(), *pt->mpSpawnTint, NULL, NULL);
}
if (pt->mpDeathTint) {
pt->mpDeathTint->Spawn(mTintEnv.GetEnd(), *pt->mpDeathTint, NULL, NULL);
pt->mpDeathTint->HandleRelativeParms(mTintEnv.GetEnd(), mTintEnv.GetStart(), 3);
}
if (pt->mpSpawnFade) {
pt->mpSpawnFade->Spawn(mFadeEnv.GetStart(), *pt->mpSpawnFade, NULL, NULL);
}
if (pt->mpDeathFade) {
pt->mpDeathFade->Spawn(mFadeEnv.GetEnd(), *pt->mpDeathFade, NULL, NULL);
pt->mpDeathFade->HandleRelativeParms(mFadeEnv.GetEnd(), mFadeEnv.GetStart(), 1);
}
if (pt->mpSpawnAngle) {
pt->mpSpawnAngle->Spawn(mAngleEnv.GetStart(), *pt->mpSpawnAngle, NULL, NULL);
}
if (pt->mpDeathAngle) {
pt->mpDeathAngle->Spawn(mAngleEnv.GetEnd(), *pt->mpDeathAngle, NULL, NULL);
pt->mpDeathAngle->HandleRelativeParms(mAngleEnv.GetEnd(), mAngleEnv.GetStart(), 3);
}
if (pt->mpSpawnOffset) {
pt->mpSpawnOffset->Spawn(mOffsetEnv.GetStart(), *pt->mpSpawnOffset, NULL, NULL);
}
if (pt->mpDeathOffset) {
pt->mpDeathOffset->Spawn(mOffsetEnv.GetEnd(), *pt->mpDeathOffset, NULL, NULL);
pt->mpDeathOffset->HandleRelativeParms(mOffsetEnv.GetEnd(), mOffsetEnv.GetStart(), 3);
}
if (float* initSize = GetInitSize()) {
if (pt->mpSpawnSize) {
pt->mpSpawnSize->Spawn(initSize, *pt->mpSpawnSize, NULL, NULL);
}
if (float* destSize = GetDestSize()) {
if (pt->mpDeathSize) {
pt->mpDeathSize->Spawn(destSize, *pt->mpDeathSize, NULL, NULL);
pt->mpDeathSize->HandleRelativeParms(destSize, initSize, pt->mNumSizeParms);
}
}
}
if (float* initRotate = GetInitRotation()) {
if (pt->mpSpawnRotate) {
pt->mpSpawnRotate->Spawn(initRotate, *pt->mpSpawnRotate, NULL, NULL);
}
if (float* destRotate = GetDestRotation()) {
if (pt->mpDeathRotate) {
pt->mpDeathRotate->Spawn(destRotate, *pt->mpDeathRotate, NULL, NULL);
pt->mpDeathRotate->HandleRelativeParms(destRotate, initRotate, pt->mNumRotateParms);
}
}
}
// Angles/rotation in decls are specified in turns; runtime evaluates radians.
ScaleRotation(idMath::TWO_PI);
ScaleAngle(idMath::TWO_PI);
const idAngles normalAngles = normal.ToAngles();
rvAngles orient(DEG2RAD(normalAngles.pitch), DEG2RAD(normalAngles.yaw), DEG2RAD(normalAngles.roll));
HandleOrientation(orient);
if (float* initLength = GetInitLength()) {
if (pt->mpSpawnLength) {
HandleEndLength(effect, pt, *pt->mpSpawnLength, initLength);
}
if (float* destLength = GetDestLength()) {
if (pt->mpDeathLength) {
pt->mpDeathLength->Spawn(destLength, *pt->mpDeathLength, NULL, NULL);
pt->mpDeathLength->HandleRelativeParms(destLength, initLength, 3);
}
}
}
if (transformByNormal) {
TransformLength(normal);
}
if (flipNormal) {
ScaleLength(-1.0f);
}
mTrailTime = pt->GetTrailTime();
mTrailCount = idMath::ClampInt(0, 128, idMath::FtoiFast(rvRandom::flrand(pt->mTrailInfo->mTrailCount.x, pt->mTrailInfo->mTrailCount.y)));
SetModel(pt->GetModel());
SetupElectricity(pt);
const float attenuation = effect->GetAttenuation(st);
if (pt->mpSpawnFade) {
AttenuateFade(attenuation, *pt->mpSpawnFade);
}
if (pt->mpSpawnSize) {
AttenuateSize(attenuation, *pt->mpSpawnSize);
}
if (pt->mpSpawnLength) {
AttenuateLength(attenuation, *pt->mpSpawnLength);
}
const float gravityScale = pt->GetGravity();
if (gravityScale != 0.0f) {
// Particle gravity is authored in effect-space and must be reprojected
// into the current local frame used by the particle simulation.
const idVec3 gravityWorld = effect->GetGravity() * gravityScale;
mAcceleration += effect->GetCurrentAxisTransposed() * gravityWorld;
}
HandleTiling(pt);
mPosition = mInitPos;
}
void rvLineParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);
}
void rvLinkedParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);
}
void rvDebrisParticle::FinishSpawn(rvBSE* effect, rvSegment* segment, float birthTime, float fraction, const idVec3& initOffset, const idMat3& initAxis) {
if (!bse_debris.GetBool() || !effect || !segment || !game || session->readDemo) {
return;
}
rvParticle::FinishSpawn(effect, segment, birthTime, fraction, initOffset, initAxis);
rvSegmentTemplate* st = segment->GetSegmentTemplate();
if (!st) {
return;
}
rvParticleTemplate* pt = st->GetParticleTemplate();
if (!pt) {
return;
}
const char* entityDefName = pt->GetEntityDefName();
if (!entityDefName || entityDefName[0] == '\0') {
// Keep compatibility with legacy data that may author debris as plain particles.
return;
}
idVec3 localPosition;
EvaluatePosition(effect, pt, localPosition, birthTime);
idVec3 localVelocity;
EvaluateVelocity(effect, localVelocity, birthTime);
idVec3 angularVelocity(vec3_origin);
if (float* initRotate = GetInitRotation()) {
angularVelocity.Set(initRotate[0], initRotate[1], initRotate[2]);
}
idVec3 worldOrigin;
idVec3 worldVelocity;
idMat3 worldAxis;
if (GetLocked()) {
worldOrigin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * localPosition;
worldVelocity = effect->GetCurrentAxis() * localVelocity;
angularVelocity = effect->GetCurrentAxis() * angularVelocity;
worldAxis = effect->GetCurrentAxis();
}
else {
worldOrigin = mInitEffectPos + mInitAxis * localPosition;
worldVelocity = mInitAxis * localVelocity;
angularVelocity = mInitAxis * angularVelocity;
worldAxis = mInitAxis;
}
const int maxLifetimeMs = idMath::FtoiFast(BSE_MAX_DURATION * 1000.0f);
const int lifetimeMs = idMath::ClampInt(1, maxLifetimeMs, idMath::FtoiFast(GetDuration() * 1000.0f));
game->SpawnClientMoveable(entityDefName, lifetimeMs, worldOrigin, worldAxis, worldVelocity, angularVelocity);
// Debris is represented by spawned client entities, not by CPU-side BSE quads.
mEndTime = mStartTime;
mTrailTime = 0.0f;
mTrailCount = 0;
}
void rvLineParticle::Refresh(rvBSE* effect, rvSegmentTemplate* st, rvParticleTemplate* pt) {
if (!effect || !pt || !pt->UsesEndOrigin()) {
return;
}
if (float* initLength = GetInitLength()) {
if (pt->mpSpawnLength) {
HandleEndLength(effect, pt, *pt->mpSpawnLength, initLength);
}
if (float* destLength = GetDestLength()) {
if (pt->mpDeathLength) {
if ((pt->mpDeathLength->mFlags & PPFLAG_USEENDORIGIN) != 0) {
SetLengthUsingEndOrigin(effect, *pt->mpDeathLength, destLength);
}
else {
pt->mpDeathLength->Spawn(destLength, *pt->mpDeathLength, NULL, NULL);
}
pt->mpDeathLength->HandleRelativeParms(destLength, initLength, 3);
}
}
}
HandleTiling(pt);
const float attenuation = effect->GetAttenuation(st);
if (pt->mpSpawnLength) {
AttenuateLength(attenuation, *pt->mpSpawnLength);
}
// Keep the refreshed start/end length values intact. Re-initializing the
// particle length envelope here can zero out the freshly recomputed
// useEndOrigin vector and collapse the beam to fallback directions.
}
// ---------------------------------------------------------------------------
// simulation
// ---------------------------------------------------------------------------
bool rvParticle::GetEvaluationTime(float time, float& evalTime, bool infinite) {
evalTime = time - mStartTime;
if (time >= mEndTime - BSE_TIME_EPSILON) {
evalTime = (mEndTime - mStartTime) - BSE_TIME_EPSILON;
}
if (infinite) {
return true;
}
return (time > mStartTime - BSE_TIME_EPSILON) && (time < mEndTime);
}
void rvParticle::EvaluateVelocity(const rvBSE* effect, idVec3& velocity, float time) {
(void)effect;
const float t = Max(0.0f, time - mMotionStartTime);
if (GetStationary()) {
velocity.Set(1.0f, 0.0f, 0.0f);
return;
}
const float damp = Max(0.0f, 1.0f - mFriction * t);
velocity = (mVelocity + mAcceleration * t) * damp;
}
void rvParticle::EvaluatePosition(const rvBSE* effect, rvParticleTemplate* pt, idVec3& pos, float time) {
const float t = Max(0.0f, time - mMotionStartTime);
if (GetStationary()) {
pos = mInitPos;
mPosition = pos;
return;
}
const float halfT2 = 0.5f * t * t;
const float damp = Max(0.0f, 1.0f - mFriction * t);
pos = mInitPos + (mVelocity * t + mAcceleration * halfT2) * damp;
if (GetHasOffset() && pt && pt->mpAngleEnvelope && pt->mpOffsetEnvelope) {
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
rvAngles angle;
idVec3 offset;
EvaluateAngle(pt->mpAngleEnvelope, t, oneOverDuration, angle);
EvaluateOffset(pt->mpOffsetEnvelope, t, oneOverDuration, offset);
idMat3 rotation;
angle.ToMat3(rotation);
pos += rotation * offset;
}
if (effect && !GetLocked()) {
const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
pos = initToCurrent * pos;
const idVec3 delta = mInitEffectPos - effect->GetCurrentOrigin();
pos += effect->GetCurrentAxisTransposed() * delta;
}
mPosition = pos;
}
bool rvParticle::RunPhysics(rvBSE* effect, rvSegmentTemplate* st, float time) {
if (!effect || !st || !bse_physics.GetBool() || session->readDemo) {
return false;
}
if (GetStationary()) {
return false;
}
rvParticleTemplate* pt = st->GetParticleTemplate();
if (!pt || !pt->GetHasPhysics()) {
return false;
}
if (time - mMotionStartTime < BSE_PHYSICS_TIME_SAMPLE) {
return false;
}
float sourceTime = time - BSE_PHYSICS_TIME_SAMPLE;
if (sourceTime < mMotionStartTime) {
sourceTime = mMotionStartTime;
}
idVec3 sourceLocal;
idVec3 destLocal;
EvaluatePosition(effect, pt, sourceLocal, sourceTime);
EvaluatePosition(effect, pt, destLocal, time);
const idVec3 sourceWorld = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * sourceLocal;
const idVec3 destWorld = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * destLocal;
idTraceModel* trm = NULL;
if (pt->mTraceModelIndex >= 0) {
trm = bse->GetTraceModel(pt->mTraceModelIndex);
}
trace_t trace;
idVec3 source = sourceWorld;
idVec3 dest = destWorld;
game->Translation(trace, source, dest, trm, CONTENTS_SOLID | CONTENTS_OPAQUE);
if (trace.fraction >= 1.0f) {
return false;
}
if (pt->mNumImpactEffects > 0 && bse->CanPlayRateLimited(EC_IMPACT_PARTICLES)) {
idVec3 impactPos = trace.endpos;
if (trm) {
const idVec3 motion = (destWorld - sourceWorld) * trace.fraction;
CalcImpactPoint(impactPos, trace.endpos, motion, trm->bounds, trace.c.normal);
}
const int idx = rvRandom::irand(0, pt->mNumImpactEffects - 1);
const rvDeclEffect* impactEffect = pt->mImpactEffects[idx];
if (impactEffect) {
game->PlayEffect(
impactEffect,
impactPos,
trace.c.normal.ToMat3(),
false,
vec3_origin,
false,
false,
EC_IGNORE,
vec4_one);
}
}
if (pt->mBounce > 0.0f) {
Bounce(effect, pt, trace.endpos, trace.c.normal, time);
}
return pt->GetDeleteOnImpact();
}
void rvParticle::Bounce(rvBSE* effect, rvParticleTemplate* pt, idVec3 endPos, idVec3 normal, float time) {
if (!effect || !pt) {
return;
}
const idMat3 currentAxis = effect->GetCurrentAxis();
const idMat3 currentAxisTransposed = effect->GetCurrentAxisTransposed();
idVec3 oldVelocity;
EvaluateVelocity(effect, oldVelocity, time);
idVec3 worldVelocity = currentAxis * oldVelocity;
const float proj = worldVelocity * normal;
worldVelocity -= (proj + proj) * normal;
worldVelocity *= pt->mBounce;
const float speedSqr = worldVelocity.LengthSqr();
if (speedSqr < BSE_BOUNCE_LIMIT) {
// Match vanilla settle behavior: only stick when the impact normal is
// sufficiently opposite gravity direction (roughly "floor" contacts).
const float gravityDot = normal * effect->GetGravityDir();
if (gravityDot < -idMath::SQRT_1OVER2) {
SetStationary(true);
worldVelocity.Zero();
}
}
const idVec3 currentLocalVelocity = currentAxisTransposed * worldVelocity;
const idVec3 currentLocalPos = currentAxisTransposed * (endPos - effect->GetCurrentOrigin());
if (GetLocked()) {
mVelocity = currentLocalVelocity;
mInitPos = currentLocalPos;
}
else {
// Preserve the original unlocked init frame and persist bounce results
// via the exact inverse of EvaluatePosition/EvaluateVelocity's
// init->current mapping.
const idMat3 currentToInit = BuildCurrentToInitAxis(effect, mInitAxis);
const idVec3 originDelta = mInitEffectPos - effect->GetCurrentOrigin();
const idVec3 currentOriginOffset = currentAxisTransposed * originDelta;
mVelocity = currentToInit * currentLocalVelocity;
mInitPos = currentToInit * (currentLocalPos - currentOriginOffset);
}
mMotionStartTime = time;
}
void rvParticle::CheckTimeoutEffect(rvBSE* effect, rvSegmentTemplate* st, float time) {
if (!effect || !st || !game) {
return;
}
rvParticleTemplate* pt = st->GetParticleTemplate();
if (!pt || pt->GetNumTimeoutEffects() <= 0) {
return;
}
const int idx = rvRandom::irand(0, pt->GetNumTimeoutEffects() - 1);
const rvDeclEffect* timeoutEffect = pt->mTimeoutEffects[idx];
if (!timeoutEffect) {
return;
}
idVec3 position;
idVec3 velocity;
EvaluatePosition(effect, pt, position, time);
EvaluateVelocity(effect, velocity, time);
if (velocity.LengthSqr() > 1e-8f) {
velocity.NormalizeFast();
}
else {
velocity.Set(1.0f, 0.0f, 0.0f);
}
const idVec3 worldPos = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
const idVec3 worldDir = effect->GetCurrentAxis() * velocity;
game->PlayEffect(
timeoutEffect,
worldPos,
worldDir.ToMat3(),
false,
vec3_origin,
false,
false,
EC_IGNORE,
vec4_one);
}
void rvParticle::CalcImpactPoint(idVec3& endPos, const idVec3& origin, const idVec3& motion, const idBounds& bounds, const idVec3& normal) {
endPos = origin;
if (motion.LengthSqr() <= 1e-8f || bounds.IsCleared()) {
return;
}
idVec3 work = motion;
const idVec3 size = bounds[1] - bounds[0];
if (idMath::Fabs(size.x) > BSE_TIME_EPSILON) {
work.x /= size.x;
}
if (idMath::Fabs(size.y) > BSE_TIME_EPSILON) {
work.y /= size.y;
}
if (idMath::Fabs(size.z) > BSE_TIME_EPSILON) {
work.z /= size.z;
}
if (work.LengthSqr() > 1e-8f) {
work.NormalizeFast();
}
int axis = 0;
const idVec3 absWork(idMath::Fabs(work.x), idMath::Fabs(work.y), idMath::Fabs(work.z));
if (absWork.y >= absWork.x && absWork.y >= absWork.z) {
axis = 1;
}
else if (absWork.z >= absWork.x && absWork.z >= absWork.y) {
axis = 2;
}
const float dominant = Max(idMath::Fabs(work[axis]), 1e-6f);
const float invLen = 0.5f / dominant;
const idVec3 push((size.x * invLen) * work.x, (size.y * invLen) * work.y, (size.z * invLen) * work.z);
endPos += normal + normal + push;
}
void rvParticle::EmitSmokeParticles(rvBSE* effect, rvSegment* child, rvParticleTemplate* pt, float time) {
if (!effect || !child || !pt) {
return;
}
rvSegmentTemplate* childTemplate = child->GetSegmentTemplate();
if (!childTemplate) {
return;
}
const float timeEnd = time + 0.016000001f;
while (mLastTrailTime < timeEnd) {
if (mLastTrailTime >= mStartTime && mLastTrailTime < mEndTime) {
idVec3 position;
idVec3 velocity;
EvaluatePosition(effect, pt, position, mLastTrailTime);
EvaluateVelocity(effect, velocity, mLastTrailTime);
if (velocity.LengthSqr() > 1e-8f) {
velocity.NormalizeFast();
}
else {
velocity.Set(1.0f, 0.0f, 0.0f);
}
child->SpawnParticle(effect, childTemplate, mLastTrailTime, position, velocity.ToMat3());
}
const float interval = child->AttenuateInterval(effect, childTemplate);
if (interval <= BSE_TIME_EPSILON) {
break;
}
mLastTrailTime += interval;
}
}
// ---------------------------------------------------------------------------
// render helpers
// ---------------------------------------------------------------------------
dword rvParticle::HandleTint(const rvBSE* effect, idVec4& colour, float alpha) {
idVec4 out = colour;
out[3] *= alpha;
if (effect) {
const float bright = effect->GetBrightness();
out[0] *= effect->GetRed() * bright;
out[1] *= effect->GetGreen() * bright;
out[2] *= effect->GetBlue() * bright;
out[3] *= effect->GetAlpha();
}
// Additive stages author fade in the alpha envelope, but blend-add paths
// do not consume destination alpha for attenuation. Premultiply RGB so
// additive particles fade over time like stock BSE.
if (GetAdditive()) {
out[0] *= out[3];
out[1] *= out[3];
out[2] *= out[3];
}
return PackColorLocal(out);
}
void rvParticle::RenderQuadTrail(const rvBSE* effect, srfTriangles_t* tri, idVec3 offset, float fraction, idVec4& colour, idVec3& pos, bool first) {
if (!tri) {
return;
}
if (tri->numAllocedVerts > 0 && tri->numVerts + 2 > tri->numAllocedVerts) {
return;
}
if (!first && tri->numAllocedIndices > 0 && tri->numIndexes + 6 > tri->numAllocedIndices) {
return;
}
const dword rgba = HandleTint(effect, colour, 1.0f);
const int base = tri->numVerts;
SetDrawVert(tri->verts[base + 0], pos + offset, 0.0f, fraction, rgba);
SetDrawVert(tri->verts[base + 1], pos - offset, 1.0f, fraction, rgba);
if (!first) {
const int indexBase = tri->numIndexes;
tri->indexes[indexBase + 0] = base - 2;
tri->indexes[indexBase + 1] = base - 1;
tri->indexes[indexBase + 2] = base + 0;
tri->indexes[indexBase + 3] = base - 1;
tri->indexes[indexBase + 4] = base + 0;
tri->indexes[indexBase + 5] = base + 1;
tri->numIndexes += 6;
}
tri->numVerts += 2;
}
void rvParticle::RenderMotion(rvBSE* effect, rvParticleTemplate* pt, srfTriangles_t* tri, const renderEffect_s* owner, float time) {
if (!effect || !pt || !tri || !owner) {
return;
}
if (mTrailCount <= 0) {
return;
}
const float startTime = Max(time - mTrailTime, mStartTime);
const float delta = time - startTime;
if (delta <= BSE_TIME_EPSILON) {
return;
}
const float evalTime = Max(0.0f, time - mStartTime);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 color;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
idVec3 size(1.0f, 1.0f, 1.0f);
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
const float width = size.x;
idVec3 position;
EvaluatePosition(effect, pt, position, time);
const idMat3 ownerAxisTranspose = owner->axis.Transpose();
const idVec3 localView = ownerAxisTranspose * (effect->GetViewOrg() - owner->origin);
const idVec3 toView = localView - mInitPos;
idVec3 motionDir = mVelocity.Cross(toView);
if (motionDir.LengthSqr() <= 1e-6f) {
// Vanilla fallback uses world up in owner-local space when velocity and view vectors align.
motionDir = idVec3(0.0f, 1.0f, 0.0f).Cross(toView);
}
if (motionDir.LengthSqr() > 1e-6f) {
motionDir.NormalizeFast();
}
const idVec3 halfWidth = motionDir * (width * 0.5f);
for (int segment = 0; segment < mTrailCount; ++segment) {
const float t = static_cast<float>(segment) / static_cast<float>(mTrailCount);
idVec4 segmentColor = color;
segmentColor.w *= (1.0f - t);
RenderQuadTrail(effect, tri, halfWidth, t, segmentColor, position, segment == 0);
const float sampleTime = time - t * delta;
EvaluatePosition(effect, pt, position, sampleTime);
}
idVec4 endColor = color;
endColor.w = 0.0f;
RenderQuadTrail(effect, tri, halfWidth, 1.0f, endColor, position, false);
}
void rvParticle::DoRenderBurnTrail(rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time) {
if (mTrailCount <= 0 || mTrailTime <= 0.0f) {
return;
}
const float delta = mTrailTime / Max(1, mTrailCount);
for (int i = 1; i <= mTrailCount; ++i) {
const float trailTime = time - static_cast<float>(i) * delta;
if (trailTime < mStartTime || trailTime >= mEndTime) {
continue;
}
const float fade = static_cast<float>(mTrailCount - i) / Max(1, mTrailCount);
Render(effect, pt, view, tri, trailTime, fade);
}
}
// ---------------------------------------------------------------------------
// per-type spawn data
// ---------------------------------------------------------------------------
void rvSpriteParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
const float* sizeStart = mSizeEnv.GetStart();
size.Set(sizeStart[0], sizeStart[1], 0.0f);
rotate.Set(mRotationEnv.GetStart()[0], 0.0f, 0.0f);
}
void rvLineParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
size.Set(mSizeEnv.GetStart()[0], 0.0f, 0.0f);
rotate.Zero();
}
void rvOrientedParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
const float* sizeStart = mSizeEnv.GetStart();
size.Set(sizeStart[0], sizeStart[1], 0.0f);
rotate.Set(mRotationEnv.GetStart()[0], mRotationEnv.GetStart()[1], mRotationEnv.GetStart()[2]);
}
void rvModelParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
size.Set(mSizeEnv.GetStart()[0], mSizeEnv.GetStart()[1], mSizeEnv.GetStart()[2]);
rotate.Set(mRotationEnv.GetStart()[0], mRotationEnv.GetStart()[1], mRotationEnv.GetStart()[2]);
}
void rvLightParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
size.Set(mSizeEnv.GetStart()[0], mSizeEnv.GetStart()[1], mSizeEnv.GetStart()[2]);
rotate.Zero();
}
void rvDecalParticle::GetSpawnInfo(idVec4& tint, idVec3& size, idVec3& rotate) {
const float* tintStart = mTintEnv.GetStart();
tint.Set(tintStart[0], tintStart[1], tintStart[2], mFadeEnv.GetStart()[0]);
const float* sizeStart = mSizeEnv.GetStart();
size.Set(sizeStart[0], sizeStart[1], Max(idMath::Fabs(sizeStart[0]), idMath::Fabs(sizeStart[1])));
rotate.Set(mRotationEnv.GetStart()[0], 0.0f, 0.0f);
}
// ---------------------------------------------------------------------------
// per-type render
// ---------------------------------------------------------------------------
bool rvSpriteParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
BSETraceRenderDrop("sprite", this, time, "eval", evalTime, GetEndTime() - time);
return false;
}
idVec3 pos;
EvaluatePosition(effect, pt, pos, time);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 color;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
color[3] *= override;
if (color[3] <= 0.0f) {
BSETraceRenderDrop("sprite", this, time, "alpha", color[3], override);
return false;
}
float size[2] = { 1.0f, 1.0f };
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);
const idVec2& spriteSize = effect->GetSpriteSize();
if (idMath::Fabs(spriteSize.x) > BSE_TIME_EPSILON || idMath::Fabs(spriteSize.y) > BSE_TIME_EPSILON) {
size[0] = spriteSize.x;
size[1] = spriteSize.y;
}
float rotation = 0.0f;
EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, &rotation);
float s, c;
idMath::SinCos(rotation, s, c);
idVec3 right = view[1] * c - view[2] * s;
idVec3 up = view[1] * s + view[2] * c;
right *= idMath::Fabs(size[0]) * 0.5f;
up *= idMath::Fabs(size[1]) * 0.5f;
dword rgba = HandleTint(effect, color, 1.0f);
const int baseVert = tri->numVerts;
AppendQuad(
tri,
pos - right - up,
pos + right - up,
pos + right + up,
pos - right + up,
rgba);
tri->verts[baseVert + 0].normal = pos;
tri->verts[baseVert + 1].normal = pos;
tri->verts[baseVert + 2].normal = pos;
tri->verts[baseVert + 3].normal = pos;
return true;
}
bool rvLineParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
BSETraceRenderDrop("line", this, time, "eval", evalTime, GetEndTime() - time);
return false;
}
idVec3 pos;
EvaluatePosition(effect, pt, pos, time);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 color;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
color[3] *= override;
if (color[3] <= 0.0f) {
BSETraceRenderDrop("line", this, time, "alpha", color[3], override);
return false;
}
float width = 1.0f;
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &width);
idVec3 length(0.0f, 0.0f, 1.0f);
EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);
if (!GetLocked()) {
const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
length = initToCurrent * length;
}
if (GetGeneratedLine()) {
idVec3 velocity;
EvaluateVelocity(effect, velocity, time);
const float velocitySqr = velocity.LengthSqr();
if (velocitySqr > 1e-8f) {
velocity.NormalizeFast();
length = velocity * length.LengthFast();
}
}
const idVec3 end = pos + length;
const idVec3 toView = view[0] - (pos + length * 0.5f);
idVec3 side = length.Cross(toView);
float sideLenSqr = side.LengthSqr();
if (sideLenSqr > 1e-8f) {
side *= idMath::InvSqrt(sideLenSqr);
}
side *= width;
dword rgba = HandleTint(effect, color, 1.0f);
const int base = tri->numVerts;
SetDrawVert(tri->verts[base + 0], pos + side, 0.0f, 0.0f, rgba);
SetDrawVert(tri->verts[base + 1], pos - side, 0.0f, 1.0f, rgba);
SetDrawVert(tri->verts[base + 2], end - side, mTextureScale, 1.0f, rgba);
SetDrawVert(tri->verts[base + 3], end + side, mTextureScale, 0.0f, rgba);
tri->verts[base + 0].normal = pos;
tri->verts[base + 1].normal = pos;
tri->verts[base + 2].normal = pos;
tri->verts[base + 3].normal = pos;
const int indexBase = tri->numIndexes;
tri->indexes[indexBase + 0] = base + 0;
tri->indexes[indexBase + 1] = base + 1;
tri->indexes[indexBase + 2] = base + 2;
tri->indexes[indexBase + 3] = base + 0;
tri->indexes[indexBase + 4] = base + 2;
tri->indexes[indexBase + 5] = base + 3;
tri->numVerts += 4;
tri->numIndexes += 6;
return true;
}
bool rvLinkedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
return false;
}
idVec3 pos;
EvaluatePosition(effect, pt, pos, time);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 color;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
color[3] *= override;
if (color[3] <= 0.0f) {
return false;
}
float size = 1.0f;
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &size);
size = idMath::Fabs(size);
idVec3 up = view[1] * size;
dword rgba = HandleTint(effect, color, 1.0f);
const int base = tri->numVerts;
SetDrawVert(tri->verts[base + 0], pos + up, mFraction * mTextureScale, 0.0f, rgba);
SetDrawVert(tri->verts[base + 1], pos - up, mFraction * mTextureScale, 1.0f, rgba);
tri->verts[base + 0].normal = pos;
tri->verts[base + 1].normal = pos;
if (base > 0) {
const int indexBase = tri->numIndexes;
tri->indexes[indexBase + 0] = base - 2;
tri->indexes[indexBase + 1] = base - 1;
tri->indexes[indexBase + 2] = base + 0;
tri->indexes[indexBase + 3] = base - 1;
tri->indexes[indexBase + 4] = base + 1;
tri->indexes[indexBase + 5] = base + 0;
tri->numIndexes += 6;
}
tri->numVerts += 2;
return true;
}
bool sdOrientedLinkedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
return rvLinkedParticle::Render(effect, pt, view, tri, time, override);
}
bool rvOrientedParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
BSETraceRenderDrop("oriented", this, time, "eval", evalTime, GetEndTime() - time);
return false;
}
idVec3 position;
EvaluatePosition(effect, pt, position, time);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 tint;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
tint[3] *= override;
if (tint[3] <= 0.0f) {
BSETraceRenderDrop("oriented", this, time, "alpha", tint[3], override);
return false;
}
float size[2] = { 1.0f, 1.0f };
float rotation[3] = { 0.0f, 0.0f, 0.0f };
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);
EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, rotation);
idMat3 transform;
rvAngles(rotation[0], rotation[1], rotation[2]).ToMat3(transform);
const idVec3 right = transform[1] * (size[0] * 0.5f);
const idVec3 up = transform[2] * (size[1] * 0.5f);
dword rgba = HandleTint(effect, tint, 1.0f);
const int base = tri->numVerts;
SetDrawVert(tri->verts[base + 0], position - right - up, 0.0f, 0.0f, rgba);
SetDrawVert(tri->verts[base + 1], position + right - up, 1.0f, 0.0f, rgba);
SetDrawVert(tri->verts[base + 2], position + right + up, 1.0f, 1.0f, rgba);
SetDrawVert(tri->verts[base + 3], position - right + up, 0.0f, 1.0f, rgba);
tri->verts[base + 0].normal = position;
tri->verts[base + 1].normal = position;
tri->verts[base + 2].normal = position;
tri->verts[base + 3].normal = position;
const int indexBase = tri->numIndexes;
tri->indexes[indexBase + 0] = base + 0;
tri->indexes[indexBase + 1] = base + 1;
tri->indexes[indexBase + 2] = base + 2;
tri->indexes[indexBase + 3] = base + 0;
tri->indexes[indexBase + 4] = base + 2;
tri->indexes[indexBase + 5] = base + 3;
tri->numVerts += 4;
tri->numIndexes += 6;
return true;
}
bool rvModelParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri || !mModel || mModel->NumSurfaces() <= 0) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
return false;
}
idVec3 position;
EvaluatePosition(effect, pt, position, time);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 color;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, color);
color[3] *= override;
if (color[3] <= 0.0f) {
return false;
}
float size[3] = { 1.0f, 1.0f, 1.0f };
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size);
float rotation[3] = { 0.0f, 0.0f, 0.0f };
EvaluateRotation(pt->mpRotateEnvelope, evalTime, oneOverDuration, rotation);
const modelSurface_t* surf = mModel->Surface(0);
if (!surf || !surf->geometry) {
return false;
}
const srfTriangles_t* src = surf->geometry;
const int baseVert = tri->numVerts;
const int baseIndex = tri->numIndexes;
const dword rgba = HandleTint(effect, color, 1.0f);
byte rgbaBytes[4];
UnpackColor(rgba, rgbaBytes);
idMat3 rotationMat;
rvAngles(rotation[0], rotation[1], rotation[2]).ToMat3(rotationMat);
idMat3 transform = rotationMat;
if (!GetLocked()) {
const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
transform = transform * initToCurrent;
}
for (int i = 0; i < src->numVerts; ++i) {
idDrawVert& dst = tri->verts[baseVert + i];
dst = src->verts[i];
idVec3 p = transform * src->verts[i].xyz;
p.x *= size[0];
p.y *= size[1];
p.z *= size[2];
dst.xyz = position + p;
dst.normal = transform * dst.normal;
dst.tangents[0] = transform * dst.tangents[0];
dst.tangents[1] = transform * dst.tangents[1];
dst.color[0] = rgbaBytes[0];
dst.color[1] = rgbaBytes[1];
dst.color[2] = rgbaBytes[2];
dst.color[3] = rgbaBytes[3];
}
for (int i = 0; i < src->numIndexes; ++i) {
tri->indexes[baseIndex + i] = baseVert + src->indexes[i];
}
tri->numVerts += src->numVerts;
tri->numIndexes += src->numIndexes;
return true;
}
int rvElectricityParticle::GetBoltCount(float length) {
const int bolts = static_cast<int>(ceilf(length * 0.0625f));
return idMath::ClampInt(3, static_cast<int>(BSE_ELEC_MAX_BOLTS), bolts);
}
void rvElectricityParticle::RenderBranch(const rvBSE* effect, struct SElecWork* work, idVec3 start, idVec3 end, const idDeclTable* jitterTable) {
if (!effect || !work || !work->tri || !work->coords) {
return;
}
idVec3 forward = end - start;
const float length = forward.Normalize();
if (length < 1e-6f) {
work->coordCount = 0;
return;
}
idVec3 left;
if (idMath::Fabs(forward.x) < 0.99f) {
left = idVec3(0.0f, 0.0f, 1.0f).Cross(forward);
}
else {
left = idVec3(0.0f, 1.0f, 0.0f).Cross(forward);
}
if (left.LengthSqr() > 1e-8f) {
left.NormalizeFast();
}
else {
left.Set(1.0f, 0.0f, 0.0f);
}
idVec3 up = forward.Cross(left);
const int segmentVertStart = work->tri->numVerts;
int outCount = 0;
float fraction = 0.0f;
idVec3 current = start;
work->coords[outCount++] = current;
while (fraction < 1.0f - work->step * 0.5f && outCount < 254) {
fraction += work->step;
const float noise = jitterTable ? jitterTable->TableLookup(fraction) : 0.0f;
idVec3 jitter(
rvRandom::flrand(-mJitterSize.x, mJitterSize.x),
rvRandom::flrand(-mJitterSize.y, mJitterSize.y),
rvRandom::flrand(-mJitterSize.z, mJitterSize.z));
const idVec3 offset = forward * jitter.x + left * jitter.y + up * jitter.z;
current = start + forward * (length * fraction) + offset * noise;
work->coords[outCount++] = current;
}
work->coords[outCount++] = end;
work->coordCount = outCount;
float vCoord = 0.0f;
for (int i = 0; i < outCount - 1; ++i) {
if (!RenderLineSegment(effect, work, work->coords[i], vCoord)) {
break;
}
vCoord += work->step;
}
for (int base = segmentVertStart; base + 3 < work->tri->numVerts; base += 2) {
if (!HasTriCapacity(work->tri, 0, 6)) {
break;
}
const int indexBase = work->tri->numIndexes;
work->tri->indexes[indexBase + 0] = base;
work->tri->indexes[indexBase + 1] = base + 1;
work->tri->indexes[indexBase + 2] = base + 2;
work->tri->indexes[indexBase + 3] = base;
work->tri->indexes[indexBase + 4] = base + 2;
work->tri->indexes[indexBase + 5] = base + 3;
work->tri->numIndexes += 6;
}
}
bool rvElectricityParticle::RenderLineSegment(const rvBSE* effect, struct SElecWork* work, idVec3 start, float startFraction) {
if (!effect || !work || !work->tri) {
return false;
}
if (!HasTriCapacity(work->tri, 2, 0)) {
return false;
}
idVec3 offset = work->length.Cross(work->viewPos);
const float len2 = offset.LengthSqr();
if (len2 > 1e-8f) {
offset *= idMath::InvSqrt(len2);
}
else {
offset.Set(0.0f, 0.0f, 1.0f);
}
offset *= work->size;
const dword color = HandleTint(effect, work->tint, work->alpha);
const float s = startFraction * work->step + work->fraction;
const int baseVert = work->tri->numVerts;
SetDrawVert(work->tri->verts[baseVert + 0], start + offset, s, 0.0f, color);
SetDrawVert(work->tri->verts[baseVert + 1], start - offset, s, 1.0f, color);
work->tri->numVerts += 2;
return true;
}
void rvElectricityParticle::ApplyShape(const rvBSE* effect, struct SElecWork* work, idVec3 start, idVec3 end, int count, float startFraction, float endFraction) {
if (!effect || !work) {
return;
}
if (count <= 0) {
RenderLineSegment(effect, work, start, startFraction);
return;
}
const float randA = rvRandom::flrand(0.05f, 0.09f);
const float randB = rvRandom::flrand(0.05f, 0.09f);
const float shape = rvRandom::flrand(0.56f, 0.76f);
const idVec3 dir = end - start;
const float length = dir.LengthFast() * 0.7f;
if (length <= 1e-6f) {
RenderLineSegment(effect, work, start, startFraction);
return;
}
idVec3 forward = dir;
forward.NormalizeFast();
idVec3 left = forward.Cross(idVec3(0.0f, 0.0f, 1.0f));
if (left.LengthSqr() < 1e-6f) {
left.Set(1.0f, 0.0f, 0.0f);
}
else {
left.NormalizeFast();
}
const idVec3 down = forward.Cross(left);
const float len1 = rvRandom::flrand(-randA - 0.02f, 0.02f - randA) * length;
const float len2 = rvRandom::flrand(-randB - 0.02f, 0.02f - randB) * length;
const idVec3 point1 =
start * shape +
end * (1.0f - shape) +
left * len1 +
down * rvRandom::flrand(0.23f, 0.43f) * length;
const float t2 = rvRandom::flrand(0.23f, 0.43f);
const idVec3 point2 =
start * t2 +
end * (1.0f - t2) +
left * len2 +
down * rvRandom::flrand(-0.02f, 0.02f) * length;
const float mid0 = startFraction * 0.6666667f + endFraction * 0.3333333f;
const float mid1 = startFraction * 0.3333333f + endFraction * 0.6666667f;
ApplyShape(effect, work, start, point1, count - 1, startFraction, mid0);
ApplyShape(effect, work, point1, point2, count - 1, mid0, mid1);
ApplyShape(effect, work, point2, end, count - 1, mid1, endFraction);
}
int rvElectricityParticle::Update(rvParticleTemplate* pt, float time) {
if (!pt || !pt->mpLengthEnvelope) {
mNumBolts = 0;
return 0;
}
const float evalTime = Max(0.0f, time - mStartTime);
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec3 length;
EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);
mNumBolts = GetBoltCount(length.LengthFast());
return mNumBolts;
}
bool rvElectricityParticle::Render(const rvBSE* effect, rvParticleTemplate* pt, const idMat3& view, srfTriangles_t* tri, float time, float override) {
if (!effect || !pt || !tri) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
return false;
}
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 tint;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
tint[3] *= override;
if (tint[3] <= 0.0f) {
return false;
}
float width = 1.0f;
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, &width);
idVec3 length;
EvaluateLength(pt->mpLengthEnvelope, evalTime, oneOverDuration, length);
idVec3 position;
EvaluatePosition(effect, pt, position, time);
if (!GetLocked()) {
const idMat3 initToCurrent = BuildInitToCurrentAxis(effect, mInitAxis);
length = initToCurrent * length;
}
if (GetGeneratedLine()) {
idVec3 velocity;
EvaluateVelocity(effect, velocity, time);
if (velocity.LengthSqr() > 1e-8f) {
velocity.NormalizeFast();
length = velocity * length.LengthFast();
}
}
const float mainLength = length.LengthFast();
if (mainLength < 0.1f) {
return false;
}
if (mLastJitter + mJitterRate <= time) {
mLastJitter = time;
mSeed = rvRandom::Init();
}
if (mSeed != 0) {
rvRandom::Init(static_cast<unsigned long>(mSeed));
}
const int boltCount = Max(1, (mNumBolts > 0) ? mNumBolts : GetBoltCount(mainLength));
mNumBolts = boltCount;
SElecWork work;
memset(&work, 0, sizeof(work));
idVec3 tmpCoords[256];
work.tri = tri;
work.coords = tmpCoords;
work.coordCount = 0;
work.tint = tint;
work.size = idMath::Fabs(width);
work.alpha = 1.0f;
work.length = length;
work.forward = length;
work.viewPos = view[0];
work.step = mTextureScale / static_cast<float>(boltCount);
if (work.step <= BSE_TIME_EPSILON) {
work.step = 1.0f / static_cast<float>(boltCount);
}
const idVec3 endPos = position + length;
const idDeclTable* jitterTable = NULL;
if (pt->mElecInfo != NULL && !pt->mElecInfo->mJitterTableName.IsEmpty()) {
jitterTable = declManager->FindTable(pt->mElecInfo->mJitterTableName, false);
}
if (!jitterTable) {
jitterTable = mJitterTable;
}
if (!jitterTable) {
jitterTable = declManager->FindTable("halfsintable", false);
}
mJitterTable = jitterTable;
RenderBranch(effect, &work, position, endPos, jitterTable);
idVec3 forkBases[BSE_MAX_FORKS];
const int forks = idMath::ClampInt(0, BSE_MAX_FORKS, mNumForks);
for (int i = 0; i < forks; ++i) {
if (work.coordCount > 2) {
const int idx = rvRandom::irand(1, work.coordCount - 2);
forkBases[i] = work.coords[idx];
}
else {
forkBases[i] = position;
}
}
for (int i = 0; i < forks; ++i) {
const idVec3 mid = (forkBases[i] + endPos) * 0.5f;
const idVec3 forkEnd(
mid.x + rvRandom::flrand(mForkSizeMins.x, mForkSizeMaxs.x),
mid.y + rvRandom::flrand(mForkSizeMins.y, mForkSizeMaxs.y),
mid.z + rvRandom::flrand(mForkSizeMins.z, mForkSizeMaxs.z));
const idVec3 dir = forkEnd - forkBases[i];
const float forkLength = dir.LengthFast();
if (forkLength <= 1.0f || forkLength >= mainLength) {
continue;
}
work.length = dir;
work.forward = dir;
work.step = 1.0f / static_cast<float>(GetBoltCount(forkLength));
RenderBranch(effect, &work, forkBases[i], forkEnd, jitterTable);
}
return true;
}
void rvElectricityParticle::SetupElectricity(rvParticleTemplate* pt) {
if (!pt || !pt->mElecInfo) {
mNumBolts = 0;
mNumForks = 0;
mSeed = 0;
mForkSizeMins.Zero();
mForkSizeMaxs.Zero();
mJitterSize.Zero();
mLastJitter = 0.0f;
mJitterRate = 0.0f;
mJitterTable = NULL;
return;
}
const rvElectricityInfo* info = pt->mElecInfo;
mNumBolts = 0;
mNumForks = info->mNumForks;
mSeed = rvRandom::irand(1, 0x7FFFFFFF);
mForkSizeMins = info->mForkSizeMins;
mForkSizeMaxs = info->mForkSizeMaxs;
mJitterSize = info->mJitterSize;
mLastJitter = 0.0f;
mJitterRate = info->mJitterRate;
mJitterTable = NULL;
if (!info->mJitterTableName.IsEmpty()) {
mJitterTable = declManager->FindTable(info->mJitterTableName, false);
}
if (!mJitterTable) {
mJitterTable = info->mJitterTable;
}
if (!mJitterTable) {
mJitterTable = declManager->FindTable("halfsintable", false);
}
}
bool rvLightParticle::InitLight(rvBSE* effect, rvSegmentTemplate* st, float time) {
idRenderWorld* renderWorld = effect ? effect->GetRenderWorld() : NULL;
if (!renderWorld && session) {
renderWorld = session->rw;
}
if (!effect || !st || !renderWorld) {
return false;
}
rvParticleTemplate* pt = st->GetParticleTemplate();
if (!pt) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, false)) {
return false;
}
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 tint;
idVec3 size;
idVec3 position;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
EvaluatePosition(effect, pt, position, time);
// Light shaders consume Parm0..Parm2 for RGB energy. Keep authored fade on
// Parm3 instead of premultiplying it into RGB so stock effect lights match
// Quake 4's light-material behavior.
if (effect) {
const float bright = effect->GetBrightness();
tint.x *= effect->GetRed() * bright;
tint.y *= effect->GetGreen() * bright;
tint.z *= effect->GetBlue() * bright;
tint.w *= effect->GetAlpha();
}
memset(&mLight, 0, sizeof(mLight));
mLight.origin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
mLight.lightRadius.x = Max(1.0f, idMath::Fabs(size.x));
mLight.lightRadius.y = Max(1.0f, idMath::Fabs(size.y));
mLight.lightRadius.z = Max(1.0f, idMath::Fabs(size.z));
mLight.axis = effect->GetCurrentAxis();
memcpy( mLight.shaderParms, effect->GetShaderParms(), sizeof( mLight.shaderParms ) );
mLight.shaderParms[ SHADERPARM_RED ] = tint.x;
mLight.shaderParms[ SHADERPARM_GREEN ] = tint.y;
mLight.shaderParms[ SHADERPARM_BLUE ] = tint.z;
mLight.shaderParms[ SHADERPARM_ALPHA ] = tint.w;
mLight.pointLight = true;
mLight.detailLevel = 10.0f;
mLight.noShadows = !pt->GetShadows();
mLight.noSpecular = !pt->GetSpecular();
mLight.suppressLightInViewID = effect->GetSuppressLightsInViewID();
mLight.lightId = LIGHTID_EFFECT_LIGHT;
mLight.shader = pt->GetMaterial() ? pt->GetMaterial() : declManager->FindMaterial("_default");
mLightDefHandle = renderWorld->AddLightDef(&mLight);
mLightRenderWorld = renderWorld;
return mLightDefHandle != -1;
}
bool rvLightParticle::PresentLight(rvBSE* effect, rvParticleTemplate* pt, float time, bool infinite) {
idRenderWorld* renderWorld = effect ? effect->GetRenderWorld() : NULL;
if (!renderWorld) {
renderWorld = mLightRenderWorld;
}
if (!renderWorld && session) {
renderWorld = session->rw;
}
if (!effect || !pt || !renderWorld) {
return false;
}
float evalTime;
if (!GetEvaluationTime(time, evalTime, infinite)) {
return false;
}
if (mLightDefHandle == -1 || mLightRenderWorld != renderWorld) {
if (mLightDefHandle != -1 && mLightRenderWorld != NULL) {
mLightRenderWorld->FreeLightDef(mLightDefHandle);
}
mLightDefHandle = renderWorld->AddLightDef(&mLight);
if (mLightDefHandle == -1) {
return false;
}
mLightRenderWorld = renderWorld;
}
const float oneOverDuration = 1.0f / Max(BSE_TIME_EPSILON, GetDuration());
idVec4 tint;
idVec3 size;
idVec3 position;
EvaluateTint(pt->mpTintEnvelope, pt->mpFadeEnvelope, evalTime, oneOverDuration, tint);
EvaluateSize(pt->mpSizeEnvelope, evalTime, oneOverDuration, size.ToFloatPtr());
EvaluatePosition(effect, pt, position, time);
// Keep runtime updates in sync with InitLight attenuation semantics.
if (effect) {
const float bright = effect->GetBrightness();
tint.x *= effect->GetRed() * bright;
tint.y *= effect->GetGreen() * bright;
tint.z *= effect->GetBlue() * bright;
tint.w *= effect->GetAlpha();
}
mLight.origin = effect->GetCurrentOrigin() + effect->GetCurrentAxis() * position;
mLight.lightRadius.x = Max(1.0f, idMath::Fabs(size.x));
mLight.lightRadius.y = Max(1.0f, idMath::Fabs(size.y));
mLight.lightRadius.z = Max(1.0f, idMath::Fabs(size.z));
mLight.axis = effect->GetCurrentAxis();
memcpy( mLight.shaderParms, effect->GetShaderParms(), sizeof( mLight.shaderParms ) );
mLight.shaderParms[ SHADERPARM_RED ] = tint.x;
mLight.shaderParms[ SHADERPARM_GREEN ] = tint.y;
mLight.shaderParms[ SHADERPARM_BLUE ] = tint.z;
mLight.shaderParms[ SHADERPARM_ALPHA ] = tint.w;
mLight.suppressLightInViewID = effect->GetSuppressLightsInViewID();
renderWorld->UpdateLightDef(mLightDefHandle, &mLight);
return true;
}
bool rvLightParticle::Destroy(void) {
idRenderWorld* renderWorld = mLightRenderWorld;
if (!renderWorld && session) {
renderWorld = session->rw;
}
if (mLightDefHandle != -1 && renderWorld) {
renderWorld->FreeLightDef(mLightDefHandle);
}
mLightDefHandle = -1;
mLightRenderWorld = NULL;
memset(&mLight, 0, sizeof(mLight));
return true;
}