Commits

Juha Komulainen committed f6068bd

Initial revision: basic particle physics.

Comments (0)

Files changed (14)

+syntax: glob
+Libs
+bin

CycloNet.Physics/AssemblyInfo.cs

+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+// Information about this assembly is defined by the following attributes. 
+// Change them to the values specific to your project.
+
+[assembly: AssemblyTitle("CycloNet.Physics")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
+// The form "{Major}.{Minor}.*" will automatically update the build and revision,
+// and "{Major}.{Minor}.{Build}.*" will update just the revision.
+
+[assembly: AssemblyVersion("1.0.*")]
+
+// The following attributes are used to specify the signing key for the assembly, 
+// if desired. See the Mono documentation for more information about signing.
+
+//[assembly: AssemblyDelaySign(false)]
+//[assembly: AssemblyKeyFile("")]
+

CycloNet.Physics/CycloNet.Physics.csproj

+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
+    <ProductVersion>10.0.0</ProductVersion>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>{25F61EE2-0710-4376-A40F-EC3BA1236080}</ProjectGuid>
+    <OutputType>Library</OutputType>
+    <RootNamespace>CycloNet.Physics</RootNamespace>
+    <AssemblyName>CycloNet.Physics</AssemblyName>
+    <TargetFrameworkVersion>v3.5</TargetFrameworkVersion>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <DebugSymbols>true</DebugSymbols>
+    <DebugType>full</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Debug</OutputPath>
+    <DefineConstants>DEBUG</DefineConstants>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <DebugType>none</DebugType>
+    <Optimize>false</Optimize>
+    <OutputPath>bin\Release</OutputPath>
+    <ErrorReport>prompt</ErrorReport>
+    <WarningLevel>4</WarningLevel>
+    <ConsolePause>false</ConsolePause>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System" />
+    <Reference Include="OpenTK, Version=1.0.0.0, Culture=neutral, PublicKeyToken=bad199fe84eb3df4">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\Libs\OpenTK.dll</HintPath>
+    </Reference>
+    <Reference Include="System.Core" />
+  </ItemGroup>
+  <ItemGroup>
+    <Compile Include="AssemblyInfo.cs" />
+    <Compile Include="Particles\IParticleContactGenerator.cs" />
+    <Compile Include="Particles\Particle.cs" />
+    <Compile Include="Particles\ParticleContact.cs" />
+    <Compile Include="Particles\ParticleForceRegistry.cs" />
+    <Compile Include="Particles\ParticleContactResolver.cs" />
+    <Compile Include="Particles\ParticleWorld.cs" />
+    <Compile Include="Particles\GroundContacts.cs" />
+    <Compile Include="Particles\IParticleForceGenerator.cs" />
+    <Compile Include="Particles\ParticleForceGenerators.cs" />
+    <Compile Include="MathUtils.cs" />
+  </ItemGroup>
+  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
+  <ItemGroup>
+    <Folder Include="Particles\" />
+  </ItemGroup>
+</Project>

CycloNet.Physics/MathUtils.cs

+using System;
+namespace CycloNet.Physics
+{
+    public static class MathUtils
+    {
+        public static float Sqrt(float x)
+        {
+            return (float) Math.Sqrt(x);
+        }
+
+        public static float Sin(float x)
+        {
+            return (float) Math.Sin(x);
+        }
+
+        public static float Cos(float x)
+        {
+            return (float) Math.Cos(x);
+        }
+
+        public static float Exp(float d)
+        {
+            return (float) Math.Exp(d);
+        }
+
+        public static float Pow(float x, float y)
+        {
+            return (float) Math.Pow(x, y);
+        }
+    }
+}
+

CycloNet.Physics/Particles/GroundContacts.cs

+using System;
+using System.Collections.Generic;
+using OpenTK;
+
+namespace CycloNet.Physics.Particles
+{
+    /// <summary>
+    /// A contact generator that takes particles and
+    /// collides them against the ground.
+    /// </summary>
+    public class GroundContacts : IParticleContactGenerator
+    {
+        // Reference to a mutable list
+        readonly List<Particle> particles;
+
+        public GroundContacts(List<Particle> particles)
+        {
+            this.particles = particles;
+        }
+
+        public void AddContacts(List<ParticleContact> contacts, int maxContacts)
+        {
+            foreach (var p in particles)
+            {
+                if (contacts.Count >= maxContacts) break;
+
+                var y = p.Position.Y;
+                if (y < 0.0f)
+                {
+                    contacts.Add(new ParticleContact
+                    {
+                        ContactNormal = Vector3.UnitY,
+                        Particle0 = p,
+                        Particle1 = null,
+                        Penetration = -y,
+                        Restitution = 0.2f
+                    });
+                }
+            }
+        }
+    }
+}
+

CycloNet.Physics/Particles/IParticleContactGenerator.cs

+using System;
+using System.Collections.Generic;
+
+namespace CycloNet.Physics.Particles
+{
+    /// <summary>
+    /// Contact generator for particles.
+    /// </summary>
+    public interface IParticleContactGenerator
+    {
+        /// <summary>
+        /// Adds contacts to given list of contact.
+        /// </summary>
+        void AddContacts(List<ParticleContact> contacts, int maxContacts);
+    }
+}
+

CycloNet.Physics/Particles/IParticleForceGenerator.cs

+using System;
+namespace CycloNet.Physics.Particles
+{
+    public interface IParticleForceGenerator
+    {
+        void UpdateForce(Particle particle, float duration);
+    }
+}
+

CycloNet.Physics/Particles/Particle.cs

+using System;
+using System.Diagnostics;
+using OpenTK;
+
+namespace CycloNet.Physics.Particles
+{
+    /// <summary>
+    /// A particle is the simplest object that can be simulated in the
+    /// physics system.
+    ///
+    /// It has position data (no orientation data), along with
+    /// velocity. It can be integrated forward through time, and have
+    /// linear forces, and impulses applied to it.
+    /// </summary>
+    public class Particle
+    {
+        /// <summary>
+        /// Holds the inverse of the mass of the particle. It
+        /// is more useful to hold the inverse mass because
+        /// integration is simpler, and because in real time
+        /// simulation it is more useful to have objects with
+        /// infinite mass (immovable) than zero mass
+        /// (completely unstable in numerical simulation).
+        /// </summary>
+        public float InverseMass { get; set; }
+
+        /// <summary>
+        /// Holds the amount of damping applied to linear
+        /// motion. Damping is required to remove energy added
+        /// through numerical instability in the integrator.
+        /// </summary>
+        public float Damping { get; set; }
+
+        /// <summary>
+        /// Position of the particle in world space.
+        /// </summary>
+        public Vector3 Position { get; set; }
+
+        /// <summary>
+        /// Linear velocity of the particle in world space.
+        /// </summary>
+        public Vector3 Velocity { get; set; }
+
+        /// <summary>
+        /// Holds the acceleration of the particle.  This value
+        /// can be used to set acceleration due to gravity (its primary
+        /// use), or any other constant acceleration.
+        /// </summary>
+        public Vector3 Acceleration { get; set; }
+
+        /// <summary>
+        /// Holds the accumulated force to be applied at the next
+        /// simulation iteration only. This value is zeroed at each
+        /// integration step.
+        /// </summary>
+        Vector3 forceAccum;
+
+        public void Integrate(float duration)
+        {
+            Debug.Assert(duration > 0.0f);
+
+            // We don't integrate things with zero mass.
+            if (InverseMass <= 0.0f) return;
+
+            // Update linear position.
+            Position += duration * Velocity;
+
+            // Work out the acceleration from the force
+            Vector3 resultingAcc = Acceleration + InverseMass * forceAccum;
+
+            // Update linear velocity from the acceleration.
+            Velocity += duration * resultingAcc;
+
+            // Impose drag.
+            Velocity *= MathUtils.Pow(Damping, duration);
+
+            // Clear the forces.
+            ClearAccumulator();
+        }
+
+        public float Mass
+        {
+            get
+            {
+                return (InverseMass == 0)
+                    ? float.MaxValue
+                    : (1.0f/InverseMass);
+            }
+            set
+            {
+                Debug.Assert(value != 0);
+                InverseMass = 1.0f/value;
+            }
+        }
+
+        public bool HasFiniteMass
+        {
+            get { return InverseMass >= 0.0f; }
+        }
+
+        internal void ClearAccumulator()
+        {
+            forceAccum = Vector3.Zero;
+        }
+
+        public void AddForce(Vector3 force)
+        {
+            forceAccum += force;
+        }
+    }
+}
+

CycloNet.Physics/Particles/ParticleContact.cs

+using System;
+using OpenTK;
+
+namespace CycloNet.Physics.Particles
+{
+    public class ParticleContact
+    {
+        public Vector3 ContactNormal;
+        public Particle Particle0;
+        public Particle Particle1;
+        public float Penetration;
+        public float Restitution;
+
+        public Vector3 ParticleMovement0 { get; private set; }
+        public Vector3 ParticleMovement1 { get; private set; }
+
+        public float CalculateSeparatingVelocity()
+        {
+            var relativeVelocity = Particle0.Velocity;
+            if (Particle1 != null)
+                relativeVelocity -= Particle1.Velocity;
+
+            return Vector3.Dot(relativeVelocity, ContactNormal);
+        }
+
+        public void Resolve(float duration)
+        {
+            ResolveVelocity(duration);
+            ResolveInterpenetration(duration);
+        }
+
+        private void ResolveVelocity(float duration)
+        {
+            var separatingVelocity = CalculateSeparatingVelocity();
+
+            // If the contact is either stationary or separating, no impulse is required.
+            if (separatingVelocity > 0)
+                return;
+
+            // Calculate the new separating velocity
+            var newSepVelocity = -separatingVelocity * Restitution;
+
+            // Check the velocity build-up due to acceleration only
+            var accCausedVelocity = Particle0.Acceleration;
+
+            if (Particle1 != null)
+                accCausedVelocity -= Particle1.Acceleration;
+
+            var accCausedSepVelocity = Vector3.Dot(accCausedVelocity, ContactNormal) * duration;
+
+            // If we've got a closing velocity due to acceleration buildup,
+            // remove it from the new separating velocity
+            if (accCausedSepVelocity < 0)
+            {
+                newSepVelocity += Restitution * accCausedSepVelocity;
+
+                // Make sure we haven't removed more than we have to remove.
+                if (newSepVelocity < 0)
+                    newSepVelocity = 0;
+            }
+
+            var deltaVelocity = newSepVelocity - separatingVelocity;
+
+            // We apply the change in velocity to each object in proportion to
+            // their inverse mass (i.e. those with lower inverse mass [higher
+            // actual mass] get less change in velocity)..
+            var totalInverseMass = CalculateTotalInverseMass();
+
+            // If all particles have infinite mass, then impulses have no effect
+            if (totalInverseMass <= 0) return;
+
+            // Calculate the impulse to apply
+            var impulse = deltaVelocity / totalInverseMass;
+
+            // Find the amount of impulse per unit of inverse mass
+            var impulsePerIMass = ContactNormal * impulse;
+
+            // Apply impulses: they are applied in the direction of the contact,
+            // and are proportional to the inverse mass.
+            Particle0.Velocity += impulsePerIMass * Particle0.InverseMass;
+            if (Particle1 != null)
+            {
+                // Particle 1 goes in the opposite direction
+                Particle1.Velocity += impulsePerIMass * -Particle1.InverseMass;
+            }
+        }
+
+        private void ResolveInterpenetration(float duration)
+        {
+            // If we don't have any penetration, skip this step.
+            if (Penetration <= 0) return;
+
+            // The movement of each object is based on their inverse mass, so
+            // total that.
+            var totalInverseMass = CalculateTotalInverseMass();
+
+            // If all particles have infinite mass, then we do nothing
+            if (totalInverseMass <= 0) return;
+
+            // Find the amount of penetration resolution per unit of inverse mass
+            Vector3 movePerIMass = ContactNormal * (Penetration / totalInverseMass);
+
+            // Calculate the the movement amounts
+            ParticleMovement0 = movePerIMass * Particle0.InverseMass;
+            if (Particle1 != null) {
+                ParticleMovement1 = movePerIMass * -Particle1.InverseMass;
+            } else {
+                ParticleMovement1 = Vector3.Zero;
+            }
+
+            // Apply the penetration resolution
+            Particle0.Position += ParticleMovement0;
+            if (Particle1 != null)
+                Particle1.Position += ParticleMovement1;
+        }
+
+        private float CalculateTotalInverseMass()
+        {
+            return Particle0.InverseMass + (Particle1 != null ? Particle1.InverseMass : 0);
+        }
+    }
+}
+

CycloNet.Physics/Particles/ParticleContactResolver.cs

+using System;
+using System.Collections.Generic;
+using OpenTK;
+
+namespace CycloNet.Physics.Particles
+{
+    public class ParticleContactResolver
+    {
+        /// <summary>
+        /// The number of iterations through the resolution algorithm.
+        /// This should be at least the number of contacts (otherwise
+        /// some constraints will not be resolved - although sometimes
+        /// this is not noticable). If the iterations are not needed they
+        /// will not be used, so adding more iterations may not make any
+        /// difference. But in some cases you would need millions of
+        /// iterations. Think about the number of iterations as a bound:
+        /// if you specify a large number, sometimes the algorithm WILL use
+        /// it, and you may drop frames.
+        /// </summary>
+        public int Iterations { get; set; }
+
+        public ParticleContactResolver(int iterations)
+        {
+            Iterations = iterations;
+        }
+
+        /// <summary>
+        /// Resolves a set of particle contacts for both penetration
+        /// and velocity.
+        ///
+        /// Contacts that cannot interact with each other should be
+        /// passed to separate calls to resolveContacts, as the
+        /// resolution algorithm takes much longer for lots of contacts
+        /// than it does for the same number of contacts in small sets.
+        ///
+        /// <param name="contacts">
+        /// Contacts to resolve.
+        /// </param>
+        /// <param name="duration">
+        /// The duration of the previous integration step.
+        /// This is used to compensate for forces applied.
+        /// </param>
+        public void ResolveContacts(List<ParticleContact> contacts, float duration)
+        {
+            for (int iterationsUsed = 0; iterationsUsed < Iterations; iterationsUsed++)
+            {
+                // Find the contact with the largest closing velocity;
+                var max = float.MaxValue;
+                ParticleContact maxContact = null;
+
+                foreach (var contact in contacts)
+                {
+                    var sepVel = contact.CalculateSeparatingVelocity();
+                    if (sepVel < max && (sepVel < 0 || contact.Penetration > 0))
+                    {
+                        max = sepVel;
+                        maxContact = contact;
+                    }
+                }
+
+                // Do we have anything worth resolving?
+                if (maxContact == null) break;
+
+                // Resolve this contact
+                maxContact.Resolve(duration);
+
+                // Update the interpenetrations for all particles
+                foreach (var contact in contacts)
+                {
+                    if (contact.Particle0 == maxContact.Particle0)
+                    {
+                        contact.Penetration -= Vector3.Dot(maxContact.ParticleMovement0, contact.ContactNormal);
+                    }
+                    else if (contact.Particle0 == maxContact.Particle1)
+                    {
+                        contact.Penetration -= Vector3.Dot(maxContact.ParticleMovement1, contact.ContactNormal);
+                    }
+
+                    if (contact.Particle1 != null)
+                    {
+                        if (contact.Particle1 == maxContact.Particle0)
+                        {
+                            contact.Penetration += Vector3.Dot(maxContact.ParticleMovement0, contact.ContactNormal);
+                        }
+                        else if (contact.Particle1 == maxContact.Particle1)
+                        {
+                            contact.Penetration += Vector3.Dot(maxContact.ParticleMovement1, contact.ContactNormal);
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+

CycloNet.Physics/Particles/ParticleForceGenerators.cs

+using System;
+using OpenTK;
+
+namespace CycloNet.Physics.Particles
+{
+    public class ParticleGravity : IParticleForceGenerator
+    {
+        readonly Vector3 gravity;
+
+        public ParticleGravity(Vector3 gravity)
+        {
+            this.gravity = gravity;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            if (!particle.HasFiniteMass) return;
+
+            particle.AddForce(gravity * particle.Mass);
+        }
+    }
+
+    public class ParticleDrag : IParticleForceGenerator
+    {
+        readonly float k1;
+        readonly float k2;
+
+        public ParticleDrag(float k1, float k2)
+        {
+            this.k1 = k1;
+            this.k2 = k2;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            var force = particle.Velocity;
+
+            // Calculate the total drag coefficient
+            var length = force.Length;
+            var dragCoeff = k1 * length + k2 * length * length;
+
+            // Calculate the final force and apply it
+            force.Normalize();
+            force *= -dragCoeff;
+
+            particle.AddForce(force);
+        }
+    }
+
+    public class ParticleSpring : IParticleForceGenerator
+    {
+        readonly Particle other;
+        readonly float springConstant;
+        readonly float restLength;
+
+        public ParticleSpring(Particle other, float springConstant, float restLength)
+        {
+            this.other = other;
+            this.springConstant = springConstant;
+            this.restLength = restLength;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Calculate the vector of the spring
+            var force = particle.Position - other.Position;
+
+            // Calculate the magnitude of the force
+            var magnitude = springConstant * Math.Abs(force.Length - restLength);
+
+            // Calculate the final force and apply it
+            force.Normalize();
+            force *= -magnitude;
+
+            particle.AddForce(force);
+        }
+    }
+
+    public class ParticleBuoyancy : IParticleForceGenerator
+    {
+        readonly float maxDepth;
+        readonly float volume;
+        readonly float waterHeight;
+        public float LiquidDensity { get; set; }
+
+        public ParticleBuoyancy(float maxDepth, float volume, float waterHeight)
+        {
+            this.maxDepth = maxDepth;
+            this.volume = volume;
+            this.waterHeight = waterHeight;
+            LiquidDensity = 1000;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Calculate the submersion depth
+            var depth = particle.Position.Y;
+
+            // Check if we're out of the water
+            if (depth >= waterHeight + maxDepth) return;
+
+            Vector3 force = Vector3.Zero;
+
+            // Check if we are at maximum depth (1st clause), or only partly submerged (2nd clause)
+            if (depth <= waterHeight - maxDepth)
+            {
+                force.Y = LiquidDensity * volume;
+            }
+            else
+            {
+                force.Y = LiquidDensity * volume *
+                    (depth - maxDepth - waterHeight) / 2 * maxDepth;
+            }
+            particle.AddForce(force);
+        }
+    }
+
+    public class ParticleBungee : IParticleForceGenerator
+    {
+        readonly Particle other;
+        readonly float springConstant;
+        readonly float restLength;
+
+        public ParticleBungee(Particle other, float springConstant, float restLength)
+        {
+            this.other = other;
+            this.springConstant = springConstant;
+            this.restLength = restLength;
+        }
+
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Calculate the vector of the spring
+            var force = particle.Position - other.Position;
+
+            var magnitude = force.Length;
+
+            // Check if the bungee is compressed
+            if (magnitude <= restLength) return;
+
+            // Calculate the magnitude of the force
+            magnitude = springConstant * (restLength - magnitude);
+
+            // Calculate the final force and apply it
+            force.Normalize();
+            force *= -magnitude;
+
+            particle.AddForce(force);
+        }
+    }
+
+    public class ParticleFakeSpring : IParticleForceGenerator
+    {
+        readonly Func<Vector3> anchor;
+        readonly float springConstant;
+        readonly float damping;
+
+        public ParticleFakeSpring(Vector3 vector, float springConstant, float damping):
+            this(() => vector, springConstant, damping)
+        {
+
+        }
+
+        public ParticleFakeSpring(Func<Vector3> anchor, float springConstant, float damping)
+        {
+            this.anchor = anchor;
+            this.springConstant = springConstant;
+            this.damping = damping;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Check that we do not have infinite mass
+            if (!particle.HasFiniteMass) return;
+
+            // Calculate the relative position of the particle to the anchor
+            Vector3 position = particle.Position - anchor();
+
+            // Calculate the constants and check they are in bounds.
+            var gamma = 0.5f * MathUtils.Sqrt(4 * springConstant - damping*damping);
+            if (gamma == 0.0f) return;
+
+            Vector3 c = position * (damping / (2.0f * gamma)) +
+                particle.Velocity * (1.0f / gamma);
+
+            // Calculate the target position
+            Vector3 target = position * MathUtils.Cos(gamma * duration) +
+                c * MathUtils.Sin(gamma * duration);
+            target *= MathUtils.Exp(-0.5f * duration * damping);
+
+            // Calculate the resulting acceleration and therefore the force
+            Vector3 accel = (target - position) * (1.0f / duration*duration) - particle.Velocity * duration;
+            particle.AddForce(accel * particle.Mass);
+        }
+    }
+
+    public class ParticleAnchoredSpring : IParticleForceGenerator
+    {
+        readonly Func<Vector3> anchor;
+        readonly float springConstant;
+        readonly float restLength;
+
+        public ParticleAnchoredSpring(Vector3 anchor, float springConstant, float restLength):
+            this(() => anchor, springConstant, restLength)
+        {
+        }
+
+        public ParticleAnchoredSpring(Func<Vector3> anchor, float springConstant, float restLength)
+        {
+            this.anchor = anchor;
+            this.springConstant = springConstant;
+            this.restLength = restLength;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Calculate the vector of the spring
+            var force = particle.Position - anchor();
+
+            // Calculate the magnitude of the force
+            var magnitude = (restLength - force.Length) * springConstant;
+        
+            // Calculate the final force and apply it
+            force.Normalize();
+            force *= magnitude;
+
+            particle.AddForce(force);
+        }
+    }
+
+    public class ParticleAnchroredBungee : IParticleForceGenerator
+    {
+        readonly Func<Vector3> anchor;
+        readonly float springConstant;
+        readonly float restLength;
+
+        public ParticleAnchroredBungee(Vector3 anchor, float springConstant, float restLength):
+            this(() => anchor, springConstant, restLength)
+        {
+        }
+
+        public ParticleAnchroredBungee(Func<Vector3> anchor, float springConstant, float restLength)
+        {
+            this.anchor = anchor;
+            this.springConstant = springConstant;
+            this.restLength = restLength;
+        }
+
+        public void UpdateForce(Particle particle, float duration)
+        {
+            // Calculate the vector of the spring
+            var force = particle.Position - anchor();
+        
+            // Calculate the magnitude of the force
+            float magnitude = force.Length;
+            if (magnitude < restLength) return;
+        
+            magnitude -= restLength;
+            magnitude *= springConstant;
+
+            // Calculate the final force and apply it
+            force.Normalize();
+            force *= -magnitude;
+
+            particle.AddForce(force);
+        }
+    }
+}

CycloNet.Physics/Particles/ParticleForceRegistry.cs

+using System;
+using System.Collections.Generic;
+
+namespace CycloNet.Physics.Particles
+{
+    public class ParticleForceRegistry
+    {
+        readonly List<ParticleForceRegistration> registrations = new List<ParticleForceRegistration>();
+
+        public void Add(Particle particle, IParticleForceGenerator fg)
+        {
+            registrations.Add(new ParticleForceRegistration(particle, fg));
+        }
+
+
+        //public void Remove(Particle particle, IParticleContactGenerator fg)
+        //{
+
+        //}
+
+        public void Clear()
+        {
+            registrations.Clear();
+        }
+
+        public void UpdateForces(float duration)
+        {
+            foreach (var reg in registrations)
+                reg.fg.UpdateForce(reg.particle, duration);
+        }
+
+        struct ParticleForceRegistration
+        {
+            internal Particle particle;
+            internal IParticleForceGenerator fg;
+
+            internal ParticleForceRegistration(Particle particle, IParticleForceGenerator fg)
+            {
+                this.particle = particle;
+                this.fg = fg;
+            }
+        }
+    }
+}

CycloNet.Physics/Particles/ParticleWorld.cs

+using System;
+using System.Collections.Generic;
+
+namespace CycloNet.Physics.Particles
+{
+    public class ParticleWorld
+    {
+        readonly List<Particle> particles = new List<Particle>();
+        readonly bool calculateIterations;
+        readonly ParticleForceRegistry registry = new ParticleForceRegistry();
+        readonly ParticleContactResolver resolver;
+        readonly List<IParticleContactGenerator> contactGenerators = new List<IParticleContactGenerator>();
+        readonly List<ParticleContact> contacts;
+        readonly int maxContacts;
+
+        public ParticleWorld(int maxContacts):
+            this(maxContacts, 0)
+        {
+        }
+
+        public ParticleWorld(int maxContacts, int iterations)
+        {
+            this.maxContacts = maxContacts;
+            this.contacts = new List<ParticleContact>(maxContacts);
+            resolver = new ParticleContactResolver(iterations);
+            calculateIterations = (iterations == 0);
+        }
+
+        private void GenerateContacts()
+        {
+            contacts.Clear();
+
+            foreach (var g in contactGenerators)
+            {
+                g.AddContacts(contacts, maxContacts);
+
+                // We've run out of contacts to fill. This means we're missing contacts.
+                if (contacts.Count >= maxContacts)
+                    break;
+            }
+        }
+
+        /// <summary>
+        /// Integrates all the particles in this world forward in time by given duration.
+        /// </summary>
+        public void Integrate(float duration)
+        {
+            foreach (var p in particles)
+                p.Integrate(duration);
+        }
+
+        /// <summary>
+        /// Processes all the physics for the particle world.
+        /// </summary>
+        public void RunPhysics(float duration)
+        {
+            registry.UpdateForces(duration);
+
+            Integrate(duration);
+
+            GenerateContacts();
+
+            if (contacts.Count != 0)
+            {
+                if (calculateIterations)
+                    resolver.Iterations = contacts.Count * 2;
+                resolver.ResolveContacts(contacts, duration);
+            }
+        }
+
+        /// <summary>
+        /// Initializes the world for a simulation frame. This clears
+        /// the force accumulators for particles in the world. After
+        /// calling this, the particles can have their forces for this
+        /// frame added.
+        /// </summary>
+        public void StartFrame()
+        {
+            foreach (var p in particles)
+                p.ClearAccumulator();
+        }
+
+        public List<Particle> Particles
+        {
+            get { return particles; }
+        }
+
+        public List<IParticleContactGenerator> ContactGenerators
+        {
+            get { return contactGenerators; }
+        }
+    }
+}
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CycloNet.Physics", "CycloNet.Physics\CycloNet.Physics.csproj", "{25F61EE2-0710-4376-A40F-EC3BA1236080}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{25F61EE2-0710-4376-A40F-EC3BA1236080}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{25F61EE2-0710-4376-A40F-EC3BA1236080}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{25F61EE2-0710-4376-A40F-EC3BA1236080}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{25F61EE2-0710-4376-A40F-EC3BA1236080}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(MonoDevelopProperties) = preSolution
+		StartupItem = CycloNet.Physics\CycloNet.Physics.csproj
+	EndGlobalSection
+EndGlobal