diff options
Diffstat (limited to 'Runtime/Graphics/ParticleSystem')
53 files changed, 9665 insertions, 0 deletions
diff --git a/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp new file mode 100644 index 0000000..362fbff --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.cpp @@ -0,0 +1,102 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ClampVelocityModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Misc/BuildSettings.h" + +inline float DampenOutsideLimit (float v, float limit, float dampen) +{ + float sgn = Sign (v); + float abs = Abs (v); + if (abs > limit) + abs = Lerp(abs, limit, dampen); + return abs * sgn; +} + +ClampVelocityModule::ClampVelocityModule () : ParticleSystemModule(false) +, m_SeparateAxis (false) +, m_InWorldSpace (false) +, m_Dampen (1.0f) +{ +} + +template<ParticleSystemCurveEvalMode mode> +void MagnitudeUpdateTpl(const MinMaxCurve& magnitude, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dampen) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + float limit = Evaluate<mode> (magnitude, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemClampVelocityCurveId)); + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + vel = NormalizeSafe (vel) * DampenOutsideLimit (Magnitude (vel), limit, dampen); + ps.velocity[q] = vel - ps.animatedVelocity[q]; + } +} + +void ClampVelocityModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if (m_SeparateAxis) + { + Matrix4x4f matrix; + Matrix4x4f invMatrix; + bool transform; + if(IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)) + transform = GetTransformationMatrices(matrix, invMatrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + else + transform = false; // Revert to old broken behavior + + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemClampVelocityCurveId); + const float time = NormalizedTime(ps, q); + + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + if(transform) + vel = matrix.MultiplyVector3 (vel); + const Vector3f limit (Evaluate (m_X, time, random.x), Evaluate (m_Y, time, random.y), Evaluate (m_Z, time, random.z)); + vel.x = DampenOutsideLimit (vel.x, limit.x, m_Dampen); + vel.y = DampenOutsideLimit (vel.y, limit.y, m_Dampen); + vel.z = DampenOutsideLimit (vel.z, limit.z, m_Dampen); + vel = vel - ps.animatedVelocity[q]; + if(transform) + vel = invMatrix.MultiplyVector3 (vel); + ps.velocity[q] = vel; + } + } + else + { + if(m_Magnitude.minMaxState == kMMCScalar) + MagnitudeUpdateTpl<kEMScalar> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else if(m_Magnitude.IsOptimized() && m_Magnitude.UsesMinMax()) + MagnitudeUpdateTpl<kEMOptimizedMinMax> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else if(m_Magnitude.IsOptimized()) + MagnitudeUpdateTpl<kEMOptimized> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + else + MagnitudeUpdateTpl<kEMSlow> (m_Magnitude, ps, fromIndex, toIndex, m_Dampen); + } +} + +void ClampVelocityModule::CheckConsistency () +{ + m_Dampen = clamp<float> (m_Dampen, 0.0f, 1.0f); + m_X.SetScalar(std::max<float> (0.0f, m_X.GetScalar())); + m_Y.SetScalar(std::max<float> (0.0f, m_Y.GetScalar())); + m_Z.SetScalar(std::max<float> (0.0f, m_Z.GetScalar())); + m_Magnitude.SetScalar(std::max<float> (0.0f, m_Magnitude.GetScalar())); +} + + +template<class TransferFunction> +void ClampVelocityModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_Magnitude, "magnitude"); + transfer.Transfer (m_SeparateAxis, "separateAxis"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); transfer.Align (); + transfer.Transfer (m_Dampen, "dampen"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ClampVelocityModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h new file mode 100644 index 0000000..e315d61 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ClampVelocityModule.h @@ -0,0 +1,29 @@ +#ifndef SHURIKENMODULECLAMPVELOCITY_H +#define SHURIKENMODULECLAMPVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class ClampVelocityModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ClampVelocityModule) + ClampVelocityModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void CheckConsistency (); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + MinMaxCurve m_Magnitude; + bool m_InWorldSpace; + bool m_SeparateAxis; + float m_Dampen; +}; + +#endif // SHURIKENMODULECLAMPVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp new file mode 100644 index 0000000..9e16bde --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.cpp @@ -0,0 +1,603 @@ +#include "UnityPrefix.h" +#include <float.h> +#include "CollisionModule.h" + +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Geometry/Intersection.h" +#include "Runtime/Geometry/Plane.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Graphics/Transform.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Interfaces/IRaycast.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +#include "Editor/Src/Utility/DebugPrimitives.h" + +struct ParticleSystemCollisionParameters +{ + float bounceFactor; + float energyLossOnCollision; + float minKillSpeedSqr; + float particleRadius; + float dampen; + + PlaneColliderCache* planeColliderCache; + IRaycast* raycaster; + size_t rayBudget; + size_t nextParticleToTrace; + float voxelSize; +}; + +struct ColliderInfo +{ + Plane m_CollisionPlane; + bool m_Traced; + int m_ColliderInstanceID; + int m_RigidBodyOrColliderInstanceID; +}; + +struct CollisionInfo +{ + CollisionInfo():m_NumWorldCollisions(0),m_NumCachedCollisions(0),m_NumPlaneCollisions(0),m_Colliders(NULL) {} + + size_t m_NumWorldCollisions; + size_t m_NumCachedCollisions; + size_t m_NumPlaneCollisions; + ColliderInfo* m_Colliders; + + size_t AllCollisions() const { return m_NumWorldCollisions+m_NumCachedCollisions+m_NumPlaneCollisions; } +}; + +/// @TODO: Why does Vector3f has a constructor? WTF. +inline void CalculateCollisionResponse(const ParticleSystemReadOnlyState& roState, + ParticleSystemState& state, + ParticleSystemParticles& ps, + const size_t q, + const ParticleSystemCollisionParameters& params, + const Vector3f& position, + const Vector3f& velocity, + const HitInfo& hitInfo) +{ + // Reflect + dampen + Vector3f positionOffset = ReflectVector (position - hitInfo.intersection, hitInfo.normal) * params.dampen; + Vector3f newVelocity = ReflectVector(velocity, hitInfo.normal) * params.dampen; + + // Apply bounce + positionOffset -= hitInfo.normal * (Dot(positionOffset, hitInfo.normal)) * params.bounceFactor; + newVelocity -= hitInfo.normal * Dot(newVelocity, hitInfo.normal) * params.bounceFactor; + + ps.position[q] = hitInfo.intersection + positionOffset; + ps.velocity[q] = newVelocity - ps.animatedVelocity[q]; + + for(int s = 0; s < state.numCachedSubDataCollision; s++) + { + ParticleSystemEmissionState emissionState; + RecordEmit(emissionState, state.cachedSubDataCollision[s], roState, state, ps, kParticleSystemSubTypeCollision, s, q, 0.0f, 0.0001f, 1.0f); + } + ps.lifetime[q] -= params.energyLossOnCollision * ps.startLifetime[q]; + + if (ps.GetUsesCollisionEvents () && !(hitInfo.colliderInstanceID == 0)) + { + Vector3f wcIntersection = hitInfo.intersection; + Vector3f wcNormal = hitInfo.normal; + Vector3f wcVelocity = velocity; + if ( roState.useLocalSpace ) + { + wcIntersection = state.localToWorld.MultiplyPoint3 (wcIntersection); + wcNormal = state.localToWorld.MultiplyVector3 (wcNormal); + wcVelocity = state.localToWorld.MultiplyVector3 (wcVelocity); + } + ps.collisionEvents.AddEvent (ParticleCollisionEvent (wcIntersection, wcNormal, wcVelocity, hitInfo.colliderInstanceID, hitInfo.rigidBodyOrColliderInstanceID)); + //printf_console (Format ("Intersected '%s' -> id: %d -> go id: %d.\n", hitInfo.collider->GetName (), hitInfo.collider->GetInstanceID (), hitInfo.collider->GetGameObject ().GetInstanceID ()).c_str ()); +} +} + +CollisionModule::CollisionModule () : ParticleSystemModule(false) +, m_Type(0) +, m_Dampen(0.0f) +, m_Bounce(1.0f) +, m_EnergyLossOnCollision(0.0f) +, m_MinKillSpeed(0.0f) +, m_ParticleRadius(0.01f) +, m_Quality(0) +, m_VoxelSize(0.5f) +, m_CollisionMessages(false) +{ + m_CollidesWith.m_Bits = 0xFFFFFFFF; +} + +void CollisionModule::AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state) +{ + Assert(!state.cachedCollisionPlanes); + + Matrix4x4f matrix = Matrix4x4f::identity; + if(roState.useLocalSpace) + matrix = state.worldToLocal; + + state.numCachedCollisionPlanes = 0; + for (unsigned i=0; i<kMaxNumPrimitives; ++i) + { + if (m_Primitives[i].IsNull ()) + continue; + state.numCachedCollisionPlanes++; + } + + state.cachedCollisionPlanes = ALLOC_TEMP_MANUAL(Plane, state.numCachedCollisionPlanes); + + int planeCount = 0; + + for (unsigned i=0; i<kMaxNumPrimitives; ++i) + { + if (m_Primitives[i].IsNull ()) + continue; + + const Transform* transform = m_Primitives[i]->QueryComponent(Transform); + const Vector3f position = matrix.MultiplyPoint3(transform->GetPosition()); + + Assert(planeCount <= state.numCachedCollisionPlanes); + const Vector3f normal = matrix.MultiplyVector3(RotateVectorByQuat (transform->GetRotation (), Vector3f::yAxis)); + state.cachedCollisionPlanes[planeCount].SetNormalAndPosition (normal, position); + state.cachedCollisionPlanes[planeCount].NormalizeRobust(); + planeCount++; + } +} + +void CollisionModule::FreeCache(ParticleSystemState& state) +{ + if(state.cachedCollisionPlanes) + { + FREE_TEMP_MANUAL(state.cachedCollisionPlanes); + state.cachedCollisionPlanes = 0; + state.numCachedCollisionPlanes = 0; + } +} + +// read the plane cache for a given range +size_t ReadCache(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, CollisionInfo& collision, const size_t fromIndex, const size_t& toIndex, const float dt) +{ + size_t numIntersections = 0; + for (size_t q = fromIndex; q < toIndex; ++q) + { + // initialise plane to value that guarantees no intersection + collision.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + collision.m_Colliders[q].m_Traced = false; + collision.m_Colliders[q].m_ColliderInstanceID = -1; + collision.m_Colliders[q].m_RigidBodyOrColliderInstanceID = -1; + + // build start/end points + Vector3f to = ps.position[q]; + const Vector3f v = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f d = v * dt; + Vector3f from = to - d; + // convert to WC + if ( roState.useLocalSpace ) + { + from = state.localToWorld.MultiplyPoint3(from); + to = state.localToWorld.MultiplyPoint3(to); + } + + // lookup the cache + Plane plane; + int colliderInstanceID; + int rigidBodyOrColliderInstanceID; + if ( params.planeColliderCache->Find (from, to-from, plane, colliderInstanceID, rigidBodyOrColliderInstanceID, params.voxelSize) ) + { + collision.m_Colliders[q].m_CollisionPlane = plane; + collision.m_Colliders[q].m_ColliderInstanceID = colliderInstanceID; + collision.m_Colliders[q].m_RigidBodyOrColliderInstanceID = rigidBodyOrColliderInstanceID; + numIntersections++; + } + } + return numIntersections; +} + +// Perform ray casts. Ray casts are done in WC and results are transformed back into sim space if necessary. +// There are three sets of indices: +// [fromIndex ; params.nextParticleToTrace[ for these we do caching if the cache is available +// [params.nextParticleToTrace ; params.nextParticleToTrace + params.rayBudget[ for these we trace fresh rays +// [params.nextParticleToTrace + params.rayBudget ; toIndex[ for these we do caching if the cache is available +CollisionInfo WorldCollision(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const size_t fromIndex, const int filter, const float dt) +{ + CollisionInfo collisionCounter; + const bool approximate = ( params.planeColliderCache ? true : false ); + size_t numIntersections = 0; + + const size_t toIndex = ps.array_size(); + + // pre range that is cached + const size_t traceRangeFrom = approximate ? params.nextParticleToTrace : fromIndex; + const size_t traceRangeTo = approximate ? std::min(params.nextParticleToTrace+params.rayBudget, toIndex) : toIndex; + + const size_t fromIndex0 = fromIndex; + const size_t toIndex0 = traceRangeFrom; + // range to actually ray trace + const size_t fromIndex1 = traceRangeFrom; + const size_t toIndex1 = traceRangeTo; + // post range that is cached + const size_t fromIndex2 = traceRangeTo; + const size_t toIndex2 = toIndex; + + collisionCounter.m_Colliders = ALLOC_TEMP_MANUAL(ColliderInfo, ps.array_size()); + collisionCounter.m_NumCachedCollisions = (toIndex0-fromIndex0)+(toIndex2-fromIndex2); + collisionCounter.m_NumWorldCollisions = toIndex1-fromIndex1; + + if ( toIndex1-fromIndex1 > 0 ) + { + // batch trace selected range + dynamic_array< BatchedRaycast > rayBatch(toIndex1-fromIndex1,kMemTempAlloc); + dynamic_array< BatchedRaycastResult > rayResults(toIndex1-fromIndex1,kMemTempAlloc); + // build request array + size_t i = 0; + for (size_t q = fromIndex1; q < toIndex1; ++q, ++i) + { + // build start/end points + const Vector3f to = ps.position[q]; + const Vector3f v = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f d = v * dt; + const Vector3f from = to - d; + + if ( approximate ) + { + // extend ray to trace across the entire voxel (and then some) + const float voxSize = std::max(params.voxelSize,params.voxelSize*kVoxelHeightMultiplier); + const Vector3f displacement = to-from; + + ///@TODO: handle magnitude 0. Should we skip the raycast??? + const Vector3f direction = NormalizeFast(displacement); + const Vector3f extendedTo = from + direction * voxSize; + + // insert into batch + rayBatch[i] = BatchedRaycast(q,from,extendedTo); + } + else + rayBatch[i] = BatchedRaycast(q,from,to); + + // initialise plane to value that guarantees no intersection + collisionCounter.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + collisionCounter.m_Colliders[q].m_Traced = (!approximate); + collisionCounter.m_Colliders[q].m_ColliderInstanceID = -1; + collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID = -1; + } + // convert to WC + if ( roState.useLocalSpace ) + { + const Matrix4x4f m = state.localToWorld; + for (size_t i = 0; i < rayBatch.size(); ++i) + { + rayBatch[i].from = m.MultiplyPoint3(rayBatch[i].from); + rayBatch[i].to = m.MultiplyPoint3(rayBatch[i].to); + } + } + // trace the rays + const size_t numIx = params.raycaster->BatchIntersect( rayBatch, rayResults, filter, approximate ); + + // convert back to local space + if ( roState.useLocalSpace ) + { + const Matrix4x4f m = state.worldToLocal; + for (size_t i = 0; i < numIx; ++i) + { + // the plane intersection was computed in WC, transform intersection to local space. + rayResults[i].hitInfo.intersection = m.MultiplyPoint3( rayResults[i].hitInfo.intersection ); + rayResults[i].hitInfo.normal = m.MultiplyVector3( rayResults[i].hitInfo.normal ); + } + } + + // store planes in the particles that intersected something + for (size_t i = 0; i < numIx; ++i) + { + const size_t q = rayBatch[rayResults[i].index].index; + collisionCounter.m_Colliders[q].m_CollisionPlane.normal = rayResults[i].hitInfo.normal; + collisionCounter.m_Colliders[q].m_CollisionPlane.distance = -Dot(rayResults[i].hitInfo.intersection, rayResults[i].hitInfo.normal); + collisionCounter.m_Colliders[q].m_ColliderInstanceID = rayResults[i].hitInfo.colliderInstanceID; + collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID = rayResults[i].hitInfo.rigidBodyOrColliderInstanceID; + } + + // store intersections in cache + if (approximate) + { + for (size_t i = 0; i < numIx; ++i) + { + const size_t r = rayResults[i].index; + const size_t q = rayBatch[rayResults[i].index].index; + params.planeColliderCache->Replace (rayBatch[r].from, rayBatch[r].to-rayBatch[r].from, Plane(collisionCounter.m_Colliders[q].m_CollisionPlane), collisionCounter.m_Colliders[q].m_ColliderInstanceID, collisionCounter.m_Colliders[q].m_RigidBodyOrColliderInstanceID, params.voxelSize); + } + } + numIntersections += numIx; + } + + // process pre cache range + if ( toIndex0-fromIndex0 > 0 ) + { + numIntersections += ReadCache(roState, state, ps, params, collisionCounter, fromIndex0, toIndex0, dt); + } + // process post cache range + if ( toIndex2-fromIndex2 > 0 ) + { + numIntersections += ReadCache(roState, state, ps, params, collisionCounter, fromIndex2, toIndex2, dt); + } + + return collisionCounter; +} + + +// Plane collide all particles in simulation space (remember the cached planes are defined in sim space). +CollisionInfo PlaneCollision(const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const int fromIndex, const float dt) +{ + CollisionInfo collisionInfo; + collisionInfo.m_Colliders = ALLOC_TEMP_MANUAL(ColliderInfo, ps.array_size()); + + const bool newBehaviour = IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_0_a1); + + size_t toIndex = ps.array_size(); + for (size_t q = fromIndex; q < toIndex; ++q) + { + // initialise to value that guarantees no intersection + collisionInfo.m_Colliders[q].m_CollisionPlane.distance = FLT_MAX; + + const Vector3f position = ps.position[q]; + const Vector3f velocity = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f displacement = velocity * dt; + const Vector3f origin = position - displacement; + + // walk the planes + for (unsigned i=0; i<state.numCachedCollisionPlanes; ++i) + { + const Plane plane = state.cachedCollisionPlanes[i]; + + if ( newBehaviour ) + { + // new behaviour: + // plane collisions are single sided only, all particles 'behind' the plane will be forced onto the plane + const float dist = plane.GetDistanceToPoint(position); + if (dist > params.particleRadius) + continue; + } + else + { + // old (but fixed) behaviour, particles can collide with both front and back plane + const float d0 = plane.GetDistanceToPoint(origin); + const float d1 = plane.GetDistanceToPoint(position); + const bool sameSide = ( (d0 > 0.0f && d1 > 0.0f) || (d0 <= 0.0f && d1 <= 0.0f) ); + const float aD0 = Abs(d0); + const float aD1 = Abs(d1); + if ( sameSide && aD0 > params.particleRadius && aD1 > params.particleRadius ) + continue; + } + + collisionInfo.m_Colliders[q].m_CollisionPlane = plane; + collisionInfo.m_Colliders[q].m_ColliderInstanceID = 0; + collisionInfo.m_Colliders[q].m_RigidBodyOrColliderInstanceID = 0; + collisionInfo.m_NumPlaneCollisions++; + break; + } + } + + return collisionInfo; +} + +// Compute the collision plane to use for each particle +CollisionInfo UpdateCollisionPlanes(bool worldCollision, UInt32 collisionFilter, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemCollisionParameters& params, ParticleSystemParticles& ps, size_t fromIndex, float dt) +{ + if( worldCollision ) + { + // Check if we support raycasting + if (params.raycaster == NULL) + return CollisionInfo(); + + ////@TODO: dtPerParticle + return WorldCollision(roState, state, ps, params, fromIndex, collisionFilter, dt); + } + else + { + ////@TODO: dtPerParticle + return PlaneCollision(roState, state, ps, params, fromIndex, dt); + } +} + +// Collide the particles against their selected planes and update collision response - this is done purely in simulation space +static const float kEpsilon = 0.000001f; +static const float kRayEpsilon = 0.00001f; +void PerformPlaneCollisions(bool worldCollision, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemCollisionParameters& params, const CollisionInfo& collisionInfo, const int fromIndex, const float dt) +{ + // for world collisions we use and epsilon but for plane intersections we use the particle radius + const float radius = worldCollision ? kRayEpsilon : params.particleRadius; + const bool newBehaviour = IS_CONTENT_NEWER_OR_SAME(kUnityVersion4_0_a1); + //const bool approximate = ( params.planeColliderCache ? true : false ); + + // collide with plane + size_t toIndex = ps.array_size(); + + for (size_t q = fromIndex; q < toIndex; q++) + { + // check if the actual ray was traced in which case we want to disable the ray testing. Note: does not apply to approximate mode as we trace longer rays to improve cache quality + const bool tracedParticle = collisionInfo.m_Colliders[q].m_Traced; + const Plane plane = collisionInfo.m_Colliders[q].m_CollisionPlane; + if ( plane.distance != FLT_MAX ) + { + const Vector3f position = ps.position[q]; + const Vector3f velocity = ps.velocity[q] + ps.animatedVelocity[q]; + const Vector3f displacement = velocity * dt; + const Vector3f origin = position - displacement; + + HitInfo hit; + hit.normal = plane.GetNormal(); + hit.colliderInstanceID = collisionInfo.m_Colliders[q].m_ColliderInstanceID; + hit.rigidBodyOrColliderInstanceID = collisionInfo.m_Colliders[q].m_RigidBodyOrColliderInstanceID; + + if ( worldCollision ) + { + // plane ray dot product + const float VdN = Dot( plane.GetNormal(), displacement ); + if ( !tracedParticle && VdN >= 0 ) + continue; // only pick up front face intersections + + // Recreate hit point. + const float t = -( Dot( origin, plane.GetNormal() ) + plane.distance ) / VdN; + if ( !tracedParticle && (t < 0 || t > 1) ) + continue; + + // build intersection description from t value and ray + hit.intersection = origin + displacement * t; + + // Adjust intersection along normal to make sure the particle doesn't fall through geometry when it comes to rest. + // This is also an issue when dampen and bounce is zero in which case CalculateCollisionResponse will set the particle + // position to *exactly* the intersection point, which will then have issues next time the intersection is executed + // where it will try and compare two floating point numbers which are equal and the intersection test will come out + // either way depending on fp accuracy + // + // for world collisions we use and epsilon but for plane intersections we use the particle radius + hit.intersection += radius * hit.normal; + } + else + { + if (newBehaviour) + { + const float dist = plane.GetDistanceToPoint(position); + + if (dist > radius) + continue; + + const float VdN = Dot( plane.GetNormal(), velocity ); + if (VdN == 0.0F || VdN == -0.0F) + continue; + + const float t = -( Dot( position, plane.GetNormal() ) + plane.distance - radius ) / VdN; + + hit.intersection = position + velocity * t; + } + else + { + const float OriginDotPlane = Dot (plane.GetNormal(), origin); + const float signedDistanceToOrigin = OriginDotPlane + plane.distance; + const float PositionDotPlane = Dot (plane.GetNormal(), position); + const float signedDistanceToPosition = PositionDotPlane + plane.distance; + const bool originInside = Abs(signedDistanceToOrigin)<radius; + const bool positionInside = Abs(signedDistanceToPosition)<=radius; + const bool oppositeSide = !( (signedDistanceToOrigin > 0.0f && signedDistanceToPosition > 0.0f) || (signedDistanceToOrigin <= 0.0f && signedDistanceToPosition <= 0.0f) ); + + // if the points are both inside or outside the radius we can bail if they're not on opposite sides outside the radius + if ( originInside==positionInside && !(oppositeSide && !originInside && !positionInside ) ) + continue; + + // compute the side of the face we are on - this determines if we are trying to intersect with the front or back face of the plane + const float signOrigin = signedDistanceToOrigin < 0.0 ? -1.f : 1.f; + const float signedRadius = radius*signOrigin; + + // check the direction of the ray is opposite to the plane normal (the sign flips the normal as appropriate) + const float VdN = Dot( plane.GetNormal(), velocity ); + if (VdN*signOrigin >= 0) + continue; + + // calculate intersection point + const float t = -( signedDistanceToPosition - signedRadius ) / VdN; + hit.intersection = position + velocity * t; + } + } + + // compute the bounce + CalculateCollisionResponse(roState, state, ps, q, params, ps.position[q], ps.velocity[q] + ps.animatedVelocity[q], hit); + + // Kill particle? + const float speedSqr = SqrMagnitude(ps.velocity[q] + ps.animatedVelocity[q]); + if (ps.lifetime[q] < 0.0f || speedSqr < params.minKillSpeedSqr) + { + // when killing a particle the last element from the array is pulled into the position of the killed particle and toIndex is updated accordingly + // we do a q-- in order to make sure the new particle at q is also collided + collisionInfo.m_Colliders[q] = collisionInfo.m_Colliders[toIndex-1]; + KillParticle(roState, state, ps, q, toIndex); + q--; + } + } + } + + // resize array to account for killed particles + ps.array_resize(toIndex); +} + +// Update collision for the particle system. +void CollisionModule::Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt ) +{ + // any work todo? + if (fromIndex == ps.array_size()) + return; + + ps.SetUsesCollisionEvents (m_CollisionMessages); + + // setup params + ParticleSystemCollisionParameters params; + params.bounceFactor = 1.0f - m_Bounce; + params.energyLossOnCollision = m_EnergyLossOnCollision; + params.minKillSpeedSqr = m_MinKillSpeed * m_MinKillSpeed; + params.particleRadius = m_ParticleRadius; + params.dampen = 1.0f - m_Dampen; + params.planeColliderCache = ( IsApproximate() ? &m_ColliderCache : NULL ); + params.raycaster = GetRaycastInterface(); + params.rayBudget = state.rayBudget; + params.voxelSize = m_VoxelSize; + params.nextParticleToTrace = ( state.nextParticleToTrace >= ps.array_size() ? fromIndex : std::max(state.nextParticleToTrace,fromIndex) ); + + // update particle collision planes + const CollisionInfo collisionCounter = UpdateCollisionPlanes(m_Type != kPlaneCollision, m_CollidesWith.m_Bits, roState, state, params, ps, fromIndex, dt); + + // update collider index + state.nextParticleToTrace = params.nextParticleToTrace + params.rayBudget; + + // decrement ray budget + state.rayBudget = (state.rayBudget>collisionCounter.m_NumWorldCollisions ? state.rayBudget-collisionCounter.m_NumWorldCollisions : 0); + + // early out if there were no collisions at all + if ( collisionCounter.AllCollisions() <= 0 ) + { + FREE_TEMP_MANUAL(collisionCounter.m_Colliders); + return; + } + + // perform plane collisions + PerformPlaneCollisions(m_Type != kPlaneCollision, roState, state, ps, params, collisionCounter, fromIndex, dt); + + FREE_TEMP_MANUAL(collisionCounter.m_Colliders); + + if (ps.GetUsesCollisionEvents ()) + { + ps.collisionEvents.SortCollisionEventThreadArray (); + } +} + +void CollisionModule::CheckConsistency () +{ + m_Dampen = clamp<float> (m_Dampen, 0.0f, 1.0f); + m_Bounce = clamp<float> (m_Bounce, 0.0f, 2.0f); + m_EnergyLossOnCollision = clamp<float> (m_EnergyLossOnCollision, 0.0f, 1.0f); + m_ParticleRadius = max<float>(m_ParticleRadius, 0.01f); +} + +template<class TransferFunction> +void CollisionModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Type, "type"); + + const char* kPrimitiveNames [kMaxNumPrimitives] = { "plane0", "plane1", "plane2", "plane3", "plane4", "plane5"}; + for(int i = 0; i < kMaxNumPrimitives; i++) + transfer.Transfer (m_Primitives[i], kPrimitiveNames[i]); + + transfer.Transfer (m_Dampen, "dampen"); + transfer.Transfer (m_Bounce, "bounce"); + transfer.Transfer (m_EnergyLossOnCollision, "energyLossOnCollision"); + transfer.Transfer (m_MinKillSpeed, "minKillSpeed"); + transfer.Transfer (m_ParticleRadius, "particleRadius"); + transfer.Align(); + transfer.Transfer (m_CollidesWith, "collidesWith"); + transfer.Transfer (m_Quality, "quality"); + transfer.Align(); + transfer.Transfer (m_VoxelSize, "voxelSize"); + transfer.Transfer (m_CollisionMessages, "collisionMessages"); +} + +INSTANTIATE_TEMPLATE_TRANSFER(CollisionModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h new file mode 100644 index 0000000..77b5541 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/CollisionModule.h @@ -0,0 +1,62 @@ +#pragma once +#include "ParticleSystemModule.h" +#include "Runtime/BaseClasses/BitField.h" +#include "Runtime/BaseClasses/GameObject.h" +#include "Runtime/Geometry/Plane.h" +#include "Runtime/Utilities/SpatialHash.h" + +struct ParticleSystemParticles; +class Transform; + +class CollisionModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (CollisionModule) + CollisionModule (); + + void AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state); + static void FreeCache(ParticleSystemState& state); + +#if UNITY_EDITOR + float GetEnergyLoss() const { return m_EnergyLossOnCollision; } + void SetEnergyLoss(float value) { m_EnergyLossOnCollision = value; }; + float GetMinKillSpeed() const { return m_MinKillSpeed; } + void SetMinKillSpeed(float value) { m_MinKillSpeed = value; }; +#endif + + bool GetUsesCollisionMessages () const {return m_CollisionMessages;} + bool IsWorldCollision() const {return m_Type==kWorldCollision;} + bool IsApproximate() const {return m_Quality>0;} + int GetQuality() const {return m_Quality;} + + void Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt); + + void CheckConsistency (); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + void ClearPrimitives(); + + enum { kPlaneCollision, kWorldCollision }; + enum { kMaxNumPrimitives = 6 }; + + PlaneColliderCache m_ColliderCache; + + // Serialized + int m_Type; + float m_Dampen; + float m_Bounce; + float m_EnergyLossOnCollision; + float m_MinKillSpeed; + float m_ParticleRadius; + /// Collides the particles with every collider whose layerMask & m_CollidesWith != 0 + BitField m_CollidesWith; + /// Perform approximate world particle collisions + int m_Quality; // selected quality, 0 is high (no approximations), 1 is medium (approximate), 2 is low (approximate) + float m_VoxelSize; + bool m_CollisionMessages; + PPtr<Transform> m_Primitives [kMaxNumPrimitives]; +}; + diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp new file mode 100644 index 0000000..0437322 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.cpp @@ -0,0 +1,60 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ColorByVelocityModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<MinMaxGradientEvalMode mode> +void UpdateTpl(const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, const MinMaxGradient& gradient, const OptimizedMinMaxGradient& optGradient, const Vector2f offsetScale, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float time = InverseLerpFast01 (offsetScale, Magnitude(vel)); + const int random = GenerateRandomByte(ps.randomSeed[q] + kParticleSystemColorByVelocityGradientId); + + ColorRGBA32 value; + if(mode == kGEMGradient) + value = EvaluateGradient (optGradient, time); + else if(mode == kGEMGradientMinMax) + value = EvaluateRandomGradient (optGradient, time, random); + else + value = Evaluate (gradient, time, random); + + colorTemp[q] *= value; + } +} + +ColorBySpeedModule::ColorBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +void ColorBySpeedModule::Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size()); + + Vector2f offsetScale = CalculateInverseLerpOffsetScale (m_Range); + OptimizedMinMaxGradient gradient; + m_Gradient.InitializeOptimized(gradient); + if(m_Gradient.minMaxState == kMMGGradient) + UpdateTpl<kGEMGradient>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); + else if(m_Gradient.minMaxState == kMMGRandomBetweenTwoGradients) + UpdateTpl<kGEMGradientMinMax>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); + else + UpdateTpl<kGEMSlow>(ps, colorTemp, m_Gradient, gradient, offsetScale, fromIndex, toIndex); +} + +void ColorBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.x = std::min (m_Range.x, m_Range.y - MyEpsilon); +} + +template<class TransferFunction> +void ColorBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Gradient, "gradient"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ColorBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h new file mode 100644 index 0000000..f140e68 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULECOLORBYVELOCITY_H +#define SHURIKENMODULECOLORBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" +#include "Runtime/Math/Vector2.h" + +class ColorBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ColorBySpeedModule) + ColorBySpeedModule (); + + void Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxGradient& GetGradient() { return m_Gradient; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxGradient m_Gradient; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULECOLORBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp new file mode 100644 index 0000000..c9e78c0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.cpp @@ -0,0 +1,51 @@ +#include "UnityPrefix.h" +#include "ColorModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<MinMaxGradientEvalMode mode> +void UpdateTpl(const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, const MinMaxGradient& gradient, const OptimizedMinMaxGradient& optGradient, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const int random = GenerateRandomByte(ps.randomSeed[q] + kParticleSystemColorGradientId); + + ColorRGBA32 value; + if(mode == kGEMGradient) + value = EvaluateGradient (optGradient, time); + else if(mode == kGEMGradientMinMax) + value = EvaluateRandomGradient (optGradient, time, random); + else + value = Evaluate (gradient, time, random); + + colorTemp[q] *= value; + } +} + +ColorModule::ColorModule () : ParticleSystemModule(false) +{} + +void ColorModule::Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size()); + + OptimizedMinMaxGradient gradient; + m_Gradient.InitializeOptimized(gradient); + if(m_Gradient.minMaxState == kMMGGradient) + UpdateTpl<kGEMGradient>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); + else if(m_Gradient.minMaxState == kMMGRandomBetweenTwoGradients) + UpdateTpl<kGEMGradientMinMax>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); + else + UpdateTpl<kGEMSlow>(ps, colorTemp, m_Gradient, gradient, fromIndex, toIndex); +} + +template<class TransferFunction> +void ColorModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Gradient, "gradient"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ColorModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h new file mode 100644 index 0000000..1fe7250 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ColorModule.h @@ -0,0 +1,25 @@ +#ifndef SHURIKENMODULECOLOR_H +#define SHURIKENMODULECOLOR_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" + +class ColorModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ColorModule) + ColorModule (); + + void Update (const ParticleSystemParticles& ps, ColorRGBA32* colorTemp, size_t fromIndex, size_t toIndex); + void CheckConsistency() {}; + + inline MinMaxGradient& GetGradient() { return m_Gradient; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxGradient m_Gradient; +}; + +#endif // SHURIKENMODULECOLOR_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp new file mode 100644 index 0000000..111b06f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.cpp @@ -0,0 +1,124 @@ +#include "UnityPrefix.h" +#include "EmissionModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Vector2.h" + +static int AccumulateBursts (const ParticleSystemEmissionData& emissionData, float t0, float t1) +{ + int burstParticles = 0; + const size_t count = emissionData.burstCount; + for (size_t q = 0; q < count; ++q) + { + if (emissionData.burstTime[q] >= t0 && emissionData.burstTime[q] < t1) + burstParticles += emissionData.burstParticleCount[q]; + } + return burstParticles; +} + +static float AccumulateContinuous(const ParticleSystemEmissionData& emissionData, const float length, const float toT, const float dt) +{ + DebugAssert (length > 0.0001f); + DebugAssert (toT >= 0.0f); + DebugAssert (toT <= length); + float normalizedT = toT / length; + return std::max<float> (0.0f, Evaluate (emissionData.rate, normalizedT)) * dt; +}; + +EmissionModule::EmissionModule () : ParticleSystemModule(true) +{ + m_EmissionData.burstCount = 0; + m_EmissionData.type = kEmissionTypeTime; + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + { + m_EmissionData.burstParticleCount[i] = 30; + m_EmissionData.burstTime[i] = 0.0f; + } +} + +void EmissionModule::Emit (ParticleSystemEmissionState& emissionState, size_t& amountOfParticlesToEmit, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length) +{ + const float epsilon = 0.0001f; + if(kEmissionTypeTime == emissionData.type) + { + float rate = 0.0f; + float t0 = std::max<float> (0.0f, fromT); + float t1 = std::max<float> (0.0f, toT); + if (t1 < t0) // handle loop + { + rate += AccumulateContinuous (emissionData, length, t1, t1); // from start to current time + t1 = length; // from last time to end + } + rate += AccumulateContinuous (emissionData, length, t1, t1 - t0); // from start to current time + + const float newParticles = rate; + if(newParticles >= epsilon) + emissionState.m_ParticleSpacing = 1.0f / newParticles; + else + emissionState.m_ParticleSpacing = 1.0f; + + emissionState.m_ToEmitAccumulator += newParticles; + amountOfParticlesToEmit = (int)emissionState.m_ToEmitAccumulator; + emissionState.m_ToEmitAccumulator -= (float)amountOfParticlesToEmit; + + // Continuous emits + numContinuous = amountOfParticlesToEmit; + + // Bursts + t0 = std::max<float> (0.0f, fromT); + t1 = std::max<float> (0.0f, toT); + if (t1 < t0) // handle loop + { + amountOfParticlesToEmit += AccumulateBursts (emissionData, 0.0f, t1); // from start to current time + t1 = length + epsilon; // from last time to end + } + amountOfParticlesToEmit += AccumulateBursts (emissionData, t0, t1); // from start to current time + } + else + { + float newParticles = AccumulateContinuous (emissionData, length, toT, dt) * Magnitude (velocity); // from start to current time + if(newParticles >= epsilon) + emissionState.m_ParticleSpacing = 1.0f / newParticles; + else + emissionState.m_ParticleSpacing = 1.0f; + + emissionState.m_ToEmitAccumulator += newParticles; + amountOfParticlesToEmit = (int)emissionState.m_ToEmitAccumulator; + emissionState.m_ToEmitAccumulator -= (float)amountOfParticlesToEmit; + + // Continuous emits + numContinuous = amountOfParticlesToEmit; + } +} + +void EmissionModule::CheckConsistency () +{ + m_EmissionData.rate.SetScalar(std::max<float> (0.0f, m_EmissionData.rate.GetScalar())); + + const size_t count = m_EmissionData.burstCount; + for (size_t q = 0; q < count; ++q) + { + m_EmissionData.burstTime[q] = std::max<float> (0.0f, m_EmissionData.burstTime[q]); + } +} + +template<class TransferFunction> +void EmissionModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + + transfer.Transfer (m_EmissionData.type, "m_Type"); + transfer.Transfer (m_EmissionData.rate, "rate"); + + const char* kCountNames [ParticleSystemEmissionData::kMaxNumBursts] = { "cnt0", "cnt1", "cnt2", "cnt3", }; + const char* kTimeNames [ParticleSystemEmissionData::kMaxNumBursts] = { "time0", "time1", "time2", "time3", }; + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + transfer.Transfer (m_EmissionData.burstParticleCount[i], kCountNames[i]); + + for(int i = 0; i < ParticleSystemEmissionData::kMaxNumBursts; i++) + transfer.Transfer (m_EmissionData.burstTime[i], kTimeNames[i]); + + transfer.Transfer (m_EmissionData.burstCount, "m_BurstCount"); transfer.Align(); +} + +INSTANTIATE_TEMPLATE_TRANSFER(EmissionModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h new file mode 100644 index 0000000..75ad977 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/EmissionModule.h @@ -0,0 +1,34 @@ +#ifndef SHURIKENMODULEEMISSION_H +#define SHURIKENMODULEEMISSION_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class Vector2f; + +class EmissionModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (EmissionModule) + EmissionModule (); + + enum { kEmissionTypeTime, kEmissionTypeDistance }; + + static void Emit (ParticleSystemEmissionState& emissionState, size_t& amountOfParticlesToEmit, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length); + void CheckConsistency (); + + int CalculateMaximumEmitCountEstimate(float deltaTime) const; + + const ParticleSystemEmissionData& GetEmissionDataRef() { return m_EmissionData; } + const ParticleSystemEmissionData& GetEmissionDataRef() const { return m_EmissionData; } + void GetEmissionDataCopy(ParticleSystemEmissionData* emissionData) { *emissionData = m_EmissionData; }; + ParticleSystemEmissionData& GetEmissionData() { return m_EmissionData; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + ParticleSystemEmissionData m_EmissionData; +}; + +#endif // SHURIKENMODULEEMISSION_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp new file mode 100644 index 0000000..0b86e79 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.cpp @@ -0,0 +1,118 @@ +#include "UnityPrefix.h" +#include "ExternalForcesModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Graphics/Transform.h" +#include "Runtime/Geometry/Intersection.h" +#include "Runtime/Geometry/Sphere.h" +#include "Runtime/Terrain/Wind.h" +#include "Runtime/Input/TimeManager.h" + + +void ApplyRadialForce(ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, const Vector3f position, const float radius, const float force, const float dt) +{ + for(int q = fromIndex; q < toIndex; q++) + { + Vector3f toForce = position - ps.position[q]; + float distanceToForce = Magnitude(toForce); + toForce = NormalizeSafe(toForce); + float forceFactor = clamp(distanceToForce/radius, 0.0f, 1.0f); + forceFactor = 1.0f - forceFactor * forceFactor; + Vector3f delta = toForce * force * forceFactor * dt; + ps.velocity[q] += delta; + } +} + +void ApplyDirectionalForce(ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, const Vector3f position, const Vector3f direction, const float radius, const float force, const float dt) +{ + for(int q = fromIndex; q < toIndex; q++) + { + Vector3f delta = direction * force * dt; + ps.velocity[q] += delta; + } +} + +ExternalForcesModule::ExternalForcesModule () : ParticleSystemModule(false) +, m_Multiplier (1.0f) +{ +} + +void ExternalForcesModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) +{ + Matrix4x4f matrix = Matrix4x4f::identity; + if(roState.useLocalSpace) + Matrix4x4f::Invert_General3D(state.localToWorld, matrix); + + AABB aabb = state.minMaxAABB; + for(int i = 0; i < state.numCachedForces; i++) + { + const ParticleSystemExternalCachedForce& cachedForce = state.cachedForces[i]; + const Vector3f position = matrix.MultiplyPoint3(cachedForce.position); + const Vector3f direction = matrix.MultiplyVector3(cachedForce.direction); + const float radius = cachedForce.radius; + const float force = cachedForce.forceMain * m_Multiplier; + + const WindZone::WindZoneMode forceType = (WindZone::WindZoneMode)cachedForce.forceType; + if(WindZone::Spherical == forceType) + { + Sphere sphere (position, radius); + if(!IntersectAABBSphere (aabb, sphere)) + continue; + ApplyRadialForce(ps, fromIndex, toIndex, position, radius, force, dt); + } + else if(WindZone::Directional == forceType) + ApplyDirectionalForce(ps, fromIndex, toIndex, position, direction, radius, force, dt); + } +} + +// TODO: Perform culling here, instead of caching all of them +void ExternalForcesModule::AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state) +{ + float time = GetTimeManager ().GetTimeSinceLevelLoad (); + WindManager::WindZoneList& windZones = WindManager::GetInstance().GetList(); + + state.numCachedForces = 0; + for (WindManager::WindZoneList::iterator it = windZones.begin (); it != windZones.end (); ++it) + state.numCachedForces++; + + // Allocate + state.cachedForces = ALLOC_TEMP_MANUAL(ParticleSystemExternalCachedForce, state.numCachedForces); + + // Cache + int i = 0; + for (WindManager::WindZoneList::iterator it = windZones.begin (); it != windZones.end (); ++it) + { + const WindZone& zone = **it; + ParticleSystemExternalCachedForce& cachedForce = state.cachedForces[i++]; + cachedForce.position = zone.GetComponent (Transform).GetPosition(); + cachedForce.direction = zone.GetComponent (Transform).GetLocalToWorldMatrix().GetAxisZ(); + cachedForce.forceType = zone.GetMode(); + cachedForce.radius = zone.GetRadius(); + + float phase = time * kPI * zone.GetWindPulseFrequency(); + float pulse = (cos (phase) + cos (phase * 0.375f) + cos (phase * 0.05f)) * 0.333f; + pulse = 1.0f + (pulse * zone.GetWindPulseMagnitude()); + cachedForce.forceMain = zone.GetWindMain() * pulse; + // Maybe implement using turbulence and time based phasing? + // ForceTurbulenceMultiply (maybe) // @TODO: Figure out what to do about turbulence. Do perlin field? Expensive but maybe cool! If use it: only do it when turbulence force is set to something + + //cachedForce.force = 1.0f; + } +} + +void ExternalForcesModule::FreeCache(ParticleSystemState& state) +{ + if(state.cachedForces) + FREE_TEMP_MANUAL(state.cachedForces); + state.cachedForces = NULL; + state.numCachedForces = 0; +} + +template<class TransferFunction> +void ExternalForcesModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Multiplier, "multiplier"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ExternalForcesModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h new file mode 100644 index 0000000..2abc949 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ExternalForcesModule.h @@ -0,0 +1,25 @@ +#ifndef SHURIKENMODULEEXTERNALFORCES_H +#define SHURIKENMODULEEXTERNALFORCES_H + +#include "ParticleSystemModule.h" + +class ExternalForcesModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ExternalForcesModule) + ExternalForcesModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + void CheckConsistency() {}; + + static void AllocateAndCache(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state); + static void FreeCache(ParticleSystemState& state); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + float m_Multiplier; +}; + +#endif // SHURIKENMODULEEXTERNALFORCES_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp new file mode 100644 index 0000000..d72da93 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.cpp @@ -0,0 +1,164 @@ +#include "UnityPrefix.h" +#include "ForceModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, bool transform, const Matrix4x4f& matrix, float dt) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemForceCurveId); + const float time = NormalizedTime(ps, q); + Vector3f f = Vector3f (Evaluate<mode> (x, time, random.x), Evaluate<mode> (y, time, random.y), Evaluate<mode> (z, time, random.z)); + if(transform) + f = matrix.MultiplyVector3 (f); + ps.velocity[q] += f * dt; + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMax3DPolyCurves& pos, const DualMinMax3DPolyCurves& vel, ParticleSystemParticles& ps, const Matrix4x4f& matrix, bool transform) +{ + const size_t count = ps.array_size(); + for (int q=0; q<count; q++) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemForceCurveId); + float time = NormalizedTime(ps, q); + float range = ps.startLifetime[q]; + + Vector3f delta; + Vector3f velocity; + if(isOptimized) + { + delta = Vector3f (EvaluateDoubleIntegrated(pos.optX, time, random.x), EvaluateDoubleIntegrated(pos.optY, time, random.y), EvaluateDoubleIntegrated(pos.optZ, time, random.z)); + velocity = Vector3f (EvaluateIntegrated(vel.optX, time, random.x), EvaluateIntegrated(vel.optY, time, random.y), EvaluateIntegrated(vel.optZ, time, random.z)); + } + else + { + delta = Vector3f (EvaluateDoubleIntegrated(pos.x, time, random.x), EvaluateDoubleIntegrated(pos.y, time, random.y), EvaluateDoubleIntegrated(pos.z, time, random.z)); + velocity = Vector3f (EvaluateIntegrated(vel.x, time, random.x), EvaluateIntegrated(vel.y, time, random.y), EvaluateIntegrated(vel.z, time, random.z)); + } + + // Sqr range + delta *= range * range; + velocity *= range; + + if(transform) + { + delta = matrix.MultiplyVector3 (delta); + velocity = matrix.MultiplyVector3 (velocity); + } + + ps.position[q] += delta; + ps.velocity[q] += velocity; + } +} + +ForceModule::ForceModule () : ParticleSystemModule(false) +, m_RandomizePerFrame (false) +, m_InWorldSpace(false) +{} + +void ForceModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + if (m_RandomizePerFrame) + { + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = NormalizedTime (ps, q); + const float randomX = Random01 (m_Random); + const float randomY = Random01 (m_Random); + const float randomZ = Random01 (m_Random); + Vector3f f (Evaluate (m_X, t, randomX), Evaluate (m_Y, t, randomY), Evaluate (m_Z, t, randomZ)); + if(transform) + f = matrix.MultiplyVector3 (f); + ps.velocity[q] += f * dt; + } + } + else + { + bool usesScalar = (m_X.minMaxState == kMMCScalar) && (m_Y.minMaxState == kMMCScalar) && (m_Z.minMaxState == kMMCScalar); + bool isOptimized = m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized(); + bool usesMinMax = m_X.UsesMinMax() && m_Y.UsesMinMax() && m_Z.UsesMinMax(); + if(usesScalar) + UpdateTpl<kEMScalar>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else if(isOptimized && usesMinMax) + UpdateTpl<kEMOptimizedMinMax>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else if(isOptimized) + UpdateTpl<kEMOptimized>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + else + UpdateTpl<kEMSlow>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix, dt); + } +} + +void ForceModule::UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + Assert(!m_RandomizePerFrame); + + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + DualMinMax3DPolyCurves posCurves; + DualMinMax3DPolyCurves velCurves; + if(m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized()) + { + posCurves.optX = m_X.polyCurves; posCurves.optX.DoubleIntegrate(); + posCurves.optY = m_Y.polyCurves; posCurves.optY.DoubleIntegrate(); + posCurves.optZ = m_Z.polyCurves; posCurves.optZ.DoubleIntegrate(); + velCurves.optX = m_X.polyCurves; velCurves.optX.Integrate(); + velCurves.optY = m_Y.polyCurves; velCurves.optY.Integrate(); + velCurves.optZ = m_Z.polyCurves; velCurves.optZ.Integrate(); + UpdateProceduralTpl<true>(posCurves, velCurves, ps, matrix, transform); + } + else + { + DebugAssert(CurvesSupportProcedural (m_X.editorCurves, m_X.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Y.editorCurves, m_Y.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Z.editorCurves, m_Z.minMaxState)); + BuildCurves(posCurves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); posCurves.x.DoubleIntegrate(); + BuildCurves(posCurves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); posCurves.y.DoubleIntegrate(); + BuildCurves(posCurves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); posCurves.z.DoubleIntegrate(); + BuildCurves(velCurves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); velCurves.x.Integrate(); + BuildCurves(velCurves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); velCurves.y.Integrate(); + BuildCurves(velCurves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); velCurves.z.Integrate(); + UpdateProceduralTpl<false>(posCurves, velCurves, ps, matrix, transform); + } +} +void ForceModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime) +{ + Vector2f xRange = m_X.FindMinMaxDoubleIntegrated(); + Vector2f yRange = m_Y.FindMinMaxDoubleIntegrated(); + Vector2f zRange = m_Z.FindMinMaxDoubleIntegrated(); + bounds.m_Min = Vector3f(xRange.x, yRange.x, zRange.x) * maxLifeTime * maxLifeTime; + bounds.m_Max = Vector3f(xRange.y, yRange.y, zRange.y) * maxLifeTime * maxLifeTime; + + if(m_InWorldSpace) + { + Matrix4x4f matrix; + Matrix4x4f::Invert_General3D(localToWorld, matrix); + matrix.SetPosition(Vector3f::zero); + AABB aabb = bounds; + TransformAABBSlow(aabb, matrix, aabb); + bounds = aabb; + } +} + +template<class TransferFunction> +void ForceModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); + transfer.Transfer (m_RandomizePerFrame, "randomizePerFrame"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(ForceModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h new file mode 100644 index 0000000..9ad54ec --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ForceModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEFORCE_H +#define SHURIKENMODULEFORCE_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Random/rand.h" + +class ForceModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ForceModule) + ForceModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + void UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime); + void CheckConsistency() {}; + + inline MinMaxCurve& GetXCurve() { return m_X; } + inline MinMaxCurve& GetYCurve() { return m_Y; } + inline MinMaxCurve& GetZCurve() { return m_Z; } + inline bool GetRandomizePerFrame() { return m_RandomizePerFrame; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + bool m_InWorldSpace; + bool m_RandomizePerFrame; + Rand m_Random; +}; + +#endif // SHURIKENMODULEFORCE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp new file mode 100644 index 0000000..680d175 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.cpp @@ -0,0 +1,193 @@ +#include "UnityPrefix.h" +#include "InitialModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Interfaces/IPhysics.h" +#include "Runtime/BaseClasses/IsPlaying.h" + +InitialModule::InitialModule () : ParticleSystemModule(true) +, m_GravityModifier(0.0f) +, m_InheritVelocity(0.0f) +, m_MaxNumParticles(1000) +{ +} + +Vector3f InitialModule::GetGravity (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state) const +{ +#if ENABLE_PHYSICS + IPhysics* physicsModule = GetIPhysics(); + if (!physicsModule) + return Vector3f::zero; + + Vector3f gravity = m_GravityModifier * physicsModule->GetGravity (); + if(roState.useLocalSpace) + { + Matrix4x4f worldToLocal; + Matrix4x4f::Invert_General3D(state.localToWorld, worldToLocal); + gravity = worldToLocal.MultiplyVector3(gravity); + } + return gravity; +#else + return Vector3f::zero; +#endif +} + +void InitialModule::Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t) +{ + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + Vector3f origin = matrix.GetPosition (); + + const size_t count = ps.array_size (); + for (size_t q = fromIndex; q < count; ++q) + { + UInt32 randUInt32 = random.Get (); + float rand = Rand::GetFloatFromInt (randUInt32); + UInt32 randByte = Rand::GetByteFromInt (randUInt32); + + const ColorRGBA32 col = Evaluate (m_Color, normalizedT, randByte); + float sz = std::max<float> (0.0f, Evaluate (m_Size, normalizedT, rand)); + Vector3f vel = matrix.MultiplyVector3 (Vector3f::zAxis); + float ttl = std::max<float> (0.0f, Evaluate (m_Lifetime, normalizedT, rand)); + float rot = Evaluate (m_Rotation, normalizedT, rand); + + ps.position[q] = origin; + ps.velocity[q] = vel; + ps.animatedVelocity[q] = Vector3f::zero; + ps.lifetime[q] = ttl; + ps.startLifetime[q] = ttl; + ps.size[q] = sz; + ps.rotation[q] = rot; + if(ps.usesRotationalSpeed) + ps.rotationalSpeed[q] = 0.0f; + ps.color[q] = col; + ps.randomSeed[q] = random.Get(); // One more iteration to avoid visible patterns between random spawned parameters and those used in update + if(ps.usesAxisOfRotation) + ps.axisOfRotation[q] = Vector3f::zAxis; + for(int acc = 0; acc < ps.numEmitAccumulators; acc++) + ps.emitAccumulator[acc][q] = 0.0f; + + } +} + +void InitialModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) const +{ + Vector3f gravityDelta = GetGravity(roState, state) * dt; + if(!CompareApproximately(gravityDelta, Vector3f::zero, 0.0001f)) + for (size_t q = fromIndex; q < toIndex; ++q) + ps.velocity[q] += gravityDelta; + + for (size_t q = fromIndex; q < toIndex; ++q) + ps.animatedVelocity[q] = Vector3f::zero; + + if(ps.usesRotationalSpeed) + for (size_t q = fromIndex; q < toIndex; ++q) + ps.rotationalSpeed[q] = 0.0f; +} + +void InitialModule::GenerateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemEmitReplay& emit) +{ + size_t count = emit.particlesToEmit; + float t = emit.t; + float alreadyPassedTime = emit.aliveTime; + + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + Vector3f origin = localToWorld.GetPosition (); + for (size_t i = 0; i < count; ++i) + { + UInt32 randUInt32 = random.Get (); + float rand = Rand::GetFloatFromInt (randUInt32); + UInt32 randByte = Rand::GetByteFromInt (randUInt32); + + float frameOffset = (float(i) + emit.emissionOffset) * emit.emissionGap * float(i < emit.numContinuous); + + const ColorRGBA32 col = Evaluate (m_Color, normalizedT, randByte); + float sz = std::max<float> (0.0f, Evaluate (m_Size, normalizedT, rand)); + Vector3f vel = localToWorld.MultiplyVector3 (Vector3f::zAxis); + float ttlStart = std::max<float> (0.0f, Evaluate (m_Lifetime, normalizedT, rand)); + float ttl = ttlStart - alreadyPassedTime - frameOffset; + float rot = Evaluate (m_Rotation, normalizedT, rand); + + if (ttl < 0.0F) + continue; + + size_t q = ps.array_size(); + ps.array_resize(ps.array_size() + 1); + + ps.position[q] = origin; + ps.velocity[q] = vel; + ps.animatedVelocity[q] = Vector3f::zero; + ps.lifetime[q] = ttl; + ps.startLifetime[q] = ttlStart; + ps.size[q] = sz; + ps.rotation[q] = rot; + if(ps.usesRotationalSpeed) + ps.rotationalSpeed[q] = 0.0f; + ps.color[q] = col; + ps.randomSeed[q] = random.Get(); // One more iteration to avoid visible patterns between random spawned parameters and those used in update + if(ps.usesAxisOfRotation) + ps.axisOfRotation[q] = Vector3f::zAxis; + for(int acc = 0; acc < ps.numEmitAccumulators; acc++) + ps.emitAccumulator[acc][q] = 0.0f; + } +} + +void InitialModule::CheckConsistency () +{ + m_Lifetime.SetScalar(clamp<float> (m_Lifetime.GetScalar(), 0.05f, 100000.0f)); + m_Size.SetScalar(std::max<float> (0.0f, m_Size.GetScalar())); + m_MaxNumParticles = std::max<int> (0, m_MaxNumParticles); +} + +void InitialModule::AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState) +{ + ResetSeed(roState); +} + +void InitialModule::ResetSeed(const ParticleSystemReadOnlyState& roState) +{ + if(roState.randomSeed == 0) + m_Random.SetSeed(GetGlobalRandomSeed ()); + else + m_Random.SetSeed(roState.randomSeed); +} + +Rand& InitialModule::GetRandom() +{ +#if UNITY_EDITOR + if(!IsWorldPlaying()) + return m_EditorRandom; + else +#endif + return m_Random; +} + +template<class TransferFunction> +void InitialModule::Transfer (TransferFunction& transfer) +{ + SetEnabled(true); // always enabled + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Lifetime, "startLifetime"); + transfer.Transfer (m_Speed, "startSpeed"); + transfer.Transfer (m_Color, "startColor"); + transfer.Transfer (m_Size, "startSize"); + transfer.Transfer (m_Rotation, "startRotation"); + transfer.Transfer (m_GravityModifier, "gravityModifier"); + transfer.Transfer (m_InheritVelocity, "inheritVelocity"); + transfer.Transfer (m_MaxNumParticles, "maxNumParticles"); +} +INSTANTIATE_TEMPLATE_TRANSFER(InitialModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h new file mode 100644 index 0000000..cd602a9 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/InitialModule.h @@ -0,0 +1,66 @@ +#ifndef SHURIKENMODULEINITIAL_H +#define SHURIKENMODULEINITIAL_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h" +#include "Runtime/Math/Random/rand.h" + +struct ParticleSystemState; + +class InitialModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (InitialModule) + InitialModule (); + + void Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t); + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt) const; + void GenerateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const ParticleSystemEmitReplay& emit); + void CheckConsistency (); + void AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState); + void ResetSeed(const ParticleSystemReadOnlyState& roState); + + inline MinMaxCurve& GetLifeTimeCurve() { return m_Lifetime; } + inline const MinMaxCurve& GetLifeTimeCurve() const { return m_Lifetime; } + inline MinMaxCurve& GetSpeedCurve() { return m_Speed; } + inline const MinMaxCurve& GetSpeedCurve() const { return m_Speed; } + inline MinMaxCurve& GetSizeCurve() { return m_Size; } + inline const MinMaxCurve& GetSizeCurve() const { return m_Size; } + inline MinMaxCurve& GetRotationCurve() { return m_Rotation; } + inline const MinMaxCurve& GetRotationCurve() const { return m_Rotation; } + inline MinMaxGradient& GetColor() { return m_Color; } + inline const MinMaxGradient& GetColor() const { return m_Color; } + inline void SetGravityModifier(float value) { m_GravityModifier = value; } + inline float GetGravityModifier() const { return m_GravityModifier; } + + inline void SetMaxNumParticles(int value) { m_MaxNumParticles = value; } + inline int GetMaxNumParticles() const { return m_MaxNumParticles; } + inline float GetInheritVelocity() const { return m_InheritVelocity; } + Vector3f GetGravity (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + Rand& GetRandom(); + + MinMaxCurve m_Lifetime; + MinMaxCurve m_Speed; + MinMaxGradient m_Color; + MinMaxCurve m_Size; + MinMaxCurve m_Rotation; + float m_GravityModifier; + float m_InheritVelocity; + int m_MaxNumParticles; + + Rand m_Random; + +#if UNITY_EDITOR +public: + Rand m_EditorRandom; +#endif +}; + + +#endif // SHURIKENMODULEINITIAL_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp new file mode 100644 index 0000000..dee4872 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.cpp @@ -0,0 +1,141 @@ +#include "UnityPrefix.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "ParticleSystemModule.h" + +ParticleSystemReadOnlyState::ParticleSystemReadOnlyState() +: lengthInSec (5.0f) +, startDelay (0.0f) +, speed (1.0f) +, randomSeed (0) +, looping (true) +, prewarm (false) +, playOnAwake (true) +, useLocalSpace (true) +{ +} + +void ParticleSystemReadOnlyState::CheckConsistency() +{ + lengthInSec = std::max(lengthInSec, 0.1f); + lengthInSec = std::min(lengthInSec, 100000.0f); // Very large values can lead to editor locking up due to numerical instability. + startDelay = std::max(startDelay, 0.0f); + speed = std::max(speed, 0.0f); +} + +template<class TransferFunction> +void ParticleSystemReadOnlyState::Transfer (TransferFunction& transfer) +{ + TRANSFER (lengthInSec); + TRANSFER (startDelay); + TRANSFER (speed); + TRANSFER (randomSeed); + TRANSFER (looping); + TRANSFER (prewarm); + TRANSFER (playOnAwake); + transfer.Transfer (useLocalSpace, "moveWithTransform"); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemReadOnlyState) + +ParticleSystemState::ParticleSystemState () +: playing (false) +, needRestart (true) +, stopEmitting (false) +, accumulatedDt (0.0f) +, delayT (0.0f) +, t (0.0f) +, maxSize (0.0f) +, isSubEmitter (false) +, recordSubEmits(false) +, cullTime(0.0) +, culled(false) +, numLoops(0) +, invalidateProcedural(false) +, supportsProcedural(true) +, cachedForces(0) +, numCachedForces(0) +, cachedSubDataBirth(0) +, numCachedSubDataBirth(0) +, cachedSubDataCollision(0) +, numCachedSubDataCollision(0) +, cachedSubDataDeath(0) +, numCachedSubDataDeath(0) +, cachedCollisionPlanes(0) +, numCachedCollisionPlanes(0) +, rayBudget(0) +, nextParticleToTrace(0) +{ + ClearSubEmitterCommandBuffer(); + + localToWorld.SetIdentity (); + emitterVelocity = Vector3f::zero; + emitterScale = Vector3f::one; + minMaxAABB = MinMaxAABB (Vector3f::zero, Vector3f::zero); +} + +void ParticleSystemState::Tick (const ParticleSystemReadOnlyState& constState, float dt) +{ + t += dt; + + for(int i = 0; i < subEmitterCommandBuffer.commandCount; i++) + subEmitterCommandBuffer.commands[i].timeAlive += dt; + + if (!constState.looping) + t = std::min<float> (t, constState.lengthInSec); + else + if(t > constState.lengthInSec) + { + t -= constState.lengthInSec; + numLoops++; + } +} + +void ParticleSystemState::ClearSubEmitterCommandBuffer() +{ + if(cachedSubDataBirth) + { + for (int i = 0; i < numCachedSubDataBirth; ++i) + { + (cachedSubDataBirth+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataBirth); + } + if(cachedSubDataCollision) + { + for (int i = 0; i < numCachedSubDataCollision; ++i) + { + (cachedSubDataCollision+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataCollision); + } + if(cachedSubDataDeath) + { + for (int i = 0; i < numCachedSubDataDeath; ++i) + { + (cachedSubDataDeath+i)->~ParticleSystemSubEmitterData(); + } + FREE_TEMP_MANUAL(cachedSubDataDeath); + } + if(subEmitterCommandBuffer.commands) + FREE_TEMP_MANUAL(subEmitterCommandBuffer.commands); + + cachedSubDataBirth = cachedSubDataCollision = cachedSubDataDeath = 0; + numCachedSubDataBirth = numCachedSubDataCollision = numCachedSubDataDeath = 0; + subEmitterCommandBuffer.commands = 0; + subEmitterCommandBuffer.commandCount = subEmitterCommandBuffer.maxCommandCount = 0; +} + +template<class TransferFunction> +void ParticleSystemState::Transfer (TransferFunction& transfer) +{ + TRANSFER_DEBUG (t); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemState) + + +template<class TransferFunction> +void ParticleSystemModule::Transfer (TransferFunction& transfer) +{ + transfer.Transfer (m_Enabled, "enabled"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(ParticleSystemModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h new file mode 100644 index 0000000..b4746de --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ParticleSystemModule.h @@ -0,0 +1,238 @@ +#ifndef SHURIKENMODULE_H +#define SHURIKENMODULE_H + +#include "../ParticleSystemCommon.h" +#include "../ParticleSystemCurves.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Geometry/AABB.h" +#include "Runtime/Utilities/dynamic_array.h" + +#define ENABLE_MULTITHREADED_PARTICLES ENABLE_MULTITHREADED_CODE + +#define DECLARE_MODULE(name) const char* GetName () { return #name; } DEFINE_GET_TYPESTRING(name) + +class ParticleSystem; +class Plane; + +struct ParticleSystemEmissionState +{ + ParticleSystemEmissionState() { Clear(); } + inline void Clear() + { + m_ToEmitAccumulator = 0.0f; + m_ParticleSpacing = 0.0f; + } + float m_ParticleSpacing; + float m_ToEmitAccumulator; +}; + +struct ParticleSystemEmissionData +{ + enum { kMaxNumBursts = 4 }; + + int type; + MinMaxCurve rate; + float burstTime[kMaxNumBursts]; + UInt16 burstParticleCount[kMaxNumBursts]; + UInt8 burstCount; +}; + + +struct ParticleSystemSubEmitterData +{ + ParticleSystemSubEmitterData() + :maxLifetime(0.0f) + ,startDelayInSec(0.0f) + ,lengthInSec(0.0f) + {} + + ParticleSystemEmissionData emissionData; + float maxLifetime; + float startDelayInSec; + float lengthInSec; + ParticleSystem* emitter; +}; + +// @TODO: Find "pretty" place for shared structs and enums? +struct ParticleSystemEmitReplay +{ + float t; + float aliveTime; + float emissionOffset; + float emissionGap; + int particlesToEmit; + size_t numContinuous; + UInt32 randomSeed; + + ParticleSystemEmitReplay (float inT, int inParticlesToEmit, float inEmissionOffset, float inEmissionGap, size_t inNumContinuous, UInt32 inRandomSeed) + : t (inT), particlesToEmit (inParticlesToEmit), aliveTime(0.0F), emissionOffset(inEmissionOffset), emissionGap(inEmissionGap), numContinuous(inNumContinuous), randomSeed(inRandomSeed) + {} +}; + +struct SubEmitterEmitCommand +{ + SubEmitterEmitCommand(ParticleSystemEmissionState inEmissionState, Vector3f inPosition, Vector3f inVelocity, ParticleSystemSubType inSubEmitterType, int inSubEmitterIndex, int inParticlesToEmit, int inParticlesToEmitContinuous, float inParentT, float inDeltaTime) + :emissionState(inEmissionState) + ,position(inPosition) + ,velocity(inVelocity) + ,subEmitterType(inSubEmitterType) + ,subEmitterIndex(inSubEmitterIndex) + ,particlesToEmit(inParticlesToEmit) + ,particlesToEmitContinuous(inParticlesToEmitContinuous) + ,deltaTime(inDeltaTime) + ,parentT(inParentT) + ,timeAlive(0.0f) + { + } + + ParticleSystemEmissionState emissionState; + Vector3f position; + Vector3f velocity; + ParticleSystemSubType subEmitterType; + int subEmitterIndex; + int particlesToEmit; + int particlesToEmitContinuous; + float deltaTime; + float parentT; // Used for StartModules + float timeAlive; +}; + +struct ParticleSystemSubEmitCmdBuffer +{ + ParticleSystemSubEmitCmdBuffer() + :commands(0) + ,commandCount(0) + ,maxCommandCount(0) + {} + + inline void AddCommand(const ParticleSystemEmissionState& emissionState, const Vector3f& initialPosition, const Vector3f& initialVelocity, const ParticleSystemSubType type, const int index, const int particlesToEmit, const int particlesToEmitContinuous, const float parentT, const float dt) + { + commands[commandCount++] = SubEmitterEmitCommand(emissionState, initialPosition, initialVelocity, type, index, particlesToEmit, particlesToEmitContinuous, parentT, dt); + } + inline bool IsFull() { return commandCount >= maxCommandCount; } + + SubEmitterEmitCommand* commands; + int commandCount; + int maxCommandCount; // Mostly to assert/test against trashing memory +}; + +struct ParticleSystemExternalCachedForce +{ + Vector3f position; + Vector3f direction; + int forceType; + float radius; + float forceMain; + float forceTurbulence; // not yet implemented +}; + +// @TODO: Maybe there's a better name for this? ParticleSystemSerializedState? Some shit like that :) +struct ParticleSystemReadOnlyState +{ + ParticleSystemReadOnlyState(); + + void CheckConsistency(); + + float lengthInSec; + float startDelay; + float speed; + UInt32 randomSeed; + bool looping; + bool prewarm; + bool playOnAwake; + bool useLocalSpace; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + +// Some of these aren't actually state, but more like context. Separate it? +struct ParticleSystemState +{ + // state + float accumulatedDt; + float delayT; + bool playing; + bool needRestart; + bool stopEmitting; + size_t rayBudget; + size_t nextParticleToTrace; + + bool GetIsSubEmitter() const { return isSubEmitter; } +private: + // When setting this we need to ensure some other things happen as well + bool isSubEmitter; + +public: + + bool recordSubEmits; + + // Procedural mode / culling + bool supportsProcedural; // With the current parameter set, does the emitter support procedural mode? + bool invalidateProcedural; // This is set if anything changes from script at some point when running a system + bool culled; // Is particle system currently culled? + double cullTime; // Absolute time, so we need as double in case it runs for ages + int numLoops; // Number of loops executed + + // per-frame + Matrix4x4f localToWorld; + Matrix4x4f worldToLocal; + Vector3f emitterVelocity; + Vector3f emitterScale; + + MinMaxAABB minMaxAABB; + float maxSize; // Maximum size of particles due to setting from script + float t; + + // Temp alloc stuff + ParticleSystemSubEmitterData* cachedSubDataBirth; + size_t numCachedSubDataBirth; + ParticleSystemSubEmitterData* cachedSubDataCollision; + size_t numCachedSubDataCollision; + ParticleSystemSubEmitterData* cachedSubDataDeath; + size_t numCachedSubDataDeath; + ParticleSystemExternalCachedForce* cachedForces; + size_t numCachedForces; + Plane* cachedCollisionPlanes; + size_t numCachedCollisionPlanes; + ParticleSystemSubEmitCmdBuffer subEmitterCommandBuffer; + + dynamic_array<ParticleSystemEmitReplay> emitReplay; + ParticleSystemEmissionState emissionState; + + ParticleSystemState (); + + void Tick (const ParticleSystemReadOnlyState& constState, float dt); + void ClearSubEmitterCommandBuffer(); + + void SetIsSubEmitter(bool inIsSubEmitter) + { + if(inIsSubEmitter) + { + stopEmitting = true; + invalidateProcedural = true; + } + isSubEmitter = inIsSubEmitter; + } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + + +class ParticleSystemModule +{ +public: + DECLARE_SERIALIZE (ParticleSystemModule) + ParticleSystemModule (bool enabled) : m_Enabled (enabled) {} + virtual ~ParticleSystemModule () {} + + inline bool GetEnabled() const { return m_Enabled; } + inline void SetEnabled(bool enabled) { m_Enabled = enabled; } + +private: + // shared data + bool m_Enabled; +}; + +#endif // SHURIKENMODULE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp new file mode 100644 index 0000000..c194808 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.cpp @@ -0,0 +1,51 @@ +#include "UnityPrefix.h" +#include "RotationByVelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + + +RotationBySpeedModule::RotationBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, const Vector2f offsetScale) +{ + if (!ps.usesRotationalSpeed) return; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float t = InverseLerpFast01 (offsetScale, Magnitude(vel)); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationBySpeedCurveId); + ps.rotationalSpeed[q] += Evaluate<mode> (curve, t, random); + } +} + +void RotationBySpeedModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + Vector2f offsetScale = CalculateInverseLerpOffsetScale (m_Range); + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, fromIndex, toIndex, offsetScale); + else + UpdateTpl<kEMSlow>(m_Curve, ps, fromIndex, toIndex, offsetScale); +} + +void RotationBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.y = std::max (m_Range.x + MyEpsilon, m_Range.y); +} + +template<class TransferFunction> +void RotationBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(RotationBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h new file mode 100644 index 0000000..548f9c3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULEROTATIONBYVELOCITY_H +#define SHURIKENMODULEROTATIONBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Vector2.h" + +class RotationBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (RotationBySpeedModule) + RotationBySpeedModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULEROTATIONBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp new file mode 100644 index 0000000..7f331d6 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.cpp @@ -0,0 +1,83 @@ +#include "UnityPrefix.h" +#include "RotationModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Math/Vector2.h" + +struct DualMinMaxPolyCurves +{ + MinMaxOptimizedPolyCurves optRot; + MinMaxPolyCurves rot; +}; + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if ( !ps.usesRotationalSpeed ) return; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationCurveId); + ps.rotationalSpeed[q] += Evaluate<mode> (curve, time, random); + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMaxPolyCurves& curves, ParticleSystemParticles& ps) +{ + const size_t count = ps.array_size (); + for (int q=0; q<count; q++) + { + float time = NormalizedTime(ps, q); + float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemRotationCurveId); + float range = ps.startLifetime[q]; + float value; + if(isOptimized) + value = EvaluateIntegrated (curves.optRot, time, random); + else + value = EvaluateIntegrated (curves.rot, time, random); + ps.rotation[q] += value * range; + } +} + +RotationModule::RotationModule() : ParticleSystemModule(false) +{} + +void RotationModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, fromIndex, toIndex); + else if(m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, fromIndex, toIndex); + else + UpdateTpl<kEMSlow>(m_Curve, ps, fromIndex, toIndex); + +} + +void RotationModule::UpdateProcedural (const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + DualMinMaxPolyCurves curves; + if(m_Curve.IsOptimized()) + { + curves.optRot = m_Curve.polyCurves; curves.optRot.Integrate(); + UpdateProceduralTpl<true>(curves, ps); + } + else + { + DebugAssert(CurvesSupportProcedural (m_Curve.editorCurves, m_Curve.minMaxState)); + BuildCurves(curves.rot, m_Curve.editorCurves, m_Curve.GetScalar(), m_Curve.minMaxState); curves.rot.Integrate(); + UpdateProceduralTpl<false>(curves, ps); + } +} + +template<class TransferFunction> +void RotationModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); +} +INSTANTIATE_TEMPLATE_TRANSFER(RotationModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h new file mode 100644 index 0000000..494414e --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/RotationModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULEROTATION_H +#define SHURIKENMODULEROTATION_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class RotationModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (RotationModule) + + RotationModule(); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void UpdateProcedural (const ParticleSystemState& state, ParticleSystemParticles& ps); + void CheckConsistency() {}; + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; +}; + +#endif // SHURIKENMODULEROTATION_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp new file mode 100644 index 0000000..3cdb007 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.cpp @@ -0,0 +1,650 @@ +#include "UnityPrefix.h" +#include "ShapeModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystem.h" +#include "Runtime/Graphics/TriStripper.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Geometry/ComputionalGeometry.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Utilities/StrideIterator.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +enum MeshDistributionMode +{ + kDistributionVertex, + kDistributionTriangle, +}; + + +/// This gives a random barycentric coord (on the edge of triangle) +// @TODO: Stupid: Make this in a faster way +inline Vector3f RandomBarycentricCoordEdge (Rand& rand) +{ + float u = rand.GetFloat (); + float v = rand.GetFloat (); + if (u + v > 1.0F) + { + u = 1.0F - u; + v = 1.0F - v; + } + float w = 1.0F - u - v; + + int edge = RangedRandom(rand, 0, 2); + if(0 == edge) + { + v += 0.5f * u; + w += 0.5f * u; + u = 0.0f; + } + else if(1 == edge) + { + u += 0.5f * v; + w += 0.5f * v; + v = 0.0f; + } + else + { + u += 0.5f * w; + v += 0.5f * w; + w = 0.0f; + } + + return Vector3f (u, v, w); +} + + +// TODO: It could make sense to initialize in separated loops. i.e. separate position and velcoity vectors +inline void EmitterStoreData(const Matrix4x4f& localToWorld, const Vector3f& scale, ParticleSystemParticles& ps, size_t q, Vector3f& pos, Vector3f& n, Rand& random, bool randomDirection) +{ + if(randomDirection) + n = RandomUnitVector (random); + + n = NormalizeSafe(n); + + pos = Scale(pos, scale); + + Vector3f vel = Magnitude (ps.velocity[q]) * n; + vel = localToWorld.MultiplyVector3 (vel); + + // @TODO: Sooo... why multiply point and then undo the result of it? Why not just MultiplyVector? + pos = localToWorld.MultiplyPoint3 (pos) - localToWorld.GetPosition(); + ps.position[q] += pos; + ps.velocity[q] = vel; + +#if 0 // WIP code for converting to spherical + Vector3f sp = ps.position[q]; + ps.position[q].x = Sqrt(sp.x*sp.x + sp.y*sp.y + sp.z*sp.z); + ps.position[q].y = acosf(sp.z/ps.position[q].x); + ps.position[q].z = acosf(sp.y/ps.position[q].x); +#endif + + if(ps.usesAxisOfRotation) + { + Vector3f tan = Cross (-n, Vector3f::zAxis); + if (SqrMagnitude (tan) <= 0.01) + tan = Cross (-pos, Vector3f::zAxis); + if (SqrMagnitude (tan) <= 0.01) + tan = Vector3f::yAxis; + ps.axisOfRotation[q] = Normalize (tan); + } +} + +inline void EmitterStoreData(const Matrix4x4f& localToWorld, const Vector3f& scale, ParticleSystemParticles& ps, size_t q, Vector3f& pos, Vector3f& n, ColorRGBA32& color, Rand& random, bool randomDirection) +{ + EmitterStoreData(localToWorld, scale, ps, q, pos, n, random, randomDirection); + ps.color[q] *= color; +} + + +template<MeshDistributionMode distributionMode> +void GetPositionMesh (Vector3f& pos, + Vector3f& n, + ColorRGBA32& color, + const ParticleSystemEmitterMeshVertex* vertexData, + const int vertexCount, + const MeshTriangleData* triangleData, + const UInt32 numPrimitives, + float totalTriangleArea, + Rand& random, + bool edge) +{ + // position/normal of particle is vertex/vertex normal from mesh + if(kDistributionVertex == distributionMode) + { + int vertexIndex = RangedRandom (random, 0, vertexCount); + pos = vertexData[vertexIndex].position; + n = vertexData[vertexIndex].normal; + color = vertexData[vertexIndex].color; + } + else if(kDistributionTriangle == distributionMode) + { + float randomArea = RangedRandom(random, 0.0f, totalTriangleArea); + float accArea = 0.0f; + UInt32 triangleIndex = 0; + + for(UInt32 i = 0; i < numPrimitives; i++) + { + const MeshTriangleData& data = triangleData[i]; + accArea += data.area; + if(accArea >= randomArea) + { + triangleIndex = i; + break; + } + } + + const MeshTriangleData& data = triangleData[triangleIndex]; + UInt16 a = data.indices[0]; + UInt16 b = data.indices[1]; + UInt16 c = data.indices[2]; + + Vector3f barycenter; + if(edge) + barycenter = RandomBarycentricCoordEdge (random); + else + barycenter = RandomBarycentricCoord (random); + + // Interpolate vertex with barycentric coordinate + pos = barycenter.x * vertexData[a].position + barycenter.y * vertexData[b].position + barycenter.z * vertexData[c].position; + n = barycenter.x * vertexData[a].normal + barycenter.y * vertexData[b].normal + barycenter.z * vertexData[c].normal; + + // TODO: Don't convert to floats!!! + ColorRGBAf color1 = vertexData[a].color; + ColorRGBAf color2 = vertexData[b].color; + ColorRGBAf color3 = vertexData[c].color; + color = barycenter.x * color1 + barycenter.y * color2 + barycenter.z * color3; + } +} + +static bool CompareMeshTriangleData (const MeshTriangleData& a, const MeshTriangleData& b) +{ + return (a.area > b.area); +} + +static float BuildMeshAreaTable(MeshTriangleData* triData, const StrideIterator<Vector3f> vertices, const UInt16* indices, int numTriangles) +{ + float result = 0.0f; + for(int i = 0; i < numTriangles; i++) + { + const UInt16 a = indices[i * 3 + 0]; + const UInt16 b = indices[i * 3 + 1]; + const UInt16 c = indices[i * 3 + 2]; + float area = TriangleArea3D (vertices[a], vertices[b], vertices[c]); + result += area; + + triData[i].indices[0] = a; + triData[i].indices[1] = b; + triData[i].indices[2] = c; + triData[i].area = area; + } + + return result; +} + +// ------------------------------------------------------------------------------------------ + +ShapeModule::ShapeModule () : ParticleSystemModule(true) +, m_Type (kCone) +, m_RandomDirection (false) +, m_Angle(25.0f) +, m_Radius(1.0f) +, m_Length(5.0f) +, m_BoxX(1.0f) +, m_BoxY(1.0f) +, m_BoxZ(1.0f) +, m_PlacementMode(kVertex) +, m_CachedMesh(NULL) +, m_MeshNode (NULL) +{ +} + +void ShapeModule::Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t) +{ + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + Rand& random = GetRandom(); + + if (m_Type == kMesh) + { + if(!m_CachedMesh) + return; + + if(!m_CachedVertexData.size()) + return; + + if(!m_CachedTriangleData.size()) + return; + + const ParticleSystemEmitterMeshVertex* vertexData = &m_CachedVertexData[0]; + const int vertexCount = m_CachedVertexData.size(); + size_t count = ps.array_size (); + switch(m_PlacementMode) + { + case kVertex: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionVertex>(pos, n, color, vertexData, vertexCount, NULL, 0, m_CachedTotalTriangleArea, random, false); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + case kEdge: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionTriangle>(pos, n, color, vertexData, vertexCount, m_CachedTriangleData.begin(), m_CachedTriangleData.size(), m_CachedTotalTriangleArea, random, true); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + case kTriangle: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos; + Vector3f n; + ColorRGBA32 color; + GetPositionMesh<kDistributionTriangle>(pos, n, color, vertexData, vertexCount, m_CachedTriangleData.begin(), m_CachedTriangleData.size(), m_CachedTotalTriangleArea, random, false); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, color, random, m_RandomDirection); + } + break; + } + default: + { + DebugAssert(0 && "PlacementMode Not Supported"); + } + } + } + else + { + const float r = m_Radius; + + float a = Deg2Rad (m_Angle); + float sinA = Sin (a); + float cosA = Cos (a); + float length = m_Length; + + const size_t count = ps.array_size (); + switch(m_Type) + { + case kSphere: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideUnitSphere (random) * r; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kSphereShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomUnitVector(random) * r; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kHemiSphere: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideUnitSphere (random) * r; + if (pos.z < 0.0f) + pos.z *= -1.0f; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kHemiSphereShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomUnitVector (random) * r; + if (pos.z < 0.0f) + pos.z *= -1.0f; + Vector3f n = pos; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kCone: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = RandomPointInsideUnitCircle (random); + Vector2f nXY; + if(m_RandomDirection) + nXY = RandomPointInsideUnitCircle (random) * sinA; + else + nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, false); + } + break; + } + case kConeShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = NormalizeSafe(RandomUnitVector2 (random)); + + Vector2f nXY; + if(m_RandomDirection) + nXY = RandomPointInsideUnitCircle (random) * sinA; + else + nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, false); + } + break; + } + case kConeVolume: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = RandomPointInsideUnitCircle (random); + Vector2f nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos (posXY.x * r, posXY.y * r, 0.0f); + pos += length * Random01(random) * NormalizeSafe(n); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kConeVolumeShell: + { + for (int q = fromIndex; q < count; ++q) + { + Vector2f posXY = NormalizeSafe(RandomUnitVector2 (random)); + Vector2f nXY = Vector2f(posXY.x, posXY.y)* sinA; + Vector3f n (nXY.x, nXY.y, cosA); + Vector3f pos = Vector3f(posXY.x * r, posXY.y * r, 0.0f); + pos += length * Random01(random) * NormalizeSafe(n); + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + break; + } + case kBox: + { + const Vector3f extents (0.5f * m_BoxX, 0.5f * m_BoxY, 0.5f * m_BoxZ); + for (int q = fromIndex; q < count; ++q) + { + Vector3f pos = RandomPointInsideCube (random, extents); + Vector3f n = Vector3f::zAxis; + EmitterStoreData(matrix, state.emitterScale, ps, q, pos, n, random, m_RandomDirection); + } + } + break; + default: + { + DebugAssert(0 && "Shape not supported"); + } + } + } +} + +void ShapeModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Vector3f& emitterScale, Vector2f minMaxBounds) const +{ + DebugAssert(minMaxBounds.x <= minMaxBounds.y); + + switch(m_Type) + { + case kSphere: + case kSphereShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, m_Radius); + bounds.m_Min = -bounds.m_Max; + break; + case kHemiSphere: + case kHemiSphereShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, m_Radius); + bounds.m_Min = Vector3f(-m_Radius, -m_Radius, 0.0f); + break; + case kCone: + case kConeShell: + bounds.m_Max = Vector3f(m_Radius, m_Radius, 0.0f); + bounds.m_Min = -bounds.m_Max; + break; + case kConeVolume: + case kConeVolumeShell: + { + const float a = Deg2Rad (m_Angle); + const float coneRadius2 = m_Radius + m_Length * Sin (a); + const float coneLength = m_Length * Cos (a); + bounds.m_Max = Vector3f(coneRadius2, coneRadius2, coneLength); + bounds.m_Min = -Vector3f(coneRadius2, coneRadius2, 0.0f); + break; + } + case kBox: + bounds.m_Max = Vector3f(m_BoxX, m_BoxY, m_BoxZ) * 0.5f; + bounds.m_Min = -bounds.m_Max; + break; + case kMesh: + { + if(m_CachedMesh) + bounds = m_CachedMesh->GetBounds(0); + else + bounds = MinMaxAABB(Vector3f::zero, Vector3f::zero); + break; + } + default: + { + AssertBreak(!"Shape not implemented."); + } + } + + bounds.m_Min = Scale(bounds.m_Min, emitterScale); + bounds.m_Max = Scale(bounds.m_Max, emitterScale); + + MinMaxAABB speedBounds; + + // Cone and cone shell random direction only deviate inside the bound + if(m_RandomDirection && (m_Type != kCone) && (m_Type != kConeShell)) + { + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = -Vector3f::one; + minMaxBounds = Abs(minMaxBounds); + } + else + { + switch(m_Type) + { + case kSphere: + case kSphereShell: + case kMesh: + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = -Vector3f::one; + break; + case kHemiSphere: + case kHemiSphereShell: + speedBounds.m_Max = Vector3f::one; + speedBounds.m_Min = Vector3f(-1.0f, -1.0f, 0.0f); + break; + case kCone: + case kConeShell: + case kConeVolume: + case kConeVolumeShell: + { + const float a = Deg2Rad (m_Angle); + const float sinA = Sin (a); + speedBounds.m_Max = Vector3f(sinA, sinA, 1.0f); + speedBounds.m_Min = Vector3f(-sinA, -sinA, 0.0f); + break; + } + case kBox: + speedBounds.m_Max = Vector3f::zAxis; + speedBounds.m_Min = Vector3f::zero; + break; + default: + { + AssertBreak(!"Shape not implemented."); + } + } + } + + MinMaxAABB speedBound; + speedBound.m_Min = bounds.m_Min + speedBounds.m_Min * minMaxBounds.y; + speedBound.m_Max = bounds.m_Max + speedBounds.m_Max * minMaxBounds.y; + bounds.Encapsulate(speedBound); + + MinMaxAABB negSpeedBound; + negSpeedBound.m_Min = speedBounds.m_Min * minMaxBounds.x; + negSpeedBound.m_Max = speedBounds.m_Max * minMaxBounds.x; + speedBound.m_Min = min(negSpeedBound.m_Min, negSpeedBound.m_Max); + speedBound.m_Max = max(negSpeedBound.m_Min, negSpeedBound.m_Max); + bounds.Encapsulate(speedBound); +} + +void ShapeModule::CheckConsistency () +{ + m_Type = clamp<int> (m_Type, kSphere, kMax-1); + m_PlacementMode = clamp<int> (m_PlacementMode, kVertex, kModeMax-1); + + m_Angle = clamp(m_Angle, 0.0f, 90.0f); + m_Radius = max(0.01f, m_Radius); + m_Length = max(0.0f, m_Length); + m_BoxX = max(0.0f, m_BoxX); + m_BoxY = max(0.0f, m_BoxY); + m_BoxZ = max(0.0f, m_BoxZ); +} + +void ShapeModule::AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState) +{ + m_MeshNode.RemoveFromList(); + m_MeshNode.SetData(system); + m_CachedMesh = m_Mesh; + if (m_CachedMesh != NULL) + m_CachedMesh->AddObjectUser( m_MeshNode ); + DidModifyMeshData(); + + ResetSeed(roState); +} + +void ShapeModule::ResetSeed(const ParticleSystemReadOnlyState& roState) +{ + if(roState.randomSeed == 0) + m_Random.SetSeed(GetGlobalRandomSeed ()); + else + m_Random.SetSeed(roState.randomSeed); +} + +void ShapeModule::DidDeleteMesh (ParticleSystem* system) +{ + m_CachedMesh = NULL; +} + +void ShapeModule::DidModifyMeshData () +{ + if (m_CachedMesh == NULL) + { + m_CachedTriangleData.resize_uninitialized(0); + m_CachedVertexData.resize_uninitialized(0); + m_CachedTotalTriangleArea = 0; + return; + } + + + const StrideIterator<Vector3f> vertexBuffer = m_CachedMesh->GetVertexBegin(); + const UInt16* indexBuffer = m_CachedMesh->GetSubMeshBuffer16(0); + const SubMesh& submesh = m_CachedMesh->GetSubMeshFast (0); + if (submesh.topology == kPrimitiveTriangleStripDeprecated) + { + const int numTriangles = CountTrianglesInStrip(indexBuffer, submesh.indexCount); + const int capacity = numTriangles * 3; + UNITY_TEMP_VECTOR(UInt16) tempIndices(capacity); + Destripify(indexBuffer, submesh.indexCount, &tempIndices[0], capacity); + m_CachedTriangleData.resize_uninitialized(numTriangles); + m_CachedTotalTriangleArea = BuildMeshAreaTable(m_CachedTriangleData.begin(), vertexBuffer, &tempIndices[0], numTriangles); + } + else if (submesh.topology == kPrimitiveTriangles) + { + const int numTriangles = submesh.indexCount/3; + m_CachedTriangleData.resize_uninitialized(numTriangles); + m_CachedTotalTriangleArea = BuildMeshAreaTable(m_CachedTriangleData.begin(), vertexBuffer, indexBuffer, numTriangles); + } + else + { + m_CachedMesh = NULL; + } + + // Optimization: This sorts so big triangles comes before small, which means finding the right triangle is faster + std::sort(m_CachedTriangleData.begin(), m_CachedTriangleData.begin() + m_CachedTriangleData.size(), CompareMeshTriangleData); + + // Cache vertices + const int vertexCount = m_CachedMesh->GetVertexCount(); + const StrideIterator<Vector3f> vertices = m_CachedMesh->GetVertexBegin(); + const StrideIterator<Vector3f> normals = m_CachedMesh->GetNormalBegin(); + const StrideIterator<ColorRGBA32> colors = m_CachedMesh->GetColorBegin(); + m_CachedVertexData.resize_uninitialized(vertexCount); + for(int i = 0; i < vertexCount; i++) + { + m_CachedVertexData[i].position = vertices[i]; + + if(!normals.IsNull()) + m_CachedVertexData[i].normal = normals[i]; + else + m_CachedVertexData[i].normal = Vector3f::zero; + + if(!colors.IsNull()) + m_CachedVertexData[i].color = colors[i]; + else + m_CachedVertexData[i].color = ColorRGBA32(0xffffffff); + } +} + +Rand& ShapeModule::GetRandom() +{ +#if UNITY_EDITOR + if(!IsWorldPlaying()) + return m_EditorRandom; + else +#endif + return m_Random; +} + +template<class TransferFunction> +void ShapeModule::Transfer (TransferFunction& transfer) +{ + transfer.SetVersion(2); + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Type, "type"); + + // Primitive + transfer.Transfer(m_Radius, "radius"); + transfer.Transfer(m_Angle, "angle"); + transfer.Transfer(m_Length, "length"); + transfer.Transfer(m_BoxX, "boxX"); + transfer.Transfer(m_BoxY, "boxY"); + transfer.Transfer(m_BoxZ, "boxZ"); + + // Mesh + transfer.Transfer (m_PlacementMode, "placementMode"); + TRANSFER (m_Mesh); + + transfer.Transfer (m_RandomDirection, "randomDirection"); transfer.Align(); + + // In Unity 3.5 all cone emitters had random direction set to false, but behaved as if it was true + if(transfer.IsOldVersion(1)) + if(kCone == m_Type) + m_RandomDirection = true; +} + +INSTANTIATE_TEMPLATE_TRANSFER(ShapeModule) + diff --git a/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h new file mode 100644 index 0000000..1f95d33 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/ShapeModule.h @@ -0,0 +1,80 @@ +#ifndef SHURIKENMODULESHAPE_H +#define SHURIKENMODULESHAPE_H + +#include "Runtime/BaseClasses/BaseObject.h" +#include "ParticleSystemModule.h" +#include "Runtime/Math/Random/rand.h" +#include "Runtime/Utilities/LinkedList.h" + +struct MeshTriangleData +{ + float area; + UInt16 indices[3]; +}; + +struct ParticleSystemEmitterMeshVertex +{ + Vector3f position; + Vector3f normal; + ColorRGBA32 color; +}; + +class Mesh; + +class ShapeModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (ShapeModule) + ShapeModule (); + + enum MeshPlacementMode { kVertex, kEdge, kTriangle, kModeMax }; + enum { kSphere, kSphereShell, kHemiSphere, kHemiSphereShell, kCone, kBox, kMesh, kConeShell, kConeVolume, kConeVolumeShell, kMax }; + + void AwakeFromLoad (ParticleSystem* system, const ParticleSystemReadOnlyState& roState); + void ResetSeed(const ParticleSystemReadOnlyState& roState); + void DidModifyMeshData (); + void DidDeleteMesh (ParticleSystem* system); + + PPtr<Mesh> GetMeshEmitterShape () { return m_Mesh; } + + void Start (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const Matrix4x4f& matrix, size_t fromIndex, float t); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Vector3f& emitterScale, Vector2f minMaxBounds) const; + void CheckConsistency (); + + inline void SetShapeType(int type) { m_Type = type; }; + inline void SetRadius(float radius) { m_Radius = radius; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + Rand& GetRandom(); + + int m_Type; + + // Primitive stuff + float m_Radius; + float m_Angle; + float m_Length; + float m_BoxX; + float m_BoxY; + float m_BoxZ; + + // Mesh stuff + int m_PlacementMode; + PPtr<Mesh> m_Mesh; + Mesh* m_CachedMesh; + dynamic_array<ParticleSystemEmitterMeshVertex> m_CachedVertexData; + dynamic_array<MeshTriangleData> m_CachedTriangleData; + float m_CachedTotalTriangleArea; + ListNode<Object> m_MeshNode; + + bool m_RandomDirection; + Rand m_Random; +#if UNITY_EDITOR +public: + Rand m_EditorRandom; +#endif +}; + +#endif // SHURIKENMODULESHAPE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp new file mode 100644 index 0000000..21cd5df --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.cpp @@ -0,0 +1,50 @@ +#include "UnityPrefix.h" +#include "SizeByVelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +SizeBySpeedModule::SizeBySpeedModule () : ParticleSystemModule(false) +, m_Range (0.0f, 1.0f) +{} + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex, const Vector2f offsetScale) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const Vector3f vel = ps.velocity[q] + ps.animatedVelocity[q]; + const float t = InverseLerpFast01 (offsetScale, Magnitude (vel)); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemSizeBySpeedCurveId); + tempSize[q] *= max<float> (0.0f, Evaluate<mode> (curve, t, random)); + } +} + +void SizeBySpeedModule::Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size ()); + Vector2f offsetScale = CalculateInverseLerpOffsetScale(m_Range); + if (m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax()) + UpdateTpl<kEMOptimizedMinMax> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); + else + UpdateTpl<kEMSlow> (m_Curve, ps, tempSize, fromIndex, toIndex, offsetScale); +} + +void SizeBySpeedModule::CheckConsistency () +{ + const float MyEpsilon = 0.001f; + m_Range.x = std::min (m_Range.x, m_Range.y - MyEpsilon); +} + +template<class TransferFunction> +void SizeBySpeedModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); + transfer.Transfer (m_Range, "range"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SizeBySpeedModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h new file mode 100644 index 0000000..c8ad729 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeByVelocityModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULESIZEBYVELOCITY_H +#define SHURIKENMODULESIZEBYVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Vector2.h" + +class SizeBySpeedModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SizeBySpeedModule) + SizeBySpeedModule (); + + void Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; + Vector2f m_Range; +}; + +#endif // SHURIKENMODULESIZEBYVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp new file mode 100644 index 0000000..ad08a4a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.cpp @@ -0,0 +1,41 @@ +#include "UnityPrefix.h" +#include "SizeModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float time = NormalizedTime(ps, q); + const float random = GenerateRandom(ps.randomSeed[q] + kParticleSystemSizeCurveId); + tempSize[q] *= max<float>(0.0f, Evaluate<mode> (curve, time, random)); + } +} + +SizeModule::SizeModule() : ParticleSystemModule(false) +{} + +void SizeModule::Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex) +{ + DebugAssert(toIndex <= ps.array_size ()); + if(m_Curve.minMaxState == kMMCScalar) + UpdateTpl<kEMScalar>(m_Curve, ps, tempSize, fromIndex, toIndex); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax ()) + UpdateTpl<kEMOptimizedMinMax>(m_Curve, ps, tempSize, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateTpl<kEMOptimized>(m_Curve, ps, tempSize, fromIndex, toIndex); + else + UpdateTpl<kEMSlow>(m_Curve, ps, tempSize, fromIndex, toIndex); +} + +template<class TransferFunction> +void SizeModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "curve"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SizeModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h new file mode 100644 index 0000000..3c92634 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SizeModule.h @@ -0,0 +1,27 @@ +#ifndef SHURIKENMODULESIZE_H +#define SHURIKENMODULESIZE_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" + +class SizeModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SizeModule) + + SizeModule(); + + void Update (const ParticleSystemParticles& ps, float* tempSize, size_t fromIndex, size_t toIndex); + + void CheckConsistency () {}; + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_Curve; +}; + +#endif // SHURIKENMODULESIZE_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp new file mode 100644 index 0000000..895c444 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SubModule.cpp @@ -0,0 +1,148 @@ +#include "UnityPrefix.h" +#include "SubModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystem.h" // Only because of PPtr comparison + +bool IsUsingSubEmitter(ParticleSystem* emitter) +{ + const void* shurikenSelf = NULL; + return emitter != shurikenSelf; +} + +SubModule::SubModule () : ParticleSystemModule(false) +{ +} + +void SubModule::Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dt) const +{ + Assert(state.numCachedSubDataBirth <= kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < state.numCachedSubDataBirth; i++) + { + const ParticleSystemSubEmitterData& data = state.cachedSubDataBirth[i]; + //const bool culled = (data.maxLifetime < state.accumulatedDt); + const float startDelay = data.startDelayInSec; + const float length = data.lengthInSec; + for (int q = fromIndex; q < toIndex; ++q) + { + const float t = std::max(0.0f, ps.startLifetime[q] - ps.lifetime[q] - dt) - startDelay; + const bool started = (t >= 0.0f); + const bool ended = (t >= length); + if(started && !ended) + { + ParticleSystemEmissionState emissionState; + emissionState.m_ToEmitAccumulator = ps.emitAccumulator[i][q]; + RecordEmit(emissionState, data, roState, state, ps, kParticleSystemSubTypeBirth, i, q, t, dt, length); + ps.emitAccumulator[i][q] = emissionState.m_ToEmitAccumulator; + } + } + } +} + +void SubModule::RemoveDuplicatePtrs (ParticleSystem** shurikens) +{ + for(int i = 0; i < kParticleSystemMaxSubTotal-1; i++) + for(int j = i+1; j < kParticleSystemMaxSubTotal; j++) + if(shurikens[i] && (shurikens[i] == shurikens[j])) + shurikens[i] = NULL; +} + +// This can not be cached between frames since the referenced particle systems might be deleted at any point. +int SubModule::GetSubEmitterPtrs (ParticleSystem** subEmitters) const +{ + for(int i = 0; i < kParticleSystemMaxSubTotal; i++) + subEmitters[i] = NULL; + + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersBirth[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersCollision[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersDeath[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsBirth (ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersBirth[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsCollision (ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersCollision[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterPtrsDeath(ParticleSystem** subEmitters) const +{ + int numSubEmitters = 0; + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + { + ParticleSystem* subParticleSystem = m_SubEmittersDeath[i]; + if (IsUsingSubEmitter(subParticleSystem)) + subEmitters[numSubEmitters++] = subParticleSystem; + } + return numSubEmitters; +} + +int SubModule::GetSubEmitterTypeCount(ParticleSystemSubType type) const +{ + int count = 0; + const void* shurikenSelf = NULL; + if(type == kParticleSystemSubTypeBirth) { + for(int i = 0; i < kParticleSystemMaxSubBirth; i++) + if(m_SubEmittersBirth[i] != shurikenSelf) + count++; + } else if(type == kParticleSystemSubTypeCollision) { + for(int i = 0; i < kParticleSystemMaxSubCollision; i++) + if(m_SubEmittersCollision[i] != shurikenSelf) + count++; + } else if(type == kParticleSystemSubTypeDeath) { + for(int i = 0; i < kParticleSystemMaxSubDeath; i++) + if(m_SubEmittersDeath[i] != shurikenSelf) + count++; + } else { + Assert(!"Sub emitter type not implemented."); + } + return count; +} + +template<class TransferFunction> +void SubModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_SubEmittersBirth[0], "subEmitterBirth"); + transfer.Transfer (m_SubEmittersBirth[1], "subEmitterBirth1"); + transfer.Transfer (m_SubEmittersCollision[0], "subEmitterCollision"); + transfer.Transfer (m_SubEmittersCollision[1], "subEmitterCollision1"); + transfer.Transfer (m_SubEmittersDeath[0], "subEmitterDeath"); + transfer.Transfer (m_SubEmittersDeath[1], "subEmitterDeath1"); +} +INSTANTIATE_TEMPLATE_TRANSFER(SubModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/SubModule.h b/Runtime/Graphics/ParticleSystem/Modules/SubModule.h new file mode 100644 index 0000000..093c8ba --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/SubModule.h @@ -0,0 +1,38 @@ +#ifndef SHURIKENMODULESUB_H +#define SHURIKENMODULESUB_H + +#include "ParticleSystemModule.h" +#include "Runtime/BaseClasses/BaseObject.h" + +class ParticleSystem; +struct ParticleSystemParticles; + + +class SubModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (SubModule) + SubModule (); + + void Update (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, size_t toIndex, float dt) const; + void CheckConsistency() {}; + + int GetSubEmitterTypeCount(ParticleSystemSubType type) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + + static void RemoveDuplicatePtrs (ParticleSystem** shurikens); + int GetSubEmitterPtrs (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsBirth (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsCollision (ParticleSystem** shurikens) const; + int GetSubEmitterPtrsDeath(ParticleSystem** shurikens) const; + + +private: + PPtr<ParticleSystem> m_SubEmittersBirth[kParticleSystemMaxSubBirth]; + PPtr<ParticleSystem> m_SubEmittersCollision[kParticleSystemMaxSubCollision]; + PPtr<ParticleSystem> m_SubEmittersDeath[kParticleSystemMaxSubDeath]; +}; + +#endif // SHURIKENMODULESUB_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp new file mode 100644 index 0000000..67d49b3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/UVModule.cpp @@ -0,0 +1,105 @@ +#include "UnityPrefix.h" +#include "UVModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateWholeSheetTpl(float cycles, const MinMaxCurve& curve, const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + tempSheetIndex[q] = Repeat (cycles * Evaluate(curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)), 1.0f); +} + +UVModule::UVModule () : ParticleSystemModule(false) +, m_TilesX (1), m_TilesY (1) +, m_AnimationType (kWholeSheet) +, m_RowIndex (0) +, m_Cycles (1.0f) +, m_RandomRow (true) +{} + +void UVModule::Update (const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex) +{ + const float cycles = m_Cycles; + + DebugAssert(toIndex <= ps.array_size ()); + if (m_AnimationType == kSingleRow) // row + { + int rows = m_TilesY; + float animRange = (1.0f / (m_TilesX * rows)) * m_TilesX; + if(m_RandomRow) + { + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = cycles * Evaluate(m_Curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)); + const float x = Repeat (t, 1.0f); + const float randomValue = GenerateRandom(ps.randomSeed[q] + kParticleSystemUVRowSelectionId); + const float startRow = Floorf (randomValue * rows); + float from = startRow * animRange; + float to = from + animRange; + tempSheetIndex[q] = Lerp (from, to, x); + } + } + else + { + const float startRow = Floorf(m_RowIndex * animRange * rows); + float from = startRow * animRange; + float to = from + animRange; + for (size_t q = fromIndex; q < toIndex; ++q) + { + const float t = cycles * Evaluate(m_Curve, NormalizedTime(ps, q), GenerateRandom(ps.randomSeed[q] + kParticleSystemUVCurveId)); + const float x = Repeat (t, 1.0f); + tempSheetIndex[q] = Lerp (from, to, x); + } + } + } + else if (m_AnimationType == kWholeSheet) // grid || row + { + if(m_Curve.minMaxState == kMMCScalar) + UpdateWholeSheetTpl<kEMScalar>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else if (m_Curve.IsOptimized() && m_Curve.UsesMinMax ()) + UpdateWholeSheetTpl<kEMOptimizedMinMax>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else if(m_Curve.IsOptimized()) + UpdateWholeSheetTpl<kEMOptimized>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + else + UpdateWholeSheetTpl<kEMSlow>(m_Cycles, m_Curve, ps, tempSheetIndex, fromIndex, toIndex); + } + else + { + Assert(!"Animation mode not implemented!"); + } +} + +void UVModule::CheckConsistency () +{ + m_AnimationType = clamp<int> (m_AnimationType, 0, kNumAnimationTypes-1); + m_TilesX = std::max<int> (1, m_TilesX); + m_TilesY = std::max<int> (1, m_TilesY); + m_Cycles = std::max<int> (1, (int)m_Cycles); + m_RowIndex = clamp<int> (m_RowIndex, 0, m_TilesY-1); + m_Curve.SetScalar(clamp<float> (m_Curve.GetScalar(), 0.0f, 1.0f)); +} + +void UVModule::GetNumTiles(int& uvTilesX, int& uvTilesY) const +{ + uvTilesX = m_TilesX; + uvTilesY = m_TilesY; +} + +template<class TransferFunction> +void UVModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_Curve, "frameOverTime"); + transfer.Transfer (m_TilesX, "tilesX"); + transfer.Transfer (m_TilesY, "tilesY"); + transfer.Transfer (m_AnimationType, "animationType"); + transfer.Transfer (m_RowIndex, "rowIndex"); + transfer.Transfer (m_Cycles, "cycles"); + transfer.Transfer (m_RandomRow, "randomRow"); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(UVModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/UVModule.h b/Runtime/Graphics/ParticleSystem/Modules/UVModule.h new file mode 100644 index 0000000..5ed9e7f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/UVModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEUV_H +#define SHURIKENMODULEUV_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/PolynomialCurve.h" + +struct ParticleSystemParticles; + +class UVModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (UVModule) + UVModule (); + + void Update (const ParticleSystemParticles& ps, float* tempSheetIndex, size_t fromIndex, size_t toIndex); + void CheckConsistency (); + + inline MinMaxCurve& GetCurve() { return m_Curve; } + + void GetNumTiles(int& uvTilesX, int& uvTilesY) const; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + enum { kWholeSheet, kSingleRow, kNumAnimationTypes }; + + MinMaxCurve m_Curve; + int m_TilesX, m_TilesY; + int m_AnimationType; + int m_RowIndex; + float m_Cycles; + bool m_RandomRow; +}; + +#endif // SHURIKENMODULEUV_H diff --git a/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp new file mode 100644 index 0000000..edb7ed9 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.cpp @@ -0,0 +1,127 @@ +#include "UnityPrefix.h" +#include "VelocityModule.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "../ParticleSystemUtils.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/Math/Matrix4x4.h" + +template<ParticleSystemCurveEvalMode mode> +void UpdateTpl(const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, bool transform, const Matrix4x4f& matrix) +{ + for (size_t q = fromIndex; q < toIndex; ++q) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemVelocityCurveId); + + const float normalizedTime = NormalizedTime (ps, q); + Vector3f vel = Vector3f (Evaluate<mode> (x, normalizedTime, random.x), Evaluate<mode> (y, normalizedTime, random.y), Evaluate<mode> (z, normalizedTime, random.z)); + if(transform) + vel = matrix.MultiplyVector3 (vel); + ps.animatedVelocity[q] += vel; + } +} + +template<bool isOptimized> +void UpdateProceduralTpl(const DualMinMax3DPolyCurves& curves, const MinMaxCurve& x, const MinMaxCurve& y, const MinMaxCurve& z, ParticleSystemParticles& ps, const Matrix4x4f& matrix, bool transform) +{ + const size_t count = ps.array_size (); + for (int q=0;q<count;q++) + { + Vector3f random; + GenerateRandom3(random, ps.randomSeed[q] + kParticleSystemVelocityCurveId); + const float time = NormalizedTime(ps, q); + + Vector3f delta; + if(isOptimized) + delta = Vector3f(EvaluateIntegrated(curves.optX, time, random.x), EvaluateIntegrated(curves.optY, time, random.y), EvaluateIntegrated(curves.optZ, time, random.z)) * ps.startLifetime[q]; + else + delta = Vector3f(EvaluateIntegrated(curves.x, time, random.x), EvaluateIntegrated(curves.y, time, random.y), EvaluateIntegrated(curves.z, time, random.z)) * ps.startLifetime[q]; + + Vector3f velocity (Evaluate(x, time, random.x), Evaluate(y, time, random.y), Evaluate(z, time, random.z)); + if(transform) + { + delta = matrix.MultiplyVector3 (delta); + velocity = matrix.MultiplyVector3 (velocity); + } + ps.position[q] += delta; + ps.animatedVelocity[q] += velocity; + } +} + +VelocityModule::VelocityModule () : ParticleSystemModule(false) +, m_InWorldSpace (false) +{} + +void VelocityModule::Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + bool usesScalar = (m_X.minMaxState == kMMCScalar) && (m_Y.minMaxState == kMMCScalar) && (m_Z.minMaxState == kMMCScalar); + bool isOptimized = m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized(); + bool usesMinMax = m_X.UsesMinMax() && m_Y.UsesMinMax() && m_Z.UsesMinMax(); + if(usesScalar) + UpdateTpl<kEMScalar>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else if(isOptimized && usesMinMax) + UpdateTpl<kEMOptimizedMinMax>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else if(isOptimized) + UpdateTpl<kEMOptimized>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + else + UpdateTpl<kEMSlow>(m_X, m_Y, m_Z, ps, fromIndex, toIndex, transform, matrix); + +} + +void VelocityModule::UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps) +{ + Matrix4x4f matrix; + bool transform = GetTransformationMatrix(matrix, !roState.useLocalSpace, m_InWorldSpace, state.localToWorld); + + DualMinMax3DPolyCurves curves; + if(m_X.IsOptimized() && m_Y.IsOptimized() && m_Z.IsOptimized()) + { + curves.optX = m_X.polyCurves; curves.optX.Integrate(); + curves.optY = m_Y.polyCurves; curves.optY.Integrate(); + curves.optZ = m_Z.polyCurves; curves.optZ.Integrate(); + UpdateProceduralTpl<true>(curves, m_X, m_Y, m_Z, ps, matrix, transform); + } + else + { + DebugAssert(CurvesSupportProcedural (m_X.editorCurves, m_X.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Y.editorCurves, m_Y.minMaxState)); + DebugAssert(CurvesSupportProcedural (m_Z.editorCurves, m_Z.minMaxState)); + BuildCurves(curves.x, m_X.editorCurves, m_X.GetScalar(), m_X.minMaxState); curves.x.Integrate(); + BuildCurves(curves.y, m_Y.editorCurves, m_Y.GetScalar(), m_Y.minMaxState); curves.y.Integrate(); + BuildCurves(curves.z, m_Z.editorCurves, m_Z.GetScalar(), m_Z.minMaxState); curves.z.Integrate(); + UpdateProceduralTpl<false>(curves, m_X, m_Y, m_Z, ps, matrix, transform); + } +} + +void VelocityModule::CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime) +{ + Vector2f xRange = m_X.FindMinMaxIntegrated(); + Vector2f yRange = m_Y.FindMinMaxIntegrated(); + Vector2f zRange = m_Z.FindMinMaxIntegrated(); + bounds.m_Min = Vector3f(xRange.x, yRange.x, zRange.x) * maxLifeTime; + bounds.m_Max = Vector3f(xRange.y, yRange.y, zRange.y) * maxLifeTime; + if(m_InWorldSpace) + { + Matrix4x4f matrix; + Matrix4x4f::Invert_General3D(localToWorld, matrix); + matrix.SetPosition(Vector3f::zero); + AABB aabb = bounds; + TransformAABBSlow(aabb, matrix, aabb); + bounds = aabb; + } +} + +template<class TransferFunction> +void VelocityModule::Transfer (TransferFunction& transfer) +{ + ParticleSystemModule::Transfer (transfer); + transfer.Transfer (m_X, "x"); + transfer.Transfer (m_Y, "y"); + transfer.Transfer (m_Z, "z"); + transfer.Transfer (m_InWorldSpace, "inWorldSpace"); transfer.Align(); +} +INSTANTIATE_TEMPLATE_TRANSFER(VelocityModule) diff --git a/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h new file mode 100644 index 0000000..bf76f28 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/Modules/VelocityModule.h @@ -0,0 +1,36 @@ +#ifndef SHURIKENMODULEVELOCITY_H +#define SHURIKENMODULEVELOCITY_H + +#include "ParticleSystemModule.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h" +#include "Runtime/Math/Random/rand.h" + +class VelocityModule : public ParticleSystemModule +{ +public: + DECLARE_MODULE (VelocityModule) + VelocityModule (); + + void Update (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex); + void UpdateProcedural (const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps); + void CalculateProceduralBounds(MinMaxAABB& bounds, const Matrix4x4f& localToWorld, float maxLifeTime); + void CheckConsistency() {}; + + inline MinMaxCurve& GetXCurve() { return m_X; }; + inline MinMaxCurve& GetYCurve() { return m_Y; }; + inline MinMaxCurve& GetZCurve() { return m_Z; }; + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + +private: + MinMaxCurve m_X; + MinMaxCurve m_Y; + MinMaxCurve m_Z; + bool m_InWorldSpace; + + Rand m_Random; +}; + + +#endif // SHURIKENMODULEVELOCITY_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp new file mode 100644 index 0000000..dfefc6a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.cpp @@ -0,0 +1,122 @@ +#include "UnityPrefix.h" +#include "ParticleCollisionEvents.h" + +#include "Runtime/Scripting/ScriptingUtility.h" +#include "Runtime/Threads/Thread.h" + +CollisionEvents::CollisionEvents (): currentCollisionEventThreadArray(0) +{ +} + +void CollisionEvents::Clear () +{ + collisionEvents[0].clear (); + collisionEvents[1].clear (); +} + +bool CollisionEvents::AddEvent (const ParticleCollisionEvent& event) +{ + GetCollisionEventThreadArray ().push_back (event); + return true; +} + +int CollisionEvents::GetCollisionEventCount () const +{ + return GetCollisionEventScriptArray ().size (); +} + +void CollisionEvents::SwapCollisionEventArrays () +{ + Assert (Thread::CurrentThreadIsMainThread ()); + currentCollisionEventThreadArray = (currentCollisionEventThreadArray+1)%2; + collisionEvents[currentCollisionEventThreadArray].clear (); +} + +dynamic_array<ParticleCollisionEvent>& CollisionEvents::GetCollisionEventThreadArray () +{ + return collisionEvents[currentCollisionEventThreadArray]; +} + +const dynamic_array<ParticleCollisionEvent>& CollisionEvents::GetCollisionEventScriptArray () const +{ + const int currentCollisionEventScriptArray = (currentCollisionEventThreadArray+1)%2; + return collisionEvents[currentCollisionEventScriptArray]; +} + +struct SortCollisionEventsByGameObject { + bool operator()( const ParticleCollisionEvent& ra, const ParticleCollisionEvent& rb ) const; +}; + +bool SortCollisionEventsByGameObject::operator()( const ParticleCollisionEvent& ra, const ParticleCollisionEvent& rb ) const +{ + return ra.m_RigidBodyOrColliderInstanceID < rb.m_RigidBodyOrColliderInstanceID; +} + +void CollisionEvents::SortCollisionEventThreadArray () +{ + std::sort (GetCollisionEventThreadArray ().begin (), GetCollisionEventThreadArray ().end (), SortCollisionEventsByGameObject ()); +} + +static GameObject* GetGameObjectFromInstanceID (int instanceId) +{ + Object* temp = Object::IDToPointer (instanceId); + if ( temp ) + { + return (reinterpret_cast<Unity::Component*> (temp))->GetGameObjectPtr (); + } + return NULL; +} + +static int GetGameObjectIDFromInstanceID (int instanceId) +{ + Object* temp = Object::IDToPointer (instanceId); + if ( temp ) + { + return (reinterpret_cast<Unity::Component*> (temp))->GetGameObjectInstanceID (); + } + return 0; +} + +void CollisionEvents::SendCollisionEvents (Unity::Component& particleSystem) const +{ + const dynamic_array<ParticleCollisionEvent>& scriptEventArray = GetCollisionEventScriptArray (); + GameObject* pParticleSystem = &particleSystem.GetGameObject (); + GameObject* collideeGO = NULL; + int currentId = -1; + for (int e = 0; e < scriptEventArray.size (); ++e) + { + if (currentId != scriptEventArray[e].m_RigidBodyOrColliderInstanceID) + { + collideeGO = GetGameObjectFromInstanceID (scriptEventArray[e].m_RigidBodyOrColliderInstanceID); + if (!collideeGO) + continue; + currentId = scriptEventArray[e].m_RigidBodyOrColliderInstanceID; + pParticleSystem->SendMessage (kParticleCollisionEvent, collideeGO, ClassID (GameObject)); // send message to particle system + collideeGO->SendMessage (kParticleCollisionEvent, pParticleSystem, ClassID (GameObject)); // send message to object collided with + } + } +} + +int CollisionEvents::GetCollisionEvents (int instanceId, MonoParticleCollisionEvent* collisionEvents, int size) const +{ + const dynamic_array<ParticleCollisionEvent>& scriptEventArray = GetCollisionEventScriptArray (); + dynamic_array<ParticleCollisionEvent>::const_iterator iter = scriptEventArray.begin (); + for (; iter != scriptEventArray.end (); ++iter) + { + if (instanceId == GetGameObjectIDFromInstanceID (iter->m_RigidBodyOrColliderInstanceID)) + { + int count = 0; + while (iter != scriptEventArray.end () && GetGameObjectIDFromInstanceID (iter->m_RigidBodyOrColliderInstanceID) == instanceId && count < size) + { + collisionEvents[count].m_Intersection = iter->m_Intersection; + collisionEvents[count].m_Normal = iter->m_Normal; + collisionEvents[count].m_Velocity = iter->m_Velocity; + collisionEvents[count].m_ColliderInstanceID = iter->m_ColliderInstanceID; + count++; + iter++; + } + return count; + } + } + return 0; +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h new file mode 100644 index 0000000..98260ee --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Runtime/BaseClasses/GameObject.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Mono/MonoScript.h" +#include "Runtime/Utilities/dynamic_array.h" + + +struct ParticleCollisionEvent +{ + ParticleCollisionEvent (const Vector3f& intersection, const Vector3f& normal, const Vector3f& velocity, int colliderInstanceID, int rigidBodyOrColliderInstanceID); + Vector3f m_Intersection; + Vector3f m_Normal; + Vector3f m_Velocity; + int m_ColliderInstanceID; + int m_RigidBodyOrColliderInstanceID; // This can be a Collider or a RigidBody component +}; + +struct MonoParticleCollisionEvent +{ + Vector3f m_Intersection; + Vector3f m_Normal; + Vector3f m_Velocity; + int m_ColliderInstanceID; +}; + +struct CollisionEvents +{ + CollisionEvents (); + dynamic_array<ParticleCollisionEvent> collisionEvents[2]; + int currentCollisionEventThreadArray; + + void Clear (); + bool AddEvent (const ParticleCollisionEvent& event); + int GetCollisionEventCount () const; + void SwapCollisionEventArrays (); + void SortCollisionEventThreadArray (); + void SendCollisionEvents (Unity::Component& particleSystem) const; + int GetCollisionEvents (int instanceId, MonoParticleCollisionEvent* collisionEvents, int size) const; + dynamic_array<ParticleCollisionEvent>& GetCollisionEventThreadArray (); + const dynamic_array<ParticleCollisionEvent>& GetCollisionEventScriptArray () const; +}; diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp new file mode 100644 index 0000000..47e952f --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystem.cpp @@ -0,0 +1,2110 @@ +#include "UnityPrefix.h" +#include "ParticleSystem.h" +#include "ParticleSystemRenderer.h" +#include "ParticleSystemCurves.h" +#include "ParticleSystemUtils.h" +#include "Modules/ParticleSystemModule.h" +#include "Modules/InitialModule.h" +#include "Modules/ShapeModule.h" +#include "Modules/EmissionModule.h" +#include "Modules/SizeModule.h" +#include "Modules/RotationModule.h" +#include "Modules/ColorModule.h" +#include "Modules/UVModule.h" +#include "Modules/VelocityModule.h" +#include "Modules/ForceModule.h" +#include "Modules/ExternalForcesModule.h" +#include "Modules/ClampVelocityModule.h" +#include "Modules/SizeByVelocityModule.h" +#include "Modules/RotationByVelocityModule.h" +#include "Modules/ColorByVelocityModule.h" +#include "Modules/CollisionModule.h" +#include "Modules/SubModule.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Math/Matrix4x4.h" +#include "Runtime/Math/FloatConversion.h" +#include "Runtime/Math/Random/rand.h" +#include "Runtime/Math/Random/Random.h" +#include "Runtime/BaseClasses/IsPlaying.h" +#include "Runtime/Math/AnimationCurve.h" +#include "Runtime/Input/TimeManager.h" +#include "Runtime/Threads/JobScheduler.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Serialize/TransferFunctions/TransferNameConversions.h" +#include "Runtime/Shaders/GraphicsCaps.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Threads/Thread.h" +#include "Runtime/Misc/GameObjectUtility.h" +#include "Runtime/Misc/ResourceManager.h" +#include "Runtime/Shaders/Material.h" +#include "Runtime/Misc/QualitySettings.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/Core/Callbacks/GlobalCallbacks.h" + +#if UNITY_EDITOR +#include "Editor/Src/ParticleSystem/ParticleSystemEditor.h" +#include "Runtime/Mono/MonoManager.h" +#endif + +// P1: +// . Calling ps.Play() in Start(). Does it actually stsrt playing when game starts? No. + +// P2: +// Automatic Culling: +// . Improve procedural AABB +// . Gravity: make it work with transforming into local space the same way velocity and force modules work +// . Get tighter bounds by forces counteracting eachother instead of always expanding +// . For procedural systems, no gain in calculating AABB every frame. Just do it when something changes. +// . Solving for roots analytically +// . http://en.wikipedia.org/wiki/Cubic_function +// . http://en.wikipedia.org/wiki/Quartic_function +// . http://en.wikipedia.org/wiki/Quintic_function + +// External Forces: +// . Currently not using turbulence. Start using that? + +// Other: +// . Batching: Make it work with meshes as well +// . !!! Add runtime tests for playback code. Playing, stopping, simulating etc. Make sure it's 100%. + +struct ParticleSystemManager +{ + ParticleSystemManager() + :needSync(false) + { + activeEmitters.reserve(32); + } + + dynamic_array<ParticleSystem*> activeEmitters; + +#if ENABLE_MULTITHREADED_PARTICLES + JobScheduler::JobGroupID worldCollisionJobGroup; // group for particle systems performing world collisions, PhysX is currently not threadsafe so deleting objects in LateUpdate could cause crashes as that could overlap with collision testing + JobScheduler::JobGroupID jobGroup; // for any other collisions +#endif + bool needSync; +}; + +Material* GetDefaultParticleMaterial () +{ + #if WEBPLUG + if (!IS_CONTENT_NEWER_OR_SAME(kUnityVersion_OldWebResourcesAdded)) + return GetBuiltinOldWebResource<Material> ("Default-Particle.mat"); + #endif + + #if UNITY_EDITOR + return GetBuiltinExtraResource<Material> ("Default-Particle.mat"); + #endif + + // If someone happens to create a new particle system component at runtime, + // just don't assign any material. The default one might not even be included + // into the build. + return NULL; +} + +ParticleSystemManager gParticleSystemManager; + +PROFILER_INFORMATION(gParticleSystemProfile, "ParticleSystem.Update", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemPrewarm, "ParticleSystem.Prewarm", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemWait, "ParticleSystem.WaitForUpdateThreads", kProfilerParticles) +PROFILER_INFORMATION(gParticleSystemJobProfile, "ParticleSystem.UpdateJob", kProfilerParticles) + +PROFILER_INFORMATION(gParticleSystemUpdateCollisions, "ParticleSystem.UpdateCollisions", kProfilerParticles) + + +#define MAX_TIME_STEP (0.03f) +static float GetTimeStep(float dt, bool fixedTimeStep) +{ + if(fixedTimeStep) + return GetTimeManager().GetFixedDeltaTime(); + else if(dt > MAX_TIME_STEP) + return dt / Ceilf(dt / MAX_TIME_STEP); + else + return dt; +} + +static void ApplyStartDelay (float& delayT, float& accumulatedDt) +{ + if(delayT > 0.0f) + { + delayT -= accumulatedDt; + accumulatedDt = max(-delayT, 0.0f); + delayT = max(delayT, 0.0f); + } + DebugAssert(delayT >= 0.0f); +} + +static inline void CheckParticleConsistency(ParticleSystemState& state, ParticleSystemParticle& particle) +{ + particle.lifetime = min(particle.lifetime, particle.startLifetime); + state.maxSize = max(state.maxSize, particle.size); +} + +ParticleSystem::ParticleSystem (MemLabelId label, ObjectCreationMode mode) +: Super(label, mode) +, m_RayBudget(0) +, m_EmittersIndex (-1) +#if UNITY_EDITOR +, m_EditorListNode ( this ) +#endif +{ + m_State = new ParticleSystemState (); + m_ReadOnlyState = new ParticleSystemReadOnlyState (); + + m_SizeModule = new SizeModule (); + m_RotationModule = new RotationModule (); + m_ColorModule = new ColorModule (); + m_UVModule = new UVModule (); + m_VelocityModule = new VelocityModule (); + m_ForceModule = new ForceModule (); + m_ExternalForcesModule = new ExternalForcesModule (); + m_ClampVelocityModule = new ClampVelocityModule (); + m_SizeBySpeedModule = new SizeBySpeedModule (); + m_RotationBySpeedModule = new RotationBySpeedModule (); + m_ColorBySpeedModule = new ColorBySpeedModule (); + m_CollisionModule = new CollisionModule (); + m_SubModule = new SubModule (); + +#if UNITY_EDITOR + ParticleSystemEditor::SetupDefaultParticleSystem(*this); + m_EditorRandomSeedIndex = 0; +#endif +} + +ParticleSystem::~ParticleSystem () +{ + delete m_State; + delete m_ReadOnlyState; + + delete m_SizeModule; + delete m_RotationModule; + delete m_ColorModule; + delete m_UVModule; + delete m_VelocityModule; + delete m_ForceModule; + delete m_ExternalForcesModule; + delete m_ClampVelocityModule; + delete m_SizeBySpeedModule; + delete m_RotationBySpeedModule; + delete m_ColorBySpeedModule; + delete m_CollisionModule; + delete m_SubModule; +} + +void ParticleSystem::InitializeClass () +{ +#if UNITY_EDITOR + // This is only necessary to avoid pain during development (Pre 3.5) - can be removed later... + RegisterAllowTypeNameConversion ("MinMaxColorCurve", "MinMaxColor"); + // Only needed due to 3.5 beta content + RegisterAllowNameConversion ("MinMaxCurve", "maxScalar", "scalar"); + RegisterAllowNameConversion ("InitialModule", "lifetime", "startLifetime"); + RegisterAllowNameConversion ("InitialModule", "speed", "startSpeed"); + RegisterAllowNameConversion ("InitialModule", "color", "startColor"); + RegisterAllowNameConversion ("InitialModule", "size", "startSize"); + RegisterAllowNameConversion ("InitialModule", "rotation", "startRotation"); + RegisterAllowNameConversion ("ShapeModule", "m_Radius", "radius"); + RegisterAllowNameConversion ("ShapeModule", "m_Angle", "angle"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxX", "boxX"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxY", "boxY"); + RegisterAllowNameConversion ("ShapeModule", "m_BoxZ", "boxZ"); + + REGISTER_MESSAGE_VOID (ParticleSystem, kTransformChanged, TransformChanged); +#endif + + REGISTER_MESSAGE_VOID(ParticleSystem, kDidDeleteMesh, DidDeleteMesh); + REGISTER_MESSAGE_VOID(ParticleSystem, kDidModifyMesh, DidModifyMesh); +} + +void ParticleSystem::SetRayBudget (int rayBudget) +{ + m_RayBudget = rayBudget; +}; + +int ParticleSystem::GetRayBudget() const +{ + return m_RayBudget; +}; + +void ParticleSystem::DidModifyMesh () +{ + m_ShapeModule.DidModifyMeshData(); +} + +void ParticleSystem::DidDeleteMesh () +{ + m_ShapeModule.DidDeleteMesh(this); +} + +void ParticleSystem::Cull() +{ + if(!IsWorldPlaying()) + return; + + m_State->culled = true; + + Clear(false); + m_State->cullTime = GetCurTime (); + RemoveFromManager(); +} + +void ParticleSystem::RendererBecameVisible() +{ + if(m_State->culled) + { + m_State->culled = false; + if(!m_State->stopEmitting && IsPlaying() && IsWorldPlaying() && CheckSupportsProcedural (*this)) + { + double timeDiff = GetCurTime() - m_State->cullTime; + Simulate(m_State->numLoops * m_ReadOnlyState->lengthInSec + m_State->t + timeDiff, true); + Play(); + } + } +} + +void ParticleSystem::RendererBecameInvisible() +{ + if(!m_State->culled && CheckSupportsProcedural(*this)) + Cull(); +} + +void ParticleSystem::AddToManager() +{ + if(m_EmittersIndex >= 0) + return; + size_t index = gParticleSystemManager.activeEmitters.size(); + gParticleSystemManager.activeEmitters.push_back(this); + m_EmittersIndex = index; +} + +void ParticleSystem::RemoveFromManager() +{ + if(m_EmittersIndex < 0) + return; + const int index = m_EmittersIndex; + gParticleSystemManager.activeEmitters[index]->m_EmittersIndex = -1; + gParticleSystemManager.activeEmitters[index] = gParticleSystemManager.activeEmitters.back(); + if(gParticleSystemManager.activeEmitters[index] != this) // corner case + gParticleSystemManager.activeEmitters[index]->m_EmittersIndex = index; + gParticleSystemManager.activeEmitters.resize_uninitialized(gParticleSystemManager.activeEmitters.size() - 1); +} + +void ParticleSystem::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + m_InitialModule.AwakeFromLoad(this, *m_ReadOnlyState); + m_ShapeModule.AwakeFromLoad(this, *m_ReadOnlyState); + + if (!IsActive () || (kDefaultAwakeFromLoad == awakeMode)) + return; + + m_State->localToWorld = GetComponent (Transform).GetLocalToWorldMatrixNoScale(); + Matrix4x4f::Invert_General3D(m_State->localToWorld, m_State->worldToLocal); + m_State->maxSize = 0.0f; + m_State->invalidateProcedural = false; + + if (IsWorldPlaying () && m_ReadOnlyState->playOnAwake) + Play (); + + // Does this even happen? + if(GetParticleCount() || IsPlaying()) + AddToManager(); +} + +void ParticleSystem::Deactivate (DeactivateOperation operation) +{ + Super::Deactivate(operation); + SyncJobs(); + RemoveFromManager(); +} + +void ParticleSystem::SyncJobs() +{ + if(gParticleSystemManager.needSync) + { +#if ENABLE_MULTITHREADED_PARTICLES + PROFILER_BEGIN(gParticleSystemWait, NULL); + JobScheduler& scheduler = GetJobScheduler(); + scheduler.WaitForGroup (gParticleSystemManager.jobGroup); + PROFILER_END; + +#endif + const float deltaTimeEpsilon = 0.0001f; + float deltaTime = GetDeltaTime(); + if(deltaTime < deltaTimeEpsilon) + return; + + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); ++i) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + ParticleSystemState& state = *system.m_State; + system.Update2 (system, *system.m_ReadOnlyState, state, false); + } + + gParticleSystemManager.needSync = false; + } +} + +#if ENABLE_MULTITHREADED_PARTICLES +void* ParticleSystem::UpdateFunction (void* rawData) +{ + Assert (rawData); + ParticleSystem* system = reinterpret_cast<ParticleSystem*>(rawData); + ParticleSystem::Update1 (*system, system->GetParticles((int)ParticleSystem::kParticleBuffer0), system->GetThreadScratchPad().deltaTime, false, false, system->m_RayBudget); + return NULL; +} +#endif + +#define MAX_NUM_SUB_EMIT_CMDS (1024) +void ParticleSystem::Update(ParticleSystem& system, float deltaTime, bool fixedTimeStep, bool useProcedural, int rayBudget) +{ + system.m_State->recordSubEmits = false; + Update0 (system, *system.m_ReadOnlyState, *system.m_State, deltaTime, fixedTimeStep); + Update1 (system, system.GetParticles((int)ParticleSystem::kParticleBuffer0), deltaTime, fixedTimeStep, useProcedural, rayBudget); + Update2 (system, *system.m_ReadOnlyState, *system.m_State, fixedTimeStep); +#if UNITY_EDITOR + ParticleSystemEditor::PerformInterpolationStep(&system); +#endif +} + +size_t ParticleSystem::EmitFromModules (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemEmissionState& emissionState, size_t& numContinuous, const Vector3f velocity, float fromT, float toT, float dt) +{ + if(system.m_EmissionModule.GetEnabled()) + return EmitFromData(emissionState, numContinuous, system.m_EmissionModule.GetEmissionDataRef(), velocity, fromT, toT, dt, roState.lengthInSec); + return 0; +} + +size_t ParticleSystem::EmitFromData (ParticleSystemEmissionState& emissionState, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length) +{ + size_t amountOfParticlesToEmit = 0; + EmissionModule::Emit(emissionState, amountOfParticlesToEmit, numContinuous, emissionData, velocity, fromT, toT, dt, length); + return amountOfParticlesToEmit; +} + +size_t ParticleSystem::LimitParticleCount(size_t requestSize) const +{ + const size_t maxNumParticles = m_InitialModule.GetMaxNumParticles(); + return min<size_t>(requestSize, maxNumParticles); +} + +size_t ParticleSystem::AddNewParticles(ParticleSystemParticles& particles, size_t newParticles) const +{ + const size_t fromIndex = particles.array_size(); + const size_t newSize = LimitParticleCount(fromIndex + newParticles); + particles.array_resize(newSize); + return min(fromIndex, newSize); +} + +void ParticleSystem::SimulateParticles (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt) +{ + size_t particleCount = ps.array_size(); + for (size_t q = fromIndex; q < particleCount; ) + { + ps.lifetime[q] -= dt; + +#if UNITY_EDITOR + if(ParticleSystemEditor::GetIsExtraInterpolationStep()) + { + ps.lifetime[q] = max(ps.lifetime[q], 0.0f); + } + else +#endif + + if (ps.lifetime[q] < 0) + { + KillParticle(roState, state, ps, q, particleCount); + continue; + } + ++q; + } + ps.array_resize(particleCount); + + for (size_t q = fromIndex; q < particleCount; ++q) + ps.position[q] += (ps.velocity[q] + ps.animatedVelocity[q]) * dt; + + if(ps.usesRotationalSpeed) + for (size_t q = fromIndex; q < particleCount; ++q) + ps.rotation[q] += ps.rotationalSpeed[q] * dt; +} + +// Copy staging particles into real particle buffer +void ParticleSystem::AddStagingBuffer(ParticleSystem& system) +{ + if(0 == system.m_ParticlesStaging.array_size()) + return; + + bool needsAxisOfRotation = system.m_Particles[kParticleBuffer0].usesAxisOfRotation; + bool needsEmitAccumulator = system.m_Particles[kParticleBuffer0].numEmitAccumulators > 0; + + ASSERT_RUNNING_ON_MAIN_THREAD; + const int numParticles = system.m_Particles[kParticleBuffer0].array_size(); + const int numStaging = system.m_ParticlesStaging.array_size(); + system.m_Particles->array_resize(numParticles + numStaging); + system.m_Particles->array_merge_preallocated(system.m_ParticlesStaging, numParticles, needsAxisOfRotation, needsEmitAccumulator); + system.m_ParticlesStaging.array_resize(0); +} + +void ParticleSystem::Emit(ParticleSystem& system, const SubEmitterEmitCommand& command, ParticleSystemEmitMode emitMode) +{ + int amountOfParticlesToEmit = command.particlesToEmit; + if (amountOfParticlesToEmit > 0) + { + ParticleSystemState& state = *system.m_State; + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + + const int numContinuous = command.particlesToEmitContinuous; + const float deltaTime = command.deltaTime; + float parentT = command.parentT; + Vector3f position = command.position; + Vector3f initialVelocity = command.velocity; + + Matrix3x3f rotMat; + Vector3f normalizedVelocity = NormalizeSafe(initialVelocity); + float angle = Abs(Dot(normalizedVelocity, Vector3f::zAxis)); + Vector3f up = Lerp(Vector3f::zAxis, Vector3f::yAxis, angle); + if (!LookRotationToMatrix(normalizedVelocity, up, &rotMat)) + rotMat.SetIdentity(); + + Matrix4x4f parentParticleMatrix = rotMat; + parentParticleMatrix.SetPosition(position); + + // Transform into local space of sub emitter + Matrix4x4f concatMatrix; + if(roState.useLocalSpace) + MultiplyMatrices3x4(state.worldToLocal, parentParticleMatrix, concatMatrix); + else + concatMatrix = parentParticleMatrix; + + if(roState.useLocalSpace) + initialVelocity = state.worldToLocal.MultiplyVector3(initialVelocity); + + DebugAssert(state.GetIsSubEmitter()); + DebugAssert(state.stopEmitting); + + const float commandAliveTime = command.timeAlive; + const float timeStep = GetTimeStep(commandAliveTime, true); + + // @TODO: Perform culling: if max lifetime < timeAlive, then just skip emit + + // Perform sub emitter loop + if(roState.looping) + parentT = fmodf(parentT, roState.lengthInSec); + + ParticleSystemParticles& particles = (kParticleSystemEMDirect == emitMode) ? system.m_Particles[kParticleBuffer0] : system.m_ParticlesStaging; + + size_t fromIndex = system.AddNewParticles(particles, amountOfParticlesToEmit); + StartModules (system, roState, state, command.emissionState, initialVelocity, concatMatrix, particles, fromIndex, deltaTime, parentT, numContinuous, 0.0f); + + // Make sure particles get updated + if(0 == fromIndex) + system.KeepUpdating(); + + // Update incremental + if(timeStep > 0.0001f) + { + float accumulatedDt = commandAliveTime; + while (accumulatedDt >= timeStep) + { + accumulatedDt -= timeStep; + UpdateModulesIncremental(system, roState, state, particles, fromIndex, timeStep); + } + } + } +} + + +void ParticleSystem::PlaybackSubEmitterCommandBuffer(const ParticleSystem& parent, ParticleSystemState& state, bool fixedTimeStep) +{ + ParticleSystem* subEmittersBirth[kParticleSystemMaxSubBirth]; + ParticleSystem* subEmittersCollision[kParticleSystemMaxSubCollision]; + ParticleSystem* subEmittersDeath[kParticleSystemMaxSubDeath]; + int numBirth = parent.m_SubModule->GetSubEmitterPtrsBirth(&subEmittersBirth[0]); + int numCollision = parent.m_SubModule->GetSubEmitterPtrsCollision(&subEmittersCollision[0]); + int numDeath = parent.m_SubModule->GetSubEmitterPtrsDeath(&subEmittersDeath[0]); + + const int numCommands = state.subEmitterCommandBuffer.commandCount; + const SubEmitterEmitCommand* commands = state.subEmitterCommandBuffer.commands; + for(int i = 0; i < numCommands; i++) + { + const SubEmitterEmitCommand& command = commands[i]; + ParticleSystem* shuriken = NULL; + if(command.subEmitterType == kParticleSystemSubTypeBirth) + shuriken = (command.subEmitterIndex < numBirth) ? subEmittersBirth[command.subEmitterIndex] : NULL; + else if(command.subEmitterType == kParticleSystemSubTypeCollision) + shuriken = (command.subEmitterIndex < numCollision) ? subEmittersCollision[command.subEmitterIndex] : NULL; + else if(command.subEmitterType == kParticleSystemSubTypeDeath) + shuriken = (command.subEmitterIndex < numDeath) ? subEmittersDeath[command.subEmitterIndex] : NULL; + else + Assert(!"PlaybackSubEmitterCommandBuffer: Sub emitter type not implemented"); + + DebugAssert(shuriken && "Y U NO HERE ANYMORE?"); + if(!shuriken) + continue; + + ParticleSystem::Emit(*shuriken, command, kParticleSystemEMDirect); + } + + state.subEmitterCommandBuffer.commandCount = 0; +} + +void ParticleSystem::UpdateBounds(const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemState& state) +{ + const size_t particleCount = ps.array_size(); + if (particleCount == 0) + { + state.minMaxAABB = MinMaxAABB (Vector3f::zero, Vector3f::zero); + return; + } + + state.minMaxAABB.Init(); + + if(CheckSupportsProcedural(system)) + { + const Transform& transform = system.GetComponent (Transform); + Matrix4x4f localToWorld = transform.GetLocalToWorldMatrixNoScale (); + + // Lifetime and max speed + const Vector2f minMaxLifeTime = system.m_InitialModule.GetLifeTimeCurve().FindMinMax(); + + const float maxLifeTime = minMaxLifeTime.y; + const Vector2f minMaxStartSpeed = system.m_InitialModule.GetSpeedCurve().FindMinMax() * maxLifeTime; + + state.minMaxAABB = MinMaxAABB(Vector3f::zero, Vector3f::zero); + state.minMaxAABB.Encapsulate(Vector3f::zAxis * minMaxStartSpeed.x); + state.minMaxAABB.Encapsulate(Vector3f::zAxis * minMaxStartSpeed.y); + if(system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.CalculateProceduralBounds(state.minMaxAABB, state.emitterScale, minMaxStartSpeed); + + // Gravity + // @TODO: Do what we do for force and velocity here, i.e. transform bounds properly + const Vector3f gravityBounds = system.m_InitialModule.GetGravity(*system.m_ReadOnlyState, *system.m_State) * maxLifeTime * maxLifeTime * 0.5f; + + state.minMaxAABB.m_Max += max(gravityBounds, Vector3f::zero); + state.minMaxAABB.m_Min += min(gravityBounds, Vector3f::zero); + + MinMaxAABB velocityBounds (Vector3f::zero, Vector3f::zero); + if(system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->CalculateProceduralBounds(velocityBounds, localToWorld, maxLifeTime); + state.minMaxAABB.m_Max += velocityBounds.m_Max; + state.minMaxAABB.m_Min += velocityBounds.m_Min; + + MinMaxAABB forceBounds (Vector3f::zero, Vector3f::zero); + if(system.m_ForceModule->GetEnabled()) + system.m_ForceModule->CalculateProceduralBounds(forceBounds, localToWorld, maxLifeTime); + state.minMaxAABB.m_Max += forceBounds.m_Max; + state.minMaxAABB.m_Min += forceBounds.m_Min; + + // Modules that are not supported by procedural + DebugAssert(!system.m_RotationBySpeedModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ClampVelocityModule->GetEnabled()); // unsupported: (Need to compute velocity by deriving position curves...), possible to support? + DebugAssert(!system.m_CollisionModule->GetEnabled()); + DebugAssert(!system.m_SubModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ExternalForcesModule->GetEnabled()); + } + else + { + for (size_t q = 0; q < particleCount; ++q) + state.minMaxAABB.Encapsulate (ps.position[q]); + + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && (renderer->GetRenderMode() == kSRMStretch3D)) + { + const float velocityScale = renderer->GetVelocityScale(); + const float lengthScale = renderer->GetLengthScale(); + for(size_t q = 0; q < particleCount; ++q ) + { + float sqrVelocity = SqrMagnitude (ps.velocity[q]+ps.animatedVelocity[q]); + if (sqrVelocity > Vector3f::epsilon) + { + float scale = velocityScale + FastInvSqrt (sqrVelocity) * lengthScale * ps.size[q]; + state.minMaxAABB.Encapsulate(ps.position[q] - ps.velocity[q] * scale); + } + } + } + } + + // Expand with maximum particle size * sqrt(2) (the length of a diagonal of a particle, if it should happen to be rotated) + const float kSqrtOf2 = 1.42f; + float maxSize = kSqrtOf2 * 0.5f * system.m_InitialModule.GetSizeCurve().FindMinMax().y; + if(system.m_SizeModule->GetEnabled()) + maxSize *= system.m_SizeModule->GetCurve().FindMinMax().y; + if(system.m_SizeBySpeedModule->GetEnabled()) + maxSize *= system.m_SizeBySpeedModule->GetCurve().FindMinMax().y; + state.minMaxAABB.Expand (max(maxSize, state.maxSize)); + + Assert (state.minMaxAABB.IsValid ()); +} + +IMPLEMENT_CLASS_HAS_INIT (ParticleSystem) +IMPLEMENT_OBJECT_SERIALIZE (ParticleSystem) + + +template<class TransferFunction> +void ParticleSystem::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + + m_ReadOnlyState->Transfer (transfer); m_ReadOnlyState->CheckConsistency(); + m_State->Transfer (transfer); + transfer.Transfer(m_InitialModule, m_InitialModule.GetName ()); m_InitialModule.CheckConsistency (); + transfer.Transfer(m_ShapeModule, m_ShapeModule.GetName ()); m_ShapeModule.CheckConsistency (); + transfer.Transfer(m_EmissionModule, m_EmissionModule.GetName ()); m_EmissionModule.CheckConsistency (); + transfer.Transfer(*m_SizeModule, m_SizeModule->GetName ()); m_SizeModule->CheckConsistency (); + transfer.Transfer(*m_RotationModule, m_RotationModule->GetName ()); m_RotationModule->CheckConsistency (); + transfer.Transfer(*m_ColorModule, m_ColorModule->GetName ()); m_ColorModule->CheckConsistency (); + transfer.Transfer(*m_UVModule, m_UVModule->GetName ()); m_UVModule->CheckConsistency (); + transfer.Transfer(*m_VelocityModule, m_VelocityModule->GetName ()); m_VelocityModule->CheckConsistency (); + transfer.Transfer(*m_ForceModule, m_ForceModule->GetName ()); m_ForceModule->CheckConsistency (); + transfer.Transfer(*m_ExternalForcesModule, m_ExternalForcesModule->GetName ()); m_ExternalForcesModule->CheckConsistency (); + transfer.Transfer(*m_ClampVelocityModule, m_ClampVelocityModule->GetName ()); m_ClampVelocityModule->CheckConsistency (); + transfer.Transfer(*m_SizeBySpeedModule, m_SizeBySpeedModule->GetName ()); m_SizeBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_RotationBySpeedModule, m_RotationBySpeedModule->GetName ()); m_RotationBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_ColorBySpeedModule, m_ColorBySpeedModule->GetName ()); m_ColorBySpeedModule->CheckConsistency (); + transfer.Transfer(*m_CollisionModule, m_CollisionModule->GetName ()); m_CollisionModule->CheckConsistency (); + transfer.Transfer(*m_SubModule, m_SubModule->GetName ()); m_SubModule->CheckConsistency (); + if(transfer.IsReading()) + { + m_State->supportsProcedural = DetermineSupportsProcedural(*this); + m_State->invalidateProcedural = true; // Stuff might have changed which we can't support (example: start speed has become smaller) + } +} + +bool ParticleSystem::CheckSupportsProcedural(const ParticleSystem& system) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && (renderer->GetRenderMode() == kSRMStretch3D)) + return false; + return system.m_State->supportsProcedural && !system.m_State->invalidateProcedural; +} + +// TODO: Needs to match UpdateCullingSupportedString in all xxModule.cs files +bool ParticleSystem::DetermineSupportsProcedural(const ParticleSystem& system) +{ + bool supportsProcedural = true; + supportsProcedural = supportsProcedural && system.m_ReadOnlyState->useLocalSpace; + + // Can't be random as we recreate it every frame. TODO: Would be great if we can support this in procedural mode. + const short lifeTimeMinMaxState = system.m_InitialModule.GetLifeTimeCurve().minMaxState; + supportsProcedural = supportsProcedural && (lifeTimeMinMaxState == kMMCScalar || lifeTimeMinMaxState == kMMCCurve); + + supportsProcedural = supportsProcedural && (EmissionModule::kEmissionTypeDistance != system.m_EmissionModule.GetEmissionDataRef().type); + + supportsProcedural = supportsProcedural && !system.m_ExternalForcesModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_ClampVelocityModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_RotationBySpeedModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_CollisionModule->GetEnabled(); + supportsProcedural = supportsProcedural && !system.m_SubModule->GetEnabled(); + + if(system.m_RotationModule->GetEnabled()) + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_RotationModule->GetCurve().editorCurves, system.m_RotationModule->GetCurve().minMaxState); + + if(system.m_VelocityModule->GetEnabled()) + { + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetXCurve().editorCurves, system.m_VelocityModule->GetXCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetYCurve().editorCurves, system.m_VelocityModule->GetYCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_VelocityModule->GetZCurve().editorCurves, system.m_VelocityModule->GetZCurve().minMaxState); + } + + if(system.m_ForceModule->GetEnabled()) + { + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetXCurve().editorCurves, system.m_ForceModule->GetXCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetYCurve().editorCurves, system.m_ForceModule->GetYCurve().minMaxState); + supportsProcedural = supportsProcedural && CurvesSupportProcedural (system.m_ForceModule->GetZCurve().editorCurves, system.m_ForceModule->GetZCurve().minMaxState); + supportsProcedural = supportsProcedural && !system.m_ForceModule->GetRandomizePerFrame(); + } + + return supportsProcedural; +} + +bool ParticleSystem::ComputePrewarmStartParameters(float& prewarmTime, float t) +{ + const float fixedDelta = GetTimeManager().GetFixedDeltaTime(); + const float maxLifetime = m_InitialModule.GetLifeTimeCurve().FindMinMax().y; + const float length = m_ReadOnlyState->lengthInSec; + if(!m_ReadOnlyState->looping && ((maxLifetime + length) < t)) + return false; + + prewarmTime = m_SubModule->GetEnabled() ? CalculateSubEmitterMaximumLifeTime(maxLifetime) : 0.0f; + prewarmTime = max(prewarmTime, maxLifetime); + + float frac = fmodf(t, fixedDelta); + float startT = t - prewarmTime - frac; + prewarmTime += frac; + + // Clamp to start + if(!m_ReadOnlyState->prewarm) + { + startT = max(startT, 0.0f); + prewarmTime = min(t, prewarmTime); + } + + // This is needed to figure out if emitacc should be inverted or not + const float signStartT = Sign(startT); + const float absStartT = Abs(startT); + + while(startT < 0.0f) + startT += length; + float endT = startT + absStartT; + m_State->t = fmodf(startT, length); + + ParticleSystemEmissionState emissionState; + const Vector3f emitVelocity = Vector3f::zero; + const float epsilon = 0.0001f; + int seedIndex = 0; + + const float prevStartT = startT; + const float nextStartT = startT + fixedDelta; + const float prevEndT = endT; + const float nextEndT = endT + fixedDelta; + if (!(nextStartT > prevStartT && nextEndT > prevEndT)) + { + ErrorStringObject ("Precision issue while prewarming particle system - 'Duration' or 'Start Lifetime' is likely a too large value.", this); + return false; + } + + while((startT + epsilon) < endT) + { + const float toT = fmodf(startT + fixedDelta, length); + const float fromT = fmodf(startT, length); + + size_t numContinuous; + int numParticles = ParticleSystem::EmitFromModules(*this, *m_ReadOnlyState, emissionState, numContinuous, emitVelocity, fromT, toT, fixedDelta); + if(numParticles > 0) + seedIndex++; + startT += fixedDelta; + } + m_State->emissionState.m_ToEmitAccumulator = ((signStartT > 0.0f ? emissionState.m_ToEmitAccumulator : 1.0f - emissionState.m_ToEmitAccumulator)) + epsilon; +#if UNITY_EDITOR + m_EditorRandomSeedIndex = signStartT > 0.0f ? seedIndex : -seedIndex-1; +#endif + + return true; +} + +void ParticleSystem::Simulate (float t, bool restart) +{ + PROFILER_AUTO(gParticleSystemPrewarm, NULL) + + if(restart) + { + m_InitialModule.ResetSeed(*m_ReadOnlyState); + m_ShapeModule.ResetSeed(*m_ReadOnlyState); + Stop(); + Clear(); + Play (false); + + ApplyStartDelay (m_State->delayT, t); + float prewarmTime; + if(ComputePrewarmStartParameters(prewarmTime, t)) + { + Update(*this, prewarmTime, true, CheckSupportsProcedural(*this)); + Pause (); + } + else + { + Stop(); + Clear(); + } + } + else + { + m_State->playing = true; + Update(*this, t, true, false); + Pause (); + } +} + +void ParticleSystem::Play (bool autoPrewarm) +{ + Assert (m_State); + if(!IsActive () || m_State->GetIsSubEmitter()) + return; + + m_State->stopEmitting = false; + m_State->playing = true; + if (m_State->needRestart) + { + if(m_ReadOnlyState->prewarm) + { + if (autoPrewarm) + AutoPrewarm(); + } + else + { + m_State->delayT = m_ReadOnlyState->startDelay; + } + + m_State->playing = true; + m_State->t = 0.0f; + m_State->numLoops = 0; + m_State->invalidateProcedural = false; + m_State->accumulatedDt = 0.0f; +#if UNITY_EDITOR + m_EditorRandomSeedIndex = 0; +#endif + m_State->emissionState.Clear(); + } + + if(m_State->culled && CheckSupportsProcedural(*this)) + Cull(); + else + AddToManager(); +} + +void ParticleSystem::AutoPrewarm() +{ + if(m_ReadOnlyState->prewarm && m_ReadOnlyState->looping) + { + DebugAssert(!m_State->GetIsSubEmitter()); + DebugAssert(m_State->playing); // Only use together with play + Simulate (0.0f, true); + //DebugAssert(CompareApproximately(m_State->emissionState.m_ToEmitAccumulator, 1.0f, 0.01f) && "Particle emission gaps may occur"); + } +} + +void ParticleSystem::Stop () +{ + Assert (m_State); + m_State->needRestart = true; + m_State->stopEmitting = true; +} + +void ParticleSystem::Pause () +{ + Assert (m_State); + m_State->playing = false; + m_State->needRestart = false; + RemoveFromManager(); +} + +bool ParticleSystem::IsPaused () const +{ + Assert (m_State); + return !m_State->playing && !m_State->needRestart; +} + +bool ParticleSystem::IsStopped () const +{ + Assert (m_State); + return !m_State->playing && m_State->needRestart; +} + +void ParticleSystem::KeepUpdating() +{ + if (IsActive()) + { + // Ensure added particles will update, but stop emission + m_State->playing = true; + m_State->stopEmitting = true; + AddToManager(); + } +} + +void ParticleSystem::Emit (int count) +{ + // StartParticles() takes size_t so check for negative value here (case 495098) + if (count <= 0) + return; + + KeepUpdating(); + Assert (m_State); + + const Transform& transform = GetComponent (Transform); + Matrix4x4f oldLocalToWorld = m_State->localToWorld; + Matrix4x4f oldWorldToLocal = m_State->worldToLocal; + Vector3f oldEmitterScale = m_State->emitterScale; + m_State->localToWorld = transform.GetLocalToWorldMatrixNoScale (); + Matrix4x4f::Invert_General3D(m_State->localToWorld, m_State->worldToLocal); + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_2_a1)) + m_State->emitterScale = transform.GetWorldScaleLossy(); + else + m_State->emitterScale = transform.GetLocalScale(); + StartParticles(*this, m_Particles[kParticleBuffer0], 0.0f, m_State->t, 0.0f, 0, count, 0.0f); + m_State->localToWorld = oldLocalToWorld; + m_State->worldToLocal = oldWorldToLocal; + m_State->emitterScale = oldEmitterScale; +} + +void ParticleSystem::EmitParticleExternal(ParticleSystemParticle* particle) +{ +#if UNITY_EDITOR + if(!IsWorldPlaying() && IsStopped ()) + return; +#endif + + m_State->invalidateProcedural = true; + + CheckParticleConsistency(*m_State, *particle); + if(particle->lifetime <= 0.0f) + return; + + KeepUpdating(); + + m_Particles[kParticleBuffer0].AddParticle(particle); + +#if UNITY_EDITOR + if(!IsWorldPlaying()) + m_Particles[kParticleBuffer1].AddParticle(particle); +#endif + + if(!IsPlaying()) + UpdateBounds(*this, m_Particles[kParticleBuffer0], *m_State); +} + +void ParticleSystem::Clear (bool updateBounds) +{ + for(int i = 0; i < kNumParticleBuffers; i++) + m_Particles[i].array_resize(0); + m_ParticlesStaging.array_resize(0); + m_State->emitReplay.resize_uninitialized(0); + + if (m_State->stopEmitting) + { + // This triggers sometimes, why? (case 491684) + DebugAssert (m_State->needRestart); + m_State->playing = false; + RemoveFromManager(); + } + + if(updateBounds) + { + UpdateBounds(*this, m_Particles[kParticleBuffer0], *m_State); + Update2(*this, *m_ReadOnlyState, *m_State, false); + } +} + +float ParticleSystem::GetStartDelay() const +{ + Assert(m_State); + return m_ReadOnlyState->startDelay; +} + +void ParticleSystem::SetStartDelay(float value) +{ + Assert(m_State); + m_ReadOnlyState->startDelay = value; + SetDirty (); +} + +bool ParticleSystem::IsAlive () const +{ + Assert(m_State); + return (!IsStopped() || (GetParticleCount() > 0)); +} + +bool ParticleSystem::IsPlaying () const +{ + Assert (m_State); + return m_State->playing; +} + +bool ParticleSystem::GetLoop () const +{ + Assert (m_State); + return m_ReadOnlyState->looping; +} + +void ParticleSystem::SetLoop (bool loop) +{ + Assert (m_State); + m_ReadOnlyState->looping = loop; + SetDirty (); +} + +bool ParticleSystem::GetPlayOnAwake () const +{ + Assert (m_State); + return m_ReadOnlyState->playOnAwake; +} + +void ParticleSystem::SetPlayOnAwake (bool playOnAwake) +{ + Assert (m_State); + m_ReadOnlyState->playOnAwake = playOnAwake; + SetDirty (); +} + +ParticleSystemSimulationSpace ParticleSystem::GetSimulationSpace () const +{ + Assert (m_State); + return (m_ReadOnlyState->useLocalSpace ? kSimLocal : kSimWorld); +} + +void ParticleSystem::SetSimulationSpace (ParticleSystemSimulationSpace simulationSpace) +{ + Assert (m_State); + m_ReadOnlyState->useLocalSpace = (simulationSpace == kSimLocal); + SetDirty (); +} + +float ParticleSystem::GetSecPosition () const +{ + Assert (m_State); + return m_State->t; +} + +void ParticleSystem::SetSecPosition (float pos) +{ + Assert (m_State); + m_State->t = pos; + SetDirty (); +} + +float ParticleSystem::GetPlaybackSpeed () const +{ + Assert (m_State); + return m_ReadOnlyState->speed; +} + +void ParticleSystem::SetPlaybackSpeed (float speed) +{ + Assert (m_State); + m_ReadOnlyState->speed = speed; + SetDirty (); +} + +float ParticleSystem::GetLengthInSec () const +{ + Assert (m_State); + return m_ReadOnlyState->lengthInSec; +} + +void ParticleSystem::GetNumTiles(int& uvTilesX, int& uvTilesY) const +{ + uvTilesX = uvTilesY = 1; + if(m_UVModule->GetEnabled()) + m_UVModule->GetNumTiles(uvTilesX, uvTilesY); +} + +Matrix4x4f ParticleSystem::GetLocalToWorldMatrix() const +{ + return m_State->localToWorld; +} + +bool ParticleSystem::GetEnableEmission() const +{ + return m_EmissionModule.GetEnabled(); +} + +void ParticleSystem::SetEnableEmission(bool value) +{ + m_State->invalidateProcedural = true; + m_EmissionModule.SetEnabled(value); + SetDirty(); +} + +float ParticleSystem::GetEmissionRate() const +{ + return m_EmissionModule.GetEmissionDataRef().rate.GetScalar(); +} + +void ParticleSystem::SetEmissionRate(float value) +{ + m_State->invalidateProcedural = true; + m_EmissionModule.GetEmissionData().rate.SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartSpeed() const +{ + return m_InitialModule.GetSpeedCurve().GetScalar(); +} + +void ParticleSystem::SetStartSpeed(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.GetSpeedCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartSize() const +{ + return m_InitialModule.GetSizeCurve().GetScalar(); +} + +void ParticleSystem::SetStartSize(float value) +{ + m_InitialModule.GetSizeCurve().SetScalar(value); + SetDirty(); +} + +ColorRGBAf ParticleSystem::GetStartColor() const +{ + return m_InitialModule.GetColor().maxColor; +} + +void ParticleSystem::SetStartColor(ColorRGBAf value) +{ + m_InitialModule.GetColor().maxColor = value; + SetDirty(); +} + +float ParticleSystem::GetStartRotation() const +{ + return m_InitialModule.GetRotationCurve().GetScalar(); +} + +void ParticleSystem::SetStartRotation(float value) +{ + m_InitialModule.GetRotationCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetStartLifeTime() const +{ + return m_InitialModule.GetLifeTimeCurve().GetScalar(); +} + +void ParticleSystem::SetStartLifeTime(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.GetLifeTimeCurve().SetScalar(value); + SetDirty(); +} + +float ParticleSystem::GetGravityModifier() const +{ + return m_InitialModule.GetGravityModifier(); +} + +void ParticleSystem::SetGravityModifier(float value) +{ + m_State->invalidateProcedural = true; + m_InitialModule.SetGravityModifier(value); + SetDirty(); +} + +UInt32 ParticleSystem::GetRandomSeed() const +{ + return m_ReadOnlyState->randomSeed; +} + +void ParticleSystem::SetRandomSeed(UInt32 value) +{ + m_ReadOnlyState->randomSeed = value; + SetDirty(); +} + +int ParticleSystem::GetMaxNumParticles () const +{ + return m_InitialModule.GetMaxNumParticles (); +} + +void ParticleSystem::SetMaxNumParticles (int value) +{ + m_InitialModule.SetMaxNumParticles (value); + SetDirty(); +} + +void ParticleSystem::AllocateAllStructuresOfArrays() +{ + // Make sure all particle buffers have all elements + for(int i = 0; i < kNumParticleBuffers; i++) + { + if(!m_Particles[i].usesAxisOfRotation) + m_Particles[i].SetUsesAxisOfRotation(); + m_Particles[i].SetUsesEmitAccumulator(kParticleSystemMaxNumEmitAccumulators); + } +} + +void ParticleSystem::SetParticlesExternal (ParticleSystemParticle* particles, int size) +{ +#if UNITY_EDITOR + if(!IsWorldPlaying() && IsStopped ()) + return; +#endif + + m_State->invalidateProcedural = true; + + // Make sure particles are in the correct ranges etc. + for (size_t q = 0; q < size; q++) + CheckParticleConsistency(*m_State, particles[q]); + + AllocateAllStructuresOfArrays(); + ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + ps.array_resize(size); + ps.CopyFromArrayAOS(particles, size); + size_t particleCount = size; + for (size_t q = 0; q < particleCount; ) + { + if (ps.lifetime[q] < 0) + { + KillParticle(*m_ReadOnlyState, *m_State, ps, q, particleCount); + continue; + } + ++q; + } + ps.array_resize(particleCount); + + #if UNITY_EDITOR + if(!IsWorldPlaying()) + m_Particles[kParticleBuffer1].array_assign(ps); + #endif + + if(!IsPlaying()) + UpdateBounds(*this, ps, *m_State); +} + +void ParticleSystem::GetParticlesExternal (ParticleSystemParticle* particles, int size) +{ + AllocateAllStructuresOfArrays(); + ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + ps.CopyToArrayAOS(particles, size); +} + +int ParticleSystem::GetSafeCollisionEventSize () const +{ + const ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + return ps.collisionEvents.GetCollisionEventCount (); +} + +int ParticleSystem::GetCollisionEventsExternal (int instanceID, MonoParticleCollisionEvent* collisionEvents, int size) const +{ + const ParticleSystemParticles& ps = m_Particles[kParticleBuffer0]; + return ps.collisionEvents.GetCollisionEvents (instanceID, collisionEvents, size); +} + +ParticleSystemParticles& ParticleSystem::GetParticles (int index) +{ +#if UNITY_EDITOR + if(index == -1) + if(ParticleSystemEditor::UseInterpolation(*this)) + index = (int)kParticleBuffer1; + else + index = (int)kParticleBuffer0; + return m_Particles[index]; +#else + return m_Particles[kParticleBuffer0]; +#endif +} + +const ParticleSystemParticles& ParticleSystem::GetParticles (int index) const +{ +#if UNITY_EDITOR + if(index == -1) + if(ParticleSystemEditor::UseInterpolation(*this)) + index = (int)kParticleBuffer1; + else + index = (int)kParticleBuffer0; + return m_Particles[index]; +#else + return m_Particles[kParticleBuffer0]; +#endif +} + +size_t ParticleSystem::GetParticleCount () const +{ + return m_Particles[kParticleBuffer0].array_size (); +} + +int ParticleSystem::SetupSubEmitters(ParticleSystem& shuriken, ParticleSystemState& state) +{ + Assert(!state.cachedSubDataBirth && !state.numCachedSubDataBirth); + Assert(!state.cachedSubDataCollision && !state.numCachedSubDataCollision); + Assert(!state.cachedSubDataDeath && !state.numCachedSubDataDeath); + int subEmitterCount = 0; + + if(shuriken.m_SubModule->GetEnabled()) + { + ParticleSystem* subEmittersBirth[kParticleSystemMaxSubBirth]; + state.numCachedSubDataBirth = shuriken.m_SubModule->GetSubEmitterPtrsBirth(&subEmittersBirth[0]); + state.cachedSubDataBirth = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataBirth); + std::uninitialized_fill (state.cachedSubDataBirth, state.cachedSubDataBirth + state.numCachedSubDataBirth, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataBirth; i++) + { + ParticleSystem* subEmitter = subEmittersBirth[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataBirth[i]; + subData.startDelayInSec = subEmitter->m_ReadOnlyState->startDelay; + subData.lengthInSec = subEmitter->GetLoop() ? numeric_limits<float>::max() : subEmitter->GetLengthInSec(); + subData.maxLifetime = subEmitter->m_InitialModule.GetLifeTimeCurve().GetScalar(); + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + ParticleSystem* subEmittersCollision[kParticleSystemMaxSubCollision]; + state.numCachedSubDataCollision = shuriken.m_SubModule->GetSubEmitterPtrsCollision(&subEmittersCollision[0]); + state.cachedSubDataCollision = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataCollision); + std::uninitialized_fill (state.cachedSubDataCollision, state.cachedSubDataCollision + state.numCachedSubDataCollision, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataCollision; i++) + { + ParticleSystem* subEmitter = subEmittersCollision[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataCollision[i]; + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + ParticleSystem* subEmittersDeath[kParticleSystemMaxSubDeath]; + state.numCachedSubDataDeath = shuriken.m_SubModule->GetSubEmitterPtrsDeath(&subEmittersDeath[0]); + state.cachedSubDataDeath = ALLOC_TEMP_MANUAL(ParticleSystemSubEmitterData, state.numCachedSubDataDeath); + std::uninitialized_fill (state.cachedSubDataDeath, state.cachedSubDataDeath + state.numCachedSubDataDeath, ParticleSystemSubEmitterData()); + for(int i = 0; i < state.numCachedSubDataDeath; i++) + { + ParticleSystem* subEmitter = subEmittersDeath[i]; + ParticleSystemSubEmitterData& subData = state.cachedSubDataDeath[i]; + subData.emitter = subEmitter; + subEmitter->m_EmissionModule.GetEmissionDataCopy(&subData.emissionData); + subEmitter->m_State->SetIsSubEmitter(true); + subEmitterCount++; + } + } + return subEmitterCount; +} + +int ParticleSystem::CalculateMaximumSubEmitterEmitCount(ParticleSystem& shuriken, ParticleSystemState& state, float deltaTime, bool fixedTimeStep) +{ + // Save emission state + const ParticleSystemEmissionState orgEmissionState = state.emissionState; + ParticleSystemEmissionState emissionState; + + // Simulate emission + float timeStep = GetTimeStep(deltaTime, fixedTimeStep); + DebugAssert(timeStep > 0.0001f); + + // @TODO: Maybe we can go back to just picking the conservative solution, since no longer use this for scrubbing/prewarm we shouldn't allocate that much. + + + int existingParticleCount = shuriken.GetParticleCount(); + int totalPossibleEmits = 0; + float acc = deltaTime; + float fromT = state.t; + while(acc >= timeStep) + { + acc -= timeStep; + float toT = fromT + (deltaTime - acc); + float dt; + const float length = shuriken.m_ReadOnlyState->lengthInSec; + if(shuriken.m_ReadOnlyState->looping) + { + toT = fmodf (toT, length); + dt = timeStep; + } + else + { + toT = min(toT, length); + dt = toT - fromT; + } + + size_t numContinuous; + const Vector3f emitVelocity = state.emitterVelocity; + existingParticleCount += EmitFromModules(shuriken, *shuriken.m_ReadOnlyState, emissionState, numContinuous, emitVelocity, fromT, toT, dt); + totalPossibleEmits += existingParticleCount; + fromT = toT; + } + + totalPossibleEmits += existingParticleCount; + + // Restore emission state + state.emissionState = orgEmissionState; + + const int kExtra = 5; // Give a few extra to avoid denying due to rounding etc. + return totalPossibleEmits + kExtra; +} + +// @TODO: what about chained effects? +float ParticleSystem::CalculateSubEmitterMaximumLifeTime(float parentLifeTime) const +{ + ParticleSystem* subEmitters[kParticleSystemMaxSubTotal]; + m_SubModule->GetSubEmitterPtrs(subEmitters); + + float maxLifeTime = 0.0f; + for(int i = 0; i < kParticleSystemMaxSubTotal; i++) + { + if (subEmitters[i] && (subEmitters[i] != this)) + { + const float emitterMaximumLifetime = subEmitters[i]->m_InitialModule.GetLifeTimeCurve().FindMinMax().y; + maxLifeTime = max(maxLifeTime, parentLifeTime + emitterMaximumLifetime); + maxLifeTime = std::max(maxLifeTime, subEmitters[i]->CalculateSubEmitterMaximumLifeTime(parentLifeTime + emitterMaximumLifetime)); + } + } + return maxLifeTime; +} + +void ParticleSystem::SmartReset () +{ + Super::SmartReset(); + AddParticleSystemRenderer (); +} + +void ParticleSystem::AddParticleSystemRenderer () +{ + if (GameObject* go = GetGameObjectPtr ()) + { + ParticleSystemRenderer* renderer = go->QueryComponent (ParticleSystemRenderer); + if (renderer == NULL) + { + string error; + AddComponent (*go, ClassID(ParticleSystemRenderer), NULL, &error); + if (error.empty ()) + go->GetComponent (ParticleSystemRenderer).SetMaterial (GetDefaultParticleMaterial(), 0); + else + LogString (Format("%s", error.c_str())); + } + } +} + + +void ParticleSystem::SetUsesRotationalSpeed() +{ + ParticleSystemParticles& ps0 = m_Particles[kParticleBuffer0]; + if(!ps0.usesRotationalSpeed) + ps0.SetUsesRotationalSpeed (); +#if UNITY_EDITOR + ParticleSystemParticles& ps1 = m_Particles[kParticleBuffer1]; + if(!ps1.usesRotationalSpeed) + ps1.SetUsesRotationalSpeed (); +#endif + ParticleSystemParticles& pss = m_ParticlesStaging; + if(!pss.usesRotationalSpeed) + pss.SetUsesRotationalSpeed (); +} + +void ParticleSystem::SetUsesAxisOfRotation() +{ + ParticleSystemParticles& ps0 = m_Particles[kParticleBuffer0]; + if(!ps0.usesAxisOfRotation) + ps0.SetUsesAxisOfRotation (); +#if UNITY_EDITOR + ParticleSystemParticles& ps1 = m_Particles[kParticleBuffer1]; + if(!ps1.usesAxisOfRotation) + ps1.SetUsesAxisOfRotation (); +#endif + ParticleSystemParticles& pss = m_ParticlesStaging; + if(!pss.usesAxisOfRotation) + pss.SetUsesAxisOfRotation (); +} + +void ParticleSystem::SetUsesEmitAccumulator(int numAccumulators) +{ + m_Particles[kParticleBuffer0].SetUsesEmitAccumulator (numAccumulators); +#if UNITY_EDITOR + m_Particles[kParticleBuffer1].SetUsesEmitAccumulator (numAccumulators); +#endif + m_ParticlesStaging.SetUsesEmitAccumulator (numAccumulators); +} + +bool ParticleSystem::GetIsDistanceEmitter() const +{ + return (EmissionModule::kEmissionTypeDistance == m_EmissionModule.GetEmissionDataRef().type); +} + +// check if the system would like to use any raycasting in this frame +bool ParticleSystem::SystemWannaRayCast(const ParticleSystem& system) +{ + return system.IsActive() && system.m_CollisionModule && system.m_CollisionModule->GetEnabled() && system.m_CollisionModule->IsWorldCollision() && system.m_RayBudgetState.ReceiveRays(); +} + +// check if the system will actually use any raycasting in this frame +bool ParticleSystem::SystemWillRayCast(const ParticleSystem& system) +{ + return system.IsActive() && system.m_CollisionModule && system.m_CollisionModule->GetEnabled() && system.m_CollisionModule->IsWorldCollision() && (system.GetRayBudget() > 0); +} + +// dole out ray budgets to each system that will do raycasting +void ParticleSystem::AssignRayBudgets() +{ + int activeCount = gParticleSystemManager.activeEmitters.size(); + + // count the jobs and update quality setting + int numApproximateWorldCollisionJobs = 0; + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.m_RayBudgetState.SetQuality( system.m_CollisionModule->GetQuality() ); + system.SetRayBudget( 0 ); + if ( SystemWannaRayCast( system ) ) + { + if ( system.m_CollisionModule->IsApproximate() ) + { + numApproximateWorldCollisionJobs++; + } + else + { + // high quality always get to trace all rays! + system.SetRayBudget( (int)system.GetParticleCount() ); + } + } + } + if (numApproximateWorldCollisionJobs<1) return; + + // assign ray budget to particle systems + int totalBudget = GetQualitySettings().GetCurrent().particleRaycastBudget; + int raysPerSystem = std::max( 0, totalBudget / numApproximateWorldCollisionJobs ); + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( SystemWannaRayCast( system ) && system.m_CollisionModule->IsApproximate() ) + { + const int rays = std::min((int)system.GetParticleCount(),raysPerSystem); + system.SetRayBudget( rays ); + totalBudget = std::max( totalBudget-rays, 0); + } + } + + // assign any remaining rays + // TODO: possibly better to sort and go through the list incrementally updating the per system budget, than doing this hacky two pass thing + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( SystemWannaRayCast( system ) && system.m_CollisionModule->IsApproximate() ) + { + const int rays = std::min(totalBudget,(int)system.GetParticleCount()-system.GetRayBudget()); + system.SetRayBudget( system.GetRayBudget()+rays ); + totalBudget -= rays; + } + system.m_RayBudgetState.Update(); + } +} + +void ParticleSystem::BeginUpdateAll () +{ + const float deltaTimeEpsilon = 0.0001f; + float deltaTime = GetDeltaTime(); + if(deltaTime < deltaTimeEpsilon) + return; + + PROFILER_AUTO(gParticleSystemProfile, NULL) + + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + + if (!system.IsActive ()) + { + AssertStringObject( "UpdateParticle system should not happen on disabled vGO", &system); + system.RemoveFromManager(); + continue; + } + +#if ENABLE_MULTITHREADED_PARTICLES + system.m_State->recordSubEmits = true; +#else + system.m_State->recordSubEmits = false; +#endif + Update0 (system, *system.m_ReadOnlyState, *system.m_State, deltaTime, false); + } + + gParticleSystemManager.needSync = true; + + // make sure ray budgets are assigned for the frame + ParticleSystem::AssignRayBudgets(); + +#if ENABLE_MULTITHREADED_PARTICLES + JobScheduler& scheduler = GetJobScheduler(); + int activeCount = gParticleSystemManager.activeEmitters.size(); + + // count the jobs + int numActiveWorldCollisionJobs = 0; + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if ( system.GetRayBudget() > 0 ) + numActiveWorldCollisionJobs++; + } + + // add collision jobs + gParticleSystemManager.worldCollisionJobGroup = scheduler.BeginGroup(numActiveWorldCollisionJobs); + gParticleSystemManager.jobGroup = scheduler.BeginGroup(activeCount-numActiveWorldCollisionJobs); + for(int i = 0; i < activeCount; i++) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.GetThreadScratchPad().deltaTime = deltaTime; + if ( system.GetRayBudget() > 0 ) + scheduler.SubmitJob (gParticleSystemManager.worldCollisionJobGroup, ParticleSystem::UpdateFunction, &system, NULL); + else + scheduler.SubmitJob (gParticleSystemManager.jobGroup, ParticleSystem::UpdateFunction, &system, NULL); + } + scheduler.WaitForGroup(gParticleSystemManager.worldCollisionJobGroup); +#else + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + { + //printf_console("BeginUpdateAll [%d]:\n",i); + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + system.Update1 (system, system.GetParticles((int)ParticleSystem::kParticleBuffer0), deltaTime, false, false); + } +#endif + +} + +void ParticleSystem::EndUpdateAll () +{ + SyncJobs(); + + // messages + for (int i = 0; i < gParticleSystemManager.activeEmitters.size(); ++i) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + if (!system.IsActive ()) + continue; + if (!system.m_CollisionModule->GetUsesCollisionMessages ()) + continue; + ParticleSystemParticles& ps = system.GetParticles((int)ParticleSystem::kParticleBuffer0); + ps.collisionEvents.SwapCollisionEventArrays (); + ps.collisionEvents.SendCollisionEvents (system); + } + + // Remove emitters that are finished (no longer emitting) + for(int i = 0; i < gParticleSystemManager.activeEmitters.size();) + { + ParticleSystem& system = *gParticleSystemManager.activeEmitters[i]; + ParticleSystemState& state = *system.m_State; + const size_t particleCount = system.GetParticleCount(); + if ((particleCount == 0) && state.playing && state.stopEmitting) + { + // collision subemitters may not have needRestart==true when being restarted + // from a paused state + //Assert (state.needRestart); + state.playing = false; + system.RemoveFromManager(); + continue; + } + + i++; + } + + // Interpolate in the editor for very low preview speeds +#if UNITY_EDITOR + for(int i = 0; i < gParticleSystemManager.activeEmitters.size(); i++) + ParticleSystemEditor::PerformInterpolationStep(gParticleSystemManager.activeEmitters[i]); +#endif +} + +void ParticleSystem::StartParticles(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset) +{ + if (amountOfParticlesToEmit <= 0) + return; + + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + ParticleSystemState& state = *system.m_State; + size_t fromIndex = system.AddNewParticles(ps, amountOfParticlesToEmit); + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + StartModules (system, roState, state, state.emissionState, state.emitterVelocity, localToWorld, ps, fromIndex, dt, t, numContinuous, frameOffset); +} + +void ParticleSystem::StartParticlesProcedural(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset) +{ + DebugAssert(CheckSupportsProcedural(system)); + + ParticleSystemState& state = *system.m_State; + + int numParticlesRecorded = 0; + for (int i=0;i<state.emitReplay.size();i++) + numParticlesRecorded += state.emitReplay[i].particlesToEmit; + + float emissionOffset = state.emissionState.m_ToEmitAccumulator; + float emissionGap = state.emissionState.m_ParticleSpacing * dt; + amountOfParticlesToEmit = system.LimitParticleCount(numParticlesRecorded + amountOfParticlesToEmit) - numParticlesRecorded; + + if (amountOfParticlesToEmit > 0) + { + UInt32 randomSeed = 0; +#if UNITY_EDITOR + ParticleSystemEditor::UpdateRandomSeed(system); + randomSeed = ParticleSystemEditor::GetRandomSeed(system); +#endif + state.emitReplay.push_back(ParticleSystemEmitReplay(t, amountOfParticlesToEmit, emissionOffset, emissionGap, numContinuous, randomSeed)); + } +} + +void ParticleSystem::StartModules (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemEmissionState& emissionState, Vector3f initialVelocity, const Matrix4x4f& matrix, ParticleSystemParticles& ps, size_t fromIndex, float dt, float t, size_t numContinuous, float frameOffset) +{ +#if UNITY_EDITOR + ParticleSystemEditor::UpdateRandomSeed(system); + ParticleSystemEditor::ApplyRandomSeed(system, ParticleSystemEditor::GetRandomSeed(system)); +#endif + + system.m_InitialModule.Start (roState, state, ps, matrix, fromIndex, t); + if(system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.Start (roState, state, ps, matrix, fromIndex, t); + + DebugAssert(roState.lengthInSec > 0.0001f); + const float normalizedT = t / roState.lengthInSec; + DebugAssert (normalizedT >= 0.0f); + DebugAssert (normalizedT <= 1.0f); + + size_t count = ps.array_size(); + const Vector3f velocityOffset = system.m_InitialModule.GetInheritVelocity() * initialVelocity; + for(size_t q = fromIndex; q < count; q++) + { + const float randomValue = GenerateRandom(ps.randomSeed[q] + kParticleSystemStartSpeedCurveId); + ps.velocity[q] *= Evaluate (system.m_InitialModule.GetSpeedCurve(), normalizedT, randomValue); + ps.velocity[q] += velocityOffset; + } + + for(size_t q = fromIndex; q < count; ) // array size changes + { + // subFrameOffset allows particles to be spawned at increasing times, thus spacing particles within a single frame. + // For example if you spawn particles with high velocity you will get a continous streaming instead of a clump of particles. + const int particleIndex = q - fromIndex; + float subFrameOffset = (particleIndex < numContinuous) ? (float(particleIndex) + emissionState.m_ToEmitAccumulator) * emissionState.m_ParticleSpacing : 0.0f; + DebugAssert(subFrameOffset >= -0.01f); + DebugAssert(subFrameOffset <= 1.5f); // Not 1 due to possibly really bad precision + subFrameOffset = clamp01(subFrameOffset); + + // Update from curves and apply forces etc. + UpdateModulesPreSimulationIncremental (system, roState, state, ps, q, q+1, subFrameOffset * dt); + + // Position change due to where the emitter was at time of emission + ps.position[q] -= initialVelocity * (frameOffset + subFrameOffset) * dt; + + // Position, rotation and energy change due to how much the particle has travelled since time of emission + // @TODO: Call Simulate instead? + ps.lifetime[q] -= subFrameOffset * dt; + if((ps.lifetime[q] < 0.0f) && (count > 0)) + { + KillParticle(roState, state, ps, q, count); + continue; + } + + ps.position[q] += (ps.velocity[q] + ps.animatedVelocity[q]) * subFrameOffset * dt; + + if(ps.usesRotationalSpeed) + ps.rotation[q] += ps.rotationalSpeed[q] * subFrameOffset * dt; + + if(system.m_SubModule->GetEnabled()) + system.m_SubModule->Update (roState, state, ps, q, q+1, subFrameOffset * dt); + + ++q; + } + ps.array_resize(count); +} + +void ParticleSystem::UpdateModulesPreSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& particles, const size_t fromIndex, const size_t toIndex, float dt) +{ + const size_t count = particles.array_size(); + system.m_InitialModule.Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_RotationModule->GetEnabled()) + system.m_RotationModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_ForceModule->GetEnabled()) + system.m_ForceModule->Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_ExternalForcesModule->GetEnabled()) + system.m_ExternalForcesModule->Update (roState, state, particles, fromIndex, toIndex, dt); + if(system.m_ClampVelocityModule->GetEnabled()) + system.m_ClampVelocityModule->Update (roState, state, particles, fromIndex, toIndex); + if(system.m_RotationBySpeedModule->GetEnabled()) + system.m_RotationBySpeedModule->Update (roState, state, particles, fromIndex, toIndex); + + Assert(count >= toIndex); + Assert(particles.array_size() == count); +} + +void ParticleSystem::UpdateModulesPostSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& particles, const size_t fromIndex, float dt) +{ + const size_t count = particles.array_size(); + if(system.m_SubModule->GetEnabled()) + system.m_SubModule->Update (roState, state, particles, fromIndex, particles.array_size(), dt); + Assert(count == particles.array_size()); + + if(system.m_CollisionModule->GetEnabled()) + { +#if !ENABLE_MULTITHREADED_PARTICLES + PROFILER_AUTO(gParticleSystemUpdateCollisions, NULL) +#endif + system.m_CollisionModule->Update (roState, state, particles, fromIndex, dt); + } +} + +void ParticleSystem::UpdateModulesNonIncremental (const ParticleSystem& system, const ParticleSystemParticles& particles, ParticleSystemParticlesTempData& psTemp, size_t fromIndex, size_t toIndex) +{ + Assert(particles.array_size() == psTemp.particleCount); + + for(int i = fromIndex; i < toIndex; i++) + psTemp.color[i] = particles.color[i]; + for(int i = fromIndex; i < toIndex; i++) + psTemp.size[i] = particles.size[i]; + + if(system.m_ColorModule->GetEnabled()) + system.m_ColorModule->Update (particles, psTemp.color, fromIndex, toIndex); + if(system.m_ColorBySpeedModule->GetEnabled()) + system.m_ColorBySpeedModule->Update (particles, psTemp.color, fromIndex, toIndex); + if(system.m_SizeModule->GetEnabled()) + system.m_SizeModule->Update (particles, psTemp.size, fromIndex, toIndex); + if(system.m_SizeBySpeedModule->GetEnabled()) + system.m_SizeBySpeedModule->Update (particles, psTemp.size, fromIndex, toIndex); + + if (gGraphicsCaps.needsToSwizzleVertexColors) + std::transform(&psTemp.color[fromIndex], &psTemp.color[toIndex], &psTemp.color[fromIndex], SwizzleColorForPlatform); + + if(system.m_UVModule->GetEnabled()) + { + // No other systems used sheet index yet, allocate! + if(!psTemp.sheetIndex) + { + psTemp.sheetIndex = ALLOC_TEMP_MANUAL(float, psTemp.particleCount); + for(int i = 0; i < fromIndex; i++) + psTemp.sheetIndex[i] = 0.0f; + } + system.m_UVModule->Update (particles, psTemp.sheetIndex, fromIndex, toIndex); + } + else if(psTemp.sheetIndex) // if this is present with disabled module, that means we have a combined buffer with one system not using UV module, just initislize to 0.0f + for(int i = fromIndex; i < toIndex; i++) + psTemp.sheetIndex[i] = 0.0f; +} + +void ParticleSystem::UpdateModulesIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& particles, size_t fromIndex, float dt) +{ + UpdateModulesPreSimulationIncremental (system, roState, state, particles, fromIndex, particles.array_size(), dt); + SimulateParticles(roState, state, particles, fromIndex, dt); + UpdateModulesPostSimulationIncremental (system, roState, state, particles, fromIndex, dt); +} + +void ParticleSystem::Update0 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, float dt, bool fixedTimeStep) +{ + const Transform& transform = system.GetComponent (Transform); + Vector3f oldPosition = state.localToWorld.GetPosition(); + state.localToWorld = transform.GetLocalToWorldMatrixNoScale (); + Matrix4x4f::Invert_General3D(state.localToWorld, state.worldToLocal); + + if (IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_1_a1)) + state.emitterScale = transform.GetWorldScaleLossy(); + else + state.emitterScale = transform.GetLocalScale(); + + if (state.playing && (dt > 0.0001f)) + { + const Vector3f position = transform.GetPosition(); + + if (roState.useLocalSpace) + state.emitterVelocity = Vector3f::zero; + else + state.emitterVelocity = (position - oldPosition) / dt; + } + + AddStagingBuffer(system); + + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(renderer && !renderer->GetScreenSpaceRotation()) + ParticleSystemRenderer::SetUsesAxisOfRotationRec(system, true); + + if(system.m_RotationModule->GetEnabled() || system.m_RotationBySpeedModule->GetEnabled()) + system.SetUsesRotationalSpeed(); + + int subEmitterBirthTypeCount = system.m_SubModule->GetSubEmitterTypeCount(kParticleSystemSubTypeBirth); + if(system.m_SubModule->GetEnabled() && subEmitterBirthTypeCount) + system.SetUsesEmitAccumulator (subEmitterBirthTypeCount); + + int subEmitterCount = SetupSubEmitters(system, *system.m_State); + +#if ENABLE_MULTITHREADED_PARTICLES + if(state.recordSubEmits) + { + const int numCommands = min(ParticleSystem::CalculateMaximumSubEmitterEmitCount(system, *system.m_State, dt, fixedTimeStep) * subEmitterCount, MAX_NUM_SUB_EMIT_CMDS); + ParticleSystemSubEmitCmdBuffer& buffer = system.m_State->subEmitterCommandBuffer; + Assert(NULL == buffer.commands); + buffer.commands = ALLOC_TEMP_MANUAL(SubEmitterEmitCommand, numCommands); + buffer.commandCount = 0; + buffer.maxCommandCount = numCommands; + } +#endif + + if(system.m_CollisionModule->GetEnabled()) + system.m_CollisionModule->AllocateAndCache(roState, state); + if(system.m_ExternalForcesModule->GetEnabled()) + system.m_ExternalForcesModule->AllocateAndCache(roState, state); +} + +void ParticleSystem::Update1 (ParticleSystem& system, ParticleSystemParticles& ps, float dt, bool fixedTimeStep, bool useProcedural, int rayBudget) +{ + PROFILER_AUTO(gParticleSystemJobProfile, NULL) + + const ParticleSystemReadOnlyState& roState = *system.m_ReadOnlyState; + ParticleSystemState& state = *system.m_State; + state.rayBudget = rayBudget; + + // Exposed through script + dt *= std::max<float> (roState.speed, 0.0f); + + float timeStep = GetTimeStep(dt, fixedTimeStep); + if(timeStep < 0.00001f) + return; + + if (state.playing) + { + state.accumulatedDt += dt; + + if(system.GetIsDistanceEmitter()) + { + float t = state.t + state.accumulatedDt; + const float length = roState.lengthInSec; + t = roState.looping ? fmodf(t, length) : min(t, length); + size_t numContinuous = 0; + size_t amountOfParticlesToEmit = system.EmitFromModules (system, roState, state.emissionState, numContinuous, state.emitterVelocity, state.t, t, dt); + StartParticles(system, ps, state.t, t, dt, numContinuous, amountOfParticlesToEmit, 0.0f); + } + + Update1Incremental(system, roState, state, ps, 0, timeStep, useProcedural); + + if (useProcedural) + UpdateProcedural(system, roState, state, ps); + } + + UpdateBounds(system, ps, state); +} + +void ParticleSystem::Update2 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, bool fixedTimeStep) +{ + if(state.subEmitterCommandBuffer.commandCount > 0) + PlaybackSubEmitterCommandBuffer(system, state, fixedTimeStep); + state.ClearSubEmitterCommandBuffer(); + + CollisionModule::FreeCache(state); + ExternalForcesModule::FreeCache(state); + + AddStagingBuffer(system); + + // + // Update renderer + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if (renderer) + { + MinMaxAABB result; + ParticleSystemRenderer::CombineBoundsRec(system, result, true); + renderer->Update (result); + } +} + +// Returns true if update loop is executed at least once +void ParticleSystem::Update1Incremental(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt, bool useProcedural) +{ + ApplyStartDelay (state.delayT, state.accumulatedDt); + + int numTimeSteps = 0; + const int numTimeStepsTotal = int(state.accumulatedDt / dt); + + while (state.accumulatedDt >= dt) + { + const float prevT = state.t; + state.Tick (roState, dt); + const float t = state.t; + const bool timePassedDuration = t >= (roState.lengthInSec); + const float frameOffset = float(numTimeStepsTotal - 1 - numTimeSteps); + + if(!roState.looping && timePassedDuration) + system.Stop(); + + // Update simulation + if (!useProcedural) + UpdateModulesIncremental(system, roState, state, ps, fromIndex, dt); + else + for (int i=0;i<state.emitReplay.size();i++) + state.emitReplay[i].aliveTime += dt; + + // Emission + bool emit = !system.GetIsDistanceEmitter() && !state.stopEmitting; + if(emit) + { + size_t numContinuous = 0; + size_t amountOfParticlesToEmit = system.EmitFromModules (system, roState, state.emissionState, numContinuous, state.emitterVelocity, prevT, t, dt); + if(useProcedural) + StartParticlesProcedural(system, ps, prevT, t, dt, numContinuous, amountOfParticlesToEmit, frameOffset); + else + StartParticles(system, ps, prevT, t, dt, numContinuous, amountOfParticlesToEmit, frameOffset); + } + + state.accumulatedDt -= dt; + + AddStagingBuffer(system); + + // Workaround for external forces being dependent on AABB (need to update it before the next time step) + if(!useProcedural && (state.accumulatedDt >= dt) && system.m_ExternalForcesModule->GetEnabled()) + UpdateBounds(system, ps, state); + + numTimeSteps++; + } +} + +void ParticleSystem::UpdateProcedural (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps) +{ + DebugAssert(CheckSupportsProcedural(system)); + + // Clear all particles + ps.array_resize(0); + + const Matrix4x4f localToWorld = !roState.useLocalSpace ? state.localToWorld : Matrix4x4f::identity; + + // Emit all particles + for (int i=0; i<state.emitReplay.size(); i++) + { + const ParticleSystemEmitReplay& emit = state.emitReplay[i]; + +#if UNITY_EDITOR + ParticleSystemEditor::ApplyRandomSeed(system, emit.randomSeed); +#endif + //@TODO: remove passing m_State since that is very dangerous when making things procedural compatible + size_t previousParticleCount = ps.array_size(); + system.m_InitialModule.GenerateProcedural (roState, state, ps, emit); + + //@TODO: This can be moved out of the emit all particles loop... + if (system.m_ShapeModule.GetEnabled()) + system.m_ShapeModule.Start (roState, state, ps, localToWorld, previousParticleCount, emit.t); + + // Apply gravity & integrated velocity after shape module so that it picks up any changes done in shapemodule (for example rotating the velocity) + Vector3f gravity = system.m_InitialModule.GetGravity(roState, state); + float particleIndex = 0.0f; + const size_t particleCount = ps.array_size(); + for (int q = previousParticleCount; q < particleCount; q++) + { + const float normalizedT = emit.t / roState.lengthInSec; + ps.velocity[q] *= Evaluate (system.m_InitialModule.GetSpeedCurve(), normalizedT, GenerateRandom(ps.randomSeed[q] + kParticleSystemStartSpeedCurveId)); + Vector3f velocity = ps.velocity[q]; + float frameOffset = (particleIndex + emit.emissionOffset) * emit.emissionGap * float(particleIndex < emit.numContinuous); + float aliveTime = emit.aliveTime + frameOffset; + + ps.position[q] += velocity * aliveTime + gravity * aliveTime * aliveTime * 0.5F; + ps.velocity[q] += gravity * aliveTime; + + particleIndex += 1.0f; + } + + // If no particles were emitted we can get rid of the emit replay state... + if (previousParticleCount == ps.array_size()) + { + state.emitReplay[i] = state.emitReplay.back(); + state.emitReplay.pop_back(); + i--; + } + } + + if (system.m_RotationModule->GetEnabled()) + system.m_RotationModule->UpdateProcedural (state, ps); + + if (system.m_VelocityModule->GetEnabled()) + system.m_VelocityModule->UpdateProcedural (roState, state, ps); + + if (system.m_ForceModule->GetEnabled()) + system.m_ForceModule->UpdateProcedural (roState, state, ps); + + // Modules that are not supported by procedural + DebugAssert(!system.m_RotationBySpeedModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ClampVelocityModule->GetEnabled()); // unsupported: (Need to compute velocity by deriving position curves...), possible to support? + DebugAssert(!system.m_CollisionModule->GetEnabled()); + DebugAssert(!system.m_SubModule->GetEnabled()); // find out if possible to support it + DebugAssert(!system.m_ExternalForcesModule->GetEnabled()); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// // Editor only +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if UNITY_EDITOR +void ParticleSystem::TransformChanged() +{ + if(!IsWorldPlaying() && ParticleSystemEditor::GetResimulation()) + { + const Transform& transform = GetComponent (Transform); + Matrix4x4f newMatrix = transform.GetLocalToWorldMatrixNoScale (); + newMatrix.SetPosition(Vector3f::zero); + Matrix4x4f oldMatrix = m_State->localToWorld; + oldMatrix.SetPosition(Vector3f::zero); + bool rotationChanged = !CompareApproximately(oldMatrix, newMatrix); + if(m_ReadOnlyState->useLocalSpace || rotationChanged) + ParticleSystemEditor::PerformCompleteResimulation(this); + } +} +#endif + diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystem.h b/Runtime/Graphics/ParticleSystem/ParticleSystem.h new file mode 100644 index 0000000..10c8f2a --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystem.h @@ -0,0 +1,298 @@ +#ifndef SHURIKEN_H +#define SHURIKEN_H + +#include "Runtime/BaseClasses/GameObject.h" +#include "ParticleSystemParticle.h" +#include "Runtime/Utilities/LinkedList.h" +#include "Modules/InitialModule.h" +#include "Modules/ShapeModule.h" +#include "Modules/EmissionModule.h" + +struct ParticleSystemEmitReplay; +struct ParticleSystemState; +class ParticleSystemModule; +class SizeModule; +class RotationModule; +class ColorModule; +class UVModule; +class VelocityModule; +class ForceModule; +class ExternalForcesModule; +class ClampVelocityModule; +class SizeBySpeedModule; +class RotationBySpeedModule; +class ColorBySpeedModule; +class CollisionModule; +class SubModule; + +enum ParticleSystemSimulationSpace { + kSimLocal = 0, + kSimWorld = 1, +}; + +// TODO: rename +struct ParticleSystemThreadScratchPad +{ + ParticleSystemThreadScratchPad () + : deltaTime (1.0f) + {} + + float deltaTime; +}; + +enum { kGoodQualityDelay = 0, kMediumQualityDelay = 0, kLowQualityDelay = 4 }; +struct RayBudgetState +{ + void SetQuality(int quality) + { + if ( m_Quality != quality ) + { + switch (quality) + { + case 0: + m_QualityFrameDelay = kGoodQualityDelay; + break; + case 1: + m_QualityFrameDelay = kMediumQualityDelay; + break; + case 2: + m_QualityFrameDelay = kLowQualityDelay; + break; + default: + m_QualityFrameDelay = kGoodQualityDelay; + } + m_FramesRemaining = m_QualityFrameDelay; + m_Quality = quality; + }; + } + RayBudgetState() { m_Quality=m_QualityFrameDelay=m_FramesRemaining=0; } + bool ReceiveRays() const { return m_FramesRemaining==0; } + void Update() { m_FramesRemaining = ( m_FramesRemaining ? m_FramesRemaining-1 : m_QualityFrameDelay ); } + int m_Quality; + int m_QualityFrameDelay; + int m_FramesRemaining; +}; + +class ParticleSystem : public Unity::Component +{ +public: + REGISTER_DERIVED_CLASS (ParticleSystem, Unity::Component) + DECLARE_OBJECT_SERIALIZE (ParticleSystem) + + enum + { + kParticleBuffer0, +#if UNITY_EDITOR // Double buffered + interpolation + kParticleBuffer1, +#endif + kNumParticleBuffers, + }; + + ParticleSystem (MemLabelId label, ObjectCreationMode mode); + + // ParticleSystem (); declared-by-macro + + void SmartReset (); + void AddParticleSystemRenderer (); + + void Deactivate (DeactivateOperation operation); + void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + #if UNITY_EDITOR + // ParticleSystemRenderer always goes with ParticleSystem + virtual int GetCoupledComponentClassID() const { return ClassID(ParticleSystemRenderer); } + #endif + + + static void* UpdateFunction (void* rawData); + + static void BeginUpdateAll(); + static void EndUpdateAll(); + static void Update(ParticleSystem& system, float deltaTime, bool fixedTimeStep, bool useProcedural, int rayBudget = 0); + + static bool SystemWannaRayCast(const ParticleSystem& system); + static bool SystemWillRayCast(const ParticleSystem& system); + static void AssignRayBudgets(); + + static void SyncJobs(); + + static void Emit(ParticleSystem& system, const SubEmitterEmitCommand& command, ParticleSystemEmitMode emitMode); + + // Client interface + void Simulate (float t, bool restart); // Fastforwards the particle system by simulating particles over given period of time, then pauses it. + void Play (bool autoPrewarm = true); + void Stop (); + void Pause (); + void Emit (int count); + void EmitParticleExternal(ParticleSystemParticle* particle); + void Clear (bool updateBounds = true); + void AutoPrewarm(); + + bool IsAlive () const; + bool IsPlaying () const; + bool IsPaused () const; + bool IsStopped () const; + float GetStartDelay() const; + void SetStartDelay(float value); + bool GetLoop () const; + void SetLoop (bool loop); + bool GetPlayOnAwake () const; + void SetPlayOnAwake (bool playOnAwake); + ParticleSystemSimulationSpace GetSimulationSpace () const; + void SetSimulationSpace (ParticleSystemSimulationSpace simulationSpace); + float GetSecPosition () const; + void SetSecPosition (float pos); + float GetLengthInSec () const; + void SetPlaybackSpeed (float speed); + float GetPlaybackSpeed () const; + void SetRayBudget (int rayBudget); + int GetRayBudget() const; + + bool GetEnableEmission() const; + void SetEnableEmission(bool value); + float GetEmissionRate() const; + void SetEmissionRate(float value); + float GetStartSpeed() const; + void SetStartSpeed(float value); + float GetStartSize() const; + void SetStartSize(float value); + ColorRGBAf GetStartColor() const; + void SetStartColor(ColorRGBAf value); + float GetStartRotation() const; + void SetStartRotation(float value); + float GetStartLifeTime() const; + void SetStartLifeTime(float value); + float GetGravityModifier() const; + void SetGravityModifier(float value); + UInt32 GetRandomSeed() const; + void SetRandomSeed(UInt32 value); + int GetMaxNumParticles() const; + void SetMaxNumParticles(int value); + + ShapeModule& GetShapeModule () { return m_ShapeModule; } + + + Matrix4x4f GetLocalToWorldMatrix() const; + + void GetNumTiles(int& uvTilesX, int& uvTilesY) const; + + void AllocateAllStructuresOfArrays(); + void SetParticlesExternal (ParticleSystemParticle* particles, int size); + void GetParticlesExternal (ParticleSystemParticle* particles, int size); + + int GetSafeCollisionEventSize () const; + int GetCollisionEventsExternal (int instanceID, MonoParticleCollisionEvent* collisionEvents, int size) const; + + ParticleSystemParticles& GetParticles (int index = -1); + const ParticleSystemParticles& GetParticles (int index = -1) const; + size_t GetParticleCount () const; + + ParticleSystemThreadScratchPad& GetThreadScratchPad () { return m_ThreadScratchpad; } + + static void InitializeClass (); + static void CleanupClass () {}; + + void DidModifyMesh (); + void DidDeleteMesh (); + +#if UNITY_EDITOR + void TransformChanged(); +#endif + + void RendererBecameVisible(); + void RendererBecameInvisible(); + + static size_t EmitFromData (ParticleSystemEmissionState& emissionState, size_t& numContinuous, const ParticleSystemEmissionData& emissionData, const Vector3f velocity, float fromT, float toT, float dt, float length); +private: + static void Update0 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, float dt, bool fixedTimeStep); + static void Update1 (ParticleSystem& system, ParticleSystemParticles& ps, float dt, bool fixedTimeStep, bool useProcedural, int rayBudget = 0); + static void Update2 (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, bool fixedTimeStep); + static void Update1Incremental(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt, bool useProcedural); + + static size_t EmitFromModules (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemEmissionState& emissionState, size_t& numContinuous, const Vector3f velocity, float fromT, float toT, float dt); + static void StartModules (ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, const ParticleSystemEmissionState& emissionState, Vector3f initialVelocity, const Matrix4x4f& matrix, ParticleSystemParticles& ps, size_t fromIndex, float dt, float t, size_t numContinuous, float frameOffset); + static void StartParticles(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset); + static void StartParticlesProcedural(ParticleSystem& system, ParticleSystemParticles& ps, const float prevT, const float t, const float dt, const size_t numContinuous, size_t amountOfParticlesToEmit, float frameOffset); + static void UpdateProcedural(ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps); + static void UpdateModulesPreSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, const size_t toIndex, float dt); + static void UpdateModulesPostSimulationIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt); + static void UpdateModulesIncremental (const ParticleSystem& system, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t fromIndex, float dt); + static void UpdateModulesNonIncremental (const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemParticlesTempData& psTemp, size_t fromIndex, size_t toIndex); + static void SimulateParticles (const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, const size_t fromIndex, float dt); + static void PlaybackSubEmitterCommandBuffer(const ParticleSystem& shuriken, ParticleSystemState& state, bool fixedTimeStep); + static void UpdateBounds(const ParticleSystem& system, const ParticleSystemParticles& ps, ParticleSystemState& state); + + static void AddStagingBuffer(ParticleSystem& system); + static int CalculateMaximumSubEmitterEmitCount(ParticleSystem& shuriken, ParticleSystemState& state, float deltaTime, bool fixedTimeStep); + static int SetupSubEmitters(ParticleSystem& shuriken, ParticleSystemState& state); + + static bool CheckSupportsProcedural(const ParticleSystem& system); + static bool DetermineSupportsProcedural(const ParticleSystem& system); + + bool ComputePrewarmStartParameters(float& prewarmTime, float t); + + void SetUsesAxisOfRotation(); + void SetUsesEmitAccumulator(int numAccumulators); + void SetUsesRotationalSpeed(); + void KeepUpdating(); + void Cull(); + + size_t AddNewParticles(ParticleSystemParticles& particles, size_t newParticles) const; + size_t LimitParticleCount(size_t requestSize) const; + float CalculateSubEmitterMaximumLifeTime(float parentLifeTime) const; + + bool GetIsDistanceEmitter() const; + + void AddToManager(); + void RemoveFromManager(); + + ParticleSystemParticles m_Particles[kNumParticleBuffers]; + ParticleSystemParticles m_ParticlesStaging; // staging buffer for emitting into the emitter + ParticleSystemReadOnlyState* m_ReadOnlyState; + ParticleSystemState* m_State; + InitialModule m_InitialModule; + ShapeModule m_ShapeModule; + EmissionModule m_EmissionModule; + + // Dependent on energy value + SizeModule* m_SizeModule; + RotationModule* m_RotationModule; // @TODO: Requires outputs angular velocity and thus requires integration (Inconsistent with other modules in this group) + ColorModule* m_ColorModule; + UVModule* m_UVModule; + + // Dependent on energy value + VelocityModule* m_VelocityModule; + ForceModule* m_ForceModule; + ExternalForcesModule* m_ExternalForcesModule; + + // Depends on velocity and modifies velocity + ClampVelocityModule* m_ClampVelocityModule; + + // Dependent on velocity value + SizeBySpeedModule* m_SizeBySpeedModule; + RotationBySpeedModule* m_RotationBySpeedModule; + ColorBySpeedModule* m_ColorBySpeedModule; + + // Dependent on a position and velocity + CollisionModule* m_CollisionModule; + + SubModule* m_SubModule; + + ParticleSystemThreadScratchPad m_ThreadScratchpad; + + RayBudgetState m_RayBudgetState; + int m_RayBudget; + +private: + int m_EmittersIndex; + +#if UNITY_EDITOR +public: + int m_EditorRandomSeedIndex; + ListNode<ParticleSystem> m_EditorListNode; + friend class ParticleSystemEditor; +#endif + friend class ParticleSystemRenderer; +}; + +#endif // SHURIKEN_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h b/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h new file mode 100644 index 0000000..fd59987 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCommon.h @@ -0,0 +1,48 @@ +#ifndef PARTICLESYSTEMCOMMON_H +#define PARTICLESYSTEMCOMMON_H + +enum ParticleSystemSubType +{ + kParticleSystemSubTypeBirth, + kParticleSystemSubTypeCollision, + kParticleSystemSubTypeDeath, +}; + +enum ParticleSystemEmitMode +{ + kParticleSystemEMDirect, + kParticleSystemEMStaging, +}; + +enum +{ + kParticleSystemMaxSubBirth = 2, + kParticleSystemMaxSubCollision = 2, + kParticleSystemMaxSubDeath = 2, + kParticleSystemMaxSubTotal = kParticleSystemMaxSubBirth + kParticleSystemMaxSubCollision + kParticleSystemMaxSubDeath, +}; + +// Curve id's needed to offset randomness for curves, to avoid visible patterns due to only storing 1 random value per particle +enum ParticleSystemRandomnessIds +{ + // Curves + kParticleSystemClampVelocityCurveId = 0x13371337, + kParticleSystemForceCurveId = 0x12460f3b, + kParticleSystemRotationCurveId = 0x6aed452e, + kParticleSystemRotationBySpeedCurveId = 0xdec4aea1, + kParticleSystemStartSpeedCurveId = 0x96aa4de3, + kParticleSystemSizeCurveId = 0x8d2c8431, + kParticleSystemSizeBySpeedCurveId = 0xf3857f6f, + kParticleSystemVelocityCurveId = 0xe0fbd834, + kParticleSystemUVCurveId = 0x13740583, + + // Gradient + kParticleSystemColorGradientId = 0x591bc05c, + kParticleSystemColorByVelocityGradientId = 0x40eb95e4, + + // Misc + kParticleSystemMeshSelectionId = 0xbc524e5f, + kParticleSystemUVRowSelectionId = 0xaf502044, +}; + +#endif // PARTICLESYSTEMCOMMON_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp new file mode 100644 index 0000000..3ac445c --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.cpp @@ -0,0 +1,196 @@ +#include "UnityPrefix.h" +#include "ParticleSystemCurves.h" +#include "Runtime/Math/Polynomials.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +// Calculates the min max range of an animation curve analytically +static void CalculateCurveRangesValue(Vector2f& minMaxValue, const AnimationCurve& curve) +{ + const int keyCount = curve.GetKeyCount(); + if (keyCount == 0) + return; + + if (keyCount == 1) + { + CalculateMinMax(minMaxValue, curve.GetKey(0).value); + } + else + { + int segmentCount = keyCount - 1; + CalculateMinMax(minMaxValue, curve.GetKey(0).value); + for (int i = 0;i<segmentCount;i++) + { + DebugAssert(i+1 < keyCount); + AnimationCurve::Cache cache; + curve.CalculateCacheData(cache, i, i + 1, 0.0F); + + // Differentiate polynomial + float a = 3.0f * cache.coeff[0]; + float b = 2.0f * cache.coeff[1]; + float c = 1.0f * cache.coeff[2]; + + const float start = curve.GetKey(i).time; + const float end = curve.GetKey(i+1).time; + + float roots[2]; + int numRoots = QuadraticPolynomialRootsGeneric(a, b, c, roots[0], roots[1]); + for(int r = 0; r < numRoots; r++) + if((roots[r] >= 0.0f) && ((roots[r] + start) < end)) + CalculateMinMax(minMaxValue, Polynomial::EvalSegment(roots[r], cache.coeff)); + CalculateMinMax(minMaxValue, Polynomial::EvalSegment(end-start, cache.coeff)); + } + } +} + +bool BuildCurves (MinMaxOptimizedPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState) +{ + bool isOptimizedCurve = polyCurves.max.BuildOptimizedCurve(editorCurves.max, scalar); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + isOptimizedCurve = isOptimizedCurve && polyCurves.min.BuildOptimizedCurve(editorCurves.max, scalar); + else + isOptimizedCurve = isOptimizedCurve && polyCurves.min.BuildOptimizedCurve(editorCurves.min, scalar); + return isOptimizedCurve; +} + +void BuildCurves (MinMaxPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState) +{ + polyCurves.max.BuildCurve(editorCurves.max, scalar); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + polyCurves.min.BuildCurve(editorCurves.max, scalar); + else + polyCurves.min.BuildCurve(editorCurves.min, scalar); +} + +bool CurvesSupportProcedural (const MinMaxAnimationCurves& editorCurves, short minMaxState) +{ + bool isValid = PolynomialCurve::IsValidCurve(editorCurves.max); + if ((minMaxState != kMMCTwoCurves) && (minMaxState != kMMCTwoConstants)) + return isValid; + else + return isValid && PolynomialCurve::IsValidCurve(editorCurves.min); +} + + +void MinMaxOptimizedPolyCurves::Integrate () +{ + max.Integrate (); + min.Integrate (); +} + +void MinMaxOptimizedPolyCurves::DoubleIntegrate () +{ + max.DoubleIntegrate (); + min.DoubleIntegrate (); +} + +Vector2f MinMaxOptimizedPolyCurves::FindMinMaxIntegrated() +{ + Vector2f minRange = min.FindMinMaxIntegrated(); + Vector2f maxRange = max.FindMinMaxIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +Vector2f MinMaxOptimizedPolyCurves::FindMinMaxDoubleIntegrated() +{ + Vector2f minRange = min.FindMinMaxDoubleIntegrated(); + Vector2f maxRange = max.FindMinMaxDoubleIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +void MinMaxPolyCurves::Integrate () +{ + max.Integrate (); + min.Integrate (); +} + +void MinMaxPolyCurves::DoubleIntegrate () +{ + max.DoubleIntegrate (); + min.DoubleIntegrate (); +} + +Vector2f MinMaxPolyCurves::FindMinMaxIntegrated() +{ + Vector2f minRange = min.FindMinMaxIntegrated(); + Vector2f maxRange = max.FindMinMaxIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +Vector2f MinMaxPolyCurves::FindMinMaxDoubleIntegrated() +{ + Vector2f minRange = min.FindMinMaxDoubleIntegrated(); + Vector2f maxRange = max.FindMinMaxDoubleIntegrated(); + Vector2f result = Vector2f(std::min(minRange.x, maxRange.x), std::max(minRange.y, maxRange.y)); + return result; +} + +MinMaxCurve::MinMaxCurve () +: scalar (1.0f) +, minMaxState (kMMCScalar) +, isOptimizedCurve(false) +{ + SetPolynomialCurveToValue (editorCurves.max, polyCurves.max, 1.0f); + SetPolynomialCurveToValue (editorCurves.min, polyCurves.min, 0.0f); +} + +Vector2f MinMaxCurve::FindMinMax() const +{ + Vector2f result = Vector2f(std::numeric_limits<float>::infinity (), -std::numeric_limits<float>::infinity ()); + CalculateCurveRangesValue(result, editorCurves.max); + if((minMaxState == kMMCTwoCurves) || (minMaxState == kMMCTwoConstants)) + CalculateCurveRangesValue(result, editorCurves.min); + return result * GetScalar(); +} + +Vector2f MinMaxCurve::FindMinMaxIntegrated() const +{ + if(IsOptimized()) + { + MinMaxOptimizedPolyCurves integrated = polyCurves; + integrated.Integrate(); + return integrated.FindMinMaxIntegrated(); + } + else + { + DebugAssert(CurvesSupportProcedural (editorCurves, minMaxState)); + MinMaxPolyCurves integrated; + BuildCurves(integrated, editorCurves, GetScalar(), minMaxState); + integrated.Integrate(); + return integrated.FindMinMaxIntegrated(); + } +} + +Vector2f MinMaxCurve::FindMinMaxDoubleIntegrated() const +{ + if(IsOptimized()) + { + MinMaxOptimizedPolyCurves doubleIntegrated = polyCurves; + doubleIntegrated.DoubleIntegrate(); + return doubleIntegrated.FindMinMaxDoubleIntegrated(); + } + else + { + DebugAssert(CurvesSupportProcedural (editorCurves, minMaxState)); + MinMaxPolyCurves doubleIntegrated; + BuildCurves(doubleIntegrated, editorCurves, GetScalar(), minMaxState); + doubleIntegrated.DoubleIntegrate(); + return doubleIntegrated.FindMinMaxDoubleIntegrated(); + } +} + +template<class TransferFunction> +void MinMaxCurve::Transfer (TransferFunction& transfer) +{ + transfer.Transfer (scalar, "scalar"); + transfer.Transfer (editorCurves.max, "maxCurve"); + transfer.Transfer (editorCurves.min, "minCurve"); + TRANSFER (minMaxState); transfer.Align (); + if (transfer.IsReading ()) + isOptimizedCurve = BuildCurves(polyCurves, editorCurves, scalar, minMaxState); +} +INSTANTIATE_TEMPLATE_TRANSFER(MinMaxCurve) diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h new file mode 100644 index 0000000..5d59eb2 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemCurves.h @@ -0,0 +1,187 @@ +#ifndef SHURIKENCURVES_H +#define SHURIKENCURVES_H + +#include "ParticleSystemParticle.h" +#include "PolynomialCurve.h" +#include "Runtime/Math/AnimationCurve.h" + + +struct MinMaxCurve; + +// Some profile numbers from a run with 250,000 particles evaluating 3 velocity properties each on Intel i7-2600 CPU @ 3.4 GHz +// Scalar: 4.6 ms +// Optimized curve: 7.2 ms +// Random between 2 scalars: 9.5 ms +// Random between 2 curves: 9.5 ms +// Non-optimized curve: 10.0 ms +// Random between 2 non-optimized curves: 12.0 ms + +enum ParticleSystemCurveEvalMode +{ + kEMScalar, + kEMOptimized, + kEMOptimizedMinMax, + kEMSlow, +}; + +enum MinMaxCurveState +{ + kMMCScalar = 0, + kMMCCurve = 1, + kMMCTwoCurves = 2, + kMMCTwoConstants = 3 +}; + +struct MinMaxOptimizedPolyCurves +{ + void Integrate(); + void DoubleIntegrate(); + Vector2f FindMinMaxIntegrated(); + Vector2f FindMinMaxDoubleIntegrated(); + + OptimizedPolynomialCurve max; + OptimizedPolynomialCurve min; +}; + +inline float EvaluateIntegrated (const MinMaxOptimizedPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateIntegrated (t); + const float v1 = curves.max.EvaluateIntegrated (t); + return Lerp (v0, v1, factor); +} + +inline float EvaluateDoubleIntegrated (const MinMaxOptimizedPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateDoubleIntegrated (t); + const float v1 = curves.max.EvaluateDoubleIntegrated (t); + return Lerp (v0, v1, factor); +} + +struct MinMaxPolyCurves +{ + void Integrate(); + void DoubleIntegrate(); + Vector2f FindMinMaxIntegrated(); + Vector2f FindMinMaxDoubleIntegrated(); + + PolynomialCurve max; + PolynomialCurve min; +}; + +inline float EvaluateIntegrated (const MinMaxPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateIntegrated (t); + const float v1 = curves.max.EvaluateIntegrated (t); + return Lerp (v0, v1, factor); +} + +inline float EvaluateDoubleIntegrated (const MinMaxPolyCurves& curves, float t, float factor) +{ + const float v0 = curves.min.EvaluateDoubleIntegrated (t); + const float v1 = curves.max.EvaluateDoubleIntegrated (t); + return Lerp (v0, v1, factor); +} + +struct MinMaxAnimationCurves +{ + bool SupportsProcedural (); + + AnimationCurve max; + AnimationCurve min; +}; + +bool BuildCurves (MinMaxOptimizedPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState); +void BuildCurves (MinMaxPolyCurves& polyCurves, const MinMaxAnimationCurves& editorCurves, float scalar, short minMaxState); +bool CurvesSupportProcedural (const MinMaxAnimationCurves& editorCurves, short minMaxState); + +struct MinMaxCurve +{ + MinMaxOptimizedPolyCurves polyCurves; +private: + float scalar; // Since scalar is baked into the optimized curve we use the setter function to modify it. + +public: + short minMaxState; // see enum MinMaxCurveState + bool isOptimizedCurve; + + MinMaxAnimationCurves editorCurves; + + MinMaxCurve (); + + inline float GetScalar() const { return scalar; } + inline void SetScalar(float value) { scalar = value; BuildCurves(polyCurves, editorCurves, scalar, minMaxState); } + + bool IsOptimized () const { return isOptimizedCurve; } + bool UsesMinMax () const { return (minMaxState == kMMCTwoCurves) || (minMaxState == kMMCTwoConstants); } + + DEFINE_GET_TYPESTRING (MinMaxCurve) + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); + + Vector2f FindMinMax() const; + Vector2f FindMinMaxIntegrated() const; + Vector2f FindMinMaxDoubleIntegrated() const; +}; + +inline float EvaluateSlow (const MinMaxCurve& curve, float t, float factor) +{ + const float v = curve.editorCurves.max.Evaluate(t) * curve.GetScalar (); + if (curve.minMaxState == kMMCTwoCurves) + return Lerp (curve.editorCurves.min.Evaluate(t) * curve.GetScalar (), v, factor); + else + return v; +} + +template<ParticleSystemCurveEvalMode mode> +inline float Evaluate (const MinMaxCurve& curve, float t, float factor = 1.0F) +{ + if(mode == kEMScalar) + { + return curve.GetScalar(); + } + if(mode == kEMOptimized) + { + DebugAssert(curve.isOptimizedCurve); + return curve.polyCurves.max.Evaluate (t); + } + else if (mode == kEMOptimizedMinMax) + { + DebugAssert(curve.isOptimizedCurve); + const float v0 = curve.polyCurves.min.Evaluate (t); + const float v1 = curve.polyCurves.max.Evaluate (t); + return Lerp (v0, v1, factor); + } + else if (mode == kEMSlow) + { + return EvaluateSlow (curve, t, factor); + } +} + +inline float Evaluate (const MinMaxCurve& curve, float t, float randomValue = 1.0F) +{ + if (curve.minMaxState == kMMCScalar) + return curve.GetScalar (); + + if (curve.minMaxState == kMMCTwoConstants) + return Lerp ( curve.editorCurves.min.GetKey(0).value * curve.GetScalar (), + curve.editorCurves.max.GetKey(0).value * curve.GetScalar (), randomValue); + + DebugAssert(t <= 1.0F && t >= 0.0F); + if (curve.isOptimizedCurve) + return Evaluate<kEMOptimizedMinMax> (curve, t, randomValue); + else + return Evaluate<kEMSlow> (curve, t, randomValue); +} + +struct DualMinMax3DPolyCurves +{ + MinMaxOptimizedPolyCurves optX; + MinMaxOptimizedPolyCurves optY; + MinMaxOptimizedPolyCurves optZ; + MinMaxPolyCurves x; + MinMaxPolyCurves y; + MinMaxPolyCurves z; +}; + +#endif // SHURIKENCURVES_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp new file mode 100644 index 0000000..efe95c0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.cpp @@ -0,0 +1,27 @@ +#include "UnityPrefix.h" +#include "ParticleSystemGradients.h" +#include "Runtime/BaseClasses/ObjectDefines.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" + +MinMaxGradient::MinMaxGradient() +: minMaxState (kMMGColor), minColor (255,255,255,255), maxColor (255,255,255,255) +{ +} + +void MinMaxGradient::InitializeOptimized(OptimizedMinMaxGradient& g) +{ + maxGradient.InitializeOptimized(g.max); + if(minMaxState == kMMGRandomBetweenTwoGradients) + minGradient.InitializeOptimized(g.min); +} + +template<class TransferFunction> +void MinMaxGradient::Transfer (TransferFunction& transfer) +{ + TRANSFER (maxGradient); + TRANSFER (minGradient); + TRANSFER (minColor); + TRANSFER (maxColor); + TRANSFER (minMaxState); transfer.Align (); +} +INSTANTIATE_TEMPLATE_TRANSFER(MinMaxGradient) diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h new file mode 100644 index 0000000..bb74cde --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemGradients.h @@ -0,0 +1,87 @@ +#ifndef SHURIKENGRADIENTS_H +#define SHURIKENGRADIENTS_H + +#include "Runtime/Math/Gradient.h" + +enum MinMaxGradientEvalMode +{ + kGEMGradient, + kGEMGradientMinMax, + kGEMSlow, +}; + +enum MinMaxGradientState +{ + kMMGColor = 0, + kMMGGradient = 1, + kMMGRandomBetweenTwoColors = 2, + kMMGRandomBetweenTwoGradients = 3 +}; + +struct OptimizedMinMaxGradient +{ + OptimizedGradient max; + OptimizedGradient min; +}; + +inline ColorRGBA32 EvaluateGradient (const OptimizedMinMaxGradient& g, float t) +{ + return g.max.Evaluate(t); +} + +inline ColorRGBA32 EvaluateRandomGradient (const OptimizedMinMaxGradient& g, float t, UInt32 factor) +{ + return Lerp (g.min.Evaluate(t), g.max.Evaluate(t), factor); +} + +struct MinMaxGradient +{ + GradientNEW maxGradient; + GradientNEW minGradient; + ColorRGBA32 minColor; // we have the colors separate to prevent destroying the gradients + ColorRGBA32 maxColor; + short minMaxState; // see enum State + + MinMaxGradient(); + + DEFINE_GET_TYPESTRING (MinMaxGradient) + + void InitializeOptimized(OptimizedMinMaxGradient& g); + + template<class TransferFunction> + void Transfer (TransferFunction& transfer); +}; + +inline ColorRGBA32 EvaluateColor (const MinMaxGradient& gradient) +{ + return gradient.maxColor; +} + +inline ColorRGBA32 EvaluateGradient (const MinMaxGradient& gradient, float t) +{ + return gradient.maxGradient.Evaluate(t); +} + +inline ColorRGBA32 EvaluateRandomColor (const MinMaxGradient& gradient, UInt32 factor) +{ + return Lerp (gradient.minColor, gradient.maxColor, factor); +} + +inline ColorRGBA32 EvaluateRandomGradient (const MinMaxGradient& gradient, float t, UInt32 factor) +{ + return Lerp (gradient.minGradient.Evaluate(t), gradient.maxGradient.Evaluate(t), factor); +} + +inline ColorRGBA32 Evaluate (const MinMaxGradient& gradient, float t, UInt32 factor = 0xff) +{ + if (gradient.minMaxState == kMMGColor) + return EvaluateColor(gradient); + else if (gradient.minMaxState == kMMGGradient) + return EvaluateGradient(gradient, t); + else if (gradient.minMaxState == kMMGRandomBetweenTwoColors) + return EvaluateRandomColor(gradient, factor); + else // gradient.minMaxState == kMMGRandomBetweenTwoGradients + return EvaluateRandomGradient(gradient, t, factor); +} + +#endif // SHURIKENGRADIENTS_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp new file mode 100644 index 0000000..d10bee0 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.cpp @@ -0,0 +1,323 @@ +#include "UnityPrefix.h" +#include "ParticleSystemParticle.h" + +#include "Runtime/Dynamics/Collider.h" +#include "Runtime/Scripting/ScriptingUtility.h" +#include "Runtime/Threads/Thread.h" + +void InitializeEmitAccumulator(dynamic_array<float>& emitAccumulator, const size_t count) +{ + emitAccumulator.resize_uninitialized(count); + memset(emitAccumulator.begin(), 0, count * sizeof(float)); +} + +ParticleCollisionEvent::ParticleCollisionEvent (const Vector3f& intersection, const Vector3f& normal, const Vector3f& velocity, int colliderInstanceID, int rigidBodyOrColliderInstanceID) +{ + m_Intersection = intersection; + m_Normal = normal; + m_Velocity = velocity; + m_ColliderInstanceID = colliderInstanceID; + m_RigidBodyOrColliderInstanceID = rigidBodyOrColliderInstanceID; +} + +size_t ParticleSystemParticles::GetParticleSize() +{ + // @TODO: How do we guard against elements we remove + return sizeof(ParticleSystemParticle); +} + +void ParticleSystemParticles::SetUsesAxisOfRotation() +{ + usesAxisOfRotation = true; + const size_t count = position.size(); + axisOfRotation.resize_uninitialized(count); + for(int i = 0; i < count; i++) + axisOfRotation[i] = Vector3f::yAxis; +} + +void ParticleSystemParticles::SetUsesRotationalSpeed() +{ + usesRotationalSpeed = true; + const size_t count = position.size(); + rotationalSpeed.resize_uninitialized(count); + for(int i = 0; i < count; i++) + rotationalSpeed[i] = 0.0f; +} + +void ParticleSystemParticles::SetUsesCollisionEvents (bool wantUsesCollisionEvents) +{ + if (usesCollisionEvents == wantUsesCollisionEvents) return; + usesCollisionEvents = wantUsesCollisionEvents; + if (!usesCollisionEvents) + { + collisionEvents.Clear (); + } +} + +bool ParticleSystemParticles::GetUsesCollisionEvents () const +{ + return usesCollisionEvents; +} + +void ParticleSystemParticles::SetUsesEmitAccumulator(int numAccumulators) +{ + Assert(numAccumulators <= kParticleSystemMaxNumEmitAccumulators); + const size_t count = position.size(); + for(int i = numEmitAccumulators; i < numAccumulators; i++) + InitializeEmitAccumulator(emitAccumulator[i], count); + + numEmitAccumulators = numAccumulators; +} + +void ParticleSystemParticles::CopyFromArrayAOS(ParticleSystemParticle* particles, int size) +{ + Assert(usesAxisOfRotation); + Assert(numEmitAccumulators == kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < size; i++) + { + (*this).position[i] = particles[i].position; + (*this).velocity[i] = particles[i].velocity; + (*this).animatedVelocity[i] = particles[i].animatedVelocity; + (*this).axisOfRotation[i] = particles[i].axisOfRotation; + (*this).rotation[i] = particles[i].rotation; + if(usesRotationalSpeed) + (*this).rotationalSpeed[i] = particles[i].rotationalSpeed; + (*this).size[i] = particles[i].size; + (*this).color[i] = particles[i].color; + (*this).randomSeed[i] = particles[i].randomSeed; + (*this).lifetime[i] = particles[i].lifetime; + (*this).startLifetime[i] = particles[i].startLifetime; + for(int acc = 0; acc < kParticleSystemMaxNumEmitAccumulators; acc++) + (*this).emitAccumulator[acc][i] = particles[i].emitAccumulator[acc]; + } +} + +void ParticleSystemParticles::CopyToArrayAOS(ParticleSystemParticle* particles, int size) +{ + Assert(usesAxisOfRotation); + Assert(numEmitAccumulators == kParticleSystemMaxNumEmitAccumulators); + for(int i = 0; i < size; i++) + { + particles[i].position = (*this).position[i]; + particles[i].velocity = (*this).velocity[i]; + particles[i].animatedVelocity = (*this).animatedVelocity[i]; + particles[i].axisOfRotation = (*this).axisOfRotation[i]; + particles[i].rotation = (*this).rotation[i]; + if(usesRotationalSpeed) + particles[i].rotationalSpeed = (*this).rotationalSpeed[i]; + particles[i].size = (*this).size[i]; + particles[i].color = (*this).color[i]; + particles[i].randomSeed = (*this).randomSeed[i]; + particles[i].lifetime = (*this).lifetime[i]; + particles[i].startLifetime = (*this).startLifetime[i]; + for(int acc = 0; acc < kParticleSystemMaxNumEmitAccumulators; acc++) + particles[i].emitAccumulator[acc] = (*this).emitAccumulator[acc][i]; + } +} + +void ParticleSystemParticles::AddParticle(ParticleSystemParticle* particle) +{ + const size_t count = array_size(); + array_resize(count+1); + position[count] = particle->position; + velocity[count] = particle->velocity; + animatedVelocity[count] = Vector3f::zero; + lifetime[count] = particle->lifetime; + startLifetime[count] = particle->startLifetime; + size[count] = particle->size; + rotation[count] = particle->rotation; + if(usesRotationalSpeed) + rotationalSpeed[count] = particle->rotationalSpeed; + color[count] = particle->color; + randomSeed[count] = particle->randomSeed; + if(usesAxisOfRotation) + axisOfRotation[count] = particle->axisOfRotation; + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc][count] = 0.0f; +} + +size_t ParticleSystemParticles::array_size () const +{ + return position.size(); +} + +void ParticleSystemParticles::array_resize (size_t i) +{ + position.resize_uninitialized(i); + velocity.resize_uninitialized(i); + animatedVelocity.resize_uninitialized(i); + rotation.resize_uninitialized(i); + if(usesRotationalSpeed) + rotationalSpeed.resize_uninitialized(i); + size.resize_uninitialized(i); + color.resize_uninitialized(i); + randomSeed.resize_uninitialized(i); + lifetime.resize_uninitialized(i); + startLifetime.resize_uninitialized(i); + if(usesAxisOfRotation) + axisOfRotation.resize_uninitialized(i); + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc].resize_uninitialized(i); +} + +void ParticleSystemParticles::element_swap(size_t i, size_t j) +{ + std::swap(position[i], position[j]); + std::swap(velocity[i], velocity[j]); + std::swap(animatedVelocity[i], animatedVelocity[j]); + std::swap(rotation[i], rotation[j]); + if(usesRotationalSpeed) + std::swap(rotationalSpeed[i], rotationalSpeed[j]); + std::swap(size[i], size[j]); + std::swap(color[i], color[j]); + std::swap(randomSeed[i], randomSeed[j]); + std::swap(lifetime[i], lifetime[j]); + std::swap(startLifetime[i], startLifetime[j]); + if(usesAxisOfRotation) + std::swap(axisOfRotation[i], axisOfRotation[j]); + for(int acc = 0; acc < numEmitAccumulators; acc++) + std::swap(emitAccumulator[acc][i], emitAccumulator[acc][j]); +} + +void ParticleSystemParticles::element_assign(size_t i, size_t j) +{ + position[i] = position[j]; + velocity[i] = velocity[j]; + animatedVelocity[i] = animatedVelocity[j]; + rotation[i] = rotation[j]; + if(usesRotationalSpeed) + rotationalSpeed[i] = rotationalSpeed[j]; + size[i] = size[j]; + color[i] = color[j]; + randomSeed[i] = randomSeed[j]; + lifetime[i] = lifetime[j]; + startLifetime[i] = startLifetime[j]; + if(usesAxisOfRotation) + axisOfRotation[i] = axisOfRotation[j]; + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc][i] = emitAccumulator[acc][j]; +} + +void ParticleSystemParticles::array_assign_external(void* data, const int numParticles) +{ +#define SHURIKEN_INCREMENT_ASSIGN_PTRS(element, type) { beginPtr = endPtr; endPtr += numParticles * sizeof(type); (element).assign_external((type*)beginPtr, (type*)endPtr); } + + UInt8* beginPtr = 0; + UInt8* endPtr = (UInt8*)data; + + SHURIKEN_INCREMENT_ASSIGN_PTRS(position, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(velocity, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(animatedVelocity, Vector3f); + SHURIKEN_INCREMENT_ASSIGN_PTRS(rotation, float); + if(usesRotationalSpeed) + SHURIKEN_INCREMENT_ASSIGN_PTRS(rotationalSpeed, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(size, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(color, ColorRGBA32); + SHURIKEN_INCREMENT_ASSIGN_PTRS(randomSeed, UInt32); + SHURIKEN_INCREMENT_ASSIGN_PTRS(lifetime, float); + SHURIKEN_INCREMENT_ASSIGN_PTRS(startLifetime, float); + if(usesAxisOfRotation) + SHURIKEN_INCREMENT_ASSIGN_PTRS(axisOfRotation, Vector3f); + for(int acc = 0; acc < numEmitAccumulators; acc++) + SHURIKEN_INCREMENT_ASSIGN_PTRS(emitAccumulator[acc], float); +#undef SHURIKEN_INCREMENT_ASSIGN_PTRS +} + +void ParticleSystemParticles::array_merge_preallocated(const ParticleSystemParticles& rhs, const int offset, const bool needAxisOfRotation, const bool needEmitAccumulator) +{ +#define SHURIKEN_COPY_DATA(element, type) memcpy(&element[offset], &rhs.element[0], count * sizeof(type)) + + const size_t count = rhs.array_size(); + if(0 == count) + return; + + Assert((rhs.array_size() + offset) <= array_size()); + SHURIKEN_COPY_DATA(position, Vector3f); + SHURIKEN_COPY_DATA(velocity, Vector3f); + SHURIKEN_COPY_DATA(animatedVelocity, Vector3f); + SHURIKEN_COPY_DATA(rotation, float); + if(usesRotationalSpeed) + SHURIKEN_COPY_DATA(rotationalSpeed, float); + SHURIKEN_COPY_DATA(size, float); + SHURIKEN_COPY_DATA(color, ColorRGBA32); + SHURIKEN_COPY_DATA(randomSeed, UInt32); + SHURIKEN_COPY_DATA(lifetime, float); + SHURIKEN_COPY_DATA(startLifetime, float); + + if(needAxisOfRotation) + { + Assert(usesAxisOfRotation && rhs.usesAxisOfRotation); + SHURIKEN_COPY_DATA(axisOfRotation, Vector3f); + } + if(needEmitAccumulator) + { + Assert(numEmitAccumulators && rhs.numEmitAccumulators); + Assert(numEmitAccumulators == rhs.numEmitAccumulators); + for(int acc = 0; acc < numEmitAccumulators; acc++) + SHURIKEN_COPY_DATA(emitAccumulator[acc], float); + } + +#undef SHURIKEN_COPY_DATA +} + + +void ParticleSystemParticles::array_assign(const ParticleSystemParticles& rhs) +{ + position.assign(rhs.position.begin(), rhs.position.end()); + velocity.assign(rhs.velocity.begin(), rhs.velocity.end()); + animatedVelocity.assign(rhs.animatedVelocity.begin(), rhs.animatedVelocity.end()); + rotation.assign(rhs.rotation.begin(), rhs.rotation.end()); + if(usesRotationalSpeed) + rotationalSpeed.assign(rhs.rotationalSpeed.begin(), rhs.rotationalSpeed.end()); + size.assign(rhs.size.begin(), rhs.size.end()); + color.assign(rhs.color.begin(), rhs.color.end()); + randomSeed.assign(rhs.randomSeed.begin(), rhs.randomSeed.end()); + lifetime.assign(rhs.lifetime.begin(), rhs.lifetime.end()); + startLifetime.assign(rhs.startLifetime.begin(), rhs.startLifetime.end()); + if(usesAxisOfRotation) + axisOfRotation.assign(rhs.axisOfRotation.begin(), rhs.axisOfRotation.end()); + for(int acc = 0; acc < numEmitAccumulators; acc++) + emitAccumulator[acc].assign(rhs.emitAccumulator[acc].begin(), rhs.emitAccumulator[acc].end()); +} + +void ParticleSystemParticles::array_lerp(ParticleSystemParticles& output, const ParticleSystemParticles& a, const ParticleSystemParticles& b, float factor) +{ + DebugAssert(a.array_size() == b.array_size()); // else it doesn't really make sense + DebugAssert(a.usesRotationalSpeed == b.usesRotationalSpeed); + + const int count = a.array_size(); + output.array_resize(count); + + // Note: Not all data is interpolated here intentionally, because it doesn't change anything or because it's incorrect + + #define SHURIKEN_LERP_DATA(element, type) for(int i = 0; i < count; i++) output.element[i] = Lerp(a.element[i], b.element[i], factor) + SHURIKEN_LERP_DATA(position, Vector3f); + SHURIKEN_LERP_DATA(velocity, Vector3f); + SHURIKEN_LERP_DATA(animatedVelocity, Vector3f); + + SHURIKEN_LERP_DATA(rotation, float); + if(a.usesRotationalSpeed) + SHURIKEN_LERP_DATA(rotationalSpeed, float); + SHURIKEN_LERP_DATA(lifetime, float); + + #undef SHURIKEN_LERP_DATA +} + +ParticleSystemParticlesTempData::ParticleSystemParticlesTempData() +:color(0) +,size(0) +,sheetIndex(0) +,particleCount(0) +{} + +void ParticleSystemParticlesTempData::element_swap(size_t i, size_t j) +{ + DebugAssert(i <= particleCount); + DebugAssert(j <= particleCount); + + std::swap(color[i], color[j]); + std::swap(size[i], size[j]); + if(sheetIndex) + std::swap(sheetIndex[i], sheetIndex[j]); +} + diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h new file mode 100644 index 0000000..65d6894 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemParticle.h @@ -0,0 +1,116 @@ +#ifndef SHURIKENPARTICLE_H +#define SHURIKENPARTICLE_H + +#include "Runtime/Graphics/ParticleSystem/ParticleCollisionEvents.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Utilities/dynamic_array.h" + +class Collider; +// Keep in sync with struct ParticleSystem.Particle +enum{ kParticleSystemMaxNumEmitAccumulators = 2 }; + +// TODO: Optimization: +// . Store startLifetime as 1.0f/startLifeTime and store lifetime as 1.0f - lifetime. +// This means that a lot of fdivs turn into muls and mads turns into nothing (NormalizedTime will become cheaper). +// Remember: script must still convert into legacy format. + +// Keep in sync with struct ParticleSystem.Particle +struct ParticleSystemParticle +{ + Vector3f position; + Vector3f velocity; + Vector3f animatedVelocity; + Vector3f axisOfRotation; + float rotation; + float rotationalSpeed; + float size; + ColorRGBA32 color; + UInt32 randomSeed; + float lifetime; + float startLifetime; + float emitAccumulator[kParticleSystemMaxNumEmitAccumulators]; +}; + +typedef dynamic_array<Vector3f> ParticleSystemVector3Array; +typedef dynamic_array<float> ParticleSystemFloatArray; +typedef dynamic_array<ColorRGBA32> ParticleSystemColor32Array; +typedef dynamic_array<UInt32> ParticleSystemUInt32Array; + +// Keep in sync with struct ParticleSystem.Particle +struct ParticleSystemParticles +{ + ParticleSystemParticles() + :numEmitAccumulators(0) + ,usesAxisOfRotation(false) + ,usesRotationalSpeed(false) + ,usesCollisionEvents(false) + ,currentCollisionEventThreadArray(0) + {} + + ParticleSystemVector3Array position; + ParticleSystemVector3Array velocity; + ParticleSystemVector3Array animatedVelocity; // Would actually only need this when modules with force and velocity curves are used + ParticleSystemVector3Array axisOfRotation; + ParticleSystemFloatArray rotation; + ParticleSystemFloatArray rotationalSpeed; + ParticleSystemFloatArray size; + ParticleSystemColor32Array color; + ParticleSystemUInt32Array randomSeed; + ParticleSystemFloatArray lifetime; + ParticleSystemFloatArray startLifetime; + ParticleSystemFloatArray emitAccumulator[kParticleSystemMaxNumEmitAccumulators]; // Usage: Only needed if particle system has time sub emitter + CollisionEvents collisionEvents; + + bool usesAxisOfRotation; + bool usesRotationalSpeed; + bool usesCollisionEvents; + int currentCollisionEventThreadArray; + int numEmitAccumulators; + + void AddParticle(ParticleSystemParticle* particle); + + void SetUsesAxisOfRotation (); + void SetUsesRotationalSpeed(); + + void SetUsesCollisionEvents(bool usesCollisionEvents); + bool GetUsesCollisionEvents() const; + void SetUsesEmitAccumulator (int numAccumulators); + + static size_t GetParticleSize(); + + size_t array_size () const; + void array_resize (size_t i); + void element_swap(size_t i, size_t j); + void element_assign(size_t i, size_t j); + void array_assign_external(void* data, const int numParticles); + void array_merge_preallocated(const ParticleSystemParticles& rhs, const int offset, const bool needAxisOfRotation, const bool needEmitAccumulator); + void array_assign(const ParticleSystemParticles& rhs); + static void array_lerp(ParticleSystemParticles& output, const ParticleSystemParticles& a, const ParticleSystemParticles& b, float factor); + + void CopyFromArrayAOS(ParticleSystemParticle* particles, int size); + void CopyToArrayAOS(ParticleSystemParticle* particles, int size); +}; + +struct ParticleSystemParticlesTempData +{ + ParticleSystemParticlesTempData(); + void element_swap(size_t i, size_t j); + + ColorRGBA32* color; + float* size; + float* sheetIndex; + size_t particleCount; +}; + +inline float NormalizedTime (const ParticleSystemParticles& ps, size_t i) +{ + return (ps.startLifetime[i] - ps.lifetime[i]) / ps.startLifetime[i]; +} + +inline float NormalizedTime (float wholeTime, float currentTime) +{ + return (wholeTime - currentTime) / wholeTime; +} + +#endif // SHURIKENPARTICLE_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp new file mode 100644 index 0000000..7b14e62 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.cpp @@ -0,0 +1,1241 @@ +#include "UnityPrefix.h" +#include "ParticleSystem.h" +#include "ParticleSystemParticle.h" +#include "ParticleSystemRenderer.h" +#include "ParticleSystemUtils.h" +#include "Modules/SubModule.h" +#include "Modules/UVModule.h" +#include "Runtime/Camera/Camera.h" +#include "Runtime/Camera/RenderManager.h" +#include "Runtime/Camera/Renderqueue.h" +#include "Runtime/Shaders/VBO.h" +#include "Runtime/GfxDevice/GfxDevice.h" +#include "Runtime/Graphics/DrawUtil.h" +#include "Runtime/Serialize/TransferFunctions/SerializeTransfer.h" +#include "Runtime/Profiler/Profiler.h" +#include "Runtime/Profiler/ExternalGraphicsProfiler.h" +#include "Runtime/Threads/JobScheduler.h" +#include "Runtime/Filters/Misc/LineBuilder.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/GfxDevice/ChannelAssigns.h" +#include "Runtime/Graphics/TriStripper.h" +#include "Runtime/Misc/BuildSettings.h" +#include "Runtime/BaseClasses/GameObject.h" + +IMPLEMENT_CLASS_INIT_ONLY (ParticleSystemRenderer) +IMPLEMENT_OBJECT_SERIALIZE (ParticleSystemRenderer) + +PROFILER_INFORMATION(gParticlesSort, "ParticleSystem.Sort", kProfilerParticles) +PROFILER_INFORMATION(gParticlesSingleProfile, "ParticleSystem.RenderSingle", kProfilerParticles) +PROFILER_INFORMATION(gParticlesBatchProfile, "ParticleSystem.RenderBatch", kProfilerParticles) +PROFILER_INFORMATION(gSubmitVBOParticleProfile, "Mesh.SubmitVBO", kProfilerRender) + +#define DEBUG_PARTICLE_SORTING (0) +#if UNITY_WII +#define kMaxNumParticlesPerBatch (65536/6) +#else +#define kMaxNumParticlesPerBatch (min<int>(kDynamicBatchingIndicesThreshold/6, VBO::kMaxQuads)) +#endif + +struct ParticleSystemVertex +{ + Vector3f vert; + Vector3f normal; + ColorRGBA32 color; + Vector2f uv; + Vector4f tangent; // Here, we put 2nd uv + blend factor +}; + +struct ParticleSystemGeomConstInputData +{ + Matrix4x4f m_ViewMatrix; + Vector3f m_CameraVelocity; + Object* m_Renderer; + UInt16 const* m_MeshIndexBuffer[ParticleSystemRendererData::kMaxNumParticleMeshes]; + int m_MeshIndexCount[ParticleSystemRendererData::kMaxNumParticleMeshes]; + int m_NumTilesX; + int m_NumTilesY; + float maxPlaneScale; + float maxOrthoSize; + float numUVFrame; + float animUScale; + float animVScale; + Vector3f xSpan; + Vector3f ySpan; + bool usesSheetIndex; + float bentNormalFactor; + Vector3f bentNormalVector; +}; + +inline void ScaleMatrix(Matrix4x4f& matrix, float scale) +{ + matrix.m_Data[0] *= scale; + matrix.m_Data[1] *= scale; + matrix.m_Data[2] *= scale; + matrix.m_Data[4] *= scale; + matrix.m_Data[5] *= scale; + matrix.m_Data[6] *= scale; + matrix.m_Data[8] *= scale; + matrix.m_Data[9] *= scale; + matrix.m_Data[10] *= scale; +} + +struct ParticleSort +{ + inline static void SetValues(ParticleSort& sort, UInt32 inIndex, int inIntValue) + { + sort.index = inIndex; + sort.intValue = inIntValue; + } + + inline static bool CompareValue (const ParticleSort& left, const ParticleSort& right) + { + return (left.intValue < right.intValue); + } + + inline static void Swap(ParticleSort* oneOfThem, ParticleSort* theOtherOne) + { + ParticleSort temp = *oneOfThem; + *oneOfThem = *theOtherOne; + *theOtherOne = temp; + } + + UInt32 index; + int intValue; +}; + +void GenerateSortIndices(ParticleSort* indices, const Vector3f& distFactor, const ParticleSystemParticles& ps, ParticleSystemSortMode sortMode) +{ + const size_t particleCount = ps.array_size(); + if(IS_CONTENT_NEWER_OR_SAME (kUnityVersion4_0_a1)) + { + if(sortMode == kSSMByDistance) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(Dot (distFactor, ps.position[i]) * 40000.0f)); + else if(sortMode == kSSMOldestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)((ps.startLifetime[i]- ps.lifetime[i]) * -40000.0f)); + else if(sortMode == kSSMYoungestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)((ps.startLifetime[i]- ps.lifetime[i]) * 40000.0f)); + } + else + { + // 3.5 used lifetime - this is pretty broken if you have random lifetimes, as you get random sorting + if(sortMode == kSSMByDistance) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(Dot (distFactor, ps.position[i]) * 40000.0f)); + else if(sortMode == kSSMOldestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(ps.lifetime[i] * 40000.0f)); + else if(sortMode == kSSMYoungestFirst) + for(int i = 0; i < particleCount; i++) + ParticleSort::SetValues(indices[i], i, (int)(ps.lifetime[i] * -40000.0f)); + } +} + +template<bool sortTempData> +void ApplySortRemap(ParticleSort* particleSortIndexBuffer, ParticleSystemParticlesTempData* tempData, ParticleSystemParticles& ps) +{ + const size_t count = ps.array_size(); + for(int i = 0; i < count; i++) + { + int dst = particleSortIndexBuffer[i].intValue; + while(i != dst) + { + ParticleSort::Swap(&particleSortIndexBuffer[i], &particleSortIndexBuffer[dst]); + ps.element_swap(i, dst); + if(sortTempData) + tempData->element_swap(i, dst); + + dst = particleSortIndexBuffer[i].intValue; + } + } +} + +void Sort (const Matrix4x4f& matrix, ParticleSystemParticles& ps, ParticleSystemSortMode mode, ParticleSystemParticlesTempData* tempData, bool sortTempData) +{ + PROFILER_AUTO_GFX(gParticlesSort, 0); + + DebugAssert(mode != kSSMNone); + + const Vector3f distFactor = Vector3f (matrix.Get (2, 0), matrix.Get (2, 1), + matrix.Get (2, 2)); + const size_t count = ps.array_size(); + + ParticleSort* particleSortIndexBuffer; + ALLOC_TEMP(particleSortIndexBuffer, ParticleSort, count); + GenerateSortIndices(&particleSortIndexBuffer[0], distFactor, ps, mode); + + // Sort + std::sort(&particleSortIndexBuffer[0], &particleSortIndexBuffer[0] + count, ParticleSort::CompareValue); + + // Create inverse mapping + for(int i = 0; i < count; i++) + particleSortIndexBuffer[particleSortIndexBuffer[i].index].intValue = i; + + if(sortTempData) + ApplySortRemap<true>(particleSortIndexBuffer, tempData, ps); + else + ApplySortRemap<false>(particleSortIndexBuffer, tempData, ps); +} + +struct ParticleMeshData +{ + int vertexCount; + StrideIterator<Vector3f> positions; + StrideIterator<Vector3f> normals; + StrideIterator<Vector4f> tangents; + StrideIterator<ColorRGBA32> colors; + StrideIterator<Vector2f> texCoords; + int indexCount; + const UInt16* indexBuffer; +}; + +template<bool hasNormals, bool hasTangents> +void TransformParticleMesh(const ParticleMeshData& src, ColorRGBA32 particleColor, + const Matrix4x4f& xform, const Matrix4x4f& xformNoScale, UInt8** dest) +{ + for(int vertex = 0; vertex < src.vertexCount; vertex++) + { + // Vertex format is position, color, uv, and optional normals and tangents + xform.MultiplyPoint3(src.positions[vertex], *reinterpret_cast<Vector3f*>(*dest)); + *dest += sizeof(Vector3f); + if (hasNormals) + { + xformNoScale.MultiplyVector3(src.normals[vertex], *reinterpret_cast<Vector3f*>(*dest)); + *dest += sizeof(Vector3f); + } + *reinterpret_cast<ColorRGBA32*>(*dest) = particleColor * src.colors[vertex]; + *dest += sizeof(ColorRGBA32); + *reinterpret_cast<Vector2f*>(*dest) = src.texCoords[vertex]; + *dest += sizeof(Vector2f); + // Tangent is last in vertex format + if (hasTangents) + { + Vector3f newTangent = xformNoScale.MultiplyVector3((const Vector3f&)src.tangents[vertex]); + *reinterpret_cast<Vector4f*>(*dest) = Vector4f(newTangent, src.tangents[vertex].w); + *dest += sizeof(Vector4f); + } + } +} + + +ParticleSystemRenderer::ParticleSystemRenderer (MemLabelId label, ObjectCreationMode mode) +: Super(kRendererParticleSystem, label, mode) +, m_LocalSpaceAABB (Vector3f::zero, Vector3f::zero) +{ + SetVisible (false); + + for (int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; ++i) + m_Data.cachedMeshUserNode[i].SetData (this); + +#if UNITY_EDITOR + m_EditorEnabled = true; +#endif +} + +ParticleSystemRenderer::~ParticleSystemRenderer () +{ +} + +void ParticleSystemRenderer::InitializeClass () +{ + REGISTER_MESSAGE_PTR (ParticleSystemRenderer, kDidDeleteMesh, OnDidDeleteMesh, Mesh); +} + +void ParticleSystemRenderer::AwakeFromLoad (AwakeFromLoadMode awakeMode) +{ + Super::AwakeFromLoad (awakeMode); + UpdateCachedMesh (); +} + +void ParticleSystemRenderer::UpdateCachedMesh () +{ + int dst = 0; + for(int src = 0; src < ParticleSystemRendererData::kMaxNumParticleMeshes; src++) + { + m_Data.cachedMesh[src] = NULL; + m_Data.cachedMeshUserNode[src].RemoveFromList (); + + Mesh* mesh = m_Mesh[src]; + if (mesh) + { + if (mesh->GetSubMeshCount() == 1) + { + m_Data.cachedMesh[dst] = mesh; + const SubMesh& sm = mesh->GetSubMeshFast(0); + const UInt16* buffer = mesh->GetSubMeshBuffer16(0); + + if (sm.topology == kPrimitiveTriangleStripDeprecated) + { + const int capacity = CountTrianglesInStrip(buffer, sm.indexCount) * 3; + m_CachedIndexBuffer[dst].resize_uninitialized(capacity); + Destripify(buffer, sm.indexCount, m_CachedIndexBuffer[dst].begin(), capacity); + } + else if (sm.topology == kPrimitiveTriangles) + { + const int capacity = sm.indexCount; + m_CachedIndexBuffer[dst].resize_uninitialized(capacity); + memcpy(m_CachedIndexBuffer[dst].begin(), buffer, capacity*kVBOIndexSize); + } + else + { + m_CachedIndexBuffer[dst].resize_uninitialized(0); + } + + // Hook into mesh's user notifications. + mesh->AddObjectUser (m_Data.cachedMeshUserNode[dst]); + + dst++; + } + else + { + m_Data.cachedMesh[src] = NULL; + m_CachedIndexBuffer[src].resize_uninitialized(0); + AssertString ("Particle system meshes will only work with exactly one (1) sub mesh"); + } + } + } +} + +void ParticleSystemRenderer::OnDidDeleteMesh (Mesh* mesh) +{ + // Clear out cached pointer to mesh. + for (int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; ++i) + { + if (m_Data.cachedMesh[i] != mesh) + continue; + + m_Data.cachedMesh[i] = NULL; + m_Data.cachedMeshUserNode[i].RemoveFromList (); + } +} + +void ParticleSystemRenderer::GetLocalAABB (AABB& result) +{ + result = m_LocalSpaceAABB; +} + +void ParticleSystemRenderer::GetWorldAABB (AABB& result) +{ + TransformAABB (m_LocalSpaceAABB, GetTransform ().GetPosition (), GetTransform ().GetRotation (), result); +} + +float ParticleSystemRenderer::GetSortingFudge () const +{ + return m_Data.sortingFudge; +} + +void ParticleSystemRenderer::CheckConsistency () +{ + Super::CheckConsistency (); + m_Data.maxParticleSize = std::max (0.0F, m_Data.maxParticleSize); + m_Data.normalDirection = clamp<float>(m_Data.normalDirection, 0.0f, 1.0f); +} + +void ParticleSystemRenderer::Reset () +{ + Super::Reset (); + m_Data.renderMode = kSRMBillboard; + m_Data.lengthScale = 2.0F; + m_Data.velocityScale = 0.0F; + m_Data.cameraVelocityScale = 0.0F; + m_Data.maxParticleSize = 0.5F; + m_Data.sortingFudge = 0.0F; + m_Data.sortMode = kSSMNone; + m_Data.normalDirection = 1.0f; + + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + m_Mesh[i] = NULL; + m_LocalSpaceAABB.SetCenterAndExtent (Vector3f::zero, Vector3f::zero); + + + +#if UNITY_EDITOR + m_EditorEnabled = true; +#endif +} + +void ParticleSystemRenderer::UpdateRenderer () +{ + ParticleSystem* system = QueryComponent(ParticleSystem); + if (system) + { + SetVisible (true); + BoundsChanged(); + } + else + { + UpdateManagerState (false); + } + + Super::UpdateRenderer (); +} + +void ParticleSystemRenderer::Update (const AABB& aabb) +{ + m_LocalSpaceAABB = aabb; + UpdateManagerState (true); +} + +void ParticleSystemRenderer::RendererBecameVisible() +{ + Super::RendererBecameVisible(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(system) + system->RendererBecameVisible(); +} + +void ParticleSystemRenderer::RendererBecameInvisible() +{ + Super::RendererBecameInvisible(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(system) + system->RendererBecameInvisible(); +} + +void ParticleSystemRenderer::UpdateLocalAABB() +{ + AABB aabb; + GetLocalAABB(aabb); + m_TransformInfo.localAABB = aabb; +} + +inline Rectf GetFrameUV (int index, int tilesX, float animUScale, float animVScale) +{ + int vIdx = index / tilesX; + int uIdx = index - vIdx * tilesX; // slightly faster than index % m_UVAnimation.xTile + float uOffset = (float)uIdx * animUScale; + float vOffset = 1.0f - animVScale - (float)vIdx * animVScale; + + return Rectf(uOffset, vOffset, animUScale, animVScale); +} + +template<ParticleSystemRenderMode renderMode> +void GenerateParticleGeometry (ParticleSystemVertex* vbPtr, + const ParticleSystemGeomConstInputData& constData, + const ParticleSystemRendererData& rendererData, + const ParticleSystemParticles& ps, + const ParticleSystemParticlesTempData& psTemp, + size_t startIndex, + size_t endIndex, + const Matrix4x4f& worldViewMatrix, + const Matrix4x4f& viewToWorldMatrix) +{ + float maxPlaneScale = constData.maxPlaneScale; + float maxOrthoSize = constData.maxOrthoSize; + float numUVFrame = constData.numUVFrame; + Vector3f xSpan = constData.xSpan; + Vector3f ySpan = constData.ySpan; + Vector3f cameraVelocity = constData.m_CameraVelocity * rendererData.cameraVelocityScale; + int numTilesX = constData.m_NumTilesX; + float animUScale = constData.animUScale; + float animVScale = constData.animVScale; + bool usesSheetIndex = constData.usesSheetIndex; + float lengthScale = rendererData.lengthScale; + float velocityScale = rendererData.velocityScale; + + float bentNormalFactor = constData.bentNormalFactor; + Vector3f bentNormalVector = constData.bentNormalVector; + + Vector2f uv[4] = { Vector2f(0.0f, 1.0f), + Vector2f(1.0f, 1.0f), + Vector2f(1.0f, 0.0f), + Vector2f(0.0f, 0.0f)}; + Vector4f uv2[4] = { Vector4f(0.0f, 1.0f, 0.0f, 0.0f), + Vector4f(1.0f, 1.0f, 0.0f, 0.0f), + Vector4f(1.0f, 0.0f, 0.0f, 0.0f), + Vector4f(0.0f, 0.0f, 0.0f, 0.0f)}; + + float invAnimVScale = 1.0f - animVScale; + + for( int i = startIndex; i < endIndex; ++i ) + { + Vector3f vert[4]; + Vector3f n0, n1; + + Vector3f position; + worldViewMatrix.MultiplyPoint3 (ps.position[i], position); + + // Constrain the size to be a fraction of the viewport size. + // v[0].z * / farPlaneZ * farPlaneWorldSpaceLength * maxLength[0...1] + // Also all valid z's are negative so we just negate the whole equation + float maxWorldSpaceLength = position.z * maxPlaneScale + maxOrthoSize; + float hsize = std::min (psTemp.size[i], maxWorldSpaceLength) * 0.5f; + if (renderMode == kSRMBillboard) + { + float s = Sin (ps.rotation[i]); + float c = Cos (ps.rotation[i]); + n0 = Vector3f(-c+s, s+c, 0.0f); + n1 = Vector3f( c+s, -s+c, 0.0f); + vert[0] = position + n0 * hsize; + vert[1] = position + n1 * hsize; + vert[2] = position - n0 * hsize; + vert[3] = position - n1 * hsize; + } + else if (renderMode == kSRMBillboardFixedHorizontal || renderMode == kSRMBillboardFixedVertical) + { + float s = Sin (ps.rotation[i]+0.78539816339744830961566084581988f); + float c = Cos (ps.rotation[i]+0.78539816339744830961566084581988f); + n0 = xSpan*c + ySpan*s; + n1 = ySpan*c - xSpan*s; + vert[0] = position + n0 * hsize; + vert[1] = position + n1 * hsize; + vert[2] = position - n0 * hsize; + vert[3] = position - n1 * hsize; + } + else if (renderMode == kSRMStretch3D) + { + //RH BUG FOR LATER: Here we see the stretching bug as described by case no 434115...this is a Flash VM error, where a writeFloat (or readFloat) fails. + Vector3f velocity; + worldViewMatrix.MultiplyVector3(ps.velocity[i] + ps.animatedVelocity[i], velocity); + velocity -= cameraVelocity; + float sqrVelocity = SqrMagnitude (velocity); + + Vector2f delta; + Vector3f endProj; + bool nonZeroVelocity = sqrVelocity > Vector3f::epsilon; + if (nonZeroVelocity) + { + endProj = position - velocity * (velocityScale + FastInvSqrt (sqrVelocity) * (lengthScale * psTemp.size[i])); + delta.x = position.z*endProj.y - position.y*endProj.z; + delta.y = position.x*endProj.z - position.z*endProj.x; + delta = NormalizeFast(delta); + } + else + { + endProj = position; + delta = Vector2f::xAxis; + } + n0 = n1 = Vector3f(delta.x, delta.y, 0.0f); + vert[0] = position + n0 * hsize; + vert[1] = endProj + n1 * hsize; + vert[2] = endProj - n0 * hsize; + vert[3] = position - n1 * hsize; + } + + // UV animation + float sheetIndex; + if(usesSheetIndex) + { + // TODO: Pretty much the perfect candidate for SIMD + + sheetIndex = psTemp.sheetIndex[i] * numUVFrame; + Assert (psTemp.sheetIndex[i] >= 0.0f && psTemp.sheetIndex[i] <= 1.0f); + + const int index0 = FloorfToIntPos (sheetIndex); + const int index1 = index0 + 1; + Vector2f offset0, offset1; + const float blend = sheetIndex - (float)index0; + + int vIdx = index0 / numTilesX; + int uIdx = index0 - vIdx * numTilesX; + offset0.x = (float)uIdx * animUScale; + offset0.y = invAnimVScale - (float)vIdx * animVScale; + + vIdx = index1 / numTilesX; + uIdx = index1 - vIdx * numTilesX; + offset1.x = (float)uIdx * animUScale; + offset1.y = invAnimVScale - (float)vIdx * animVScale; + + uv[0].Set(offset0.x, offset0.y + animVScale ); + uv[1].Set(offset0.x + animUScale, offset0.y + animVScale ); + uv[2].Set(offset0.x + animUScale, offset0.y ); + uv[3].Set(offset0.x, offset0.y ); + + uv2[0].Set(offset1.x, offset1.y + animVScale, blend, 0.0f ); + uv2[1].Set(offset1.x + animUScale, offset1.y + animVScale, blend, 0.0f ); + uv2[2].Set(offset1.x + animUScale, offset1.y, blend, 0.0f ); + uv2[3].Set(offset1.x, offset1.y, blend, 0.0f ); + } + + n0 = viewToWorldMatrix.MultiplyVector3(n0 * bentNormalFactor); + n1 = viewToWorldMatrix.MultiplyVector3(n1 * bentNormalFactor); + + ColorRGBA32 color = psTemp.color[i]; + + vbPtr[0].vert = vert[0]; + vbPtr[0].normal = bentNormalVector + n0; + vbPtr[0].color = color; + vbPtr[0].uv = uv[0]; + vbPtr[0].tangent = uv2[0]; + + vbPtr[1].vert = vert[1]; + vbPtr[1].normal = bentNormalVector + n1; + vbPtr[1].color = color; + vbPtr[1].uv = uv[1]; + vbPtr[1].tangent = uv2[1]; + + vbPtr[2].vert = vert[2]; + vbPtr[2].normal = bentNormalVector - n0; + vbPtr[2].color = color; + vbPtr[2].uv = uv[2]; + vbPtr[2].tangent = uv2[2]; + + vbPtr[3].vert = vert[3]; + vbPtr[3].normal = bentNormalVector - n1; + vbPtr[3].color = color; + vbPtr[3].uv = uv[3]; + vbPtr[3].tangent = uv2[3]; + + // Next four vertices + vbPtr += 4; + } +} + +static void DrawMeshParticles (const ParticleSystemGeomConstInputData& constInput, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, const ChannelAssigns& channels) +{ + int numMeshes = 0; + ParticleMeshData particleMeshes[ParticleSystemRendererData::kMaxNumParticleMeshes]; + Vector3f defaultNormal(0, 0, 0); + Vector4f defaultTangent(0, 0, 0, 0); + ColorRGBA32 defaultColor(255, 255, 255, 255); + Vector2f defaultTexCoords(0, 0); + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + { + if(constInput.m_MeshIndexCount[i] == 0) + break; + const Mesh* mesh = rendererData.cachedMesh[i]; + if(mesh == NULL || !mesh->HasVertexData()) + break; + ParticleMeshData& dest = particleMeshes[i]; + dest.vertexCount = mesh->GetVertexCount(); + dest.positions = mesh->GetVertexBegin(); + dest.normals = mesh->GetNormalBegin(); + if (dest.normals.IsNull()) + dest.normals = StrideIterator<Vector3f>(&defaultNormal, 0); + dest.tangents = mesh->GetTangentBegin(); + if (dest.tangents.IsNull()) + dest.tangents = StrideIterator<Vector4f>(&defaultTangent, 0); + dest.texCoords = mesh->GetUvBegin(); + if (dest.texCoords.IsNull()) + dest.texCoords = StrideIterator<Vector2f>(&defaultTexCoords, 0); + dest.colors = mesh->GetColorBegin(); + if (dest.colors.IsNull()) + dest.colors = StrideIterator<ColorRGBA32>(&defaultColor, 0); + dest.indexCount = constInput.m_MeshIndexCount[i]; + dest.indexBuffer = constInput.m_MeshIndexBuffer[i]; + numMeshes++; + } + + if(0 == numMeshes) + return; + + GfxDevice& device = GetGfxDevice(); + + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + const size_t particleCount = ps.array_size (); + + float probability = 1.0f / (float)numMeshes; + + // @TODO: We should move all these platform dependent numbers into Gfx specific code and get it from there. + const int kMaxVertices = 65536; + +#if UNITY_WII + const int kMaxIndices = 65536; +#else + const int kMaxIndices = kDynamicBatchingIndicesThreshold; +#endif + + int particleOffset = 0; + while (particleOffset < particleCount) + { + int numVertices = 0; + int numIndices = 0; + int particleCountBatch = 0; + + // Figure out batch size + for(int i = particleOffset; i < particleCount; i++) + { + const float randomValue = GenerateRandom(ps.randomSeed[i] + kParticleSystemMeshSelectionId); + int lastNumVertices = 0; + int lastNumIndices = 0; + for(int j = 0; j < numMeshes; j++) + { + const float lower = probability * j; + const float upper = probability * (j + 1); + if((randomValue >= lower) && (randomValue <= upper)) + { + lastNumVertices = particleMeshes[j].vertexCount; + lastNumIndices = particleMeshes[j].indexCount; + break; + } + } + if((numVertices >= kMaxVertices) || (numIndices >= kMaxIndices)) + { + break; + } + else + { + numVertices += lastNumVertices; + numIndices += lastNumIndices; + particleCountBatch++; + } + } + + const int vertexCount = numVertices; + const int indexCount = numIndices; + + // Figure out if normals and tangents are needed by shader + UInt32 normalTangentMask = channels.GetSourceMap() & VERTEX_FORMAT2(Normal, Tangent); + + // Tangents requires normals + if( normalTangentMask & VERTEX_FORMAT1(Tangent) ) + normalTangentMask |= VERTEX_FORMAT1(Normal); + + // Get VBO chunk + DynamicVBO& vbo = device.GetDynamicVBO(); + UInt8* vbPtr = NULL; + UInt16* ibPtr = NULL; + const UInt32 mandatoryChannels = VERTEX_FORMAT3(Vertex, Color, TexCoord0); + if( !vbo.GetChunk( mandatoryChannels | normalTangentMask, + vertexCount, indexCount, + DynamicVBO::kDrawIndexedTriangles, + (void**)&vbPtr, (void**)&ibPtr ) ) + { + return; + } + + int vertexOffset = 0; + int indexOffset = 0; + const int startIndex = particleOffset; + const int endIndex = particleOffset + particleCountBatch; + for( int i = startIndex; i < endIndex; ++i ) + { + const Vector3f position = ps.position[i]; + const float rotation = ps.rotation[i]; + const float size = psTemp.size[i]; + const Vector3f axisOfRotation = NormalizeSafe (ps.axisOfRotation[i], Vector3f::yAxis); + const ColorRGBA32 particleColor = psTemp.color[i]; + + // Only shared part is actually rotation. xformNoScale doesn't need a translation, so no need to copy that data + Matrix4x4f xformNoScale; + xformNoScale.SetTR (position, AxisAngleToQuaternion (axisOfRotation, rotation)); + + Matrix4x4f xform = xformNoScale; + ScaleMatrix(xform, size); + + // Figure out which mesh to use + const float randomValue = GenerateRandom(ps.randomSeed[i] + kParticleSystemMeshSelectionId); + int meshIndex = 0; + for(int j = 0; j < numMeshes; j++) + { + const float lower = probability * j; + const float upper = probability * (j + 1); + if((randomValue >= lower) && (randomValue <= upper)) + { + meshIndex = j; + break; + } + } + + const ParticleMeshData& mesh = particleMeshes[meshIndex]; + + // Fill up vbo here + if( normalTangentMask == VERTEX_FORMAT2(Normal, Tangent) ) + TransformParticleMesh<true, true>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else if( normalTangentMask == VERTEX_FORMAT1(Normal) ) + TransformParticleMesh<true, false>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else if( normalTangentMask == 0 ) + TransformParticleMesh<false, false>(mesh, particleColor, xform, xformNoScale, &vbPtr); + else + ErrorString("Invalid normalTangentMask"); + + const int meshIndexMax = mesh.indexCount - 2; + for(int index = 0; index < meshIndexMax; index+=3) + { + ibPtr[index+0] = mesh.indexBuffer[index+0] + vertexOffset; + ibPtr[index+1] = mesh.indexBuffer[index+1] + vertexOffset; + ibPtr[index+2] = mesh.indexBuffer[index+2] + vertexOffset; + } + ibPtr += mesh.indexCount; + + vertexOffset += mesh.vertexCount; + indexOffset += mesh.indexCount; + } + + vbo.ReleaseChunk (vertexCount, indexCount); + device.SetViewMatrix(viewMatrix.GetPtr()); + device.SetWorldMatrix(worldMatrix.GetPtr()); + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + + particleOffset += particleCountBatch; + } +} + +static void DrawParticlesInternal(const ParticleSystemGeomConstInputData& constData, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldViewMatrix, const Matrix4x4f& viewToWorldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, ParticleSystemVertex* vbPtr, const size_t particleOffset, const size_t numParticles, int renderMode) +{ + const size_t endIndex = particleOffset + numParticles; + + if (renderMode == kSRMBillboard) + GenerateParticleGeometry<kSRMBillboard> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMStretch3D) + GenerateParticleGeometry<kSRMStretch3D> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMBillboardFixedHorizontal) + GenerateParticleGeometry<kSRMBillboardFixedHorizontal> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); + if (renderMode == kSRMBillboardFixedVertical) + GenerateParticleGeometry<kSRMBillboardFixedVertical> (vbPtr, constData, rendererData, ps, psTemp, particleOffset, endIndex, worldViewMatrix, viewToWorldMatrix); +} + +static void DrawParticles(const ParticleSystemGeomConstInputData& constData, const ParticleSystemRendererData& rendererData, const Matrix4x4f& worldViewMatrix, const Matrix4x4f& viewToWorldMatrix, const ParticleSystemParticles& ps, const ParticleSystemParticlesTempData& psTemp, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr) +{ + GfxDevice& device = GetGfxDevice(); + const size_t particleCount = ps.array_size(); + + if(vbPtr) + { + DrawParticlesInternal(constData, rendererData, worldViewMatrix, viewToWorldMatrix, ps, psTemp, vbPtr, 0, particleCount, rendererData.renderMode); + } + else + { + int particleOffset = 0; + while (particleOffset < particleCount) + { + const int particleCountBatch = min(kMaxNumParticlesPerBatch, (int)particleCount - particleOffset); + + // Get VBO chunk + DynamicVBO& vbo = device.GetDynamicVBO(); + if( !vbo.GetChunk( (1<<kShaderChannelVertex) | (1<<kShaderChannelNormal) | (1<<kShaderChannelTexCoord0) | (1<<kShaderChannelColor) | (1<<kShaderChannelTangent), + particleCountBatch * 4, 0, + DynamicVBO::kDrawQuads, + (void**)&vbPtr, NULL ) ) + { + continue; + } + + DrawParticlesInternal(constData, rendererData, worldViewMatrix, viewToWorldMatrix, ps, psTemp, vbPtr, particleOffset, particleCountBatch, rendererData.renderMode); + particleOffset += particleCountBatch; + + vbo.ReleaseChunk (particleCountBatch * 4, 0); + + // Draw + device.SetViewMatrix (Matrix4x4f::identity.GetPtr()); // implicitly sets world to identity + + PROFILER_BEGIN(gSubmitVBOParticleProfile, constData.m_Renderer) + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + PROFILER_END + + device.SetViewMatrix(constData.m_ViewMatrix.GetPtr ()); + } + } +} + +void ParticleSystemRenderer::CalculateTotalParticleCount(UInt32& totalNumberOfParticles, ParticleSystem& system, bool first) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + totalNumberOfParticles += system.GetParticleCount(); + + Transform* t = system.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* child = (**i).QueryComponent(ParticleSystem); + if (child) + CalculateTotalParticleCount(totalNumberOfParticles, *child, false); + } + } +} + +void ParticleSystemRenderer::CombineParticleBuffersRec(int& offset, ParticleSystemParticles& ps, ParticleSystemParticlesTempData& psTemp, ParticleSystem& system, bool first, bool needsAxisOfRotation) +{ + ParticleSystemRenderer* renderer = system.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + int particleCount = system.GetParticleCount(); + ps.array_merge_preallocated(system.GetParticles(), offset, needsAxisOfRotation, false); + + if(system.m_ReadOnlyState->useLocalSpace) + { + Matrix4x4f localToWorld = system.GetComponent (Transform).GetLocalToWorldMatrixNoScale (); + + int endIndex = offset + particleCount; + for(int i = offset; i < endIndex; i++) + ps.position[i] = localToWorld.MultiplyPoint3(ps.position[i]); + for(int i = offset; i < endIndex; i++) + ps.velocity[i] = localToWorld.MultiplyVector3(ps.velocity[i]); + for(int i = offset; i < endIndex; i++) + ps.animatedVelocity[i] = localToWorld.MultiplyVector3(ps.animatedVelocity[i]); + if(ps.usesAxisOfRotation) + for(int i = offset; i < endIndex; i++) + ps.axisOfRotation[i] = localToWorld.MultiplyVector3(ps.axisOfRotation[i]); + } + + ParticleSystem::UpdateModulesNonIncremental(system, ps, psTemp, offset, offset + particleCount); + offset += particleCount; + + Transform* t = system.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* child = (**i).QueryComponent(ParticleSystem); + if (child) + CombineParticleBuffersRec(offset, ps, psTemp, *child, false, needsAxisOfRotation); + } + } +} + +void ParticleSystemRenderer::SetUsesAxisOfRotationRec(ParticleSystem& shuriken, bool first) +{ + ParticleSystemRenderer* renderer = shuriken.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + shuriken.SetUsesAxisOfRotation(); + + Transform* t = shuriken.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* shuriken = (**i).QueryComponent(ParticleSystem); + if (shuriken) + SetUsesAxisOfRotationRec(*shuriken, false); + } + } +} + +void ParticleSystemRenderer::CombineBoundsRec(ParticleSystem& shuriken, MinMaxAABB& aabb, bool first) +{ + ParticleSystemRenderer* renderer = shuriken.QueryComponent(ParticleSystemRenderer); + if(!renderer || first) + { + AABB result = shuriken.m_State->minMaxAABB; + if(!shuriken.m_ReadOnlyState->useLocalSpace) + InverseTransformAABB (result, renderer->GetTransform().GetPosition (), renderer->GetTransform().GetRotation (), result); + + if(first) + aabb = result; + else + aabb.Encapsulate(result); + + Transform* t = shuriken.QueryComponent (Transform); + if (t == NULL) + return; + for (Transform::iterator i=t->begin ();i != t->end ();i++) + { + ParticleSystem* shuriken = (**i).QueryComponent(ParticleSystem); + if (shuriken) + CombineBoundsRec(*shuriken, aabb, false); + } + } +} + +void ParticleSystemRenderer::Render (int/* materialIndex*/, const ChannelAssigns& channels) +{ + ParticleSystem::SyncJobs(); + + ParticleSystem* system = QueryComponent(ParticleSystem); + if(!system) + return; + + // Can't render without an active camera (case 568930) + // Can remove check when we finally kill Renderer.Render() + if (!GetCurrentCameraPtr()) + return; + + PROFILER_AUTO_GFX(gParticlesSingleProfile, this); + + UInt32 numParticles = 0; + CalculateTotalParticleCount(numParticles, *system, true); + if(numParticles) + RenderInternal(*system, *this, channels, 0, numParticles); +} + +void ParticleSystemRenderer::RenderMultiple (const BatchInstanceData* instances, size_t count, const ChannelAssigns& channels) +{ + ParticleSystem::SyncJobs(); + + size_t numParticlesBatch = 0; + + BatchInstanceData const* instancesEnd = instances + count; + BatchInstanceData const* iBatchBegin = instances; + BatchInstanceData const* iBatchEnd = instances; + while(iBatchEnd != instancesEnd) + { + Assert(iBatchEnd->renderer->GetRendererType() == kRendererParticleSystem); + ParticleSystemRenderer* psRenderer = (ParticleSystemRenderer*)iBatchEnd->renderer; + Assert(psRenderer->GetRenderMode() != kSRMMesh); + ParticleSystem* system = psRenderer->QueryComponent(ParticleSystem); + if (!system ) + { + iBatchEnd++; + continue; + } + UInt32 numParticles = 0; + psRenderer->CalculateTotalParticleCount(numParticles, *system, true); + + if((numParticlesBatch + numParticles) <= kMaxNumParticlesPerBatch) + { + numParticlesBatch += numParticles; + iBatchEnd++; + } + else + { + if(numParticlesBatch) + { + RenderBatch(iBatchBegin, iBatchEnd - iBatchBegin, numParticlesBatch, channels); + numParticlesBatch = 0; + iBatchBegin = iBatchEnd; + } + else // Can't fit in one draw call + { + RenderBatch(iBatchEnd, 1, numParticles, channels); + iBatchEnd++; + iBatchBegin = iBatchEnd; + } + } + } + + if((iBatchBegin != iBatchEnd) && numParticlesBatch) + RenderBatch(iBatchBegin, iBatchEnd - iBatchBegin, numParticlesBatch, channels); +} + +void ParticleSystemRenderer::RenderBatch (const BatchInstanceData* instances, size_t count, size_t numParticles, const ChannelAssigns& channels) +{ + DebugAssert(numParticles); + + GfxDevice& device = GetGfxDevice(); + + const MaterialPropertyBlock* customProps = count > 0 ? instances[0].renderer->GetCustomProperties() : NULL; + if (customProps) + device.SetMaterialProperties (*customProps); + + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + ParticleSystemVertex* vbPtr = 0; + DynamicVBO& vbo = device.GetDynamicVBO(); + if(numParticles <= kMaxNumParticlesPerBatch) + { + if( !vbo.GetChunk( (1<<kShaderChannelVertex) | (1<<kShaderChannelNormal) | (1<<kShaderChannelTexCoord0) | (1<<kShaderChannelColor) | (1<<kShaderChannelTangent), + numParticles * 4, 0, + DynamicVBO::kDrawQuads, + (void**)&vbPtr, NULL ) ) + { + return; + } + } + + PROFILER_AUTO_GFX(gParticlesBatchProfile, 0); + + // Allocate VBO if count is not above threshold. Else just pass null down + BatchInstanceData const* iBatchBegin = instances; + BatchInstanceData const* instancesEnd = instances + count; + size_t particleOffset = 0; + while (iBatchBegin != instancesEnd) + { + Assert(iBatchBegin->renderer->GetRendererType() == kRendererParticleSystem); + ParticleSystemRenderer* psRenderer = (ParticleSystemRenderer*)iBatchBegin->renderer; + Assert(psRenderer->GetRenderMode() != kSRMMesh); + ParticleSystem* system = psRenderer->QueryComponent(ParticleSystem); + UInt32 particleCountTotal = 0; + if (system) + { + // It would be nice to filter out NULL particle systems earlier, but we don't (case 504744) + CalculateTotalParticleCount(particleCountTotal, *system, true); + if(particleCountTotal) + RenderInternal(*system, *psRenderer, channels, vbPtr + particleOffset * 4, particleCountTotal); + } + iBatchBegin++; + particleOffset += particleCountTotal; + } + + if(vbPtr) + { + vbo.ReleaseChunk (numParticles * 4, 0); + + // Draw + device.SetViewMatrix (Matrix4x4f::identity.GetPtr()); // implicitly sets world to identity + + PROFILER_BEGIN(gSubmitVBOParticleProfile, 0) + vbo.DrawChunk (channels); + GPU_TIMESTAMP(); + PROFILER_END + + if (count > 1) + device.AddBatchingStats(numParticles * 2, numParticles * 4, count); + + device.SetViewMatrix(viewMatrix.GetPtr()); + } +} + +void ParticleSystemRenderer::RenderInternal (ParticleSystem& system, const ParticleSystemRenderer& renderer, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr, UInt32 particleCountTotal) +{ + Assert(particleCountTotal); + +#if UNITY_EDITOR + if (!renderer.m_EditorEnabled) + return; +#endif + + GfxDevice& device = GetGfxDevice(); + + // Render matrix + Matrix4x4f viewMatrix; + CopyMatrix (device.GetViewMatrix (), viewMatrix.GetPtr ()); + + ParticleSystemParticles* ps = &system.GetParticles (); + size_t particleCount = ps->array_size (); + AssertBreak(particleCountTotal >= particleCount); + + UInt8* combineBuffer = 0; + ParticleSystemParticles combineParticles; + if(particleCountTotal > particleCount) + { + particleCount = particleCountTotal; + combineBuffer = ALLOC_TEMP_MANUAL(UInt8, particleCountTotal * ParticleSystemParticles::GetParticleSize()); + combineParticles.array_assign_external((void*)&combineBuffer[0], particleCountTotal); + ps = &combineParticles; + } + + if(!particleCount) + return; + + const bool needsAxisOfRotation = !renderer.GetScreenSpaceRotation(); + if(needsAxisOfRotation) + { + if ( IS_CONTENT_NEWER_OR_SAME (GetNumericVersion ("4.0.0f7")) && !IS_CONTENT_NEWER_OR_SAME (GetNumericVersion ("4.1.1a1")) ) + { + // this was introduced in 4.0 and is wrong, it will effectively force the rotation axis to be the y-axis for all particles, the function is meant only + // to initialize the axis array (once) and should only be called from SetUsesAxisOfRotationRec + ps->SetUsesAxisOfRotation (); + } + else + { + // this is the intended functionality, but was broken for 4.0 and 4.0.1 see above + SetUsesAxisOfRotationRec (system, true); + } + } + + ParticleSystemSortMode sortMode = (ParticleSystemSortMode)renderer.m_Data.sortMode; + bool isInLocalSpace = system.m_ReadOnlyState->useLocalSpace && !combineBuffer; + Matrix4x4f worldMatrix = Matrix4x4f::identity; + if(isInLocalSpace) + worldMatrix = system.GetComponent (Transform).GetLocalToWorldMatrixNoScale (); + + ParticleSystemParticlesTempData psTemp; + psTemp.color = ALLOC_TEMP_MANUAL(ColorRGBA32, particleCount); + psTemp.size = ALLOC_TEMP_MANUAL(float, particleCount); + psTemp.sheetIndex = 0; + psTemp.particleCount = particleCount; + if(combineBuffer) + { + int offset = 0; + CombineParticleBuffersRec(offset, *ps, psTemp, system, true, needsAxisOfRotation); + if (kSSMNone != sortMode) + Sort(viewMatrix, *ps, sortMode, &psTemp, true); + } + else + { + if(system.m_UVModule->GetEnabled()) + psTemp.sheetIndex = ALLOC_TEMP_MANUAL(float, particleCount); + + if (kSSMNone != sortMode) + { + Matrix4x4f objectToViewMatrix; + MultiplyMatrices3x4(viewMatrix, worldMatrix, objectToViewMatrix); + Sort(objectToViewMatrix, *ps, sortMode, 0, false); + } + ParticleSystem::UpdateModulesNonIncremental(system, *ps, psTemp, 0, particleCount); + } + + // Constrain the size to be a fraction of the viewport size. + // In perspective case, max size is (z*factorA). In ortho case, max size is just factorB. To have both + // without branches, we do (z*factorA+factorB) and set one of factors to zero. + float maxPlaneScale = 0.0f; + float maxOrthoSize = 0.0f; + // Getting the camera isn't totally free, so do it once. + const Camera& camera = GetCurrentCamera(); + if (!camera.GetOrthographic()) + maxPlaneScale = -camera.CalculateFarPlaneWorldSpaceLength() * renderer.m_Data.maxParticleSize / camera.GetFar(); + else + maxOrthoSize = camera.CalculateFarPlaneWorldSpaceLength() * renderer.m_Data.maxParticleSize; + + int numMeshes = 0; + for(int i = 0; i < ParticleSystemRendererData::kMaxNumParticleMeshes; i++) + if(renderer.m_Data.cachedMesh[i]) + numMeshes++; + + ParticleSystemGeomConstInputData constData; + constData.m_ViewMatrix = viewMatrix; + constData.m_CameraVelocity = viewMatrix.MultiplyVector3(camera.GetVelocity ()); + constData.m_Renderer = (Object*)&renderer; + for(int i = 0; i < numMeshes; i++) + { + constData.m_MeshIndexBuffer[i] = renderer.m_CachedIndexBuffer[i].begin(); + constData.m_MeshIndexCount[i] = renderer.m_CachedIndexBuffer[i].size(); + AssertBreak((constData.m_MeshIndexCount[i] % 3) == 0); + } + + system.GetNumTiles(constData.m_NumTilesX, constData.m_NumTilesY); + constData.maxPlaneScale = maxPlaneScale; + constData.maxOrthoSize = maxOrthoSize; + constData.numUVFrame = constData.m_NumTilesX * constData.m_NumTilesY; + constData.animUScale = 1.0f / (float)constData.m_NumTilesX; + constData.animVScale = 1.0f / (float)constData.m_NumTilesY; + constData.xSpan = Vector3f(-1.0f,0.0f,0.0f); + constData.ySpan = Vector3f(0.0f,0.0f,1.0f); + if (renderer.m_Data.renderMode == kSRMBillboardFixedVertical) + { + constData.ySpan = Vector3f(0.0f,1.0f,0.0f); + const Vector3f zSpan = viewMatrix.MultiplyVector3 (Vector3f::zAxis);// (RotateVectorByQuat (cameraRotation, Vector3f(0.0f,0.0f,1.0f)); + constData.xSpan = NormalizeSafe (Cross (constData.ySpan, zSpan)); + } + constData.xSpan = viewMatrix.MultiplyVector3(constData.xSpan); + constData.ySpan = viewMatrix.MultiplyVector3(constData.ySpan); + constData.usesSheetIndex = psTemp.sheetIndex != NULL; + + const float bentNormalAngle = renderer.m_Data.normalDirection * 90.0f * kDeg2Rad; + const float scale = (renderer.m_Data.renderMode == kSRMBillboard) ? 0.707106781f : 1.0f; + + Matrix4x4f viewToWorldMatrix; + Matrix4x4f::Invert_General3D(viewMatrix, viewToWorldMatrix); + + Matrix4x4f worldViewMatrix; + MultiplyMatrices4x4(&viewMatrix, &worldMatrix, &worldViewMatrix); + + Vector3f billboardNormal = Vector3f::zAxis; + if((renderer.m_Data.renderMode == kSRMBillboardFixedHorizontal) || (renderer.m_Data.renderMode == kSRMBillboardFixedVertical)) + billboardNormal = viewMatrix.MultiplyVector3 (NormalizeSafe (Cross (constData.xSpan, constData.ySpan))); + constData.bentNormalVector = viewToWorldMatrix.MultiplyVector3(Sin(bentNormalAngle) * billboardNormal); + constData.bentNormalFactor = Cos(bentNormalAngle) * scale; + + if (renderer.m_Data.renderMode == kSRMMesh) + DrawMeshParticles (constData, renderer.m_Data, worldMatrix, *ps, psTemp, channels); + else + DrawParticles(constData, renderer.m_Data, worldViewMatrix, viewToWorldMatrix, *ps, psTemp, channels, vbPtr); + + FREE_TEMP_MANUAL(psTemp.color); + FREE_TEMP_MANUAL(psTemp.size); + if(psTemp.sheetIndex) + FREE_TEMP_MANUAL(psTemp.sheetIndex); + if(combineBuffer) + FREE_TEMP_MANUAL(combineBuffer); +} + +template<class TransferFunction> inline +void ParticleSystemRenderer::Transfer (TransferFunction& transfer) +{ + Super::Transfer (transfer); + transfer.Transfer (m_Data.renderMode, "m_RenderMode"); + transfer.Transfer (m_Data.maxParticleSize, "m_MaxParticleSize"); + transfer.Transfer (m_Data.cameraVelocityScale, "m_CameraVelocityScale"); + transfer.Transfer (m_Data.velocityScale, "m_VelocityScale"); + transfer.Transfer (m_Data.lengthScale, "m_LengthScale"); + transfer.Transfer (m_Data.sortingFudge, "m_SortingFudge"); + transfer.Transfer (m_Data.normalDirection, "m_NormalDirection"); + transfer.Transfer (m_Data.sortMode, "m_SortMode"); + transfer.Transfer (m_Mesh[0], "m_Mesh"); + transfer.Transfer (m_Mesh[1], "m_Mesh1"); + transfer.Transfer (m_Mesh[2], "m_Mesh2"); + transfer.Transfer (m_Mesh[3], "m_Mesh3"); +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h new file mode 100644 index 0000000..07bcb0d --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h @@ -0,0 +1,134 @@ +#ifndef SHURIKENRENDERER_H +#define SHURIKENRENDERER_H + +#include "Runtime/Filters/Renderer.h" +#include "Runtime/Math/Color.h" +#include "Runtime/Math/Rect.h" +#include "Runtime/Math/Vector3.h" +#include "Runtime/Math/Vector2.h" + + + + +class Mesh; +class MinMaxAABB; +struct ParticleSystemVertex; + +struct ParticleSystemRendererData +{ + // Must match the one in RendererModuleUI.cs + enum { kMaxNumParticleMeshes = 4 }; + + int renderMode; ///< enum { Billboard = 0, Stretched = 1, Horizontal Billboard = 2, Vertical Billboard = 3, Mesh = 4 } + int sortMode; ///< enum { None = 0, By Distance = 1, Youngest First = 2, Oldest First = 3 } + float maxParticleSize; ///< How large is a particle allowed to be on screen at most? 1 is entire viewport. 0.5 is half viewport. + float cameraVelocityScale; ///< How much the camera motion is factored in when determining particle stretching. + float velocityScale; ///< When Stretch Particles is enabled, defines the length of the particle compared to its velocity. + float lengthScale; ///< When Stretch Particles is enabled, defines the length of the particle compared to its width. + float sortingFudge; ///< Lower the number, most likely that these particles will appear in front of other transparent objects, including other particles. + float normalDirection; ///< Value between 0.0 and 1.0. If 1.0 is used, normals will point towards camera. If 0.0 is used, normals will point out in the corner direction of the particle. + Mesh* cachedMesh[kMaxNumParticleMeshes]; + + /// Node hooked into the mesh user list of cached meshes so we get notified + /// when a mesh goes away. + /// + /// NOTE: Must be initialized properly after construction to point to the + /// ParticleSystemRenderer. + ListNode<Object> cachedMeshUserNode[kMaxNumParticleMeshes]; +}; + + +enum ParticleSystemRenderMode { + kSRMBillboard = 0, + kSRMStretch3D = 1, + kSRMBillboardFixedHorizontal = 2, + kSRMBillboardFixedVertical = 3, + kSRMMesh = 4, +}; + +enum ParticleSystemSortMode +{ + kSSMNone, + kSSMByDistance, + kSSMYoungestFirst, + kSSMOldestFirst, +}; + +struct ParticleSystemParticles; +struct ParticleSystemParticlesTempData; +class ParticleSystem; +struct ParticleSystemGeomConstInputData; +class ParticleSystemRenderer : public Renderer { +public: + REGISTER_DERIVED_CLASS (ParticleSystemRenderer, Renderer) + DECLARE_OBJECT_SERIALIZE (ParticleSystemRenderer) + static void InitializeClass (); + + ParticleSystemRenderer (MemLabelId label, ObjectCreationMode mode); + // ParticleSystemRenderer(); declared-by-macro + + virtual void Render (int materialIndex, const ChannelAssigns& channels); + static void RenderMultiple (const BatchInstanceData* instances, size_t count, const ChannelAssigns& channels); + + virtual void GetLocalAABB (AABB& result); + virtual void GetWorldAABB (AABB& result); + virtual float GetSortingFudge () const; + + virtual void CheckConsistency (); + virtual void Reset (); + + virtual void AwakeFromLoad (AwakeFromLoadMode awakeMode); + + void Update (const AABB& aabb); + void UpdateLocalAABB(); + + virtual void RendererBecameVisible(); + virtual void RendererBecameInvisible(); + + static void SetUsesAxisOfRotationRec(ParticleSystem& system, bool first); + static void CombineBoundsRec(ParticleSystem& shuriken, MinMaxAABB& aabb, bool first); + + GET_SET_DIRTY (ParticleSystemRenderMode, RenderMode, m_Data.renderMode) ; + GET_SET_DIRTY (ParticleSystemSortMode, SortMode, m_Data.sortMode) ; + GET_SET_DIRTY (float, MaxParticleSize, m_Data.maxParticleSize) ; + GET_SET_DIRTY (float, CameraVelocityScale, m_Data.cameraVelocityScale) ; + GET_SET_DIRTY (float, VelocityScale, m_Data.velocityScale) ; + GET_SET_DIRTY (float, LengthScale, m_Data.lengthScale) ; + void SetMesh (PPtr<Mesh> mesh) { m_Mesh[0] = mesh; SetDirty(); UpdateCachedMesh (); } + PPtr<Mesh> GetMesh () const { return m_Mesh[0]; } + + const PPtr<Mesh>* GetMeshes () const { return m_Mesh; } + const ParticleSystemRendererData& GetData() const { return m_Data; } + +#if UNITY_EDITOR + bool GetEditorEnabled() const { return m_EditorEnabled; } + void SetEditorEnabled(bool value) { m_EditorEnabled = value; } +#endif + + // For mesh we use world space rotation, else screen space + bool GetScreenSpaceRotation() const { return m_Data.renderMode != kSRMMesh; }; +private: + // from Renderer + virtual void UpdateRenderer (); + void UpdateCachedMesh (); + void OnDidDeleteMesh (Mesh* mesh); + +private: + static void CalculateTotalParticleCount(UInt32& totalNumberOfParticles, ParticleSystem& shuriken, bool first); + static void CombineParticleBuffersRec(int& offset, ParticleSystemParticles& particles, ParticleSystemParticlesTempData& psTemp, ParticleSystem& shuriken, bool first, bool needsAxisOfRotation); + + static void RenderBatch (const BatchInstanceData* instances, size_t count, size_t numParticles, const ChannelAssigns& channels); + static void RenderInternal (ParticleSystem& system, const ParticleSystemRenderer& renderer, const ChannelAssigns& channels, ParticleSystemVertex* vbPtr, UInt32 particleCountTotal); + + ParticleSystemRendererData m_Data; + dynamic_array<UInt16> m_CachedIndexBuffer[ParticleSystemRendererData::kMaxNumParticleMeshes]; + PPtr<Mesh> m_Mesh[ParticleSystemRendererData::kMaxNumParticleMeshes]; + + AABB m_LocalSpaceAABB; + +#if UNITY_EDITOR + bool m_EditorEnabled; +#endif +}; + +#endif // SHURIKENRENDERER_H diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp new file mode 100644 index 0000000..47db7ee --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemRendererTests.cpp @@ -0,0 +1,29 @@ +#include "UnityPrefix.h" + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" +#include "Runtime/Graphics/ParticleSystem/ParticleSystemRenderer.h" +#include "Runtime/Filters/Mesh/LodMesh.h" +#include "Runtime/Testing/TestFixtures.h" + + +SUITE (ParticleSystemRendererTests) +{ + typedef ObjectTestFixture<ParticleSystemRenderer> Fixture; + + TEST_FIXTURE (Fixture, DeletingMeshClearsOutCachedMeshPointers) + { + // Arrange. + PPtr<Mesh> mesh (NEW_OBJECT_RESET_AND_AWAKE (Mesh)); + m_ObjectUnderTest->SetMesh (mesh); + + // Act. + DestroySingleObject (mesh); + + // Assert. + CHECK (m_ObjectUnderTest->GetData().cachedMesh[0] == NULL); + } +} + +#endif diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp new file mode 100644 index 0000000..032cd19 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.cpp @@ -0,0 +1,113 @@ +#include "UnityPrefix.h" +#include "ParticleSystemUtils.h" +#include "ParticleSystem.h" +#include "ParticleSystemCurves.h" +#include "Modules/ParticleSystemModule.h" +#include "Runtime/BaseClasses/IsPlaying.h" + +#if UNITY_EDITOR +#include "Editor/Src/ParticleSystem/ParticleSystemEditor.h" +#endif + +UInt32 randomSeed = 0x1337; + +UInt32 GetGlobalRandomSeed () +{ + return ++randomSeed; +} + +void ResetGlobalRandomSeed () +{ + randomSeed = 0x1337; +} + +Vector2f CalculateInverseLerpOffsetScale (const Vector2f& range) +{ + Assert (range.x < range.y); + float scale = 1.0F / (range.y - range.x); + return Vector2f (scale, -range.x * scale); +} + +void CalculatePositionAndVelocity(Vector3f& initialPosition, Vector3f& initialVelocity, const ParticleSystemReadOnlyState& roState, const ParticleSystemState& state, const ParticleSystemParticles& ps, const size_t index) +{ + initialPosition = ps.position[index]; + initialVelocity = ps.velocity[index] + ps.animatedVelocity[index]; + if(roState.useLocalSpace) + { + // If we are in local space, transform to world space to make independent of this emitters transform + initialPosition = state.localToWorld.MultiplyPoint3(initialPosition); + initialVelocity = state.localToWorld.MultiplyVector3(initialVelocity); + } +} + +void KillParticle(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t index, size_t& particleCount) +{ + Assert(particleCount > 0); + + for(int i = 0; i < state.numCachedSubDataDeath; i++) + { + ParticleSystemEmissionState emissionState; + RecordEmit(emissionState, state.cachedSubDataDeath[i], roState, state, ps, kParticleSystemSubTypeDeath, i, index, 0.0f, 0.0001f, 1.0f); + } + + ps.element_assign (index, particleCount - 1); + --particleCount; + +} + +void RecordEmit(ParticleSystemEmissionState& emissionState, const ParticleSystemSubEmitterData& subEmitterData, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, ParticleSystemSubType type, int subEmitterIndex, size_t particleIndex, float t, float dt, float length) +{ + size_t numContinuous = 0; + Vector3f initialPosition; + Vector3f initialVelocity; + CalculatePositionAndVelocity(initialPosition, initialVelocity, roState, state, ps, particleIndex); + int amountOfParticlesToEmit = ParticleSystem::EmitFromData (emissionState, numContinuous, subEmitterData.emissionData, initialVelocity, t, std::min(t + dt, length), dt, length); + if(amountOfParticlesToEmit) + { + if(!state.recordSubEmits) + ParticleSystem::Emit(*subEmitterData.emitter, SubEmitterEmitCommand(emissionState, initialPosition, initialVelocity, type, subEmitterIndex, amountOfParticlesToEmit, numContinuous, t, dt), kParticleSystemEMStaging); + else if(!state.subEmitterCommandBuffer.IsFull()) + state.subEmitterCommandBuffer.AddCommand(emissionState, initialPosition, initialVelocity, type, subEmitterIndex, amountOfParticlesToEmit, numContinuous, t, dt); + } +} + +bool GetTransformationMatrix(Matrix4x4f& output, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld) +{ + if(isCurveInWorld != isSystemInWorld) + { + if(isSystemInWorld) + output = localToWorld; + else + Matrix4x4f::Invert_General3D(localToWorld, output); + return true; + } + else + { + output = Matrix4x4f::identity; + return false; + } +} + +bool GetTransformationMatrices(Matrix4x4f& output, Matrix4x4f& outputInverse, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld) +{ + if(isCurveInWorld != isSystemInWorld) + { + if(isSystemInWorld) + { + output = localToWorld; + Matrix4x4f::Invert_General3D(localToWorld, outputInverse); + } + else + { + Matrix4x4f::Invert_General3D(localToWorld, output); + outputInverse = localToWorld; + } + return true; + } + else + { + output = Matrix4x4f::identity; + outputInverse = Matrix4x4f::identity; + return false; + } +} diff --git a/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h new file mode 100644 index 0000000..45ec0a3 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/ParticleSystemUtils.h @@ -0,0 +1,48 @@ +#ifndef SHURIKENUTILS_H +#define SHURIKENUTILS_H + +#include "ParticleSystemCommon.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Random/Random.h" + +class Matrix4x4f; +struct ParticleSystemParticles; +struct ParticleSystemReadOnlyState; +struct ParticleSystemState; +struct ParticleSystemEmissionState; +struct ParticleSystemSubEmitterData; + +inline float InverseLerpFast01 (const Vector2f& scaleOffset, float v) +{ + return clamp01 (v * scaleOffset.x + scaleOffset.y); +} + +inline float GenerateRandom(UInt32 randomIn) +{ + Rand rand(randomIn); + return Random01(rand); +} + +inline void GenerateRandom3(Vector3f& randomOut, UInt32 randomIn) +{ + Rand rand(randomIn); + randomOut.x = Random01(rand); + randomOut.y = Random01(rand); + randomOut.z = Random01(rand); +} + +inline UInt8 GenerateRandomByte (UInt32 seed) +{ + Rand rand (seed); + return Rand::GetByteFromInt (rand.Get ()); +} + +UInt32 GetGlobalRandomSeed (); +void ResetGlobalRandomSeed (); +Vector2f CalculateInverseLerpOffsetScale (const Vector2f& range); +void KillParticle(const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, size_t index, size_t& particleCount); +void RecordEmit(ParticleSystemEmissionState& emissionState, const ParticleSystemSubEmitterData& subEmitterData, const ParticleSystemReadOnlyState& roState, ParticleSystemState& state, ParticleSystemParticles& ps, ParticleSystemSubType type, int subEmitterIndex, size_t particleIndex, float t, float dt, float length); +bool GetTransformationMatrix(Matrix4x4f& output, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld); +bool GetTransformationMatrices(Matrix4x4f& output, Matrix4x4f& outputInverse, const bool isSystemInWorld, const bool isCurveInWorld, const Matrix4x4f& localToWorld); + +#endif // SHURIKENUTILS_H diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp b/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp new file mode 100644 index 0000000..f20b0ac --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurve.cpp @@ -0,0 +1,405 @@ +#include "UnityPrefix.h" +#include "PolynomialCurve.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Math/Polynomials.h" +#include "Runtime/Math/AnimationCurve.h" + +static void DoubleIntegrateSegment (float* coeff) +{ + coeff[0] /= 20.0F; + coeff[1] /= 12.0F; + coeff[2] /= 6.0F; + coeff[3] /= 2.0F; +} + +static void IntegrateSegment (float* coeff) +{ + coeff[0] /= 4.0F; + coeff[1] /= 3.0F; + coeff[2] /= 2.0F; + coeff[3] /= 1.0F; +} + +void CalculateMinMax(Vector2f& minmax, float value) +{ + minmax.x = std::min(minmax.x, value); + minmax.y = std::max(minmax.y, value); +} + +void ConstrainToPolynomialCurve (AnimationCurve& curve) +{ + const int max = OptimizedPolynomialCurve::kMaxPolynomialKeyframeCount; + + // Maximum 3 keys + if (curve.GetKeyCount () > max) + curve.RemoveKeys(curve.begin() + max, curve.end()); + + // Clamp begin and end to 0...1 range + if (curve.GetKeyCount () >= 2) + { + curve.GetKey(0).time = 0; + curve.GetKey(curve.GetKeyCount ()-1).time = 1; + } +} + +bool IsValidPolynomialCurve (const AnimationCurve& curve) +{ + // Maximum 3 keys + if (curve.GetKeyCount () > OptimizedPolynomialCurve::kMaxPolynomialKeyframeCount) + return false; + // One constant key can always be representated + else if (curve.GetKeyCount () <= 1) + return true; + // First and last keyframe must be at 0 and 1 time + else + { + float beginTime = curve.GetKey(0).time; + float endTime = curve.GetKey(curve.GetKeyCount ()-1).time; + + return CompareApproximately(beginTime, 0.0F, 0.0001F) && CompareApproximately(endTime, 1.0F, 0.0001F); + } +} + +void SetPolynomialCurveToValue (AnimationCurve& a, OptimizedPolynomialCurve& c, float value) +{ + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, value), AnimationCurve::Keyframe(1.0f, value) }; + a.Assign(keys, keys + 2); + c.BuildOptimizedCurve(a, 1.0f); +} + +void SetPolynomialCurveToLinear (AnimationCurve& a, OptimizedPolynomialCurve& c) +{ + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + keys[0].inSlope = 0.0f; keys[0].outSlope = 1.0f; + keys[1].inSlope = 1.0f; keys[1].outSlope = 0.0f; + a.Assign(keys, keys + 2); + c.BuildOptimizedCurve(a, 1.0f); +} + +bool OptimizedPolynomialCurve::BuildOptimizedCurve (const AnimationCurve& editorCurve, float scale) +{ + if (!IsValidPolynomialCurve(editorCurve)) + return false; + + const size_t keyCount = editorCurve.GetKeyCount (); + + timeValue = 1.0F; + memset(segments, 0, sizeof(segments)); + + // Handle corner case 1 & 0 keyframes + if (keyCount == 0) + ; + else if (keyCount == 1) + { + // Set constant value coefficient + for (int i=0;i<kSegmentCount;i++) + segments[i].coeff[3] = editorCurve.GetKey(0).value * scale; + } + else + { + float segmentStartTime[kSegmentCount]; + + for (int i=0;i<kSegmentCount;i++) + { + bool hasSegment = i+1 < keyCount; + if (hasSegment) + { + AnimationCurve::Cache cache; + editorCurve.CalculateCacheData(cache, i, i + 1, 0.0F); + + memcpy(segments[i].coeff, cache.coeff, sizeof(Polynomial)); + segmentStartTime[i] = editorCurve.GetKey(i).time; + } + else + { + memcpy(segments[i].coeff, segments[i-1].coeff, sizeof(Polynomial)); + segmentStartTime[i] = 1.0F;//timeValue[i-1]; + } + } + + // scale curve + for (int i=0;i<kSegmentCount;i++) + { + segments[i].coeff[0] *= scale; + segments[i].coeff[1] *= scale; + segments[i].coeff[2] *= scale; + segments[i].coeff[3] *= scale; + } + + // Timevalue 0 is always 0.0F. No need to store it. + timeValue = segmentStartTime[1]; + + #if !UNITY_RELEASE + // Check that UI editor curve matches polynomial curve except if the + // scale happens to be infinite (trying to subtract infinity values from + // each other yields NaN with IEEE floats). + if (scale != std::numeric_limits<float>::infinity () && + scale != -std::numeric_limits<float>::infinity()) + { + for (int i=0;i<=50;i++) + { + // The very last element at 1.0 can be different when using step curves. + // The AnimationCurve implementation will sample the position of the last key. + // The OptimizedPolynomialCurve will keep value of the previous key (Continuing the trajectory of the segment) + // In practice this is probably not a problem, since you don't sample 1.0 because then the particle will be dead. + // thus we just don't do the automatic assert when the curves are not in sync in that case. + float t = std::min(i / 50.0F, 0.99999F); + float dif; + + DebugAssert((dif = Abs(Evaluate(t) - editorCurve.Evaluate(t) * scale)) < 0.01F); + } + } + #endif + } + + return true; +} + +void OptimizedPolynomialCurve::Integrate () +{ + for (int i=0;i<kSegmentCount;i++) + IntegrateSegment(segments[i].coeff); +} + +void OptimizedPolynomialCurve::DoubleIntegrate () +{ + Polynomial velocity0 = segments[0]; + IntegrateSegment (velocity0.coeff); + + velocityValue = Polynomial::EvalSegment(timeValue, velocity0.coeff) * timeValue; + + for (int i=0;i<kSegmentCount;i++) + DoubleIntegrateSegment(segments[i].coeff); +} + +Vector2f OptimizedPolynomialCurve::FindMinMaxDoubleIntegrated() const +{ + // Because of velocityValue * t, this becomes a quartic polynomial (4th order polynomial). + // TODO: Find all roots of quartic polynomial + Vector2f result = Vector2f::zero; + const int numSteps = 20; + const float delta = 1.0f / float(numSteps); + float acc = delta; + for(int i = 0; i < numSteps; i++) + { + CalculateMinMax(result, EvaluateDoubleIntegrated(acc)); + acc += delta; + } + return result; +} + +// Find the maximum of the integrated curve (x: min, y: max) +Vector2f OptimizedPolynomialCurve::FindMinMaxIntegrated() const +{ + Vector2f result = Vector2f::zero; + + float start[kSegmentCount] = {0.0f, timeValue}; + float end[kSegmentCount] = {timeValue, 1.0f}; + for(int i = 0; i < kSegmentCount; i++) + { + // Differentiate coefficients + float a = 4.0f*segments[i].coeff[0]; + float b = 3.0f*segments[i].coeff[1]; + float c = 2.0f*segments[i].coeff[2]; + float d = 1.0f*segments[i].coeff[3]; + + float roots[3]; + int numRoots = CubicPolynomialRootsGeneric(roots, a, b, c, d); + for(int r = 0; r < numRoots; r++) + { + float root = roots[r] + start[i]; + if((root >= start[i]) && (root < end[i])) + CalculateMinMax(result, EvaluateIntegrated(root)); + } + + // TODO: Don't use eval integrated, use eval segment (and integrate in loop) + CalculateMinMax(result, EvaluateIntegrated(end[i])); + } + return result; +} + +// Find the maximum of a double integrated curve (x: min, y: max) +Vector2f PolynomialCurve::FindMinMaxDoubleIntegrated() const +{ + // Because of velocityValue * t, this becomes a quartic polynomial (4th order polynomial). + // TODO: Find all roots of quartic polynomial + Vector2f result = Vector2f::zero; + const int numSteps = 20; + const float delta = 1.0f / float(numSteps); + float acc = delta; + for(int i = 0; i < numSteps; i++) + { + CalculateMinMax(result, EvaluateDoubleIntegrated(acc)); + acc += delta; + } + return result; +} + +Vector2f PolynomialCurve::FindMinMaxIntegrated() const +{ + Vector2f result = Vector2f::zero; + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + // Differentiate coefficients + float a = 4.0f*segments[i].coeff[0]; + float b = 3.0f*segments[i].coeff[1]; + float c = 2.0f*segments[i].coeff[2]; + float d = 1.0f*segments[i].coeff[3]; + + float roots[3]; + int numRoots = CubicPolynomialRootsGeneric(roots, a, b, c, d); + for(int r = 0; r < numRoots; r++) + { + float root = roots[r] + prevTimeValue; + if((root >= prevTimeValue) && (root < times[i])) + CalculateMinMax(result, EvaluateIntegrated(root)); + } + + // TODO: Don't use eval integrated, use eval segment (and integrate in loop) + CalculateMinMax(result, EvaluateIntegrated(times[i])); + prevTimeValue = times[i]; + } + return result; +} + +bool PolynomialCurve::IsValidCurve(const AnimationCurve& editorCurve) +{ + int keyCount = editorCurve.GetKeyCount(); + int segmentCount = keyCount - 1; + if(editorCurve.GetKey(0).time != 0.0f) + segmentCount++; + if(editorCurve.GetKey(keyCount-1).time != 1.0f) + segmentCount++; + return segmentCount <= kMaxNumSegments; +} + +bool PolynomialCurve::BuildCurve(const AnimationCurve& editorCurve, float scale) +{ + int keyCount = editorCurve.GetKeyCount(); + segmentCount = 1; + + const float kMaxTime = 1.01f; + + memset(segments, 0, sizeof(segments)); + memset(integrationCache, 0, sizeof(integrationCache)); + memset(doubleIntegrationCache, 0, sizeof(doubleIntegrationCache)); + memset(times, 0, sizeof(times)); + times[0] = kMaxTime; + + // Handle corner case 1 & 0 keyframes + if (keyCount == 0) + ; + else if (keyCount == 1) + { + // Set constant value coefficient + segments[0].coeff[3] = editorCurve.GetKey(0).value * scale; + } + else + { + segmentCount = keyCount - 1; + int segmentOffset = 0; + + // Add extra key to start if it doesn't match up + if(editorCurve.GetKey(0).time != 0.0f) + { + segments[0].coeff[3] = editorCurve.GetKey(0).value; + times[0] = editorCurve.GetKey(0).time; + segmentOffset = 1; + } + + for (int i = 0;i<segmentCount;i++) + { + DebugAssert(i+1 < keyCount); + AnimationCurve::Cache cache; + editorCurve.CalculateCacheData(cache, i, i + 1, 0.0F); + memcpy(segments[i+segmentOffset].coeff, cache.coeff, 4 * sizeof(float)); + times[i+segmentOffset] = editorCurve.GetKey(i+1).time; + } + segmentCount += segmentOffset; + + // Add extra key to start if it doesn't match up + if(editorCurve.GetKey(keyCount-1).time != 1.0f) + { + segments[segmentCount].coeff[3] = editorCurve.GetKey(keyCount-1).value; + segmentCount++; + } + + // Fixup last key time value + times[segmentCount-1] = kMaxTime; + + for (int i = 0;i<segmentCount;i++) + { + segments[i].coeff[0] *= scale; + segments[i].coeff[1] *= scale; + segments[i].coeff[2] *= scale; + segments[i].coeff[3] *= scale; + } + } + + DebugAssert(segmentCount <= kMaxNumSegments); + +#if !UNITY_RELEASE + // Check that UI editor curve matches polynomial curve + for (int i=0;i<=10;i++) + { + // The very last element at 1.0 can be different when using step curves. + // The AnimationCurve implementation will sample the position of the last key. + // The PolynomialCurve will keep value of the previous key (Continuing the trajectory of the segment) + // In practice this is probably not a problem, since you don't sample 1.0 because then the particle will be dead. + // thus we just don't do the automatic assert when the curves are not in sync in that case. + float t = std::min(i / 50.0F, 0.99999F); + float dif; + DebugAssert((dif = Abs(Evaluate(t) - editorCurve.Evaluate(t) * scale)) < 0.01F); + } +#endif + return true; +} + +void GenerateIntegrationCache(PolynomialCurve& curve) +{ + curve.integrationCache[0] = 0.0f; + float prevTimeValue0 = curve.times[0]; + float prevTimeValue1 = 0.0f; + for (int i=1;i<curve.segmentCount;i++) + { + float coeff[4]; + memcpy(coeff, curve.segments[i-1].coeff, 4*sizeof(float)); + IntegrateSegment (coeff); + float time = prevTimeValue0 - prevTimeValue1; + curve.integrationCache[i] = curve.integrationCache[i-1] + Polynomial::EvalSegment(time, coeff) * time; + prevTimeValue1 = prevTimeValue0; + prevTimeValue0 = curve.times[i]; + } +} + +// Expects double integrated segments and valid integration cache +void GenerateDoubleIntegrationCache(PolynomialCurve& curve) +{ + float sum = 0.0f; + float prevTimeValue = 0.0f; + for(int i = 0; i < curve.segmentCount; i++) + { + curve.doubleIntegrationCache[i] = sum; + float time = curve.times[i] - prevTimeValue; + time = std::max(time, 0.0f); + sum += Polynomial::EvalSegment(time, curve.segments[i].coeff) * time * time + curve.integrationCache[i] * time; + prevTimeValue = curve.times[i]; + } +} + +void PolynomialCurve::Integrate () +{ + GenerateIntegrationCache(*this); + for (int i=0;i<segmentCount;i++) + IntegrateSegment(segments[i].coeff); +} + +void PolynomialCurve::DoubleIntegrate () +{ + GenerateIntegrationCache(*this); + for (int i=0;i<segmentCount;i++) + DoubleIntegrateSegment(segments[i].coeff); + GenerateDoubleIntegrationCache(*this); +} diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurve.h b/Runtime/Graphics/ParticleSystem/PolynomialCurve.h new file mode 100644 index 0000000..d9e7270 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurve.h @@ -0,0 +1,229 @@ +#ifndef POLYONOMIAL_CURVE_H +#define POLYONOMIAL_CURVE_H + +template<class T> +class AnimationCurveTpl; +typedef AnimationCurveTpl<float> AnimationCurve; +class Vector2f; + +struct Polynomial +{ + static float EvalSegment (float t, const float* coeff) + { + return (t * (t * (t * coeff[0] + coeff[1]) + coeff[2])) + coeff[3]; + } + + float coeff[4]; +}; + +// Smaller, optimized version +struct OptimizedPolynomialCurve +{ + enum { kMaxPolynomialKeyframeCount = 3, kSegmentCount = kMaxPolynomialKeyframeCount-1, }; + + Polynomial segments[kSegmentCount]; + + float timeValue; + float velocityValue; + + // Evaluate double integrated Polynomial curve. + // Example: position = EvaluateDoubleIntegrated (normalizedTime) * startEnergy^2 + // Use DoubleIntegrate function to for example turn a force curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateDoubleIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + float res0, res1; + + // All segments are added together. At t = 0, the integrated curve is always zero. + + // 0 segment is sampled up to the 1 keyframe + // First key is always assumed to be at 0 time + float t1 = std::min(t, timeValue); + + // 1 segment is sampled from 1 key to 2 key + // Last key is always assumed to be at 1 time + float t2 = std::max(0.0F, t - timeValue); + + res0 = Polynomial::EvalSegment(t1, segments[0].coeff) * t1 * t1; + res1 = Polynomial::EvalSegment(t2, segments[1].coeff) * t2 * t2; + + float finalResult = res0 + res1; + + // Add velocity of previous segments + finalResult += velocityValue * std::max(t - timeValue, 0.0F); + + return finalResult; + } + + // Evaluate integrated Polynomial curve. + // Example: position = EvaluateIntegrated (normalizedTime) * startEnergy + // Use Integrate function to for example turn a velocity curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + float res0, res1; + + // All segments are added together. At t = 0, the integrated curve is always zero. + + // 0 segment is sampled up to the 1 keyframe + // First key is always assumed to be at 0 time + float t1 = std::min(t, timeValue); + + // 1 segment is sampled from 1 key to 2 key + // Last key is always assumed to be at 1 time + float t2 = std::max(0.0F, t - timeValue); + + res0 = Polynomial::EvalSegment(t1, segments[0].coeff) * t1; + res1 = Polynomial::EvalSegment(t2, segments[1].coeff) * t2; + + return (res0 + res1); + } + + // Evaluate the curve + // extects that t is in the 0...1 range + float Evaluate (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float res0 = Polynomial::EvalSegment(t, segments[0].coeff); + float res1 = Polynomial::EvalSegment(t - timeValue, segments[1].coeff); + + float result; + if (t > timeValue) + result = res1; + else + result = res0; + + return result; + } + + // Find the maximum of a double integrated curve (x: min, y: max) + Vector2f FindMinMaxDoubleIntegrated() const; + + // Find the maximum of the integrated curve (x: min, y: max) + Vector2f FindMinMaxIntegrated() const; + + // Precalculates polynomials from the animation curve and a scale factor + bool BuildOptimizedCurve (const AnimationCurve& editorCurve, float scale); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateIntegrated to evaluate the curve + void Integrate (); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateDoubleIntegrated to evaluate the curve + void DoubleIntegrate (); + + // Add a constant force to a velocity curve + // Assumes that you have already called Integrate on the velocity curve. + void AddConstantForceToVelocityCurve (float gravity) + { + for (int i=0;i<kSegmentCount;i++) + segments[i].coeff[1] += 0.5F * gravity; + } +}; + +// Bigger, not so optimized version +struct PolynomialCurve +{ + enum{ kMaxNumSegments = 8 }; + + Polynomial segments[kMaxNumSegments]; // Cached polynomial coefficients + float integrationCache[kMaxNumSegments]; // Cache of integrated values up until start of segments + float doubleIntegrationCache[kMaxNumSegments]; // Cache of double integrated values up until start of segments + float times[kMaxNumSegments]; // Time value for end of segment + + int segmentCount; + + // Find the maximum of a double integrated curve (x: min, y: max) + Vector2f FindMinMaxDoubleIntegrated() const; + + // Find the maximum of the integrated curve (x: min, y: max) + Vector2f FindMinMaxIntegrated() const; + + // Precalculates polynomials from the animation curve and a scale factor + bool BuildCurve(const AnimationCurve& editorCurve, float scale); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateIntegrated to evaluate the curve + void Integrate (); + + // Integrates a velocity curve to be a position curve. + // You have to call EvaluateDoubleIntegrated to evaluate the curve + void DoubleIntegrate (); + + // Evaluates if it is possible to represent animation curve as PolynomialCurve + static bool IsValidCurve(const AnimationCurve& editorCurve); + + // Evaluate double integrated Polynomial curve. + // Example: position = EvaluateDoubleIntegrated (normalizedTime) * startEnergy^2 + // Use DoubleIntegrate function to for example turn a force curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateDoubleIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + { + const float time = t - prevTimeValue; + return doubleIntegrationCache[i] + integrationCache[i] * time + Polynomial::EvalSegment(time, segments[i].coeff) * time * time; + } + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } + + // Evaluate integrated Polynomial curve. + // Example: position = EvaluateIntegrated (normalizedTime) * startEnergy + // Use Integrate function to for example turn a velocity curve into a position curve. + // Expects that t is in the 0...1 range. + float EvaluateIntegrated (float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + { + const float time = t - prevTimeValue; + return integrationCache[i] + Polynomial::EvalSegment(time, segments[i].coeff) * time; + } + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } + + // Evaluate the curve + // extects that t is in the 0...1 range + float Evaluate(float t) const + { + DebugAssert(t >= -0.01F && t <= 1.01F); + + float prevTimeValue = 0.0f; + for(int i = 0; i < segmentCount; i++) + { + if(t <= times[i]) + return Polynomial::EvalSegment(t - prevTimeValue, segments[i].coeff); + prevTimeValue = times[i]; + } + DebugAssert(!"PolyCurve: Outside segment range!"); + return 1.0f; + } +}; + + +void SetPolynomialCurveToValue (AnimationCurve& a, OptimizedPolynomialCurve& c, float value); +void SetPolynomialCurveToLinear (AnimationCurve& a, OptimizedPolynomialCurve& c); +void ConstrainToPolynomialCurve (AnimationCurve& curve); +bool IsValidPolynomialCurve (const AnimationCurve& curve); +void CalculateMinMax(Vector2f& minmax, float value); + +#endif // POLYONOMIAL_CURVE_H diff --git a/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp b/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp new file mode 100644 index 0000000..2a74710 --- /dev/null +++ b/Runtime/Graphics/ParticleSystem/PolynomialCurveTests.cpp @@ -0,0 +1,259 @@ +#include "UnityPrefix.h" +#include "Configuration/UnityConfigure.h" + +#if ENABLE_UNIT_TESTS + +#include "External/UnitTest++/src/UnitTest++.h" +#include "Runtime/Animation/AnimationCurveUtility.h" +#include "Runtime/Math/Vector2.h" +#include "Runtime/Graphics/ParticleSystem/PolynomialCurve.h" + +static float PhysicsSimulate (float gravity, float velocity, float time) +{ + return velocity * time + gravity * time * time * 0.5F; +} + +SUITE (PolynomialCurveTests) +{ +TEST (PolynomialCurve_ConstantIntegrate) +{ + AnimationCurve editorCurve; + OptimizedPolynomialCurve curve; + PolynomialCurve polyCurve; + + SetPolynomialCurveToValue(editorCurve, curve, 1.0f); + curve.Integrate(); + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.Integrate(); + + CHECK_CLOSE(0.0F, curve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.5F, curve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(1.0F, curve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_CLOSE(0.0F, polyCurve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.5F, polyCurve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(1.0F, polyCurve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 1.0f), curve.FindMinMaxIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 1.01f), polyCurve.FindMinMaxIntegrated(), 0.0001F)); + + + SetPolynomialCurveToLinear(editorCurve, curve); + curve.Integrate(); + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.Integrate(); + + CHECK_CLOSE(0.0F, curve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.125F, curve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(0.5F, curve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_CLOSE(0.0F, polyCurve.EvaluateIntegrated(0.0F), 0.0001F); + CHECK_CLOSE(0.125F, polyCurve.EvaluateIntegrated(0.5F), 0.0001F); + CHECK_CLOSE(0.5F, polyCurve.EvaluateIntegrated(1.0F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.51005f), polyCurve.FindMinMaxIntegrated(), 0.0001F)); +} + +// @TODO: Add generic poly +TEST (PolynomialCurve_ConstantDoubleIntegrate) +{ + AnimationCurve editorCurve; + OptimizedPolynomialCurve curve; + PolynomialCurve polyCurve; + + SetPolynomialCurveToValue(editorCurve, curve, 1.0f); + curve.DoubleIntegrate(); + + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.DoubleIntegrate(); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), curve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), curve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), curve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), curve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), curve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), polyCurve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), polyCurve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), polyCurve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), polyCurve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), polyCurve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxDoubleIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), polyCurve.FindMinMaxDoubleIntegrated(), 0.0001F)); + + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, 1.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + editorCurve.Assign(keys, keys + 3); + curve.BuildOptimizedCurve(editorCurve, 1); + curve.DoubleIntegrate(); + + polyCurve.BuildCurve(editorCurve, 1.0f); + polyCurve.DoubleIntegrate(); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), curve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), curve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), curve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), curve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), curve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.00F), polyCurve.EvaluateDoubleIntegrated(0.00F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.25F), polyCurve.EvaluateDoubleIntegrated(0.25F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.50F), polyCurve.EvaluateDoubleIntegrated(0.50F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 0.75F), polyCurve.EvaluateDoubleIntegrated(0.75F), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(1, 0, 1.00F), polyCurve.EvaluateDoubleIntegrated(1.00F), 0.0001F); + + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), curve.FindMinMaxDoubleIntegrated(), 0.0001F)); + CHECK_EQUAL(true, CompareApproximately(Vector2f(0.0f, 0.5f), polyCurve.FindMinMaxDoubleIntegrated(), 0.0001F)); +} + +static float EvaluateGravityIntegrated (OptimizedPolynomialCurve& gravityCurve, float time, float maximumRange) +{ + float res = gravityCurve.EvaluateDoubleIntegrated(time / maximumRange) * maximumRange * maximumRange; + return res; +} + +TEST (PolynomialCurve_GravityIntegrate) +{ + const float kGravity = -9.81f; + AnimationCurve editorCurve; + OptimizedPolynomialCurve gravityCurve; + SetPolynomialCurveToValue(editorCurve, gravityCurve, kGravity); + gravityCurve.DoubleIntegrate(); + + const float kMaxRange = 5.0F; + + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 0.0F), EvaluateGravityIntegrated(gravityCurve, 0.0F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 0.5F), EvaluateGravityIntegrated(gravityCurve, 0.5F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 1.0F), EvaluateGravityIntegrated(gravityCurve, 1.0F, kMaxRange), 0.0001F); + CHECK_CLOSE(PhysicsSimulate(kGravity, 0.0F, 5.0F), EvaluateGravityIntegrated(gravityCurve, 5.0F, kMaxRange), 0.0001F); +} + +float TriangleShapeIntegralFirstHalf (float x) +{ + return (2.0F / 3.0F) *x*x*x - 0.5F*x*x; +} + +// Test that a simple triangle shape 0,-1 to 0.5,1 to 1,-1 +// Gives expected results during double integration +TEST (PolynomialCurve_TriangleShapeDoubleIntegrate) +{ + OptimizedPolynomialCurve curve; + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, -1.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, -1.0f) }; + AnimationCurve editorCurve; + editorCurve.Assign(keys, keys + 3); + + RecalculateSplineSlopeLinear(editorCurve); + + curve.BuildOptimizedCurve(editorCurve, 1.0F); + + CHECK_CLOSE(-1, curve.Evaluate(0), 0.0001F); + CHECK_CLOSE(0, curve.Evaluate(0.25F), 0.0001F); + CHECK_CLOSE(1.0F, curve.Evaluate(0.5F), 0.0001F); + CHECK_CLOSE(0.0F, curve.Evaluate(0.75F), 0.0001F); + CHECK_CLOSE(-1, curve.Evaluate(1), 0.0001F); + + curve.DoubleIntegrate(); + + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0), EvaluateGravityIntegrated(curve, 0.0F , 1.0F), 0.0001F); + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0.25F), EvaluateGravityIntegrated(curve, 0.25F, 1.0F), 0.0001F); + CHECK_CLOSE(TriangleShapeIntegralFirstHalf(0.5F), EvaluateGravityIntegrated(curve, 0.5F , 1.0F), 0.0001F); + CHECK_CLOSE(-0.0208333, EvaluateGravityIntegrated(curve, 0.75F, 1.0F), 0.0001F); + CHECK_CLOSE(0, EvaluateGravityIntegrated(curve, 1.0F , 1.0F), 0.0001F); +} + +void CompareIntegrateCurve (const AnimationCurve& curve, const OptimizedPolynomialCurve& integratedCurve) +{ + CHECK_CLOSE(0, integratedCurve.EvaluateIntegrated(0), 0.0001F); + + int intervals = 100; + + float sum = 0.0F; + for (int i=1;i<intervals;i++) + { + float t = (float)i / (float)intervals; + float dt = 1.0F / (float)intervals; + + float avgValue = curve.Evaluate(t - dt * 0.5F); + sum += avgValue * dt; + + float integratedValue = integratedCurve.EvaluateIntegrated(t); + CHECK_CLOSE(sum, integratedValue, 0.0001F); + } +} + +void CompareDoubleIntegrateCurve (const AnimationCurve& curve, const OptimizedPolynomialCurve& integratedCurve) +{ + CHECK_CLOSE(0, integratedCurve.EvaluateIntegrated(0), 0.0001F); + + int intervals = 1000; + + float integratedSum = 0.0F; + float sum = 0.0F; + for (int i=1;i<intervals;i++) + { + float t = (float)i / (float)intervals; + float dt = 1.0F / (float)intervals; + + float avgValue = curve.Evaluate(t - dt * 0.5F); + sum += avgValue * dt; + integratedSum += sum * dt; + + float integratedValue = integratedCurve.EvaluateDoubleIntegrated(t); + CHECK_CLOSE(integratedSum, integratedValue, 0.001F); + } +} + +void CompareIntegrateCurve (const AnimationCurve::Keyframe* keys, int size) +{ + AnimationCurve sourceCurve; + sourceCurve.Assign(keys, keys + size); + + OptimizedPolynomialCurve curve; + AnimationCurve editorCurve; + editorCurve = sourceCurve; + curve.BuildOptimizedCurve(editorCurve, 1); + curve.Integrate(); + + CompareIntegrateCurve(sourceCurve, curve); +} + +void CompareDoubleIntegrateCurve (const AnimationCurve::Keyframe* keys, int size) +{ + AnimationCurve sourceCurve; + sourceCurve.Assign(keys, keys + size); + + OptimizedPolynomialCurve curve; + AnimationCurve editorCurve; + editorCurve = sourceCurve; + curve.BuildOptimizedCurve(editorCurve, 1); + curve.DoubleIntegrate(); + + CompareDoubleIntegrateCurve(sourceCurve, curve); +} + +TEST (PolynomialCurve_TriangleCurve) +{ + AnimationCurve sourceCurve; + AnimationCurve::Keyframe keys[3] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(0.5f, 1.0f), AnimationCurve::Keyframe(1.0f, 0.0f) }; + sourceCurve.Assign(keys, keys + 3); + RecalculateSplineSlopeLinear(sourceCurve); + + CompareIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); + CompareDoubleIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); +} + + +TEST (PolynomialCurve_LineCurve) +{ + AnimationCurve sourceCurve; + AnimationCurve::Keyframe keys[2] = { AnimationCurve::Keyframe(0.0f, 0.0f), AnimationCurve::Keyframe(1.0f, 1.0f) }; + sourceCurve.Assign(keys, keys + 2); + RecalculateSplineSlopeLinear(sourceCurve); + + CompareIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); + CompareDoubleIntegrateCurve(&sourceCurve.GetKey(0), sourceCurve.GetKeyCount()); +} +} + +#endif |