various fixes and performance improvements

This commit is contained in:
Brian Beck 2026-03-05 15:00:05 -08:00
parent cb28b66dad
commit 0c9ddb476a
62 changed files with 3109 additions and 1286 deletions

View file

@ -295,6 +295,17 @@ export class EmitterInstance {
if (this.particles.length < this.maxParticles) {
this.addParticle(pos, axis);
// V12: when overrideAdvances is false, immediately age the newly
// spawned particle by the remaining time in this frame. If that
// exceeds its lifetime, kill it immediately (never rendered).
if (!this.data.overrideAdvances && timeLeft > 0) {
const p = this.particles[this.particles.length - 1];
p.currentAge += timeLeft;
if (p.currentAge >= p.totalLifetime) {
this.particles.pop();
}
}
}
// Compute next emission time.
@ -310,10 +321,10 @@ export class EmitterInstance {
update(dtMS: number): void {
this.emitterAge += dtMS;
// Check emitter lifetime.
// Check emitter lifetime (V12 uses strictly greater).
if (
this.emitterLifetime > 0 &&
this.emitterAge >= this.emitterLifetime
this.emitterAge > this.emitterLifetime
) {
this.emitterDead = true;
}
@ -339,9 +350,9 @@ export class EmitterInstance {
// a = acc - vel*drag - wind*windCoeff + gravity*gravCoeff
// We skip wind for now (no wind system yet).
const ax = -p.vel[0] * drag;
const ay = -p.vel[1] * drag;
const az = -p.vel[2] * drag + GRAVITY_Z * gravCoeff;
const ax = p.acc[0] - p.vel[0] * drag;
const ay = p.acc[1] - p.vel[1] * drag;
const az = p.acc[2] - p.vel[2] * drag + GRAVITY_Z * gravCoeff;
// Symplectic Euler: update vel first, then pos with new vel.
p.vel[0] += ax * dt;
@ -430,14 +441,13 @@ export class EmitterInstance {
ejZ * speed,
];
// V12: acc = vel * constantAcceleration (set once, never changes).
// We fold this into the initial velocity for simplicity since the
// constant acceleration just biases the starting velocity direction.
// Actually, in V12 acc is a separate constant vector added each frame.
// For faithfulness, we should track it. But since it's constant, we can
// just apply it in the update loop as a per-particle property.
// For now, bake it into the velocity since most Tribes 2 datablocks
// have constantAcceleration = 0.
// V12: acc = vel * constantAcceleration, set once at spawn, applied every frame.
const ca = pData.constantAcceleration;
const acc: [number, number, number] = [
vel[0] * ca,
vel[1] * ca,
vel[2] * ca,
];
// Particle lifetime with variance.
let lifetime = pData.lifetimeMS;
@ -456,6 +466,7 @@ export class EmitterInstance {
this.particles.push({
pos: spawnPos,
vel,
acc,
orientDir: [ejX, ejY, ejZ],
currentAge: 0,
totalLifetime: lifetime,

View file

@ -4,6 +4,10 @@ attribute vec4 particleColor;
attribute float particleSize;
attribute float particleSpin;
attribute vec2 quadCorner; // (-0.5,-0.5) to (0.5,0.5)
attribute vec3 orientDir;
uniform bool uOrientParticles;
// cameraPosition is a built-in Three.js uniform.
varying vec2 vUv;
varying vec4 vColor;
@ -12,27 +16,47 @@ void main() {
vUv = quadCorner + 0.5; // [0,1] range
vColor = particleColor;
// Transform particle center to view space for billboarding.
vec3 viewPos = (modelViewMatrix * vec4(position, 1.0)).xyz;
if (uOrientParticles) {
if (length(orientDir) < 0.0001) {
// V12: don't render oriented particles with zero velocity.
gl_Position = vec4(0.0, 0.0, 0.0, 0.0);
return;
}
// V12 oriented particle: quad aligned along direction, facing camera.
vec3 worldPos = (modelMatrix * vec4(position, 1.0)).xyz;
vec3 dir = normalize(orientDir);
vec3 dirFromCam = worldPos - cameraPosition;
vec3 crossDir = normalize(cross(dirFromCam, dir));
// Apply spin rotation to quad corner.
float c = cos(particleSpin);
float s = sin(particleSpin);
vec2 rotated = vec2(
c * quadCorner.x - s * quadCorner.y,
s * quadCorner.x + c * quadCorner.y
);
// V12 maps U along dir (velocity) — match by using quadCorner.x for dir.
vec3 offset = dir * quadCorner.x + crossDir * quadCorner.y;
worldPos += offset * particleSize;
// Offset in view space (camera-facing billboard).
viewPos.xy += rotated * particleSize;
gl_Position = projectionMatrix * viewMatrix * vec4(worldPos, 1.0);
} else {
// Standard camera-facing billboard.
vec3 viewPos = (modelViewMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * vec4(viewPos, 1.0);
// Apply spin rotation to quad corner.
float c = cos(particleSpin);
float s = sin(particleSpin);
vec2 rotated = vec2(
c * quadCorner.x - s * quadCorner.y,
s * quadCorner.x + c * quadCorner.y
);
// Offset in view space (camera-facing billboard).
viewPos.xy += rotated * particleSize;
gl_Position = projectionMatrix * vec4(viewPos, 1.0);
}
}
`;
export const particleFragmentShader = /* glsl */ `
uniform sampler2D particleTexture;
uniform bool hasTexture;
uniform float debugOpacity;
varying vec2 vUv;
varying vec4 vColor;
@ -44,5 +68,6 @@ void main() {
} else {
gl_FragColor = vColor;
}
gl_FragColor.a *= debugOpacity;
}
`;

View file

@ -48,6 +48,8 @@ export interface EmitterDataResolved {
export interface Particle {
pos: [number, number, number];
vel: [number, number, number];
/** V12: constant acceleration = vel * constantAcceleration, set once at spawn. */
acc: [number, number, number];
orientDir: [number, number, number];
currentAge: number;
totalLifetime: number;