You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

262 lines
9.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Duality;
using Duality.Editor;
using Duality.Resources;
using Duality.Components;
using Duality.Components.Renderers;
using Duality.Drawing;
namespace ParticleSystem
{
[RequiredComponent(typeof(Transform))]
public class ParticleEffect : Renderer, ICmpUpdatable, ICmpInitializable
{
private ContentRef<Material> material = null;
private Vector2 particleSize = new Vector2(16, 16);
private Vector3 constantForce = Vector3.Zero;
private float linearDrag = 0.3f;
private float angularDrag = 0.3f;
private float fadeInAt = 0.0f;
private float fadeOutAt = 0.75f;
private List<ParticleEmitter> emitters = new List<ParticleEmitter>();
[DontSerialize]
private float boundRadius = 0.0f;
[DontSerialize]
private RawList<Particle> particles = null;
[DontSerialize]
private RawList<VertexC1P3T2> vertexBuffer = null;
public ContentRef<Material> ParticleMaterial
{
get { return this.material; }
set { this.material = value; }
}
public Vector2 ParticleSize
{
get { return this.particleSize; }
set { this.particleSize = value; }
}
public Vector3 ConstantForce
{
get { return this.constantForce; }
set { this.constantForce = value; }
}
[EditorHintRange(0.0f, 1.0f)]
public float LinearDrag
{
get { return this.linearDrag; }
set { this.linearDrag = value; }
}
[EditorHintRange(0.0f, 1.0f)]
public float AngularDrag
{
get { return this.angularDrag; }
set { this.angularDrag = value; }
}
[EditorHintRange(0.0f, 1.0f)]
public float FadeInAt
{
get { return this.fadeInAt; }
set { this.fadeInAt = value; }
}
[EditorHintRange(0.0f, 1.0f)]
public float FadeOutAt
{
get { return this.fadeOutAt; }
set { this.fadeOutAt = value; }
}
public List<ParticleEmitter> Emitters
{
get { return this.emitters; }
set { this.emitters = value ?? new List<ParticleEmitter>(); }
}
public override float BoundRadius
{
get { return this.boundRadius * this.GameObj.Transform.Scale; }
}
public void AddParticles(ParticleEmitter emitter, int count)
{
// Lookup what sprite sheet we're using to get the number of available frames
Texture tex = this.RetrieveTexture();
if (tex == null) return;
Pixmap img = tex.BasePixmap.Res;
if (img == null) return;
// Gather data for emitting particles
Vector3 effectPos = this.GameObj.Transform.Pos;
float effectAngle = this.GameObj.Transform.Angle;
float effectScale = this.GameObj.Transform.Scale;
// Reserve memory for storing the new particles we're spawning
if (this.particles == null) this.particles = new RawList<Particle>(count);
int oldCount = this.particles.Count;
this.particles.Count = this.particles.Count + count;
// Initialize all those new particles
Particle[] particleData = this.particles.Data;
for (int i = oldCount; i < this.particles.Count; i++)
{
// Initialize the current particle.
emitter.InitParticle(ref particleData[i]);
}
}
private void RemoveParticle(int index)
{
this.particles.RemoveAt(index);
}
private Texture RetrieveTexture()
{
if (material.IsAvailable && material.Res.MainTexture.IsAvailable)
return material.Res.MainTexture.Res;
else
return null;
}
private void UpdateEmitters()
{
for (int i = this.emitters.Count - 1; i >= 0; i--)
{
if (this.emitters[i] == null) continue;
this.emitters[i].Update(this);
}
}
public override void Draw(IDrawDevice device)
{
if (this.particles == null) return;
Texture tex = this.RetrieveTexture();
if (tex == null) return;
Vector2 particleHalfSize = this.particleSize * 0.5f;
float objAngle = this.GameObj.Transform.Angle;
float objScale = this.GameObj.Transform.Scale;
Vector3 objPos = this.GameObj.Transform.Pos;
Vector2 objXDot, objYDot;
MathF.GetTransformDotVec(objAngle, objScale, out objXDot, out objYDot);
if (this.vertexBuffer == null) this.vertexBuffer = new RawList<VertexC1P3T2>(this.particles.Count * 4);
this.vertexBuffer.Count = this.vertexBuffer.Count = this.particles.Count * 4;
VertexC1P3T2[] vertexData = this.vertexBuffer.Data;
Particle[] particleData = this.particles.Data;
int particleCount = this.particles.Count;
for (int i = 0; i < particleCount; i++)
{
ColorRgba color = particleData[i].Color;
float alpha = (float)color.A / 255.0f;
if (this.fadeOutAt < 1.0f) alpha *= MathF.Clamp((1.0f - particleData[i].AgeFactor) / this.fadeOutAt, 0.0f, 1.0f);
if (this.fadeInAt > 0.0f) alpha *= MathF.Clamp(particleData[i].AgeFactor / this.fadeInAt, 0.0f, 1.0f);
color.A = (byte)(alpha * 255.0f);
Rect uvRect;
tex.LookupAtlas(particleData[i].SpriteIndex, out uvRect);
Vector3 particlePos = particleData[i].Position;
MathF.TransformDotVec(ref particlePos, ref objXDot, ref objYDot);
particlePos += objPos;
float particleAngle = objAngle + particleData[i].Angle;
float particleScale = objScale;
device.PreprocessCoords(ref particlePos, ref particleScale);
Vector2 xDot, yDot;
MathF.GetTransformDotVec(particleAngle, particleScale, out xDot, out yDot);
Vector2 edgeTopLeft = new Vector2(-particleHalfSize.X, -particleHalfSize.Y);
Vector2 edgeBottomLeft = new Vector2(-particleHalfSize.X, particleHalfSize.Y);
Vector2 edgeBottomRight = new Vector2(particleHalfSize.X, particleHalfSize.Y);
Vector2 edgeTopRight = new Vector2(particleHalfSize.X, -particleHalfSize.Y);
MathF.TransformDotVec(ref edgeTopLeft, ref xDot, ref yDot);
MathF.TransformDotVec(ref edgeBottomLeft, ref xDot, ref yDot);
MathF.TransformDotVec(ref edgeBottomRight, ref xDot, ref yDot);
MathF.TransformDotVec(ref edgeTopRight, ref xDot, ref yDot);
int vertexBaseIndex = i * 4;
vertexData[vertexBaseIndex + 0].Pos.X = particlePos.X + edgeTopLeft.X;
vertexData[vertexBaseIndex + 0].Pos.Y = particlePos.Y + edgeTopLeft.Y;
vertexData[vertexBaseIndex + 0].Pos.Z = particlePos.Z;
vertexData[vertexBaseIndex + 0].TexCoord.X = uvRect.X;
vertexData[vertexBaseIndex + 0].TexCoord.Y = uvRect.Y;
vertexData[vertexBaseIndex + 0].Color = color;
vertexData[vertexBaseIndex + 1].Pos.X = particlePos.X + edgeBottomLeft.X;
vertexData[vertexBaseIndex + 1].Pos.Y = particlePos.Y + edgeBottomLeft.Y;
vertexData[vertexBaseIndex + 1].Pos.Z = particlePos.Z;
vertexData[vertexBaseIndex + 1].TexCoord.X = uvRect.X;
vertexData[vertexBaseIndex + 1].TexCoord.Y = uvRect.BottomY;
vertexData[vertexBaseIndex + 1].Color = color;
vertexData[vertexBaseIndex + 2].Pos.X = particlePos.X + edgeBottomRight.X;
vertexData[vertexBaseIndex + 2].Pos.Y = particlePos.Y + edgeBottomRight.Y;
vertexData[vertexBaseIndex + 2].Pos.Z = particlePos.Z;
vertexData[vertexBaseIndex + 2].TexCoord.X = uvRect.RightX;
vertexData[vertexBaseIndex + 2].TexCoord.Y = uvRect.BottomY;
vertexData[vertexBaseIndex + 2].Color = color;
vertexData[vertexBaseIndex + 3].Pos.X = particlePos.X + edgeTopRight.X;
vertexData[vertexBaseIndex + 3].Pos.Y = particlePos.Y + edgeTopRight.Y;
vertexData[vertexBaseIndex + 3].Pos.Z = particlePos.Z;
vertexData[vertexBaseIndex + 3].TexCoord.X = uvRect.RightX;
vertexData[vertexBaseIndex + 3].TexCoord.Y = uvRect.Y;
vertexData[vertexBaseIndex + 3].Color = color;
}
device.AddVertices(this.material, VertexMode.Quads, vertexData, this.vertexBuffer.Count);
}
void ICmpUpdatable.OnUpdate()
{
// Update all existing particles
Vector3 boundMax = Vector3.Zero;
if (this.particles != null)
{
float timeMult = Time.TimeMult;
float timePassed = Time.MsPFMult * timeMult;
Particle[] particleData = this.particles.Data;
int particleCount = this.particles.Count;
for (int i = particleCount - 1; i >= 0; i--)
{
particleData[i].Position += particleData[i].Velocity * timeMult;
particleData[i].Angle += particleData[i].AngleVelocity * timeMult;
particleData[i].Velocity += this.constantForce * 0.01f * timeMult;
particleData[i].Velocity *= MathF.Pow(1.0f - (this.linearDrag * 0.1f), timeMult);
particleData[i].AngleVelocity *= MathF.Pow(1.0f - (this.angularDrag * 0.1f), timeMult);
particleData[i].AgeFactor += timePassed / particleData[i].TimeToLive;
if (particleData[i].AgeFactor > 1.0f)
this.RemoveParticle(i);
boundMax.X = MathF.Max(boundMax.X, MathF.Abs(particleData[i].Position.X));
boundMax.Y = MathF.Max(boundMax.Y, MathF.Abs(particleData[i].Position.Y));
boundMax.Z = MathF.Max(boundMax.Z, MathF.Abs(particleData[i].Position.Z));
}
}
this.boundRadius = boundMax.Length;
this.boundRadius += this.particleSize.Length;
// Update particle emission
this.UpdateEmitters();
}
void ICmpInitializable.OnInit(Component.InitContext context)
{
if (context == InitContext.Activate)
{
// When activating, directly update particle emitters once, so there is already something to see.
this.UpdateEmitters();
}
}
void ICmpInitializable.OnShutdown(Component.ShutdownContext context) {}
}
}