Skip to content

Commit

Permalink
Added an option to emit particles on LateUpdate: 'emitOnUpdate'
Browse files Browse the repository at this point in the history
  • Loading branch information
PatPL committed Jul 6, 2019
1 parent d2cb582 commit 9327fd7
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 77 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -30,6 +30,7 @@
*.sdf
*.opensdf
*.unsuccessfulbuild
.vs/
ipch/
[Oo]bj/
[Bb]in
Expand Down
56 changes: 48 additions & 8 deletions ModelMultiShurikenPersistFX.cs
Expand Up @@ -91,14 +91,26 @@ public class ModelMultiShurikenPersistFX : EffectBehaviour
// TODO Sarbian : have the init auto fill this one
[Persistent] public float randomInitalVelocityOffsetMaxRadius = 0.0f;

// Enables particle decluttering
// Enables particle declustering
// This adds a vector to particle's position based on velocity, deltaTime, and which particle of the frame is it.
// ⁙ ⁙ ⁙ ⁙ ⁙ ⁙ ⁙
// ^ false
// SPAWNED IN ONE FRAME
// vvvvv true
// ···································
[Persistent] public bool declutter = false;
[Persistent] public bool decluster = false;

// Emits particles on LateUpdate, rather than FixedUpdate, if enabled
//
// Synchronizes particle emission with frame draws. That fixes the 'sliding away from origin' on lower FPS
// (I think new particles effectively skip first physics pass before drawing)
// Check here (Rightmost one is enabled): https://i.imgur.com/fTU2F7r.gif
//
// Also, makes decluster more consistent. Frame draws are not synchronized with FixedUpdate.
// Decluster relies on Time.deltaTime to calculate distance to last emmited particle.
// On FixedUpdate, the Time.deltaTime is always 0.02, regardless of how much time actually passed from last frame draw.
// On LateUpdate, the Time.deltaTime is the actual time from last draw, so decluster can predict last particle's position a lot better
[Persistent] public bool emitOnUpdate = false;

[Persistent]
public int particleCountLimit = 1000;
Expand Down Expand Up @@ -169,6 +181,11 @@ public int MaxActiveParticles
}
}

// Previous way of counting particle count relied on the particled being emitted/updated synchronously with each other.
// With 'emitOnUpdate' changes, particles can be updated either in FixedUpdate, or LateUpdate.
// Used to count all currently active particles, and to display current particle count of this effect in SmokeScreen UI.
public int CurrentlyActiveParticles => persistentEmitters.Sum (x => x.pe.particleCount);

public string node_backup = string.Empty;

private bool activated = true;
Expand Down Expand Up @@ -355,11 +372,13 @@ public void FixedUpdate()
// Debug.Log(vHit2.collider.name);
//}

for (int i = 0; i < persistentEmitters.Count; i++)
{
PersistentKSPShurikenEmitter persistentKspShurikenEmitter = persistentEmitters[i];
persistentKspShurikenEmitter.EmitterOnUpdate(hostPart.Rigidbody.velocity + Krakensbane.GetFrameVelocity());
}
foreach (PersistentKSPShurikenEmitter emitter in persistentEmitters) {
// This is FixedUpdate, so don't emit here if particles should emit in LateUpdate
if (!emitOnUpdate) {
emitter.EmitterOnUpdate (hostPart.Rigidbody.velocity + Krakensbane.GetFrameVelocity ());
}
}

}

private void UpdateInputs(float power)
Expand Down Expand Up @@ -477,7 +496,7 @@ public void UpdateEmitters(float power)
pkpe.logarithmicGrow = logGrow.Value(inputs);
pkpe.logarithmicGrowScale = logGrowScale.Value(inputs);

pkpe.declutter = declutter;
pkpe.decluster = decluster;

pkpe.linearGrow = linGrow.Value(inputs);

Expand Down Expand Up @@ -549,6 +568,27 @@ public void Update()
}
}

// First, I tried to emit particles on regular Update, but stuff was weird, and the plume still appeared out of sync with frame draws
// According to https://docs.unity3d.com/Manual/ExecutionOrder.html
// LateUpdate is the last thing that happens before frame draw. That makes it as synced to frame draws, as possible
// LateUpdate is called after physics calculations too, so the newly emitted plume particles are right where they should be.
public void LateUpdate () {
foreach (PersistentKSPShurikenEmitter emitter in persistentEmitters) {
if (emitter.go is null) {
continue;
}

if (emitOnUpdate) {
emitter.EmitterOnUpdate (hostPart.Rigidbody.velocity + Krakensbane.GetFrameVelocity ());
}
}

// I think it's important to call this even though it doesn't count active particles
// because it calculates how many particles should be removed on next emit pass.
SmokeScreenConfig.UpdateParticlesCount ();

}

public override void OnInitialize()
{
//Print("Init");
Expand Down
137 changes: 75 additions & 62 deletions PersistentKSPShurikenEmitter.cs
Expand Up @@ -136,16 +136,16 @@ public class PersistentKSPShurikenEmitter

public float collideRatio = 0.0f;

// Enables particle decluttering
// This adds a vector to particle's position based on velocity, deltaTime, and which particle of the frame is it.
// ⁙ ⁙ ⁙ ⁙ ⁙ ⁙ ⁙
// ^ false
// SPAWNED IN ONE FRAME
// vvvvv true
// ···································
public bool declutter = false;
// Enables particle declustering
// This adds a vector to particle's position based on velocity, deltaTime, and which particle of the frame is it.
// ⁙ ⁙ ⁙ ⁙ ⁙ ⁙ ⁙
// ^ false
// SPAWNED IN ONE FRAME
// vvvvv true
// ···································
public bool decluster = false;

private bool addedLaunchPadCollider;
private bool addedLaunchPadCollider;

private static uint physicsPass = 4;

Expand Down Expand Up @@ -231,48 +231,48 @@ public void EmissionStop()
em.enabled = false;
}
}
/// <summary>
/// Spawns a single particle
/// </summary>
/// <param name="ThisInUpdate">Which particle is it in this emitter in this frame</param>
/// <param name="TotalInUpdate">How many particles will you spawn</param>
/// <summary>
/// Spawns a single particle
/// </summary>
/// <param name="ThisInUpdate">Which particle is it in this emitter in this frame</param>
/// <param name="TotalInUpdate">How many particles will you spawn</param>
private void Emit (int ThisInUpdate, int TotalInUpdate)
{
ParticleSystem.EmitParams emitParams = new ParticleSystem.EmitParams();

Vector3 pos = Vector3.zero;
Vector3 FinalLocalVelocity = localVelocity + new Vector3 (
Random.Range (-rndVelocity.x, rndVelocity.x),
Random.Range (-rndVelocity.y, rndVelocity.y), // There's something weird going on with rotations. This Y isn't up-down, while Unity's is
Random.Range (-rndVelocity.z, rndVelocity.z)
);
Vector3 FinalLocalVelocity = localVelocity + new Vector3 (
Random.Range (-rndVelocity.x, rndVelocity.x),
Random.Range (-rndVelocity.y, rndVelocity.y), // There's something weird going on with rotations. This Y isn't up-down, while Unity's is
Random.Range (-rndVelocity.z, rndVelocity.z)
);

switch (shape)
switch (shape)
{
case KSPParticleEmitter.EmissionShape.Point:
pos = Vector3.zero;
break;
pos = Vector3.zero;
break;

case KSPParticleEmitter.EmissionShape.Line:
pos = new Vector3 (Random.Range (-shape1D, shape1D) * 0.5f, 0f, 0f);
pos = new Vector3 (Random.Range (-shape1D, shape1D) * 0.5f, 0f, 0f);
break;

case KSPParticleEmitter.EmissionShape.Ellipsoid:
pos = Random.insideUnitSphere;
pos.Scale(shape3D);

break;
break;

case KSPParticleEmitter.EmissionShape.Ellipse:
pos = Random.insideUnitCircle;
pos.x = pos.x * shape2D.x;
pos.z = pos.y * shape2D.y;
pos.y = 0f;
break;
break;

case KSPParticleEmitter.EmissionShape.Sphere:
pos = Random.insideUnitSphere * shape1D;
break;
break;

case KSPParticleEmitter.EmissionShape.Cuboid:
pos = new Vector3(
Expand All @@ -291,7 +291,7 @@ private void Emit (int ThisInUpdate, int TotalInUpdate)
break;
}

Vector3 vel;
Vector3 vel;
if (pe.main.simulationSpace == ParticleSystemSimulationSpace.Local)
{
vel = FinalLocalVelocity + go.transform.InverseTransformDirection(worldVelocity);
Expand All @@ -302,17 +302,17 @@ private void Emit (int ThisInUpdate, int TotalInUpdate)
vel = worldVelocity + go.transform.TransformDirection(FinalLocalVelocity);
}

if (declutter) {
// Apply some local velocity to prevent multiple particles spawned in one frame from clumping together
// Simulates as if some particles already were emitted between frames, and travelled some distance
pos += (
vel * // Initial velocity
(Time.fixedDeltaTime) * // How much time has passed. At this point this value should be the total distance to the last particle emmited in the last update
((float) (ThisInUpdate) / (float) (TotalInUpdate)) // Spread them out evenly, from 0 to last particle
);
}

float rotation = rndRotation ? Random.value * 360f : 0f;
if (decluster) {
// Apply some local velocity to prevent multiple particles spawned in one frame from clumping together
// Simulates as if some particles already were emitted between frames, and travelled some distance
pos += (
vel * // Initial velocity
(Time.deltaTime) * TimeWarp.CurrentRate * // How much time has passed. At this point this value should be the total distance to the last particle emmited in the last update
((float) (ThisInUpdate) / (float) (TotalInUpdate)) // Spread them out evenly, from 0 to last particle
);
}

float rotation = rndRotation ? Random.value * 360f : 0f;
float angularV = angularVelocity + Random.value * rndAngularVelocity;

emitParams.position = pos;
Expand All @@ -332,32 +332,31 @@ public void EmitterOnUpdate(Vector3 emitterWorldVelocity)
if (pe == null)
return;


// "Default", "TransparentFX", "Local Scenery", "Ignore Raycast"
int mask = (1 << LayerMask.NameToLayer("Default")) | (1 << LayerMask.NameToLayer("Local Scenery"));

Profiler.BeginSample("fixedEmit");
Profiler.BeginSample ("fixedEmit");
// Emit particles on fixedUpdate rather than Update so that we know which particles
// were just created and should be nudged, should not be collided, etc.
if (fixedEmit)
{
// double averageEmittedParticles = Random.Range (minEmission, maxEmission) * TimeWarp.fixedDeltaTime;
// double compensatedEmittedParticles = averageEmittedParticles + particleFraction;
// double emittedParticles = Math.Truncate (compensatedEmittedParticles);
// particleFraction = compensatedEmittedParticles - emittedParticles;
if (fixedEmit) {
// Changed every frame time measure to Time.deltaTime, because, as stated here: https://docs.unity3d.com/ScriptReference/Time-fixedDeltaTime.html
// Time.deltaTime is equal to fixed time, or frame time, depending on context
// If called from FixedUpdate, it will be equal to 0.02
// If called from LateUpdate, it will be equal to frame time
pendingParticles += Random.Range (minEmission, maxEmission) * Time.deltaTime; // * TimeWarp.CurrentRate
// Don't increase particle count on timewarp. KSP already has enough stuff to process

pendingParticles += Random.Range (minEmission, maxEmission) * TimeWarp.fixedDeltaTime;
// How many particles should be spawned this frame
int ParticlesThisFrame = Mathf.FloorToInt (pendingParticles);

// How many particles should be spawned this frame
int ParticlesThisFrame = Mathf.FloorToInt (pendingParticles);
// Keeps track of remaining fractions of a particle
pendingParticles -= ParticlesThisFrame;

pendingParticles -= ParticlesThisFrame;

for (int i = 0; i < ParticlesThisFrame; ++i) {
for (int i = 0; i < ParticlesThisFrame; ++i) {
Emit (i, ParticlesThisFrame);
}
}
Profiler.EndSample();
Profiler.EndSample ();

if (particles == null || pe.main.maxParticles > particles.Length)
particles = new ParticleSystem.Particle[pe.main.maxParticles];
Expand All @@ -374,9 +373,10 @@ public void EmitterOnUpdate(Vector3 emitterWorldVelocity)
Vector2 disk = new Vector2 (0,0);
//For startSpread

double logGrowConst = TimeWarp.fixedDeltaTime * logarithmicGrow * logarithmicGrowScale;
float linGrowConst = (float)(TimeWarp.fixedDeltaTime * linearGrow * averageSize);
float growConst = Mathf.Pow( 1 + sizeGrow, TimeWarp.fixedDeltaTime);
// Use Time.deltaTime, so that the time will be correct in both FixedUpdate, and LateUpdate contexts
double logGrowConst = Time.deltaTime * logarithmicGrow * logarithmicGrowScale;
float linGrowConst = (float)(Time.deltaTime * linearGrow * averageSize);
float growConst = Mathf.Pow( 1 + sizeGrow, Time.deltaTime);

Transform peTransform = pe.transform;

Expand All @@ -386,6 +386,17 @@ public void EmitterOnUpdate(Vector3 emitterWorldVelocity)

Profiler.BeginSample("Loop");

// This one is multiplicative, and relied on it being run 50 times per second.
// Now that this may be called any number of times per second,
// The force multiplier needs to be raised to the power of passed time.
// More time per frame -> apply more of the multiplier
//
// 'Time.deltaTime * 50' preserves original behavior.
// Moved out of the loop, because it doesn't change particle-to-particle.
// Doesn't need to be calculated multiple times
float xyForceMultiplier = Mathf.Pow (xyForce, Time.deltaTime * 50);
float zForceMultiplier = Mathf.Pow (zForce, Time.deltaTime * 50);

//Step through all the particles:
for (int j = 0; j < numParticlesAlive; j++)
{
Expand Down Expand Up @@ -456,10 +467,12 @@ public void EmitterOnUpdate(Vector3 emitterWorldVelocity)
else if (!useWorldSpace && particle.remainingLifetime != particle.startLifetime)
{
pPos = peTransform.TransformPoint(particle.position);
pVel = peTransform.TransformDirection(particle.velocity.x * xyForce,
particle.velocity.y * xyForce,
particle.velocity.z * zForce)
+ frameVel;

pVel = peTransform.TransformDirection (
particle.velocity.x * xyForceMultiplier,
particle.velocity.y * xyForceMultiplier,
particle.velocity.z * zForceMultiplier
) + frameVel;
}
else
{
Expand Down Expand Up @@ -650,7 +663,7 @@ private Vector3 ParticlePhysics(double radius, double initialRadius, Vector3d pP
acceleration += -0.5 * atmosphericDensity * pVel * pVel.magnitude * dragCoefficient * Math.PI * radius * radius / mass;

// Euler is good enough for graphics.
return pVel + acceleration * TimeWarp.fixedDeltaTime * physicsPass;
return pVel + acceleration * Time.deltaTime * physicsPass * TimeWarp.CurrentRate;
}

//[MethodImpl(MethodImplOptions.AggressiveInlining)]
Expand All @@ -661,7 +674,7 @@ private Vector3 ParticleCollision(Vector3d pPos, Vector3d pVel, int mask)
pPos,
pVel,
out hit,
(float)pVel.magnitude * TimeWarp.fixedDeltaTime * physicsPass,
(float)pVel.magnitude * Time.deltaTime * physicsPass * TimeWarp.CurrentRate,
mask))
{
//// collidersName[hit.collider.name] = true;
Expand Down
12 changes: 8 additions & 4 deletions SmokeScreenConfig.cs
Expand Up @@ -67,18 +67,22 @@ private SmokeScreenConfig()

public static void UpdateParticlesCount()
{
// Like stated before, because emission passes can be out of sync now, activeParticles is not a reliable
// way to measure how many particles are out there.
int currentlyActiveParticles = 0;
ModelMultiShurikenPersistFX.List.ForEach (x => currentlyActiveParticles += x.CurrentlyActiveParticles);
if (lastTime < Time.fixedTime)
{
if (activeParticles > Instance.maximumActiveParticles)
if (currentlyActiveParticles > Instance.maximumActiveParticles)
{
int toRemove = activeParticles - Instance.maximumActiveParticles;
int toRemove = currentlyActiveParticles - Instance.maximumActiveParticles;
if (toRemove < Instance.maximumActiveParticles)
{
particleDecimate = activeParticles / (toRemove + 1); // positive we remove each n
particleDecimate = currentlyActiveParticles / (toRemove + 1); // positive we remove each n
}
else
{
particleDecimate = -activeParticles / Instance.maximumActiveParticles;
particleDecimate = -currentlyActiveParticles / Instance.maximumActiveParticles;

// negative we keep each n
}
Expand Down

0 comments on commit 9327fd7

Please sign in to comment.