├── .gitignore ├── .semver ├── AssemblyVersion.cs ├── Gemfile ├── Gemfile.lock ├── Mercury.ParticleEngine.Core.Tests ├── AssertionModifier.cs ├── AxisTests.cs ├── ColourTests.cs ├── EmitterTests.cs ├── Mercury.ParticleEngine.Core.Tests.csproj ├── Modifiers │ └── ColourInterpolator2Tests.cs ├── ParticleBufferTests.cs ├── Profiles │ ├── PointProfileTests.cs │ └── RingProfileTests.cs ├── Properties │ └── AssemblyInfo.cs └── packages.config ├── Mercury.ParticleEngine.Core ├── Axis.cs ├── BlendMode.cs ├── Colour.cs ├── ColourRange.cs ├── Coordinate.cs ├── Emitter.cs ├── FastRand.cs ├── LineSegment.cs ├── Mercury.ParticleEngine.Core.csproj ├── ModifierExecutionStrategy.cs ├── Modifiers │ ├── ColourInterpolator2.cs │ ├── ContainerModifier.cs │ ├── DragModifier.cs │ ├── HueInterpolator2.cs │ ├── LinearGravityModifier.cs │ ├── Modifier.cs │ ├── OpacityFastFadeModifier.cs │ ├── OpacityInterpolator2.cs │ ├── RotationModifier.cs │ ├── ScaleInterpolator2.cs │ ├── VelocityColourModifier.cs │ ├── VelocityHueModifier.cs │ └── VortexModifier.cs ├── Particle.cs ├── ParticleBuffer.cs ├── ParticleEffect.cs ├── Profiles │ ├── BoxFillProfile.cs │ ├── BoxProfile.cs │ ├── CircleProfile.cs │ ├── PointProfile.cs │ ├── Profile.cs │ ├── RingProfile.cs │ └── SprayProfile.cs ├── Properties │ └── AssemblyInfo.cs ├── Range.cs ├── RangeF.cs ├── ReleaseParameters.cs ├── RenderingOrder.cs ├── Vector.cs └── packages.config ├── Mercury.ParticleEngine.SharpDX.Direct3D9.Sample ├── Cloud001.png ├── Mercury.ParticleEngine.SharpDX.Direct3D9.Sample.csproj ├── Particle.dds ├── Pixel.dds ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Ring001.png ├── app.config └── packages.config ├── Mercury.ParticleEngine.SharpDX.Direct3D9 ├── Mercury.ParticleEngine.SharpDX.Direct3D9.csproj ├── Properties │ └── AssemblyInfo.cs ├── Renderers │ └── PointSpriteRenderer.cs ├── Resources.Designer.cs ├── Resources.resx ├── Resources │ └── PointSprite.fx └── packages.config ├── Mercury.ParticleEngine.sln ├── README.md └── Rakefile /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | build/ 21 | bld/ 22 | [Bb]in/ 23 | [Oo]bj/ 24 | 25 | # Visual Studo 2015 cache/options directory 26 | .vs/ 27 | 28 | # MSTest test Results 29 | [Tt]est[Rr]esult*/ 30 | [Bb]uild[Ll]og.* 31 | 32 | # NUNIT 33 | *.VisualState.xml 34 | TestResult.xml 35 | 36 | # Build Results of an ATL Project 37 | [Dd]ebugPS/ 38 | [Rr]eleasePS/ 39 | dlldata.c 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | 106 | # MightyMoose 107 | *.mm.* 108 | AutoTest.Net/ 109 | 110 | # Web workbench (sass) 111 | .sass-cache/ 112 | 113 | # Installshield output folder 114 | [Ee]xpress/ 115 | 116 | # DocProject is a documentation generator add-in 117 | DocProject/buildhelp/ 118 | DocProject/Help/*.HxT 119 | DocProject/Help/*.HxC 120 | DocProject/Help/*.hhc 121 | DocProject/Help/*.hhk 122 | DocProject/Help/*.hhp 123 | DocProject/Help/Html2 124 | DocProject/Help/html 125 | 126 | # Click-Once directory 127 | publish/ 128 | 129 | # Publish Web Output 130 | *.[Pp]ublish.xml 131 | *.azurePubxml 132 | # TODO: Comment the next line if you want to checkin your web deploy settings 133 | # but database connection strings (with potential passwords) will be unencrypted 134 | *.pubxml 135 | *.publishproj 136 | 137 | # NuGet Packages 138 | *.nupkg 139 | # The packages folder can be ignored because of Package Restore 140 | **/packages/* 141 | # except build/, which is used as an MSBuild target. 142 | !**/packages/build/ 143 | # Uncomment if necessary however generally it will be regenerated when needed 144 | #!**/packages/repositories.config 145 | 146 | # Windows Azure Build Output 147 | csx/ 148 | *.build.csdef 149 | 150 | # Windows Store app package directory 151 | AppPackages/ 152 | 153 | # Others 154 | *.[Cc]ache 155 | ClientBin/ 156 | [Ss]tyle[Cc]op.* 157 | ~$* 158 | *~ 159 | *.dbmdl 160 | *.dbproj.schemaview 161 | *.pfx 162 | *.publishsettings 163 | node_modules/ 164 | bower_components/ 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file 170 | # to a newer Visual Studio version. Backup files are not needed, 171 | # because we have git ;-) 172 | _UpgradeReport_Files/ 173 | Backup*/ 174 | UpgradeLog*.XML 175 | UpgradeLog*.htm 176 | 177 | # SQL Server files 178 | *.mdf 179 | *.ldf 180 | 181 | # Business Intelligence projects 182 | *.rdl.data 183 | *.bim.layout 184 | *.bim_*.settings 185 | 186 | # Microsoft Fakes 187 | FakesAssemblies/ 188 | 189 | # Node.js Tools for Visual Studio 190 | .ntvs_analysis.dat 191 | 192 | # Visual Studio 6 build log 193 | *.plg 194 | 195 | # Visual Studio 6 workspace options file 196 | *.opt 197 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 5 3 | :minor: 0 4 | :patch: 0 5 | :special: '' 6 | -------------------------------------------------------------------------------- /AssemblyVersion.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | [assembly: AssemblyVersion("5.0.0")] 6 | [assembly: AssemblyFileVersion("5.0.0.e999ed")] 7 | [assembly: AssemblyInformationalVersion("5.0.0")] 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'rake', '~> 10.4', '>= 10.4.2' 3 | gem 'albacore', '~> 2.5', '>= 2.5.6' 4 | gem 'semver', '~> 1.0', '>= 1.0.1' 5 | gem 'nuget', '~> 2.8', '>= 2.8.60717.93' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | albacore (2.5.6) 5 | map (~> 6.5) 6 | nokogiri (~> 1.5) 7 | rake (~> 10) 8 | semver2 (~> 3.4) 9 | map (6.5.5) 10 | mini_portile2 (2.0.0) 11 | nokogiri (1.6.7.1) 12 | mini_portile2 (~> 2.0.0.rc2) 13 | nokogiri (1.6.7.1-x86-mingw32) 14 | mini_portile2 (~> 2.0.0.rc2) 15 | nuget (2.8.60717.93) 16 | rake (10.4.2) 17 | semver (1.0.1) 18 | semver2 (3.4.2) 19 | 20 | PLATFORMS 21 | ruby 22 | x86-mingw32 23 | 24 | DEPENDENCIES 25 | albacore (~> 2.5, >= 2.5.6) 26 | nuget (~> 2.8, >= 2.8.60717.93) 27 | rake (~> 10.4, >= 10.4.2) 28 | semver (~> 1.0, >= 1.0.1) 29 | 30 | BUNDLED WITH 31 | 1.10.6 32 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/AssertionModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | using FluentAssertions; 4 | 5 | internal class AssertionModifier : Modifier { 6 | readonly Predicate _predicate; 7 | 8 | public AssertionModifier(Predicate predicate) { 9 | _predicate = predicate; 10 | } 11 | 12 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 13 | while (count-- > 0) { 14 | _predicate(*particle).Should().BeTrue(); 15 | 16 | particle++; 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/AxisTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | public class AxisTests { 7 | public class Constructor { 8 | [Fact, Trait("Type", "Axis")] 9 | public void WhenGivenAxisValues_ReturnsInitializedAxis() { 10 | var axis = new Axis(1f, 0f); 11 | 12 | axis.Match((x, y) => { 13 | x.Should().Be(1f); 14 | y.Should().Be(0f); 15 | }); 16 | } 17 | 18 | [Fact, Trait("Type", "Axis")] 19 | public void WhenGivenAngle_ReturnsInitializedAxis() { 20 | var axis = new Axis((float)Math.PI / 2f); 21 | 22 | axis.Match((x, y) => { 23 | x.Should().BeApproximately(0f, 0.0000001f); 24 | y.Should().Be(1f); 25 | }); 26 | } 27 | } 28 | 29 | public class MultiplyMethod { 30 | [Fact, Trait("Type", "Axis")] 31 | public void WhenMultiplied_ReturnsVectorOfMagnitude() { 32 | var result = Axis.Left.Multiply(2f); 33 | 34 | result.Magnitude.Should().Be(2f); 35 | } 36 | } 37 | 38 | public class UnsafeCopyToMethod { 39 | [Fact, Trait("Type", "Axis")] 40 | public void WhenGivenBuffer_CopiesAxisValues() { 41 | var result = new float[2]; 42 | 43 | unsafe { 44 | fixed (float* p = result) { 45 | Axis.Right.CopyTo(p); 46 | 47 | p[0].Should().Be(1f); 48 | p[1].Should().Be(0f); 49 | } 50 | } 51 | } 52 | } 53 | 54 | public class DestructureMethod { 55 | [Fact, Trait("Type", "Axis")] 56 | public void CopiesAxisValues() { 57 | float x, y; 58 | Axis.Right.Destructure(out x, out y); 59 | 60 | x.Should().Be(1f); 61 | y.Should().Be(0f); 62 | } 63 | } 64 | 65 | public class MatchMethod { 66 | [Fact, Trait("Type", "Axis")] 67 | public void WhenGivenCallback_CallsCallbackWithAxisValues() { 68 | Axis.Right.Match((x, y) => { 69 | x.Should().Be(1f); 70 | y.Should().Be(0f); 71 | }); 72 | } 73 | 74 | [Fact, Trait("Type", "Axis")] 75 | public void WhenGivenNullCallback_ThrowsArgumentNullException() { 76 | Action action = () => Axis.Right.Match(null); 77 | action.ShouldThrow(); 78 | } 79 | } 80 | 81 | public class MapMethod { 82 | [Fact, Trait("Type", "Axis")] 83 | public void WhenGivenMapFunction_CallsMapFunctionWithAxisValues() { 84 | var result = Axis.Right.Map((x, y) => { 85 | x.Should().Be(1f); 86 | y.Should().Be(0f); 87 | return -1f; 88 | }); 89 | 90 | result.Should().Be(-1f); 91 | } 92 | 93 | [Fact, Trait("Type", "Axis")] 94 | public void WhenGivenNullMapFunction_ThrowsArgumentNullException() { 95 | Action action = () => Axis.Right.Map(null); 96 | action.ShouldThrow(); 97 | } 98 | } 99 | 100 | public class EqualsObjectMethod { 101 | [Fact, Trait("Type", "Axis")] 102 | public void WhenGivenNull_ReturnsFalse() { 103 | var axis = new Axis(1f, 0f); 104 | 105 | axis.Equals(null).Should().BeFalse(); 106 | } 107 | 108 | [Fact, Trait("Type", "Axis")] 109 | public void WhenGivenEqualAxis_ReturnsTrue() { 110 | var x = new Axis(1f, 0f); 111 | 112 | Object y = new Axis(1f, 0f); 113 | 114 | x.Equals(y).Should().BeTrue(); 115 | } 116 | 117 | [Fact, Trait("Type", "Axis")] 118 | public void WhenGivenDifferentAxis_ReturnsFalse() { 119 | var x = new Axis(1f, 0f); 120 | 121 | Object y = new Axis(0f, 1f); 122 | 123 | x.Equals(y).Should().BeFalse(); 124 | } 125 | 126 | [Fact, Trait("Type", "Axis")] 127 | public void WhenGivenObjectOfAntotherType_ReturnsFalse() { 128 | var axis = new Axis(1f, 0f); 129 | 130 | // ReSharper disable SuspiciousTypeConversion.Global 131 | axis.Equals(DateTime.Now).Should().BeFalse(); 132 | // ReSharper restore SuspiciousTypeConversion.Global 133 | } 134 | } 135 | 136 | public class GetHashCodeMethod 137 | { 138 | [Fact, Trait("Type", "Axis")] 139 | public void WhenObjectsAreDifferent_YieldsDifferentHashCodes() { 140 | var x = new Axis(0f, 1f); 141 | var y = new Axis(1f, 0f); 142 | 143 | x.GetHashCode().Should().NotBe(y.GetHashCode()); 144 | } 145 | 146 | [Fact, Trait("Type", "Axis")] 147 | public void WhenObjectsAreSame_YieldsIdenticalHashCodes() { 148 | var x = new Axis(0f, 1f); 149 | var y = new Axis(0f, 1f); 150 | 151 | x.GetHashCode().Should().Be(y.GetHashCode()); 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/ColourTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using FluentAssertions; 4 | using Xunit; 5 | using Xunit.Extensions; 6 | 7 | public class ColourTests { 8 | public class Constructor { 9 | [Fact] 10 | [Trait("Type", "Colour")] 11 | public void WhenGivenValues_ReturnsInitializedColour() { 12 | var colour = new Colour(1f, 1f, 1f); 13 | 14 | colour._h.Should().Be(1f); 15 | colour._s.Should().Be(1f); 16 | colour._l.Should().Be(1f); 17 | } 18 | } 19 | 20 | public class EqualsColourMethod { 21 | [Fact] 22 | [Trait("Type", "Colour")] 23 | public void WhenGivenEqualValues_ReturnsTrue() { 24 | var x = new Colour(360f, 1f, 1f); 25 | var y = new Colour(360f, 1f, 1f); 26 | 27 | x.Equals(y).Should().BeTrue(); 28 | } 29 | 30 | [Fact] 31 | [Trait("Type", "Colour")] 32 | public void WhenGivenDifferentValues_ReturnsFalse() { 33 | var x = new Colour(0f, 1f, 0f); 34 | var y = new Colour(360f, 1f, 1f); 35 | 36 | x.Equals(y).Should().BeFalse(); 37 | } 38 | } 39 | 40 | public class EqualsObjectMethod { 41 | [Fact] 42 | [Trait("Type", "Colour")] 43 | public void WhenGivenNull_ReturnsFalse() { 44 | var colour = new Colour(360f, 1f, 1f); 45 | 46 | colour.Equals(null).Should().BeFalse(); 47 | } 48 | 49 | [Fact] 50 | [Trait("Type", "Colour")] 51 | public void WhenGivenEqualColour_ReturnsTrue() { 52 | var x = new Colour(360f, 1f, 1f); 53 | 54 | Object y = new Colour(360f, 1f, 1f); 55 | 56 | x.Equals(y).Should().BeTrue(); 57 | } 58 | 59 | [Fact] 60 | [Trait("Type", "Colour")] 61 | public void WhenGivenDifferentColour_ReturnsFalse() { 62 | var x = new Colour(360f, 1f, 1f); 63 | 64 | Object y = new Colour(0f, 1f, 0f); 65 | 66 | x.Equals(y).Should().BeFalse(); 67 | } 68 | 69 | [Fact] 70 | [Trait("Type", "Colour")] 71 | public void WhenGivenObjectOfAntotherType_ReturnsFalse() { 72 | var colour = new Colour(360f, 1f, 1f); 73 | 74 | // ReSharper disable SuspiciousTypeConversion.Global 75 | colour.Equals(DateTime.Now).Should().BeFalse(); 76 | // ReSharper restore SuspiciousTypeConversion.Global 77 | } 78 | } 79 | 80 | public class GetHashCodeMethod 81 | { 82 | [Fact] 83 | [Trait("Type", "Colour")] 84 | public void WhenObjectsAreDifferent_YieldsDifferentHashCodes() { 85 | var x = new Colour(0f, 1f, 0f); 86 | var y = new Colour(360f, 1f, 1f); 87 | 88 | x.GetHashCode().Should().NotBe(y.GetHashCode()); 89 | } 90 | 91 | [Fact] 92 | [Trait("Type", "Colour")] 93 | public void WhenObjectsAreSame_YieldsIdenticalHashCodes() { 94 | var x = new Colour(180f, 0.5f, 0.5f); 95 | var y = new Colour(180f, 0.5f, 0.5f); 96 | 97 | x.GetHashCode().Should().Be(y.GetHashCode()); 98 | } 99 | } 100 | 101 | public class ToStringMethod 102 | { 103 | [Theory] 104 | [Trait("Type", "Colour")] 105 | [InlineData(0f, 0f, 0f, "0°,0%,0%")] 106 | [InlineData(360f, 1f, 1f, "360°,100%,100%")] 107 | [InlineData(180f, 0.5f, 0.5f, "180°,50%,50%")] 108 | public void ReturnsCorrectValue(float h, float s, float l, String expected) { 109 | var colour = new Colour(h, s, l); 110 | 111 | colour.ToString().Should().Be(expected); 112 | } 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/EmitterTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using Xunit; 4 | using FluentAssertions; 5 | using Mercury.ParticleEngine.Modifiers; 6 | using Mercury.ParticleEngine.Profiles; 7 | 8 | public class EmitterTests { 9 | public class UpdateMethod { 10 | [Fact] 11 | public void WhenThereAreParticlesToExpire_DecreasesActiveParticleCount() { 12 | var subject = new Emitter(100, TimeSpan.FromSeconds(1), Profile.Point()) { 13 | Parameters = new ReleaseParameters { 14 | Quantity = 1 15 | } 16 | }; 17 | 18 | subject.Trigger(new Coordinate(0f, 0f)); 19 | subject.ActiveParticles.Should().Be(1); 20 | 21 | subject.Update(2f); 22 | subject.ActiveParticles.Should().Be(0); 23 | } 24 | 25 | [Fact] 26 | public void WhenThereAreParticlesToExpire_DoesNotPassExpiredParticlesToModifiers() { 27 | var subject = new Emitter(100, TimeSpan.FromSeconds(1), Profile.Point()) { 28 | Parameters = new ReleaseParameters { 29 | Quantity = 1 30 | }, 31 | Modifiers = new Modifier[] { 32 | new AssertionModifier(particle => particle.Age <= 1f) 33 | } 34 | }; 35 | 36 | subject.Trigger(new Coordinate(0f, 0f)); 37 | subject.Update(0.5f); 38 | subject.Trigger(new Coordinate(0f, 0f)); 39 | subject.Update(0.5f); 40 | subject.Trigger(new Coordinate(0f, 0f)); 41 | subject.Update(0.5f); 42 | } 43 | 44 | [Fact] 45 | public void WhenThereAreNoActiveParticles_GracefullyDoesNothing() { 46 | var subject = new Emitter(100, TimeSpan.FromSeconds(1), Profile.Point()); 47 | 48 | subject.Update(0.5f); 49 | 50 | subject.ActiveParticles.Should().Be(0); 51 | } 52 | } 53 | 54 | public class TriggerMethod { 55 | [Fact] 56 | public void WhenEnoughHeadroom_IncreasesActiveParticlesCountByReleaseQuantity() { 57 | var subject = new Emitter(100, TimeSpan.FromSeconds(1), Profile.Point()) { 58 | Parameters = new ReleaseParameters { 59 | Quantity = 10 60 | } 61 | }; 62 | 63 | subject.ActiveParticles.Should().Be(0); 64 | subject.Trigger(new Coordinate(0f, 0f)); 65 | subject.ActiveParticles.Should().Be(10); 66 | } 67 | 68 | [Fact] 69 | public void WhenNotEnoughHeadroom_IncreasesActiveParticlesCountByRemainingParticles() { 70 | var subject = new Emitter(15, TimeSpan.FromSeconds(1), Profile.Point()) { 71 | Parameters = new ReleaseParameters { 72 | Quantity = 10 73 | } 74 | }; 75 | 76 | subject.Trigger(new Coordinate(0f, 0f)); 77 | subject.ActiveParticles.Should().Be(10); 78 | subject.Trigger(new Coordinate(0f, 0f)); 79 | subject.ActiveParticles.Should().Be(15); 80 | } 81 | 82 | [Fact] 83 | public void WhenNoRemainingParticles_DoesNotIncreaseActiveParticlesCount() { 84 | var subject = new Emitter(10, TimeSpan.FromSeconds(1), Profile.Point()) { 85 | Parameters = new ReleaseParameters { 86 | Quantity = 10 87 | } 88 | }; 89 | 90 | subject.Trigger(new Coordinate(0f, 0f)); 91 | subject.ActiveParticles.Should().Be(10); 92 | subject.Trigger(new Coordinate(0f, 0f)); 93 | subject.ActiveParticles.Should().Be(10); 94 | } 95 | } 96 | 97 | public class DisposeMethod { 98 | [Fact] 99 | public void IsIdempotent() { 100 | var subject = new Emitter(10, TimeSpan.FromSeconds(1), Profile.Point()); 101 | 102 | subject.Dispose(); 103 | subject.Dispose(); 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/Mercury.ParticleEngine.Core.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E9B007E5-A803-46A3-AEF5-05DE0B56A5AF} 8 | Library 9 | Properties 10 | Mercury.ParticleEngine 11 | Mercury.ParticleEngine.Core.Tests 12 | v4.6.1 13 | 512 14 | 15 | 16 | 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | false 27 | true 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | true 38 | 39 | 40 | 41 | ..\packages\FluentAssertions.4.1.1\lib\net45\FluentAssertions.dll 42 | True 43 | 44 | 45 | ..\packages\FluentAssertions.4.1.1\lib\net45\FluentAssertions.Core.dll 46 | True 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 55 | True 56 | 57 | 58 | ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll 59 | True 60 | 61 | 62 | ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll 63 | True 64 | 65 | 66 | ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll 67 | True 68 | 69 | 70 | 71 | 72 | Properties\AssemblyVersion.cs 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | {55f2b72d-cf10-4bda-8e13-2eb8132124ea} 90 | Mercury.ParticleEngine.Core 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/Modifiers/ColourInterpolator2Tests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using Xunit; 3 | using FluentAssertions; 4 | 5 | public class ColourInterpolator2Tests { 6 | public class UpdateMethod { 7 | [Fact] 8 | public void WhenParticleLifeIsZero_SetsInitialColour() { 9 | var particle = new Particle { 10 | Age = 0.0f 11 | }; 12 | 13 | var subject = new ColourInterpolator2 { 14 | InitialColour = new Colour(1f, 0f, 0f), 15 | FinalColour = new Colour(0f, 0f, 1f) 16 | }; 17 | 18 | unsafe { 19 | subject.Update(0.01666f, &particle, 1); 20 | 21 | particle.Colour[0].Should().BeApproximately(1f, 0.000001f); 22 | particle.Colour[1].Should().BeApproximately(0f, 0.000001f); 23 | particle.Colour[2].Should().BeApproximately(0f, 0.000001f); 24 | } 25 | } 26 | 27 | [Fact] 28 | public void WhenParticleLifeIsOne_SetsFinalColour() { 29 | var particle = new Particle { 30 | Age = 1.0f 31 | }; 32 | 33 | var subject = new ColourInterpolator2 { 34 | InitialColour = new Colour(1f, 0f, 0f), 35 | FinalColour = new Colour(0f, 0f, 1f) 36 | }; 37 | 38 | unsafe { 39 | subject.Update(0.01666f, &particle, 1); 40 | 41 | particle.Colour[0].Should().BeApproximately(0f, 0.000001f); 42 | particle.Colour[1].Should().BeApproximately(0f, 0.000001f); 43 | particle.Colour[2].Should().BeApproximately(1f, 0.000001f); 44 | } 45 | } 46 | 47 | [Fact] 48 | public void WhenParticleLifeIsPointFive_SetsColourBetweenInitialAndFinal() { 49 | var particle = new Particle { 50 | Age = 0.5f 51 | }; 52 | 53 | var subject = new ColourInterpolator2 { 54 | InitialColour = new Colour(1f, 0f, 0f), 55 | FinalColour = new Colour(0f, 0f, 1f) 56 | }; 57 | 58 | unsafe { 59 | subject.Update(0.01666f, &particle, 1); 60 | 61 | particle.Colour[0].Should().BeApproximately(0.5f, 0.000001f); 62 | particle.Colour[1].Should().BeApproximately(0f, 0.000001f); 63 | particle.Colour[2].Should().BeApproximately(0.5f, 0.000001f); 64 | } 65 | } 66 | 67 | [Fact] 68 | public void IteratesOverEachParticle() { 69 | var buffer = new Particle[100]; 70 | 71 | for (int i = 0; i < buffer.Length; i++) 72 | buffer[i].Age = 1.0f; 73 | 74 | var subject = new ColourInterpolator2 { 75 | InitialColour = new Colour(1f, 0f, 0f), 76 | FinalColour = new Colour(0f, 0f, 1f) 77 | }; 78 | 79 | unsafe { 80 | fixed (Particle* particle = &buffer[0]) { 81 | subject.Update(0.1666666f, particle, buffer.Length); 82 | 83 | for (int i = 0; i < buffer.Length; i++) { 84 | particle[i].Colour[2].Should().BeApproximately(1f, 0.000001f); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/ParticleBufferTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using Xunit; 4 | using FluentAssertions; 5 | 6 | public class ParticleBufferTests { 7 | public class AvailableProperty 8 | { 9 | [Fact, Trait("Type", "ParticleBuffer")] 10 | public void WhenNoParticlesReleased_ReturnsBufferSize() { 11 | var subject = new ParticleBuffer(100); 12 | 13 | subject.Available.Should().Be(100); 14 | } 15 | 16 | [Fact, Trait("Type", "ParticleBuffer")] 17 | public void WhenSomeParticlesReleased_ReturnsAvailableCount() { 18 | var subject = new ParticleBuffer(100); 19 | 20 | unsafe { 21 | Particle* particle; 22 | subject.Release(10, out particle); 23 | } 24 | 25 | subject.Available.Should().Be(90); 26 | } 27 | 28 | [Fact, Trait("Type", "ParticleBuffer")] 29 | public void WhenAllParticlesReleased_ReturnsZero() { 30 | var subject = new ParticleBuffer(100); 31 | 32 | unsafe { 33 | Particle* particle; 34 | subject.Release(100, out particle); 35 | } 36 | 37 | subject.Available.Should().Be(0); 38 | } 39 | } 40 | 41 | public class CountProperty { 42 | [Fact, Trait("Type", "ParticleBuffer")] 43 | public void WhenNoParticlesReleased_ReturnsZero() { 44 | var subject = new ParticleBuffer(100); 45 | subject.Count.Should().Be(0); 46 | } 47 | 48 | [Fact, Trait("Type", "ParticleBuffer")] 49 | public void WhenSomeParticlesReleased_ReturnsCount() { 50 | var subject = new ParticleBuffer(100); 51 | 52 | unsafe { 53 | Particle* particle; 54 | subject.Release(10, out particle); 55 | } 56 | 57 | subject.Count.Should().Be(10); 58 | } 59 | 60 | [Fact, Trait("Type", "ParticleBuffer")] 61 | public void WhenAllParticlesReleased_ReturnsZero() { 62 | var subject = new ParticleBuffer(100); 63 | 64 | unsafe { 65 | Particle* particle; 66 | subject.Release(100, out particle); 67 | } 68 | 69 | subject.Count.Should().Be(100); 70 | } 71 | } 72 | 73 | public class ReleaseMethod 74 | { 75 | [Fact, Trait("Type", "ParticleBuffer")] 76 | public void WhenPassedReasonableQuantity_ReturnsNumberReleased() { 77 | var subject = new ParticleBuffer(100); 78 | 79 | unsafe { 80 | Particle* particle; 81 | var count = subject.Release(50, out particle); 82 | 83 | count.Should().Be(50); 84 | } 85 | } 86 | 87 | [Fact, Trait("Type", "ParticleBuffer")] 88 | public void WhenPassedImpossibleQuantity_ReturnsNumberActuallyReleased() { 89 | var subject = new ParticleBuffer(100); 90 | 91 | unsafe { 92 | Particle* particle; 93 | var count = subject.Release(200, out particle); 94 | count.Should().Be(100); 95 | } 96 | } 97 | } 98 | 99 | public class ReclaimMethod { 100 | [Fact, Trait("Type", "ParticleBuffer")] 101 | public void WhenPassedReasonableNumber_ReclaimsParticles() { 102 | var subject = new ParticleBuffer(100); 103 | 104 | unsafe { 105 | Particle* particle; 106 | subject.Release(100, out particle); 107 | } 108 | 109 | subject.Count.Should().Be(100); 110 | 111 | subject.Reclaim(50); 112 | 113 | subject.Count.Should().Be(50); 114 | } 115 | } 116 | 117 | public class CopyToMethod { 118 | [Fact, Trait("Type", "ParticleBuffer")] 119 | public void WhenBufferIsSequential_CopiesParticlesInOrder() { 120 | unsafe { 121 | var subject = new ParticleBuffer(10); 122 | Particle* particle; 123 | var count = subject.Release(5, out particle); 124 | 125 | do { 126 | particle->Age = 1f; 127 | particle++; 128 | } 129 | while (count-- > 0); 130 | 131 | var destination = new Particle[10]; 132 | 133 | fixed (Particle* buffer = destination) { 134 | subject.CopyTo((IntPtr)buffer); 135 | } 136 | 137 | destination[0].Age.Should().BeApproximately(1f, 0.0001f); 138 | destination[1].Age.Should().BeApproximately(1f, 0.0001f); 139 | destination[2].Age.Should().BeApproximately(1f, 0.0001f); 140 | destination[3].Age.Should().BeApproximately(1f, 0.0001f); 141 | destination[4].Age.Should().BeApproximately(1f, 0.0001f); 142 | } 143 | } 144 | } 145 | 146 | public class CopyToReverseMethod { 147 | [Fact, Trait("Type", "ParticleBuffer")] 148 | public void WhenBufferIsSequential_CopiesParticlesInReverseOrder() { 149 | unsafe { 150 | var subject = new ParticleBuffer(10); 151 | Particle* particle; 152 | var count = subject.Release(5, out particle); 153 | 154 | do { 155 | particle->Age = 1f; 156 | particle++; 157 | } 158 | while (count-- > 0); 159 | 160 | var destination = new Particle[10]; 161 | 162 | fixed (Particle* buffer = destination) { 163 | subject.CopyToReverse((IntPtr)buffer); 164 | } 165 | 166 | destination[0].Age.Should().BeApproximately(1f, 0.0001f); 167 | destination[1].Age.Should().BeApproximately(1f, 0.0001f); 168 | destination[2].Age.Should().BeApproximately(1f, 0.0001f); 169 | destination[3].Age.Should().BeApproximately(1f, 0.0001f); 170 | destination[4].Age.Should().BeApproximately(1f, 0.0001f); 171 | } 172 | } 173 | } 174 | 175 | public class DisposeMethod { 176 | [Fact, Trait("Type", "ParticleBuffer")] 177 | public void IsIdempotent() { 178 | var subject = new ParticleBuffer(100); 179 | subject.Dispose(); 180 | subject.Dispose(); 181 | } 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/Profiles/PointProfileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | using System; 3 | using Xunit; 4 | using FluentAssertions; 5 | 6 | public class PointProfileTests { 7 | public class GetOffsetAndHeadingMethod { 8 | [Fact] 9 | public void ReturnsZeroOffset() { 10 | var subject = new PointProfile(); 11 | var values = new float[4]; 12 | 13 | unsafe { 14 | fixed (float* offset = &values[0]) 15 | fixed (float* heading = &values[2]) { 16 | subject.GetOffsetAndHeading((Coordinate*)offset, (Axis*)heading); 17 | 18 | offset[0].Should().Be(0f); 19 | offset[1].Should().Be(0f); 20 | } 21 | } 22 | } 23 | 24 | [Fact] 25 | public void ReturnsHeadingAsUnitVector() { 26 | var subject = new PointProfile(); 27 | var values = new float[4]; 28 | 29 | unsafe { 30 | fixed (float* offset = &values[0]) 31 | fixed (float* heading = &values[2]) { 32 | subject.GetOffsetAndHeading((Coordinate*)offset, (Axis*)heading); 33 | 34 | var length = Math.Sqrt((heading[0] * heading[0]) + (heading[1] * heading[1])); 35 | length.Should().BeApproximately(1f, 0.000001); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/Profiles/RingProfileTests.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | using System; 3 | using Xunit; 4 | using FluentAssertions; 5 | 6 | public class RingProfileTests { 7 | public class GetOffsetAndHeadingMethod { 8 | [Fact] 9 | public void ReturnsOffsetEqualToRadius() { 10 | var subject = new RingProfile { 11 | Radius = 10f 12 | }; 13 | var values = new float[4]; 14 | 15 | unsafe { 16 | fixed (float* offset = &values[0]) 17 | fixed (float* heading = &values[2]) { 18 | subject.GetOffsetAndHeading((Coordinate*)offset, (Axis*)heading); 19 | 20 | var length = Math.Sqrt((offset[0] * offset[0]) + (offset[1] * offset[1])); 21 | length.Should().BeApproximately(10f, 0.000001f); 22 | } 23 | } 24 | } 25 | 26 | [Fact] 27 | public void WhenRadiateIsTrue_HeadingIsEqualToNormalizedOffset() { 28 | var subject = new RingProfile { 29 | Radius = 10f, 30 | Radiate = true 31 | }; 32 | var values = new float[4]; 33 | 34 | unsafe { 35 | fixed (float* offset = &values[0]) 36 | fixed (float* heading = &values[2]) { 37 | subject.GetOffsetAndHeading((Coordinate*)offset, (Axis*)heading); 38 | 39 | heading[0].Should().BeApproximately(offset[0] / 10f, 0.000001f); 40 | heading[1].Should().BeApproximately(offset[1] / 10f, 0.000001f); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Mercury.ParticleEngine.Core.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Mercury.ParticleEngine.Core.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("928fb741-3540-4aa8-8906-486a91846a89")] 24 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Axis.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | /// 6 | /// An immutable data structure representing a directed fixed axis. 7 | /// 8 | [StructLayout(LayoutKind.Sequential)] 9 | public struct Axis : IEquatable { 10 | internal readonly float _x; 11 | internal readonly float _y; 12 | 13 | /// 14 | /// Initializes a new instance of the structure. 15 | /// 16 | /// The X component of the unit vector representing the axis. 17 | /// The Y component of the unit vector representing the axis. 18 | public Axis(float x, float y) { 19 | var length = (float)Math.Sqrt((x * x) + (y * y)); 20 | 21 | _x = x / length; 22 | _y = y / length; 23 | } 24 | 25 | /// 26 | /// Initializes a new instance of the structure. 27 | /// 28 | /// The angle of the axis in radians. 29 | public Axis(float angle){ 30 | var x = (float)Math.Cos(angle); 31 | var y = (float)Math.Sin(angle); 32 | 33 | var length = (float)Math.Sqrt((x * x) + (y * y)); 34 | 35 | _x = x / length; 36 | _y = y / length; 37 | } 38 | 39 | /// 40 | /// Gets a directed axis which points to the left. 41 | /// 42 | static public Axis Left => new Axis(-1f, 0f); 43 | 44 | /// 45 | /// Gets a directed axis which points up. 46 | /// 47 | static public Axis Up => new Axis(0f, 1f); 48 | 49 | /// 50 | /// Gets a directed axis which points to the right. 51 | /// 52 | static public Axis Right => new Axis(1f, 0f); 53 | 54 | /// 55 | /// Gets a directed axis which points down. 56 | /// 57 | static public Axis Down => new Axis(0f, -1f); 58 | 59 | /// 60 | /// Multiplies the fixed axis by a magnitude value resulting in a directed vector. 61 | /// 62 | /// The magnitude of the vector. 63 | /// A directed vector. 64 | public Vector Multiply(float magnitude) => new Vector(this, magnitude); 65 | 66 | /// 67 | /// Copies the X and Y components of the axis to the specified memory location. 68 | /// 69 | /// The memory location to copy the axis to. 70 | public unsafe void CopyTo(float* destination) { 71 | destination[0] = _x; 72 | destination[1] = _y; 73 | } 74 | 75 | /// 76 | /// Destructures the axis, exposing the individual X and Y components. 77 | /// 78 | public void Destructure(out float x, out float y) { 79 | x = _x; 80 | y = _y; 81 | } 82 | 83 | /// 84 | /// Exposes the individual X and Y components of the axis to the specified matching function. 85 | /// 86 | /// The function which matches the individual X and Y components. 87 | /// 88 | /// Thrown if the value passed to the parameter is null. 89 | /// 90 | public void Match(Action callback) { 91 | if (callback == null) 92 | throw new ArgumentNullException(nameof(callback)); 93 | 94 | callback(_x, _y); 95 | } 96 | 97 | /// 98 | /// Exposes the individual X and Y components of the axis to the specified mapping function and returns the 99 | /// result; 100 | /// 101 | /// The type being mapped to. 102 | /// 103 | /// A function which maps the X and Y values to an instance of . 104 | /// 105 | /// 106 | /// The result of the function when passed the individual X and Y components. 107 | /// 108 | /// 109 | /// Thrown if the value passed to the parameter is null. 110 | /// 111 | public T Map(Func map) { 112 | if (map == null) 113 | throw new ArgumentNullException(nameof(map)); 114 | 115 | return map(_x, _y); 116 | } 117 | 118 | /// 119 | /// Indicates whether the current object is equal to another object of the same type. 120 | /// 121 | /// 122 | /// true if the current object is equal to the parameter; otherwise, false. 123 | /// 124 | /// An object to compare with this object. 125 | public bool Equals(Axis other) => _x.Equals(other._x) && 126 | _y.Equals(other._y); 127 | 128 | /// 129 | /// Indicates whether this instance and a specified object are equal. 130 | /// 131 | /// 132 | /// true if and this instance are the same type and represent the same value; otherwise, false. 133 | /// 134 | /// Another object to compare to. 135 | public override bool Equals(object obj) { 136 | if (ReferenceEquals(null, obj)) 137 | return false; 138 | 139 | return obj is Axis && Equals((Axis)obj); 140 | } 141 | 142 | /// 143 | /// Returns the hash code for this instance. 144 | /// 145 | /// 146 | /// A 32-bit signed integer that is the hash code for this instance. 147 | /// 148 | public override int GetHashCode() { 149 | unchecked { 150 | var hashCode = _x.GetHashCode(); 151 | 152 | hashCode = (hashCode * 397) ^ _y.GetHashCode(); 153 | 154 | return hashCode; 155 | } 156 | } 157 | 158 | /// 159 | /// Returns a that represents this instance. 160 | /// 161 | /// 162 | /// A that represents this instance. 163 | /// 164 | public override string ToString() => $"({_x.ToString("F4")}, {_y.ToString("F4")})"; 165 | 166 | public static bool operator ==(Axis x, Axis y) => x.Equals(y); 167 | public static bool operator !=(Axis x, Axis y) => !x.Equals(y); 168 | public static Vector operator *(Axis axis, float magnitude) => new Vector(axis, magnitude); 169 | } 170 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/BlendMode.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | public enum BlendMode { 3 | Alpha, 4 | Add, 5 | Subtract 6 | } 7 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Colour.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | /// 6 | /// An immutable data structure representing a 24bit colour composed of separate hue, saturation and lightness channels. 7 | /// 8 | [Serializable] 9 | [StructLayout(LayoutKind.Sequential)] 10 | public struct Colour : IEquatable { 11 | /// 12 | /// Gets the value of the hue channel. 13 | /// 14 | internal readonly float _h; 15 | 16 | /// 17 | /// Gets the value of the saturation channel. 18 | /// 19 | internal readonly float _s; 20 | 21 | /// 22 | /// Gets the value of the lightness channel. 23 | /// 24 | internal readonly float _l; 25 | 26 | /// 27 | /// Initializes a new instance of the structure. 28 | /// 29 | /// The value of the hue channel. 30 | /// The value of the saturation channel. 31 | /// The value of the lightness channel. 32 | public Colour(float h, float s, float l) { 33 | _h = h; 34 | _s = s; 35 | _l = l; 36 | } 37 | 38 | /// 39 | /// Copies the individual channels of the colour to the specified memory location. 40 | /// 41 | /// The memory location to copy the axis to. 42 | public unsafe void CopyTo(float* destination) { 43 | destination[0] = _h; 44 | destination[1] = _s; 45 | destination[2] = _l; 46 | } 47 | 48 | /// 49 | /// Destructures the colour, exposing the individual channels. 50 | /// 51 | public void Destructure(out float h, out float s, out float l) { 52 | h = _h; 53 | s = _s; 54 | l = _l; 55 | } 56 | 57 | /// 58 | /// Exposes the individual channels of the colour to the specified matching function. 59 | /// 60 | /// The function which matches the individual channels of the colour. 61 | /// 62 | /// Thrown if the value passed to the parameter is null. 63 | /// 64 | public void Match(Action callback) { 65 | if (callback == null) 66 | throw new ArgumentNullException(nameof(callback)); 67 | 68 | callback(_h, _s, _l); 69 | } 70 | 71 | /// 72 | /// Exposes the individual channels of the colour to the specified mapping function and returns the 73 | /// result; 74 | /// 75 | /// The type being mapped to. 76 | /// 77 | /// A function which maps the colour channels to an instance of . 78 | /// 79 | /// 80 | /// The result of the function when passed the individual X and Y components. 81 | /// 82 | /// 83 | /// Thrown if the value passed to the parameter is null. 84 | /// 85 | public T Map(Func map) 86 | { 87 | if (map == null) 88 | throw new ArgumentNullException(nameof(map)); 89 | 90 | return map(_h, _s, _l); 91 | } 92 | 93 | /// 94 | /// Determines whether the specified is equal to this instance. 95 | /// 96 | /// The to compare with this instance. 97 | /// 98 | /// true if the specified is equal to this instance; otherwise, false. 99 | /// 100 | public override bool Equals(object obj) { 101 | if (obj is Colour) 102 | return Equals((Colour)obj); 103 | 104 | return base.Equals(obj); 105 | } 106 | 107 | /// 108 | /// Determines whether the specified is equal to this instance. 109 | /// 110 | /// The to compare with this instance. 111 | /// 112 | /// true if the specified is equal to this instance; otherwise, false. 113 | /// 114 | public bool Equals(Colour value) => _h.Equals(value._h) && 115 | _s.Equals(value._s) && 116 | _l.Equals(value._l); 117 | 118 | /// 119 | /// Returns a hash code for this instance. 120 | /// 121 | /// 122 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 123 | /// 124 | public override int GetHashCode() => _h.GetHashCode() ^ 125 | _s.GetHashCode() ^ 126 | _l.GetHashCode(); 127 | 128 | /// 129 | /// Returns a that represents this instance. 130 | /// 131 | /// 132 | /// A that represents this instance. 133 | /// 134 | public override string ToString() => $"{_h}°,{_s:P0},{_l:P0}"; 135 | 136 | /// 137 | /// Implements the operator ==. 138 | /// 139 | /// The lvalue. 140 | /// The rvalue. 141 | /// 142 | /// true if the lvalue is equal to the rvalue; otherwise, false. 143 | /// 144 | static public bool operator ==(Colour x, Colour y) => x.Equals(y); 145 | 146 | /// 147 | /// Implements the operator !=. 148 | /// 149 | /// The lvalue. 150 | /// The rvalue. 151 | /// 152 | /// true if the lvalue is not equal to the rvalue; otherwise, false. 153 | /// 154 | static public bool operator !=(Colour x, Colour y) => !x.Equals(y); 155 | 156 | static public Colour operator -(Colour a, Colour b) => new Colour(a._h - b._h, a._s - b._s, a._l - b._l); 157 | } 158 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/ColourRange.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | public struct ColourRange { 3 | public ColourRange(Colour min, Colour max) { 4 | Min = min; 5 | Max = max; 6 | } 7 | 8 | public readonly Colour Min; 9 | public readonly Colour Max; 10 | 11 | static public implicit operator ColourRange(Colour value) => new ColourRange(value, value); 12 | } 13 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Coordinate.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | 6 | /// 7 | /// An immutable data structure encapsulating a 3D Cartesian coordinate. 8 | /// 9 | [StructLayout(LayoutKind.Sequential)] 10 | public struct Coordinate : IEquatable { 11 | internal readonly float _x; 12 | internal readonly float _y; 13 | 14 | /// 15 | /// Initializes a new instance of the structure. 16 | /// 17 | /// The value of the point on the first number line (the abscissa). 18 | /// The value of the point on the second number line (the ordinate). 19 | public Coordinate(float x, float y) { 20 | _x = x; 21 | _y = y; 22 | } 23 | 24 | /// 25 | /// Gets the cartesian origin O. 26 | /// 27 | static public Coordinate Origin => new Coordinate(0f, 0f); 28 | 29 | public Coordinate Add(Coordinate other) { 30 | var x = _x + other._x; 31 | var y = _y + other._y; 32 | 33 | return new Coordinate(x, y); 34 | } 35 | 36 | public Coordinate Subtract(Coordinate other) { 37 | var x = _x - other._x; 38 | var y = _y - other._y; 39 | 40 | return new Coordinate(x, y); 41 | } 42 | 43 | /// 44 | /// Translates the coordinate by the specified vector, using the current instance as the 45 | /// origin of the translation. 46 | /// 47 | /// The vector to translate by. 48 | /// A representing the current instance translated by the 49 | /// specified vector. 50 | public Coordinate Translate(Vector vector) { 51 | var x = _x + vector._x; 52 | var y = _y + vector._y; 53 | 54 | return new Coordinate(x, y); 55 | } 56 | 57 | /// 58 | /// Copies the X and Y components of the coordinate to the specified memory location. 59 | /// 60 | /// The memory location to copy the coordinate to. 61 | public unsafe void CopyTo(float* destination) { 62 | destination[0] = _x; 63 | destination[1] = _y; 64 | } 65 | 66 | /// 67 | /// Destructures the coordinate, exposing the individual X and Y components. 68 | /// 69 | public void Destructure(out float x, out float y) { 70 | x = _x; 71 | y = _y; 72 | } 73 | 74 | /// 75 | /// Exposes the individual X and Y components of the coordinate to the specified matching function. 76 | /// 77 | /// The function which matches the individual X and Y components. 78 | /// 79 | /// Thrown if the value passed to the parameter is null. 80 | /// 81 | public void Match(Action callback) { 82 | if (callback == null) 83 | throw new ArgumentNullException(nameof(callback)); 84 | 85 | callback(_x, _y); 86 | } 87 | 88 | /// 89 | /// Exposes the individual X and Y components of the coordinate to the specified mapping function and returns the 90 | /// result; 91 | /// 92 | /// The type being mapped to. 93 | /// 94 | /// A function which maps the X and Y values to an instance of . 95 | /// 96 | /// 97 | /// The result of the function when passed the individual X and Y components. 98 | /// 99 | /// 100 | /// Thrown if the value passed to the parameter is null. 101 | /// 102 | public T Map(Func map) { 103 | if (map == null) 104 | throw new ArgumentNullException(nameof(map)); 105 | 106 | return map(_x, _y); 107 | } 108 | 109 | /// 110 | /// Determines whether the specified is equal to this instance. 111 | /// 112 | /// The to compare with this instance. 113 | /// 114 | /// true if the specified is equal to this instance; otherwise, false. 115 | /// 116 | public override bool Equals(object obj) { 117 | if (obj is Coordinate) 118 | return Equals((Coordinate)obj); 119 | 120 | return base.Equals(obj); 121 | } 122 | 123 | /// 124 | /// Indicates whether the current object is equal to another object of the same type. 125 | /// 126 | /// An object to compare with this object. 127 | /// 128 | /// true if the current object is equal to the parameter; otherwise, false. 129 | /// 130 | public bool Equals(Coordinate other) => _x.Equals(other._x) && 131 | _y.Equals(other._y); 132 | 133 | /// 134 | /// Returns a hash code for this instance. 135 | /// 136 | /// 137 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 138 | /// 139 | public override int GetHashCode() => _x.GetHashCode() ^ 140 | _y.GetHashCode(); 141 | 142 | /// 143 | /// Returns a that represents this instance. 144 | /// 145 | /// 146 | /// A that represents this instance. 147 | /// 148 | public override string ToString() => ToString("g", CultureInfo.InvariantCulture); 149 | 150 | /// 151 | /// Formats the value of the current instance using the specified format. 152 | /// 153 | /// The format to use. 154 | /// -or- A null reference (Nothing in Visual Basic) to use the default format defined for the type of the System.IFormattable implementation. 155 | /// The provider to use to format the value. 156 | /// -or- A null reference (Nothing in Visual Basic) to obtain the numeric format information from the current locale setting of the operating system. 157 | /// The value of the current instance in the specified format. 158 | public string ToString(string format, IFormatProvider formatProvider) { 159 | if (formatProvider != null) { 160 | var formatter = formatProvider.GetFormat(GetType()) as ICustomFormatter; 161 | 162 | if (formatter != null) 163 | return formatter.Format(format, this, formatProvider); 164 | } 165 | 166 | switch (format.ToLowerInvariant()) { 167 | case "x": return _x.ToString("F4"); 168 | case "y": return _y.ToString("F4"); 169 | default: return $"({_x:F4}, {_y:F4})"; 170 | } 171 | } 172 | 173 | static public Coordinate operator +(Coordinate a, Coordinate b) => a.Add(b); 174 | static public Coordinate operator -(Coordinate a, Coordinate b) => a.Subtract(b); 175 | 176 | /// 177 | /// Implements the operator +. 178 | /// 179 | /// The first operand. 180 | /// The second operand. 181 | /// A value representing the the 182 | /// value translated by the value. 183 | static public Coordinate operator +(Coordinate coord, Vector vector) => coord.Translate(vector); 184 | } 185 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Emitter.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using Mercury.ParticleEngine.Modifiers; 4 | using Mercury.ParticleEngine.Profiles; 5 | 6 | public unsafe class Emitter : IDisposable { 7 | public Emitter(int capacity, TimeSpan term, Profile profile) { 8 | if (profile == null) 9 | throw new ArgumentNullException(nameof(profile)); 10 | 11 | _term = (float)term.TotalSeconds; 12 | 13 | Buffer = new ParticleBuffer(capacity); 14 | Profile = profile; 15 | Modifiers = new Modifier[0]; 16 | ModifierExecutionStrategy = ModifierExecutionStrategy.Serial; 17 | Parameters = new ReleaseParameters(); 18 | ReclaimFrequency = 60f; 19 | } 20 | 21 | private readonly float _term; 22 | private float _totalSeconds; 23 | 24 | internal readonly ParticleBuffer Buffer; 25 | 26 | public int ActiveParticles => Buffer.Count; 27 | 28 | public Modifier[] Modifiers { get; set; } 29 | public ModifierExecutionStrategy ModifierExecutionStrategy { get; set; } 30 | public Profile Profile { get; private set; } 31 | public ReleaseParameters Parameters { get; set; } 32 | public BlendMode BlendMode { get; set; } 33 | public RenderingOrder RenderingOrder { get; set; } 34 | public String TextureKey { get; set; } 35 | 36 | public float ReclaimFrequency { get; set; } 37 | private float _secondsSinceLastReclaim; 38 | 39 | private void ReclaimExpiredParticles() { 40 | var particle = (Particle*)Buffer.NativePointer; 41 | var count = Buffer.Count; 42 | 43 | var expired = 0; 44 | 45 | while (count-- > 0) { 46 | if ((_totalSeconds - particle->Inception) < _term) 47 | break; 48 | 49 | expired++; 50 | particle++; 51 | } 52 | 53 | Buffer.Reclaim(expired); 54 | } 55 | 56 | public void Update(float elapsedSeconds) { 57 | _totalSeconds += elapsedSeconds; 58 | _secondsSinceLastReclaim += elapsedSeconds; 59 | 60 | if (Buffer.Count == 0) 61 | return; 62 | 63 | if (_secondsSinceLastReclaim > (1f / ReclaimFrequency)) { 64 | ReclaimExpiredParticles(); 65 | _secondsSinceLastReclaim -= (1f / ReclaimFrequency); 66 | } 67 | 68 | if (Buffer.Count > 0) { 69 | var particle = (Particle*)Buffer.NativePointer; 70 | var count = Buffer.Count; 71 | 72 | while (count-- > 0) { 73 | particle->Age = (_totalSeconds - particle->Inception) / _term; 74 | 75 | particle->Position[0] += particle->Velocity[0] * elapsedSeconds; 76 | particle->Position[1] += particle->Velocity[1] * elapsedSeconds; 77 | 78 | particle++; 79 | } 80 | 81 | ModifierExecutionStrategy.ExecuteModifiers(Modifiers, elapsedSeconds, (Particle*)Buffer.NativePointer, Buffer.Count); 82 | } 83 | } 84 | 85 | public void Trigger(Coordinate position) { 86 | var numToRelease = FastRand.NextInteger(Parameters.Quantity); 87 | 88 | Release(position, numToRelease); 89 | } 90 | 91 | public void Trigger(LineSegment line) { 92 | var numToRelease = FastRand.NextInteger(Parameters.Quantity); 93 | var lineVector = line.ToVector(); 94 | 95 | for (var i = 0; i < numToRelease; i++) { 96 | var offset = lineVector * FastRand.NextSingle(); 97 | Release(line.Origin.Translate(offset), 1); 98 | } 99 | } 100 | 101 | private void Release(Coordinate position, int numToRelease) { 102 | Particle* particle; 103 | var count = Buffer.Release(numToRelease, out particle); 104 | 105 | while (count-- > 0) { 106 | Profile.GetOffsetAndHeading((Coordinate*)particle->Position, (Axis*)particle->Velocity); 107 | 108 | particle->Age = 0f; 109 | particle->Inception = _totalSeconds; 110 | 111 | particle->Position[0] += position._x; 112 | particle->Position[1] += position._y; 113 | 114 | var speed = FastRand.NextSingle(Parameters.Speed); 115 | 116 | particle->Velocity[0] *= speed; 117 | particle->Velocity[1] *= speed; 118 | 119 | FastRand.NextColour((Colour*)particle->Colour, Parameters.Colour); 120 | 121 | particle->Opacity = FastRand.NextSingle(Parameters.Opacity); 122 | particle->Scale = FastRand.NextSingle(Parameters.Scale); 123 | particle->Rotation = FastRand.NextSingle(Parameters.Rotation); 124 | particle->Mass = FastRand.NextSingle(Parameters.Mass); 125 | 126 | particle++; 127 | } 128 | } 129 | 130 | public void Dispose() { 131 | Buffer.Dispose(); 132 | GC.SuppressFinalize(this); 133 | } 134 | 135 | ~Emitter() { 136 | Dispose(); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/FastRand.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | 4 | /// 5 | /// Defines a random number generator which uses the FastRand algorithm to generate random values. 6 | /// 7 | internal static class FastRand { 8 | static int _state = 1; 9 | 10 | static public void Seed(int seed) { 11 | if (seed < 1) 12 | throw new ArgumentOutOfRangeException(nameof(seed), "seed must be greater than zero"); 13 | 14 | _state = seed; 15 | } 16 | 17 | /// 18 | /// Gets the next random integer value. 19 | /// 20 | /// A random positive integer. 21 | static public int NextInteger() { 22 | _state = 214013 * _state + 2531011; 23 | return (_state >> 16) & 0x7FFF; 24 | } 25 | 26 | /// 27 | /// Gets the next random integer value which is greater than zero and less than or equal to 28 | /// the specified maxmimum value. 29 | /// 30 | /// The maximum random integer value to return. 31 | /// A random integer value between zero and the specified maximum value. 32 | static public int NextInteger(int max) => (int)(max * NextSingle()); 33 | 34 | /// 35 | /// Gets the next random integer between the specified minimum and maximum values. 36 | /// 37 | /// The inclusive minimum value. 38 | /// The inclusive maximum value. 39 | static public int NextInteger(int min, int max) => (int)((max - min) * NextSingle()) + min; 40 | 41 | /// 42 | /// Gets the next random integer between the specified range of values. 43 | /// 44 | /// A range representing the inclusive minimum and maximum values. 45 | /// A random integer between the specified minumum and maximum values. 46 | static public int NextInteger(Range range) => NextInteger(range.X, range.Y); 47 | 48 | /// 49 | /// Gets the next random single value. 50 | /// 51 | /// A random single value between 0 and 1. 52 | static public float NextSingle() => NextInteger() / (float)Int16.MaxValue; 53 | 54 | /// 55 | /// Gets the next random single value which is greater than zero and less than or equal to 56 | /// the specified maxmimum value. 57 | /// 58 | /// The maximum random single value to return. 59 | /// A random single value between zero and the specified maximum value. 60 | static public float NextSingle(float max) => max * NextSingle(); 61 | 62 | /// 63 | /// Gets the next random single value between the specified minimum and maximum values. 64 | /// 65 | /// The inclusive minimum value. 66 | /// The inclusive maximum value. 67 | /// A random single value between the specified minimum and maximum values. 68 | static public float NextSingle(float min, float max) => ((max - min) * NextSingle()) + min; 69 | 70 | /// 71 | /// Gets the next random single value between the specified range of values. 72 | /// 73 | /// A range representing the inclusive minimum and maximum values. 74 | /// A random single value between the specified minimum and maximum values. 75 | static public float NextSingle(RangeF range) => NextSingle(range.X, range.Y); 76 | 77 | /// 78 | /// Gets the next random angle value. 79 | /// 80 | /// A random angle value. 81 | static public float NextAngle() => NextSingle((float)Math.PI * -1f, (float)Math.PI); 82 | 83 | static public unsafe void NextUnitVector(Vector* vector) { 84 | var angle = NextAngle(); 85 | 86 | *vector = new Vector((float)Math.Cos(angle), (float)Math.Sin(angle)); 87 | } 88 | 89 | static public unsafe void NextColour(Colour* colour, ColourRange range) { 90 | *colour = new Colour(NextSingle(range.Min._h, range.Max._h), 91 | NextSingle(range.Min._s, range.Max._s), 92 | NextSingle(range.Min._l, range.Max._l)); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/LineSegment.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | /// 6 | /// Defines a part of a line that is bounded by two distinct end points. 7 | /// 8 | [StructLayout(LayoutKind.Sequential)] 9 | public struct LineSegment : IEquatable { 10 | internal readonly Coordinate _point1; 11 | internal readonly Coordinate _point2; 12 | 13 | /// 14 | /// Initializes a new instance of the structure. 15 | /// 16 | /// 17 | /// 18 | public LineSegment(Coordinate point1, Coordinate point2) { 19 | _point1 = point1; 20 | _point2 = point2; 21 | } 22 | 23 | public LineSegment(Coordinate origin, Vector vector) { 24 | _point1 = origin; 25 | _point2 = origin.Translate(vector); 26 | } 27 | 28 | public Coordinate Origin => _point1; 29 | 30 | public Axis Direction { 31 | get { 32 | var coord = _point2.Subtract(_point1); 33 | return new Axis(coord._x, coord._y); 34 | } 35 | } 36 | 37 | public Vector ToVector() { 38 | var t = _point2.Subtract(_point1); 39 | return new Vector(t._x, t._y); 40 | } 41 | 42 | public unsafe void CopyTo(float* destination) { 43 | _point1.CopyTo(destination); 44 | _point2.CopyTo(destination + sizeof(Coordinate)); 45 | } 46 | 47 | public void Destructure(out Coordinate point1, out Coordinate point2) { 48 | point1 = _point1; 49 | point2 = _point2; 50 | } 51 | 52 | public void Match(Action callback) { 53 | if (callback == null) 54 | throw new ArgumentNullException(nameof(callback)); 55 | 56 | callback(_point1, _point2); 57 | } 58 | 59 | public T Map(Func map) { 60 | if (map == null) 61 | throw new ArgumentNullException(nameof(map)); 62 | 63 | return map(_point1, _point2); 64 | } 65 | 66 | public bool Equals(LineSegment other) => _point1.Equals(other._point1) && 67 | _point2.Equals(other._point2); 68 | 69 | public override bool Equals(object obj) { 70 | if (ReferenceEquals(null, obj)) 71 | return false; 72 | 73 | return obj is LineSegment & Equals((LineSegment)obj); 74 | } 75 | 76 | public override int GetHashCode() { 77 | var hashCode = _point1.GetHashCode(); 78 | 79 | hashCode = (hashCode * 397) ^ _point2.GetHashCode(); 80 | 81 | return hashCode; 82 | } 83 | 84 | public override string ToString() => $"({_point1:x}:{_point1:y},{_point2:x}:{_point2:y})"; 85 | } 86 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Mercury.ParticleEngine.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {E7B59AF1-5CCF-472B-906A-62BF48E520A8} 8 | Library 9 | Properties 10 | Mercury.ParticleEngine 11 | Mercury.ParticleEngine.Core 12 | v4.6.1 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | true 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Properties\AssemblyVersion.cs 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/ModifierExecutionStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System.Collections.Generic; 3 | using Mercury.ParticleEngine.Modifiers; 4 | using TPL = System.Threading.Tasks; 5 | 6 | public abstract class ModifierExecutionStrategy { 7 | internal abstract unsafe void ExecuteModifiers(IEnumerable modifiers, float elapsedSeconds, Particle* particle, int count); 8 | 9 | static public ModifierExecutionStrategy Serial = new SerialModifierExecutionStrategy(); 10 | static public ModifierExecutionStrategy Parallel = new ParallelModifierExecutionStrategy(); 11 | 12 | internal class SerialModifierExecutionStrategy : ModifierExecutionStrategy { 13 | internal override unsafe void ExecuteModifiers(IEnumerable modifiers, float elapsedSeconds, Particle* particle, int count) { 14 | foreach (var modifier in modifiers) { 15 | modifier.InternalUpdate(elapsedSeconds, particle, count); 16 | } 17 | } 18 | } 19 | 20 | internal class ParallelModifierExecutionStrategy : ModifierExecutionStrategy { 21 | internal override unsafe void ExecuteModifiers(IEnumerable modifiers, float elapsedSeconds, Particle* particle, int count) { 22 | TPL.Parallel.ForEach(modifiers, modifier => modifier.InternalUpdate(elapsedSeconds, particle, count)); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/ColourInterpolator2.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | /// 3 | /// Defines a modifier which interpolates the colour of a particle over the course of its lifetime. 4 | /// 5 | public sealed class ColourInterpolator2 : Modifier { 6 | /// 7 | /// Gets or sets the initial colour of particles when they are released. 8 | /// 9 | public Colour InitialColour; 10 | 11 | /// 12 | /// Gets or sets the final colour of particles when they are retired. 13 | /// 14 | public Colour FinalColour; 15 | 16 | protected internal override unsafe void Update(float elapsedseconds, Particle* particle, int count) { 17 | var delta = new Colour(FinalColour._h - InitialColour._h, 18 | FinalColour._s - InitialColour._s, 19 | FinalColour._l - InitialColour._l); 20 | 21 | while (count-- > 0) { 22 | particle->Colour[0] = (InitialColour._h + (delta._h * particle->Age)); 23 | particle->Colour[1] = (InitialColour._s + (delta._s * particle->Age)); 24 | particle->Colour[2] = (InitialColour._l + (delta._l * particle->Age)); 25 | 26 | particle++; 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/ContainerModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public sealed unsafe class ContainerModifier : Modifier { 3 | public Coordinate Position; 4 | public int Width; 5 | public int Height; 6 | public float RestitutionCoefficient; 7 | 8 | protected internal override void Update(float elapsedSeconds, Particle* particle, int count) { 9 | var left = Width * -0.5f; 10 | var right = Width * 0.5f; 11 | var top = Height * -0.5f; 12 | var bottom = Height * 0.5f; 13 | 14 | while (count-- > 0) { 15 | if ((int)particle->Position[0] < left) { 16 | particle->Position[0] = left + (left - particle->Position[0]); 17 | particle->Velocity[0] = -particle->Velocity[0] * RestitutionCoefficient; 18 | } 19 | else if ((int)particle->Position[0] > right) { 20 | particle->Position[0] = right - (particle->Position[0] - right); 21 | particle->Velocity[0] = -particle->Velocity[0] * RestitutionCoefficient; 22 | } 23 | 24 | if ((int)particle->Position[1] < top) { 25 | particle->Position[1] = top + (top - particle->Position[1]); 26 | particle->Velocity[1] = -particle->Velocity[1] * RestitutionCoefficient; 27 | } 28 | else if ((int)particle->Position[1] > bottom) { 29 | particle->Position[1] = bottom - (particle->Position[1] - bottom); 30 | particle->Velocity[1] = -particle->Velocity[1] * RestitutionCoefficient; 31 | } 32 | 33 | particle++; 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/DragModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public class DragModifier : Modifier { 3 | public float DragCoefficient = 0.47f; 4 | public float Density = .5f; 5 | 6 | protected internal unsafe override void Update(float elapsedSeconds, Particle* particle, int count) { 7 | while (count-- > 0) { 8 | var drag = -DragCoefficient * Density * particle->Mass * elapsedSeconds; 9 | 10 | particle->Velocity[0] += (particle->Velocity[0] * drag); 11 | particle->Velocity[1] += (particle->Velocity[1] * drag); 12 | 13 | particle++; 14 | } 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/HueInterpolator2.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public class HueInterpolator2 : Modifier { 3 | public float InitialHue; 4 | public float FinalHue; 5 | 6 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 7 | var delta = FinalHue - InitialHue; 8 | 9 | while (count-- > 0) { 10 | particle->Colour[0] = (delta * particle->Age) + InitialHue; 11 | particle++; 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/LinearGravityModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public class LinearGravityModifier : Modifier { 3 | public LinearGravityModifier(Axis direction, float strength) { 4 | Direction = direction; 5 | Strength = strength; 6 | } 7 | 8 | public LinearGravityModifier(Vector vector) 9 | : this(vector.Axis, vector.Magnitude) { 10 | } 11 | 12 | public Axis Direction { get; set; } 13 | public float Strength { get; set; } 14 | 15 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 16 | var vector = Direction * (Strength * elapsedSeconds); 17 | 18 | while (count-- > 0) { 19 | particle->Velocity[0] += vector._x * particle->Mass; 20 | particle->Velocity[1] += vector._y * particle->Mass; 21 | 22 | particle++; 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/Modifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | 4 | public abstract class Modifier { 5 | const float DefaultModifierFrequency = 60.0f; 6 | 7 | private float _frequency; 8 | private float _cycleTime; 9 | private int _particlesUpdatedThisCycle; 10 | 11 | protected Modifier() { 12 | Frequency = DefaultModifierFrequency; 13 | } 14 | 15 | public float Frequency { 16 | get { return _frequency; } 17 | set { 18 | if (value > 0.0f == false) 19 | throw new ArgumentOutOfRangeException(nameof(value), "Frequency must be greater than zero."); 20 | 21 | _frequency = value; 22 | _cycleTime = 1f / _frequency; 23 | } 24 | } 25 | 26 | internal unsafe void InternalUpdate(float elapsedSeconds, Particle* buffer, int count) { 27 | var particlesRemaining = count - _particlesUpdatedThisCycle; 28 | var particlesToUpdate = Math.Min(particlesRemaining, (int)Math.Ceiling((elapsedSeconds / _cycleTime) * count)); 29 | 30 | if (particlesToUpdate > 0) { 31 | Update(_cycleTime, buffer + _particlesUpdatedThisCycle, particlesToUpdate); 32 | 33 | _particlesUpdatedThisCycle += particlesToUpdate; 34 | } 35 | 36 | if (_particlesUpdatedThisCycle >= count) 37 | _particlesUpdatedThisCycle = 0; 38 | } 39 | 40 | protected internal abstract unsafe void Update(float elapsedSeconds, Particle* particle, int count); 41 | } 42 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/OpacityFastFadeModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public unsafe sealed class OpacityFastFadeModifier : Modifier { 3 | protected internal override void Update(float elapsedSeconds, Particle* particle, int count) { 4 | while (count-- > 0) { 5 | particle->Opacity = 1.0f - particle->Age; 6 | particle++; 7 | } 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/OpacityInterpolator2.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public class OpacityInterpolator2 : Modifier { 3 | public float InitialOpacity; 4 | public float FinalOpacity; 5 | 6 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 7 | var delta = FinalOpacity - InitialOpacity; 8 | 9 | while (count-- > 0) { 10 | particle->Opacity = (delta * particle->Age) + InitialOpacity; 11 | particle++; 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/RotationModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | 4 | public class RotationModifier : Modifier { 5 | public float RotationRate; 6 | 7 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 8 | var rotationRateDelta = RotationRate * elapsedSeconds; 9 | 10 | while (count-- > 0) { 11 | particle->Rotation += rotationRateDelta; 12 | particle++; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/ScaleInterpolator2.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | public class ScaleInterpolator2 : Modifier { 3 | public float InitialScale; 4 | public float FinalScale; 5 | 6 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 7 | var delta = FinalScale - InitialScale; 8 | 9 | while (count-- > 0) { 10 | particle->Scale = (delta * particle->Age) + InitialScale; 11 | particle++; 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/VelocityColourModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | 4 | public class VelocityColourModifier : Modifier { 5 | public Colour StationaryColour; 6 | public Colour VelocityColour; 7 | public float VelocityThreshold; 8 | 9 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 10 | var velocityThreshold2 = VelocityThreshold * VelocityThreshold; 11 | 12 | while (count-- > 0) { 13 | var velocity2 = ((particle->Velocity[0] * particle->Velocity[0]) + (particle->Velocity[1] * particle->Velocity[1])); 14 | var deltaColour = VelocityColour - StationaryColour; 15 | 16 | if (velocity2 >= velocityThreshold2) { 17 | VelocityColour.CopyTo(particle->Colour); 18 | } 19 | else { 20 | var t = (float)Math.Sqrt(velocity2) / VelocityThreshold; 21 | 22 | particle->Colour[0] = (deltaColour._h * t) + StationaryColour._h; 23 | particle->Colour[1] = (deltaColour._s * t) + StationaryColour._s; 24 | particle->Colour[2] = (deltaColour._l * t) + StationaryColour._l; 25 | } 26 | 27 | particle++; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/VelocityHueModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | 4 | public class VelocityHueModifier : Modifier { 5 | public float StationaryHue; 6 | public float VelocityHue; 7 | public float VelocityThreshold; 8 | 9 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 10 | var velocityThreshold2 = VelocityThreshold * VelocityThreshold; 11 | 12 | while (count-- > 0) { 13 | var velocity2 = ((particle->Velocity[0] * particle->Velocity[0]) + (particle->Velocity[1] * particle->Velocity[1])); 14 | 15 | if (velocity2 >= velocityThreshold2) { 16 | particle->Colour[0] = VelocityHue; 17 | } 18 | else { 19 | var t = (float)Math.Sqrt(velocity2) / VelocityThreshold; 20 | particle->Colour[0] = ((VelocityHue - StationaryHue) * t) + StationaryHue; 21 | } 22 | 23 | particle++; 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Modifiers/VortexModifier.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Modifiers { 2 | using System; 3 | 4 | public class VortexModifier : Modifier { 5 | public Coordinate Position; 6 | public float Mass; 7 | public float MaxSpeed; 8 | 9 | protected internal override unsafe void Update(float elapsedSeconds, Particle* particle, int count) { 10 | while (count-- > 0) { 11 | var distx = Position._x - particle->Position[0]; 12 | var disty = Position._y - particle->Position[1]; 13 | 14 | var distance2 = (distx * distx) + (disty * disty); 15 | var distance = (float)Math.Sqrt(distance2); 16 | 17 | var m = (10000f * Mass * particle->Mass) / distance2; 18 | 19 | m = Math.Max(Math.Min(m, MaxSpeed), -MaxSpeed) * elapsedSeconds; 20 | 21 | distx = (distx / distance) * m; 22 | disty = (disty / distance) * m; 23 | 24 | particle->Velocity[0] += distx; 25 | particle->Velocity[1] += disty; 26 | 27 | particle++; 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Particle.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System.Runtime.InteropServices; 3 | 4 | [StructLayout(LayoutKind.Sequential, Pack = 1)] 5 | public unsafe struct Particle { 6 | public float Inception; 7 | public float Age; 8 | public fixed float Position[2]; 9 | public fixed float Velocity[2]; 10 | public fixed float Colour[3]; 11 | public float Opacity; 12 | public float Scale; 13 | public float Rotation; 14 | public float Mass; 15 | 16 | static public readonly int SizeInBytes = Marshal.SizeOf(typeof(Particle)); 17 | } 18 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/ParticleBuffer.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | internal class ParticleBuffer : IDisposable { 6 | private int _tail; 7 | 8 | public readonly IntPtr NativePointer; 9 | public readonly int Size; 10 | 11 | public ParticleBuffer(int size) { 12 | Size = size; 13 | NativePointer = Marshal.AllocHGlobal(SizeInBytes); 14 | 15 | GC.AddMemoryPressure(SizeInBytes); 16 | } 17 | 18 | public int Available => Size - _tail; 19 | public int Count => _tail; 20 | public int SizeInBytes => Particle.SizeInBytes * Size; 21 | public int ActiveSizeInBytes => Particle.SizeInBytes * _tail; 22 | 23 | public unsafe int Release(int releaseQuantity, out Particle* first) { 24 | var numToRelease = Math.Min(releaseQuantity, Available); 25 | 26 | var oldTail = _tail; 27 | 28 | _tail += numToRelease; 29 | 30 | first = (Particle*)IntPtr.Add(NativePointer, oldTail * Particle.SizeInBytes); 31 | 32 | return numToRelease; 33 | } 34 | 35 | public void Reclaim(int number) { 36 | _tail -= number; 37 | 38 | memcpy(NativePointer, IntPtr.Add(NativePointer, number * Particle.SizeInBytes), ActiveSizeInBytes); 39 | } 40 | 41 | public void CopyTo(IntPtr destination) => memcpy(destination, NativePointer, ActiveSizeInBytes); 42 | 43 | public void CopyToReverse(IntPtr destination) { 44 | int offset = 0; 45 | for (var i = ActiveSizeInBytes - Particle.SizeInBytes; i >= 0; i -= Particle.SizeInBytes) { 46 | memcpy(IntPtr.Add(destination, offset), IntPtr.Add(NativePointer, i), Particle.SizeInBytes); 47 | offset += Particle.SizeInBytes; 48 | } 49 | } 50 | 51 | [DllImport("msvcrt.dll", EntryPoint = "memcpy", CallingConvention = CallingConvention.Cdecl, SetLastError = false)] 52 | public static extern void memcpy(IntPtr dest, IntPtr src, int count); 53 | 54 | private bool _disposed; 55 | 56 | public void Dispose() { 57 | if (!_disposed) { 58 | Marshal.FreeHGlobal(NativePointer); 59 | _disposed = true; 60 | 61 | GC.RemoveMemoryPressure(Particle.SizeInBytes * Size); 62 | } 63 | 64 | GC.SuppressFinalize(this); 65 | } 66 | 67 | ~ParticleBuffer() { 68 | Dispose(); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/ParticleEffect.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | public class ParticleEffect { 3 | public Emitter[] Emitters { get; set; } 4 | 5 | public ParticleEffect() { 6 | Emitters = new Emitter[0]; 7 | } 8 | 9 | public int ActiveParticles { 10 | get { 11 | int sum = 0; 12 | for (var i = 0; i < Emitters.Length; i++) { 13 | sum += Emitters[i].ActiveParticles; 14 | } 15 | return sum; 16 | } 17 | } 18 | 19 | public void Update(float elapsedSeconds) { 20 | for (var i = 0; i < Emitters.Length; i++) { 21 | Emitters[i].Update(elapsedSeconds); 22 | } 23 | } 24 | 25 | public void Trigger(Coordinate position) { 26 | for (var i = 0; i < Emitters.Length; i++) { 27 | Emitters[i].Trigger(position); 28 | } 29 | } 30 | 31 | public void Trigger(LineSegment line) { 32 | for (var i = 0; i < Emitters.Length; i++) 33 | Emitters[i].Trigger(line); 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/BoxFillProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public class BoxFillProfile : Profile { 3 | public float Width; 4 | public float Height; 5 | 6 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 7 | *offset = new Coordinate(FastRand.NextSingle(Width * -0.5f, Width * 0.5f), 8 | FastRand.NextSingle(Height * -0.5f, Height * 0.5f)); 9 | 10 | FastRand.NextUnitVector((Vector*)heading); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/BoxProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public class BoxProfile : Profile { 3 | public float Width; 4 | public float Height; 5 | 6 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 7 | switch (FastRand.NextInteger(3)) { 8 | case 0: { // Left 9 | *offset = new Coordinate(Width * -0.5f, FastRand.NextSingle(Height * -0.5f, Height * 0.5f)); 10 | break; 11 | } 12 | case 1: { // Top 13 | *offset = new Coordinate(FastRand.NextSingle(Width * -0.5f, Width * 0.5f), Height * -0.5f); 14 | break; 15 | } 16 | case 2: { // Right 17 | *offset = new Coordinate(Width * 0.5f, FastRand.NextSingle(Height * -0.5f, Height * 0.5f)); 18 | break; 19 | } 20 | case 3: { // Bottom 21 | *offset = new Coordinate(FastRand.NextSingle(Width * -0.5f, Width * 0.5f), Height * 0.5f); 22 | break; 23 | } 24 | } 25 | 26 | FastRand.NextUnitVector((Vector*)heading); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/CircleProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public class CircleProfile : Profile { 3 | public float Radius; 4 | public bool Radiate; 5 | 6 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 7 | var dist = FastRand.NextSingle(0f, Radius); 8 | 9 | FastRand.NextUnitVector((Vector*)heading); 10 | 11 | *offset = new Coordinate(heading->_x * dist, heading->_y * dist); 12 | 13 | if (!Radiate) 14 | FastRand.NextUnitVector((Vector*)heading); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/PointProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public class PointProfile : Profile { 3 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 4 | *offset = Coordinate.Origin; 5 | 6 | FastRand.NextUnitVector((Vector*)heading); 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/Profile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public abstract class Profile { 3 | public abstract unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading); 4 | 5 | static public Profile Point() => new PointProfile(); 6 | 7 | static public Profile Ring(float radius, bool radiate) => 8 | new RingProfile { 9 | Radius = radius, 10 | Radiate = radiate 11 | }; 12 | 13 | static public Profile Box(float width, float height) => 14 | new BoxProfile { 15 | Width = width, 16 | Height = height 17 | }; 18 | 19 | static public Profile BoxFill(float width, float height) => 20 | new BoxFillProfile { 21 | Width = width, 22 | Height = height 23 | }; 24 | 25 | static public Profile Circle(float radius, bool radiate) => 26 | new CircleProfile { 27 | Radius = radius, 28 | Radiate = radiate 29 | }; 30 | 31 | static public Profile Spray(Axis direction, float spread) => 32 | new SprayProfile { 33 | Direction = direction, 34 | Spread = spread 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/RingProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | public class RingProfile : Profile { 3 | public float Radius { get; set; } 4 | public bool Radiate { get; set; } 5 | 6 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 7 | FastRand.NextUnitVector((Vector*)heading); 8 | 9 | *offset = new Coordinate(heading->_x * Radius, heading->_y * Radius); 10 | 11 | if (!Radiate) 12 | FastRand.NextUnitVector((Vector*)heading); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Profiles/SprayProfile.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Profiles { 2 | using System; 3 | 4 | public class SprayProfile : Profile { 5 | public Axis Direction; 6 | public float Spread; 7 | 8 | public override unsafe void GetOffsetAndHeading(Coordinate* offset, Axis* heading) { 9 | var angle = Direction.Map((x, y) => (float)Math.Atan2(y, x)); 10 | 11 | angle = FastRand.NextSingle(angle - (Spread / 2f), angle + (Spread / 2f)); 12 | 13 | *offset = Coordinate.Origin; 14 | *heading = new Axis((float)Math.Cos(angle), (float)Math.Sin(angle)); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("Mercury.ParticleEngine.Core")] 6 | [assembly: AssemblyDescription("Mercury Particle Engine 5.0 Core Assembly")] 7 | [assembly: AssemblyCompany("")] 8 | [assembly: AssemblyProduct("Mercury Particle Engine 5.0")] 9 | [assembly: AssemblyCopyright("Copyright © Matt Davey 2006-2015")] 10 | [assembly: ComVisible(false)] 11 | [assembly: Guid("c7eccd77-094c-47bd-b41e-c2499deeca84")] 12 | 13 | [assembly: InternalsVisibleTo("Mercury.ParticleEngine.Core.Tests")] 14 | [assembly: InternalsVisibleTo("Mercury.ParticleEngine.SharpDX.Direct3D9")] -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Range.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | using System.Text.RegularExpressions; 6 | 7 | /// 8 | /// Represents a closed interval of values. 9 | /// 10 | [Serializable] 11 | [StructLayout(LayoutKind.Sequential)] 12 | public struct Range : IEquatable, IFormattable { 13 | /// 14 | /// Defines a template for a regex which can be used to validate a string representation 15 | /// of an interval. The template contains tokens which should be replaced with culture 16 | /// specific symbols. 17 | /// 18 | private const string RegexTemplate = @"\[([\$(PositiveSign)\$(NegativeSign)]?[0-9]+)\$(GroupSeparator)([\$(PositiveSign)\$(NegativeSign)]?[0-9]+)\]"; 19 | 20 | /// 21 | /// Gets a regex pattern which can be used to validate a string representation of an interval 22 | /// in the specified culture. 23 | /// 24 | /// The culture in which the interval is represented. 25 | /// A regex pattern which can be used to validate the interval representation. 26 | /// 27 | /// Thrown if the value passed to the parameter is null. 28 | /// 29 | private static string GetFormatPattern(IFormatProvider provider) { 30 | if (provider == null) 31 | throw new ArgumentNullException(nameof(provider)); 32 | 33 | var numberFormat = NumberFormatInfo.GetInstance(provider); 34 | 35 | return RegexTemplate.Replace("$(PositiveSign)", numberFormat.PositiveSign) 36 | .Replace("$(NegativeSign)", numberFormat.NegativeSign) 37 | .Replace("$(GroupSeparator)", numberFormat.NumberGroupSeparator); 38 | } 39 | 40 | /// 41 | /// Initializes a new instance of the struct. 42 | /// 43 | /// The left boundary value. 44 | /// The right boundary value. 45 | /// 46 | /// Thrown if either of the values passed to the or 47 | /// parameters are not finite. That is, positive infinity, negative infinity, or NaN. 48 | /// 49 | public Range(int x, int y) { 50 | X = Math.Min(x, y); 51 | Y = Math.Max(x, y); 52 | 53 | Diameter = Math.Abs(X - Y); 54 | Radius = Diameter / 2; 55 | Centre = X + Radius; 56 | } 57 | 58 | /// 59 | /// Creates a new interval which is a union of two separate intervals, representing a range 60 | /// that encompasses both of them. 61 | /// 62 | /// The first interval. 63 | /// The second interval. 64 | /// An interval that encompasses both input intervals. 65 | /// 66 | /// 67 | /// 76 | /// 77 | /// 78 | static public Range Union(Range x, Range y) => new Range(Math.Min(x.X, y.X), Math.Max(x.Y, y.Y)); 79 | 80 | /// 81 | /// Gets or sets the inclusive minimum value in the interval. 82 | /// 83 | public readonly int X; 84 | 85 | /// 86 | /// Gets or sets the inclusive maximum value in the interval. 87 | /// 88 | public readonly int Y; 89 | 90 | /// 91 | /// Gets the diameter (size) of the interval. 92 | /// 93 | public readonly int Diameter; 94 | 95 | /// 96 | /// Gets the centre of the interval. 97 | /// 98 | public readonly int Centre; 99 | 100 | /// 101 | /// Gets or sets the radius of the interval. 102 | /// 103 | public readonly int Radius; 104 | 105 | /// 106 | /// Gets a value indicating whether or not the interval is degenerate. A degenerate interval 107 | /// is one which contains only a float distinct boundary (X == Y, Diameter == 0). 108 | /// 109 | public bool IsDegenerate => X.Equals(Y); 110 | 111 | /// 112 | /// Gets a value indicating whether or not the interval is proper. A proper interval is one 113 | /// which is neither empty or degenerate. 114 | /// 115 | public bool IsProper => !X.Equals(Y); 116 | 117 | /// 118 | /// Gets the interior of the interval. The interior is the largest proper interval contained 119 | /// within this interval. 120 | /// 121 | public Range Interior { 122 | get 123 | { 124 | var x = X + 1; 125 | var y = Y - 1; 126 | 127 | return new Range(x, y); 128 | } 129 | } 130 | 131 | /// 132 | /// Gets the closure of the interval. The closure is the smallest proper interval which 133 | /// contains this interval. 134 | /// 135 | public Range Closure { 136 | get 137 | { 138 | var x = X - 1; 139 | var y = Y + 1; 140 | 141 | return new Range(x, y); 142 | } 143 | } 144 | 145 | /// 146 | /// Gets a value indicating whether or not the specified value is contained within the 147 | /// closed interval. 148 | /// 149 | /// The floating point value. 150 | /// true if the specified value is contained within the closed interval; 151 | /// else false. 152 | public bool Contains(int value) => value >= X && value <= Y; 153 | 154 | /// 155 | /// Creates a new interval by parsing an ISO 31-11 string representation of a closed interval. 156 | /// 157 | /// Input string value. 158 | /// A new interval value. 159 | /// Thrown if the input String is not in a valid ISO 31-11 160 | /// closed interval format, or if the numbers represented within the closed interval could 161 | /// not be parsed. 162 | /// 163 | /// Example of a well formed ISO 31-11 closed interval: "[0,1]". Open intervals are 164 | /// not supported. 165 | /// 166 | /// 167 | /// Thrown if the value passed to the parameter is not in the 168 | /// correct format for an ISO 31-11 closed interval, or if the numbers represented within 169 | /// the closed interval could not be parsed. 170 | /// 171 | public static Range Parse(String value) => Parse(value, CultureInfo.InvariantCulture); 172 | 173 | /// 174 | /// Creates a new interval by parsing an ISO 31-11 string representation of an interval. 175 | /// 176 | /// Input string value. 177 | /// The format provider. 178 | /// 179 | /// Example of a well formed ISO 31-11 interval: "[0,1]". 180 | /// 181 | /// 182 | /// Thrown if the value passed to the parameter is not in the 183 | /// correct format for an ISO 31-11 interval, or if the numbers represented within the 184 | /// closed interval could not be parsed. 185 | /// 186 | public static Range Parse(string value, IFormatProvider format) { 187 | if (value == null) 188 | throw new ArgumentNullException(nameof(value)); 189 | 190 | if (format == null) 191 | throw new ArgumentNullException(nameof(format)); 192 | 193 | var regex = new Regex(GetFormatPattern(format)); 194 | 195 | if (regex.IsMatch(value)) { 196 | var match = regex.Match(value); 197 | 198 | var group1 = match.Groups[1].Value; 199 | var group2 = match.Groups[2].Value; 200 | 201 | // No error handling required on boundary parsing, regex has already validated the 202 | // format of the boundary values... 203 | var x = Int32.Parse(group1, NumberStyles.Integer, format); 204 | var y = Int32.Parse(group2, NumberStyles.Integer, format); 205 | 206 | return new Range(x, y); 207 | } 208 | 209 | throw new FormatException("value is not in the correct format for an ISO 31-11 closed interval in ℝ form."); 210 | } 211 | 212 | /// 213 | /// Determines whether the specified is equal to this instance. 214 | /// 215 | /// The to compare with this instance. 216 | /// 217 | /// true if the specified is equal to this instance; otherwise, false. 218 | /// 219 | public override bool Equals(object obj) { 220 | if (obj != null) 221 | if (obj is Range) 222 | return Equals((Range)obj); 223 | 224 | return false; 225 | } 226 | 227 | /// 228 | /// Determines whether the specified is equal to this instance. 229 | /// 230 | /// The to compare with this instance. 231 | /// 232 | /// true if the specified is equal to this instance; otherwise, false. 233 | /// 234 | public bool Equals(Range value) => X.Equals(value.X) && 235 | Y.Equals(value.Y); 236 | 237 | /// 238 | /// Returns a hash code for this instance. 239 | /// 240 | /// 241 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 242 | /// 243 | public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); 244 | 245 | /// 246 | /// Returns a that represents this instance. 247 | /// 248 | /// 249 | /// A that represents this instance. 250 | /// 251 | public override string ToString() => ToString("G", CultureInfo.InvariantCulture); 252 | 253 | /// 254 | /// Returns a that represents this instance. 255 | /// 256 | /// The format provider. 257 | /// 258 | /// A that represents this instance. 259 | /// 260 | public string ToString(IFormatProvider formatProvider) => ToString("G", formatProvider); 261 | 262 | /// 263 | /// Returns a that represents this instance. 264 | /// 265 | /// The format. 266 | /// The format provider. 267 | /// 268 | /// A that represents this instance. 269 | /// 270 | public string ToString(string format, IFormatProvider formatProvider) { 271 | var numberFormat = NumberFormatInfo.GetInstance(formatProvider); 272 | 273 | var minimum = X.ToString(format, numberFormat); 274 | var maximum = Y.ToString(format, numberFormat); 275 | 276 | var seperator = numberFormat.NumberGroupSeparator; 277 | 278 | return String.Format(formatProvider, "[{0}{1}{2}]", minimum, seperator, maximum); 279 | } 280 | 281 | /// 282 | /// Implements the operator ==. 283 | /// 284 | /// The lvalue. 285 | /// The rvalue. 286 | /// 287 | /// true if the lvalue is equal to the rvalue; otherwise, false. 288 | /// 289 | public static bool operator ==(Range x, Range y) => x.Equals(y); 290 | 291 | /// 292 | /// Implements the operator !=. 293 | /// 294 | /// The lvalue. 295 | /// The rvalue. 296 | /// 297 | /// true if the lvalue is not equal to the rvalue; otherwise, false. 298 | /// 299 | public static bool operator !=(Range x, Range y) => !x.Equals(y); 300 | 301 | static public implicit operator Range(int value) => new Range(value, value); 302 | } 303 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/RangeF.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Globalization; 4 | using System.Runtime.InteropServices; 5 | using System.Text.RegularExpressions; 6 | 7 | /// 8 | /// Represents a closed interval of floating point values. 9 | /// 10 | [Serializable] 11 | [StructLayout(LayoutKind.Sequential)] 12 | public struct RangeF : IEquatable, IFormattable { 13 | /// 14 | /// Defines a template for a regex which can be used to validate a string representation 15 | /// of an interval. The template contains tokens which should be replaced with culture 16 | /// specific symbols. 17 | /// 18 | private const string RegexTemplate = @"\[([\$(PositiveSign)\$(NegativeSign)]?[0-9]+(?:\$(DecimalSeparator)[0-9]*)?)\$(GroupSeparator)([\$(PositiveSign)\$(NegativeSign)]?[0-9]+(?:\$(DecimalSeparator)[0-9]*)?)\]"; 19 | 20 | /// 21 | /// Gets a regex pattern which can be used to validate a string representation of an interval 22 | /// in the specified culture. 23 | /// 24 | /// The culture in which the interval is represented. 25 | /// A regex pattern which can be used to validate the interval representation. 26 | /// 27 | /// Thrown if the value passed to the parameter is null. 28 | /// 29 | private static string GetFormatPattern(IFormatProvider provider) { 30 | if (provider == null) 31 | throw new ArgumentNullException(nameof(provider)); 32 | 33 | var numberFormat = NumberFormatInfo.GetInstance(provider); 34 | 35 | return RegexTemplate.Replace("$(PositiveSign)", numberFormat.PositiveSign) 36 | .Replace("$(NegativeSign)", numberFormat.NegativeSign) 37 | .Replace("$(DecimalSeparator)", numberFormat.NumberDecimalSeparator) 38 | .Replace("$(GroupSeparator)", numberFormat.NumberGroupSeparator); 39 | } 40 | 41 | /// 42 | /// Initializes a new instance of the struct. 43 | /// 44 | /// The left boundary value. 45 | /// The right boundary value. 46 | /// 47 | /// Thrown if either of the values passed to the or 48 | /// parameters are not finite. That is, positive infinity, negative infinity, or NaN. 49 | /// 50 | public RangeF(float x, float y) { 51 | X = Math.Min(x, y); 52 | Y = Math.Max(x, y); 53 | 54 | Diameter = Math.Abs(X - Y); 55 | Radius = Diameter / 2f; 56 | Centre = X + Radius; 57 | } 58 | 59 | /// 60 | /// Creates a new interval which is a union of two separate intervals, representing a range 61 | /// that encompasses both of them. 62 | /// 63 | /// The first interval. 64 | /// The second interval. 65 | /// An interval that encompasses both input intervals. 66 | /// 67 | /// 68 | /// 77 | /// 78 | /// 79 | static public RangeF Union(RangeF x, RangeF y) => new RangeF(Math.Min(x.X, y.X), Math.Max(x.Y, y.Y)); 80 | 81 | /// 82 | /// Gets or sets the inclusive minimum value in the interval. 83 | /// 84 | public readonly float X; 85 | 86 | /// 87 | /// Gets or sets the inclusive maximum value in the interval. 88 | /// 89 | public readonly float Y; 90 | 91 | /// 92 | /// Gets the diameter (size) of the interval. 93 | /// 94 | public readonly float Diameter; 95 | 96 | /// 97 | /// Gets the centre of the interval. 98 | /// 99 | public readonly float Centre; 100 | 101 | /// 102 | /// Gets or sets the radius of the interval. 103 | /// 104 | public readonly float Radius; 105 | 106 | /// 107 | /// Gets a value indicating whether or not the interval is degenerate. A degenerate interval 108 | /// is one which contains only a float distinct boundary (X == Y, Diameter == 0). 109 | /// 110 | public bool IsDegenerate => X.Equals(Y); 111 | 112 | /// 113 | /// Gets a value indicating whether or not the interval is proper. A proper interval is one 114 | /// which is neither empty or degenerate. 115 | /// 116 | public bool IsProper => !X.Equals(Y); 117 | 118 | /// 119 | /// Gets the interior of the interval. The interior is the largest proper interval contained 120 | /// within this interval. 121 | /// 122 | public RangeF Interior { 123 | get { 124 | var x = X + Single.Epsilon; 125 | var y = Y - Single.Epsilon; 126 | 127 | return new RangeF(x, y); 128 | } 129 | } 130 | 131 | /// 132 | /// Gets the closure of the interval. The closure is the smallest proper interval which 133 | /// contains this interval. 134 | /// 135 | public RangeF Closure { 136 | get { 137 | var x = X - Single.Epsilon; 138 | var y = Y + Single.Epsilon; 139 | 140 | return new RangeF(x, y); 141 | } 142 | } 143 | 144 | /// 145 | /// Gets a value indicating whether or not the specified value is contained within the 146 | /// closed interval. 147 | /// 148 | /// The floating point value. 149 | /// true if the specified value is contained within the closed interval; 150 | /// else false. 151 | public bool Contains(float value) { 152 | if (Single.IsInfinity(value)) 153 | throw new ArgumentException("value is not finite", nameof(value), new NotFiniteNumberException(value)); 154 | 155 | return value >= X && value <= Y; 156 | } 157 | 158 | /// 159 | /// Creates a new interval by parsing an ISO 31-11 string representation of a closed interval. 160 | /// 161 | /// Input string value. 162 | /// A new interval value. 163 | /// Thrown if the input String is not in a valid ISO 31-11 164 | /// closed interval format, or if the numbers represented within the closed interval could 165 | /// not be parsed. 166 | /// 167 | /// Example of a well formed ISO 31-11 closed interval: "[0,1]". Open intervals are 168 | /// not supported. 169 | /// 170 | /// 171 | /// Thrown if the value passed to the parameter is not in the 172 | /// correct format for an ISO 31-11 closed interval, or if the numbers represented within 173 | /// the closed interval could not be parsed. 174 | /// 175 | public static RangeF Parse(string value) => Parse(value, CultureInfo.InvariantCulture); 176 | 177 | /// 178 | /// Creates a new interval by parsing an ISO 31-11 string representation of an interval. 179 | /// 180 | /// Input string value. 181 | /// The format provider. 182 | /// 183 | /// Example of a well formed ISO 31-11 interval: "[0,1]". 184 | /// 185 | /// 186 | /// Thrown if the value passed to the parameter is not in the 187 | /// correct format for an ISO 31-11 interval, or if the numbers represented within the 188 | /// closed interval could not be parsed. 189 | /// 190 | public static RangeF Parse(string value, IFormatProvider format) { 191 | if (value == null) 192 | throw new ArgumentNullException(nameof(value)); 193 | 194 | if (format == null) 195 | throw new ArgumentNullException(nameof(format)); 196 | 197 | var regex = new Regex(GetFormatPattern(format)); 198 | 199 | if (regex.IsMatch(value)) { 200 | var match = regex.Match(value); 201 | 202 | var group1 = match.Groups[1].Value; 203 | var group2 = match.Groups[2].Value; 204 | 205 | // No error handling required on boundary parsing, regex has already validated the 206 | // format of the boundary values... 207 | var x = Single.Parse(group1, NumberStyles.Float, format); 208 | var y = Single.Parse(group2, NumberStyles.Float, format); 209 | 210 | return new RangeF(x, y); 211 | } 212 | 213 | throw new FormatException("value is not in the correct format for an ISO 31-11 closed interval in ℝ form."); 214 | } 215 | 216 | /// 217 | /// Determines whether the specified is equal to this instance. 218 | /// 219 | /// The to compare with this instance. 220 | /// 221 | /// true if the specified is equal to this instance; otherwise, false. 222 | /// 223 | public override bool Equals(object obj) { 224 | if (obj != null) 225 | if (obj is RangeF) 226 | return Equals((RangeF)obj); 227 | 228 | return false; 229 | } 230 | 231 | /// 232 | /// Determines whether the specified is equal to this instance. 233 | /// 234 | /// The to compare with this instance. 235 | /// 236 | /// true if the specified is equal to this instance; otherwise, false. 237 | /// 238 | public bool Equals(RangeF value) => X.Equals(value.X) && 239 | Y.Equals(value.Y); 240 | 241 | /// 242 | /// Returns a hash code for this instance. 243 | /// 244 | /// 245 | /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. 246 | /// 247 | public override int GetHashCode() => X.GetHashCode() ^ Y.GetHashCode(); 248 | 249 | /// 250 | /// Returns a that represents this instance. 251 | /// 252 | /// 253 | /// A that represents this instance. 254 | /// 255 | public override string ToString() => ToString("G", CultureInfo.InvariantCulture); 256 | 257 | /// 258 | /// Returns a that represents this instance. 259 | /// 260 | /// The format provider. 261 | /// 262 | /// A that represents this instance. 263 | /// 264 | public string ToString(IFormatProvider formatProvider) => ToString("G", formatProvider); 265 | 266 | /// 267 | /// Returns a that represents this instance. 268 | /// 269 | /// The format. 270 | /// The format provider. 271 | /// 272 | /// A that represents this instance. 273 | /// 274 | public string ToString(string format, IFormatProvider formatProvider) { 275 | var numberFormat = NumberFormatInfo.GetInstance(formatProvider); 276 | 277 | var minimum = X.ToString(format, numberFormat); 278 | var maximum = Y.ToString(format, numberFormat); 279 | 280 | var seperator = numberFormat.NumberGroupSeparator; 281 | 282 | return String.Format(formatProvider, "[{0}{1}{2}]", minimum, seperator, maximum); 283 | } 284 | 285 | /// 286 | /// Implements the operator ==. 287 | /// 288 | /// The lvalue. 289 | /// The rvalue. 290 | /// 291 | /// true if the lvalue is equal to the rvalue; otherwise, false. 292 | /// 293 | public static bool operator ==(RangeF value1, RangeF value2) => value1.Equals(value2); 294 | 295 | /// 296 | /// Implements the operator !=. 297 | /// 298 | /// The lvalue. 299 | /// The rvalue. 300 | /// 301 | /// true if the lvalue is not equal to the rvalue; otherwise, false. 302 | /// 303 | public static bool operator !=(RangeF value1, RangeF value2) => !value1.Equals(value2); 304 | 305 | public static implicit operator RangeF(float value) => new RangeF(value, value); 306 | } 307 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/ReleaseParameters.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | public class ReleaseParameters { 3 | public ReleaseParameters() { 4 | Quantity = 1; 5 | Speed = RangeF.Parse("[-1.0,1.0]"); 6 | Colour = new ColourRange(new Colour(0f, 0.5f, 0.5f), new Colour(360f, 0.5f, 0.5f)); 7 | Opacity = RangeF.Parse("[0.0,1.0]"); 8 | Scale = RangeF.Parse("[1.0,10.0]"); 9 | Rotation = RangeF.Parse("[-3.14159,3.14159]"); 10 | Mass = 1f; 11 | } 12 | 13 | public Range Quantity; 14 | public RangeF Speed; 15 | public ColourRange Colour; 16 | public RangeF Opacity; 17 | public RangeF Scale; 18 | public RangeF Rotation; 19 | public RangeF Mass; 20 | } 21 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/RenderingOrder.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | public enum RenderingOrder { 3 | FrontToBack, 4 | BackToFront 5 | } 6 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/Vector.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Runtime.InteropServices; 4 | 5 | /// 6 | /// Defines a data structure representing a Euclidean vector facing a particular direction, 7 | /// including a magnitude value. 8 | /// 9 | [StructLayout(LayoutKind.Sequential)] 10 | public struct Vector { 11 | internal readonly float _x; 12 | internal readonly float _y; 13 | 14 | /// 15 | /// Initializes a new instance of the structure. 16 | /// 17 | /// The axis along which the vector points. 18 | /// The magnitude of the vector. 19 | public Vector(Axis axis, float magnitude) { 20 | _x = axis._x * magnitude; 21 | _y = axis._y * magnitude; 22 | } 23 | 24 | /// 25 | /// Initializes a new instance of the structure. 26 | /// 27 | /// The X component of the vector. 28 | /// The Y component of the vector. 29 | public Vector(float x, float y) { 30 | _x = x; 31 | _y = y; 32 | } 33 | 34 | /// 35 | /// Gets the length or magnitude of the Euclidean vector. 36 | /// 37 | public float Magnitude => (float)Math.Sqrt((_x * _x) + (_y * _y)); 38 | 39 | /// 40 | /// Gets the axis in which the vector is facing. 41 | /// 42 | /// A value representing the direction the vector is facing. 43 | public Axis Axis => new Axis(_x, _y); 44 | 45 | public Vector Multiply(float factor) => new Vector(_x * factor, _y * factor); 46 | 47 | /// 48 | /// Copies the X and Y components of the vector to the specified memory location. 49 | /// 50 | /// The memory location to copy the coordinate to. 51 | public unsafe void CopyTo(float* destination) { 52 | destination[0] = _x; 53 | destination[1] = _y; 54 | } 55 | 56 | /// 57 | /// Destructures the vector, exposing the individual X and Y components. 58 | /// 59 | public void Destructure(out float x, out float y) { 60 | x = _x; 61 | y = _y; 62 | } 63 | 64 | /// 65 | /// Exposes the individual X and Y components of the vector to the specified matching function. 66 | /// 67 | /// The function which matches the individual X and Y components. 68 | /// 69 | /// Thrown if the value passed to the parameter is null. 70 | /// 71 | public void Match(Action callback) { 72 | if (callback == null) 73 | throw new ArgumentNullException(nameof(callback)); 74 | 75 | callback(_x, _y); 76 | } 77 | 78 | /// 79 | /// Exposes the individual X and Y components of the vector to the specified mapping function and returns the 80 | /// result; 81 | /// 82 | /// The type being mapped to. 83 | /// 84 | /// A function which maps the X and Y values to an instance of . 85 | /// 86 | /// 87 | /// The result of the function when passed the individual X and Y components. 88 | /// 89 | /// 90 | /// Thrown if the value passed to the parameter is null. 91 | /// 92 | public T Map(Func map) { 93 | if (map == null) 94 | throw new ArgumentNullException(nameof(map)); 95 | 96 | return map(_x, _y); 97 | } 98 | 99 | static public Vector operator*(Vector value, float factor) => value.Multiply(factor); 100 | } 101 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.Core/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Cloud001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matthew-Davey/mercury-particle-engine/2bfc567a466e3d3bcbc17b972c830898d3c996d6/Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Cloud001.png -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Mercury.ParticleEngine.SharpDX.Direct3D9.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {A80CE58A-40BF-4766-89D2-3E81C5D9BC63} 8 | WinExe 9 | Properties 10 | Mercury.ParticleEngine 11 | Mercury.ParticleEngine.SharpDX.Direct3D9.Sample 12 | v4.6.1 13 | 512 14 | 15 | 16 | 17 | x86 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | false 26 | 27 | 28 | x86 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | 39 | 40 | ..\packages\SharpDX.3.0.0\lib\net45\SharpDX.dll 41 | True 42 | 43 | 44 | ..\packages\SharpDX.Desktop.3.0.0\lib\net45\SharpDX.Desktop.dll 45 | True 46 | 47 | 48 | ..\packages\SharpDX.Direct3D9.3.0.0\lib\net45\SharpDX.Direct3D9.dll 49 | True 50 | 51 | 52 | ..\packages\SharpDX.Mathematics.3.0.0\lib\net45\SharpDX.Mathematics.dll 53 | True 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Properties\AssemblyVersion.cs 65 | 66 | 67 | 68 | 69 | 70 | Always 71 | 72 | 73 | 74 | Always 75 | 76 | 77 | 78 | 79 | Always 80 | 81 | 82 | Always 83 | 84 | 85 | 86 | 87 | {55f2b72d-cf10-4bda-8e13-2eb8132124ea} 88 | Mercury.ParticleEngine.Core 89 | 90 | 91 | {aca557d3-43a9-424f-a35a-4fb9c93fe4e3} 92 | Mercury.ParticleEngine.SharpDX.Direct3D9 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Particle.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matthew-Davey/mercury-particle-engine/2bfc567a466e3d3bcbc17b972c830898d3c996d6/Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Particle.dds -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Pixel.dds: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matthew-Davey/mercury-particle-engine/2bfc567a466e3d3bcbc17b972c830898d3c996d6/Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Pixel.dds -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine { 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using SharpDX; 6 | using SharpDX.Direct3D9; 7 | using SharpDX.Windows; 8 | using Mercury.ParticleEngine.Modifiers; 9 | using Mercury.ParticleEngine.Profiles; 10 | using Mercury.ParticleEngine.Renderers; 11 | using System.Windows.Input; 12 | 13 | static class Program { 14 | [STAThread] 15 | static void Main() { 16 | var worldSize = new Size2(1024, 768); 17 | var renderSize = new Size2(1024, 768); 18 | const bool windowed = true; 19 | 20 | var form = new RenderForm("Mercury Particle Engine - SharpDX.Direct3D9 Sample") { 21 | Size = new System.Drawing.Size(renderSize.Width, renderSize.Height) 22 | }; 23 | 24 | var direct3d = new Direct3D(); 25 | var device = new Device(direct3d, 0, DeviceType.Hardware, form.Handle, CreateFlags.HardwareVertexProcessing, new PresentParameters(renderSize.Width, renderSize.Height) { PresentationInterval = PresentInterval.One, Windowed = windowed }); 26 | 27 | var view = new Matrix( 28 | 1.0f, 0.0f, 0.0f, 0.0f, 29 | 0.0f, -1.0f, 0.0f, 0.0f, 30 | 0.0f, 0.0f, -1.0f, 0.0f, 31 | 0.0f, 0.0f, 0.0f, 1.0f); 32 | var proj = Matrix.OrthoOffCenterLH(worldSize.Width * -0.5f, worldSize.Width * 0.5f, worldSize.Height * 0.5f, worldSize.Height * -0.5f, 0f, 1f); 33 | var wvp = Matrix.Identity * view * proj; 34 | 35 | var smokeEffect = new ParticleEffect { 36 | Emitters = new[] { 37 | new Emitter(2000, TimeSpan.FromSeconds(3), Profile.Point()) { 38 | Parameters = new ReleaseParameters { 39 | Colour = new Colour(0f, 0f, 0.6f), 40 | Opacity = 1f, 41 | Quantity = 5, 42 | Speed = new RangeF(0f, 100f), 43 | Scale = 32f, 44 | Rotation = new RangeF((float)-Math.PI, (float)Math.PI), 45 | Mass = new RangeF(8f, 12f) 46 | }, 47 | ReclaimFrequency = 5f, 48 | BlendMode = BlendMode.Alpha, 49 | RenderingOrder = RenderingOrder.BackToFront, 50 | TextureKey = "Cloud", 51 | Modifiers = new Modifier[] { 52 | new DragModifier { 53 | Frequency = 10f, 54 | DragCoefficient = 0.47f, 55 | Density = 0.125f 56 | }, 57 | new ScaleInterpolator2 { 58 | Frequency = 60f, 59 | InitialScale = 32f, 60 | FinalScale = 256f 61 | }, 62 | new RotationModifier { 63 | Frequency = 15f, 64 | RotationRate = 1f 65 | }, 66 | new OpacityInterpolator2 { 67 | Frequency = 25f, 68 | InitialOpacity = 0.3f, 69 | FinalOpacity = 0.0f 70 | } 71 | }, 72 | } 73 | } 74 | }; 75 | 76 | var sparkEffect = new ParticleEffect { 77 | Emitters = new[] { 78 | new Emitter(2000, TimeSpan.FromSeconds(2), Profile.Point()) { 79 | Parameters = new ReleaseParameters { 80 | Colour = new Colour(50f, 0.8f, 0.5f), 81 | Opacity = 1f, 82 | Quantity = 10, 83 | Speed = new RangeF(0f, 100f), 84 | Scale = 64f, 85 | Mass = new RangeF(8f, 12f) 86 | }, 87 | ReclaimFrequency = 5f, 88 | BlendMode = BlendMode.Add, 89 | RenderingOrder = RenderingOrder.FrontToBack, 90 | TextureKey = "Particle", 91 | Modifiers = new Modifier[] { 92 | new LinearGravityModifier(Axis.Down, 30f) { 93 | Frequency = 15f 94 | }, 95 | new OpacityFastFadeModifier() { 96 | Frequency = 10f 97 | } 98 | } 99 | } 100 | } 101 | }; 102 | 103 | var ringEffect = new ParticleEffect { 104 | Emitters = new[] { 105 | new Emitter(2000, TimeSpan.FromSeconds(3), Profile.Spray(Axis.Up, 0.5f)) { 106 | Parameters = new ReleaseParameters { 107 | Colour = new ColourRange(new Colour(210f, 0.5f, 0.6f), new Colour(230f, 0.7f, 0.8f)), 108 | Opacity = 1f, 109 | Quantity = 1, 110 | Speed = new RangeF(300f, 700f), 111 | Scale = 64f, 112 | Mass = new RangeF(4f, 12f), 113 | }, 114 | ReclaimFrequency = 5f, 115 | BlendMode = BlendMode.Alpha, 116 | RenderingOrder = RenderingOrder.FrontToBack, 117 | TextureKey = "Ring", 118 | Modifiers = new Modifier[] { 119 | new LinearGravityModifier(Axis.Down, 100f) { 120 | Frequency = 20f 121 | }, 122 | new OpacityFastFadeModifier() { 123 | Frequency = 10f, 124 | }, 125 | new ContainerModifier { 126 | Frequency = 15f, 127 | Width = worldSize.Width, 128 | Height = worldSize.Height, 129 | Position = new Coordinate(worldSize.Width / 2f, worldSize.Height / 2f), 130 | RestitutionCoefficient = 0.75f 131 | } 132 | } 133 | } 134 | } 135 | }; 136 | 137 | var loadTestEffect = new ParticleEffect { 138 | Emitters = new[] { 139 | new Emitter(1000000, TimeSpan.FromSeconds(2), Profile.Point()) { 140 | Parameters = new ReleaseParameters { 141 | Quantity = 10000, 142 | Speed = new RangeF(0f, 200f), 143 | Scale = 1f, 144 | Mass = new RangeF(4f, 12f), 145 | Opacity = 0.4f 146 | }, 147 | ReclaimFrequency = 5f, 148 | BlendMode = BlendMode.Add, 149 | TextureKey = "Pixel", 150 | Modifiers = new Modifier[] { 151 | new LinearGravityModifier(Axis.Down, 30f) { 152 | Frequency = 15f 153 | }, 154 | new OpacityFastFadeModifier() { 155 | Frequency = 10f 156 | }, 157 | new ContainerModifier { 158 | Frequency = 30f, 159 | Width = worldSize.Width, 160 | Height = worldSize.Height, 161 | Position = new Coordinate(worldSize.Width / 2f, worldSize.Height / 2f), 162 | RestitutionCoefficient = 0.75f 163 | }, 164 | new DragModifier { 165 | Frequency = 10f, 166 | DragCoefficient = 0.47f, 167 | Density = 0.125f 168 | }, 169 | new HueInterpolator2 { 170 | Frequency = 10f, 171 | InitialHue = 0f, 172 | FinalHue = 150f 173 | } 174 | } 175 | } 176 | } 177 | }; 178 | 179 | var textureLookup = new Dictionary { 180 | { "Particle", Texture.FromFile(device, "Particle.dds") }, 181 | { "Pixel", Texture.FromFile(device, "Pixel.dds") }, 182 | { "Cloud", Texture.FromFile(device, "Cloud001.png") }, 183 | { "Ring", Texture.FromFile(device, "Ring001.png") } 184 | }; 185 | 186 | var renderer = new PointSpriteRenderer(device, 1000000, textureLookup) { 187 | //EnableFastFade = true 188 | }; 189 | 190 | var fontDescription = new FontDescription { 191 | Height = 14, 192 | FaceName = "Consolas", 193 | PitchAndFamily = FontPitchAndFamily.Mono, 194 | Quality = FontQuality.Draft 195 | }; 196 | 197 | var font = new Font(device, fontDescription); 198 | 199 | var totalTime = 0f; 200 | var totalTimer = Stopwatch.StartNew(); 201 | var updateTimer = new Stopwatch(); 202 | var renderTimer = new Stopwatch(); 203 | 204 | var currentEffect = smokeEffect; 205 | 206 | Vector3 mousePosition = Vector3.Zero; 207 | Vector3 previousMousePosition = Vector3.Zero; 208 | 209 | RenderLoop.Run(form, () => { 210 | // ReSharper disable AccessToDisposedClosure 211 | var frameTime = ((float)totalTimer.Elapsed.TotalSeconds) - totalTime; 212 | totalTime = (float)totalTimer.Elapsed.TotalSeconds; 213 | 214 | var clientMousePosition = form.PointToClient(RenderForm.MousePosition); 215 | previousMousePosition = mousePosition; 216 | mousePosition = Vector3.Unproject(new Vector3(clientMousePosition.X, clientMousePosition.Y, 0f), 0, 0, renderSize.Width, renderSize.Height, 0f, 1f, wvp); 217 | 218 | var mouseMovementLine = new LineSegment(new Coordinate(previousMousePosition.X, previousMousePosition.Y), new Coordinate(mousePosition.X, mousePosition.Y)); 219 | 220 | if (Keyboard.IsKeyDown(Key.D1)) 221 | currentEffect = smokeEffect; 222 | 223 | if (Keyboard.IsKeyDown(Key.D2)) 224 | currentEffect = sparkEffect; 225 | 226 | if (Keyboard.IsKeyDown(Key.D3)) 227 | currentEffect = ringEffect; 228 | 229 | if (Keyboard.IsKeyDown(Key.D4)) 230 | currentEffect = loadTestEffect; 231 | 232 | if (RenderForm.MouseButtons.HasFlag(System.Windows.Forms.MouseButtons.Left)) { 233 | currentEffect.Trigger(mouseMovementLine); 234 | } 235 | 236 | updateTimer.Restart(); 237 | smokeEffect.Update(frameTime); 238 | sparkEffect.Update(frameTime); 239 | ringEffect.Update(frameTime); 240 | loadTestEffect.Update(frameTime); 241 | updateTimer.Stop(); 242 | 243 | device.Clear(ClearFlags.Target, Color.Black, 1f, 0); 244 | device.BeginScene(); 245 | 246 | renderTimer.Restart(); 247 | renderer.Render(smokeEffect, wvp); 248 | renderer.Render(sparkEffect, wvp); 249 | renderer.Render(ringEffect, wvp); 250 | renderer.Render(loadTestEffect, wvp); 251 | renderTimer.Stop(); 252 | 253 | var updateTime = (float)updateTimer.Elapsed.TotalSeconds; 254 | var renderTime = (float)renderTimer.Elapsed.TotalSeconds; 255 | 256 | font.DrawText(null, "1 - Smoke, 2 - Sparks, 3 - Rings, 4 - Load Test", 0, 0, Color.White); 257 | font.DrawText(null, String.Format("Time: {0}", totalTimer.Elapsed), 0, 32, Color.White); 258 | font.DrawText(null, String.Format("Particles: {0:n0}", currentEffect.ActiveParticles), 0, 48, Color.White); 259 | font.DrawText(null, String.Format("Update: {0:n4} ({1,8:P2})", updateTime, updateTime / 0.01666666f), 0, 64, Color.White); 260 | font.DrawText(null, String.Format("Render: {0:n4} ({1,8:P2})", renderTime, renderTime / 0.01666666f), 0, 80, Color.White); 261 | 262 | device.EndScene(); 263 | device.Present(); 264 | 265 | if (Keyboard.IsKeyDown(Key.Escape)) 266 | Environment.Exit(0); 267 | // ReSharper restore AccessToDisposedClosure 268 | }); 269 | 270 | renderer.Dispose(); 271 | form.Dispose(); 272 | font.Dispose(); 273 | device.Dispose(); 274 | direct3d.Dispose(); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Mercury.ParticleEngine.SharpDX.Direct3D9.Sample")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("Mercury.ParticleEngine.SharpDX.Direct3D9.Sample")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("893d541d-fca2-4af7-8591-0f95653c1c99")] 24 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Ring001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Matthew-Davey/mercury-particle-engine/2bfc567a466e3d3bcbc17b972c830898d3c996d6/Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/Ring001.png -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9.Sample/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Mercury.ParticleEngine.SharpDX.Direct3D9.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {ACA557D3-43A9-424F-A35A-4FB9C93FE4E3} 8 | Library 9 | Properties 10 | Mercury.ParticleEngine 11 | Mercury.ParticleEngine.SharpDX.Direct3D9 12 | v4.6.1 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\SharpDX.3.0.0\lib\net45\SharpDX.dll 36 | True 37 | 38 | 39 | ..\packages\SharpDX.Direct3D9.3.0.0\lib\net45\SharpDX.Direct3D9.dll 40 | True 41 | 42 | 43 | ..\packages\SharpDX.Mathematics.3.0.0\lib\net45\SharpDX.Mathematics.dll 44 | True 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Properties\AssemblyVersion.cs 53 | 54 | 55 | 56 | 57 | True 58 | True 59 | Resources.resx 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ResXFileCodeGenerator 68 | Resources.Designer.cs 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {55f2b72d-cf10-4bda-8e13-2eb8132124ea} 77 | Mercury.ParticleEngine.Core 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Mercury.ParticleEngine.SharpDX.Direct3D9")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("Mercury.ParticleEngine.SharpDX.Direct3D9")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("61361c78-e022-4c89-ba61-c98fa26bc934")] 24 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Renderers/PointSpriteRenderer.cs: -------------------------------------------------------------------------------- 1 | namespace Mercury.ParticleEngine.Renderers { 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Runtime.InteropServices; 5 | using SharpDX; 6 | using SharpDX.Direct3D9; 7 | 8 | public class PointSpriteRenderer : IDisposable { 9 | private readonly Device _device; 10 | private readonly int _size; 11 | private readonly IReadOnlyDictionary _textureLookup; 12 | private readonly VertexBuffer _vertexBuffer; 13 | private readonly VertexDeclaration _vertexDeclaration; 14 | private readonly Effect _effect; 15 | private readonly EffectHandle _technique; 16 | private readonly EffectHandle _matrixParameter; 17 | private readonly EffectHandle _textureParameter; 18 | 19 | private bool _enableFastFade; 20 | public bool EnableFastFade { 21 | get { return _enableFastFade; } 22 | set { 23 | if(value != _enableFastFade) { 24 | _enableFastFade = value; 25 | _effect.SetValue("FastFade", _enableFastFade); 26 | } 27 | } 28 | } 29 | 30 | public PointSpriteRenderer(Device device, int size, IReadOnlyDictionary textureLookup) { 31 | if (device == null) 32 | throw new ArgumentNullException(nameof(device)); 33 | 34 | if (textureLookup == null) 35 | throw new ArgumentNullException(nameof(textureLookup)); 36 | 37 | _device = device; 38 | _size = size; 39 | _textureLookup = textureLookup; 40 | _vertexBuffer = new VertexBuffer(_device, _size * Particle.SizeInBytes, Usage.Dynamic | Usage.Points | Usage.WriteOnly, VertexFormat.None, Pool.Default); 41 | 42 | var vertexElements = new[] { 43 | new VertexElement(0, (short)Marshal.OffsetOf(typeof(Particle), "Age"), DeclarationType.Float1, DeclarationMethod.Default, DeclarationUsage.Color, 1), 44 | new VertexElement(0, (short)Marshal.OffsetOf(typeof(Particle), "Position"), DeclarationType.Float2, DeclarationMethod.Default, DeclarationUsage.Position, 0), 45 | new VertexElement(0, (short)Marshal.OffsetOf(typeof(Particle), "Colour"), DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.Color, 0), 46 | new VertexElement(0, (short)Marshal.OffsetOf(typeof(Particle), "Scale"), DeclarationType.Float1, DeclarationMethod.Default, DeclarationUsage.PointSize, 0), 47 | new VertexElement(0, (short)Marshal.OffsetOf(typeof(Particle), "Rotation"), DeclarationType.Float1, DeclarationMethod.Default, DeclarationUsage.Color, 2), 48 | VertexElement.VertexDeclarationEnd 49 | }; 50 | 51 | _effect = Effect.FromString(device, Resources.PointSpriteShader, ShaderFlags.PartialPrecision); 52 | _technique = _effect.GetTechnique(0); 53 | _matrixParameter = _effect.GetParameter(null, "WVPMatrix"); 54 | _textureParameter = _effect.GetParameter(null, "SpriteTexture"); 55 | _vertexDeclaration = new VertexDeclaration(device, vertexElements); 56 | } 57 | 58 | public void Render(ParticleEffect effect, Matrix worldViewProjection) { 59 | for (var i = 0; i < effect.Emitters.Length; i++) { 60 | Render(effect.Emitters[i], worldViewProjection); 61 | } 62 | } 63 | 64 | internal void Render(Emitter emitter, Matrix worldViewProjection) { 65 | if (emitter.ActiveParticles == 0) 66 | return; 67 | 68 | if (emitter.ActiveParticles > _size) 69 | throw new Exception("Cannot render this emitter, vertex buffer not big enough"); 70 | 71 | _effect.SetValue(_matrixParameter, worldViewProjection); 72 | _effect.SetTexture(_textureParameter, _textureLookup[emitter.TextureKey]); 73 | 74 | var vertexDataPointer = _vertexBuffer.LockToPointer(0, emitter.Buffer.ActiveSizeInBytes, LockFlags.Discard); 75 | 76 | switch (emitter.RenderingOrder) { 77 | case RenderingOrder.FrontToBack: { 78 | emitter.Buffer.CopyTo(vertexDataPointer); 79 | break; 80 | } 81 | case RenderingOrder.BackToFront: { 82 | emitter.Buffer.CopyToReverse(vertexDataPointer); 83 | break; 84 | } 85 | } 86 | 87 | _vertexBuffer.Unlock(); 88 | 89 | SetupBlend(emitter.BlendMode); 90 | 91 | _effect.Technique = _technique; 92 | _effect.Begin(FX.DoNotSaveState); 93 | _effect.BeginPass(0); 94 | 95 | _device.SetStreamSource(0, _vertexBuffer, 0, Particle.SizeInBytes); 96 | _device.VertexDeclaration = _vertexDeclaration; 97 | _device.DrawPrimitives(PrimitiveType.PointList, 0, emitter.ActiveParticles); 98 | 99 | _effect.EndPass(); 100 | _effect.End(); 101 | } 102 | 103 | private void SetupBlend(BlendMode blendMode) { 104 | switch (blendMode) { 105 | case BlendMode.Alpha: 106 | _device.SetRenderState(RenderState.BlendOperation, BlendOperation.Add); 107 | _device.SetRenderState(RenderState.BlendOperationAlpha, BlendOperation.Add); 108 | _device.SetRenderState(RenderState.SourceBlendAlpha, Blend.SourceAlpha); 109 | _device.SetRenderState(RenderState.DestinationBlendAlpha, Blend.InverseSourceAlpha); 110 | _device.SetRenderState(RenderState.SourceBlend, Blend.SourceAlpha); 111 | _device.SetRenderState(RenderState.DestinationBlend, Blend.InverseSourceAlpha); 112 | return; 113 | case BlendMode.Add: 114 | _device.SetRenderState(RenderState.BlendOperation, BlendOperation.Add); 115 | _device.SetRenderState(RenderState.BlendOperationAlpha, BlendOperation.Add); 116 | _device.SetRenderState(RenderState.SourceBlendAlpha, Blend.SourceAlpha); 117 | _device.SetRenderState(RenderState.DestinationBlendAlpha, Blend.InverseSourceAlpha); 118 | _device.SetRenderState(RenderState.SourceBlend, Blend.SourceAlpha); 119 | _device.SetRenderState(RenderState.DestinationBlend, Blend.One); 120 | return; 121 | case BlendMode.Subtract: 122 | _device.SetRenderState(RenderState.BlendOperation, BlendOperation.ReverseSubtract); 123 | _device.SetRenderState(RenderState.BlendOperationAlpha, BlendOperation.Add); 124 | _device.SetRenderState(RenderState.SourceBlendAlpha, Blend.SourceAlpha); 125 | _device.SetRenderState(RenderState.DestinationBlendAlpha, Blend.InverseSourceAlpha); 126 | _device.SetRenderState(RenderState.SourceBlend, Blend.SourceAlpha); 127 | _device.SetRenderState(RenderState.DestinationBlend, Blend.One); 128 | return; 129 | } 130 | } 131 | 132 | public void Dispose() { 133 | Dispose(true); 134 | GC.SuppressFinalize(this); 135 | } 136 | 137 | protected void Dispose(bool disposing) { 138 | if (disposing) { 139 | _vertexBuffer.Dispose(); 140 | _vertexDeclaration.Dispose(); 141 | _matrixParameter.Dispose(); 142 | _textureParameter.Dispose(); 143 | _technique.Dispose(); 144 | _effect.Dispose(); 145 | } 146 | } 147 | 148 | ~PointSpriteRenderer() { 149 | Dispose(false); 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Mercury.ParticleEngine { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Mercury.ParticleEngine.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to uniform extern float4x4 WVPMatrix; 65 | ///uniform extern texture SpriteTexture; 66 | ///uniform extern bool FastFade; 67 | /// 68 | ///sampler Sampler = sampler_state { 69 | /// Texture = <SpriteTexture>; 70 | /// 71 | /// MinFilter = LINEAR; 72 | /// MagFilter = LINEAR; 73 | /// MipFilter = POINT; 74 | /// 75 | /// AddressU = CLAMP; 76 | /// AddressV = CLAMP; 77 | ///}; 78 | /// 79 | ///float3 HueToRgb(in float hue) { 80 | /// float r = abs(hue * 6 - 3) - 1; 81 | /// float g = 2 - abs(hue * 6 - 2); 82 | /// float b = 2 - abs(hue * 6 - 4); 83 | /// 84 | /// return saturate(float3(r, g, b)); 85 | ///} 86 | /// 87 | ///float3 HslToRgb(in fl [rest of string was truncated]";. 88 | /// 89 | internal static string PointSpriteShader { 90 | get { 91 | return ResourceManager.GetString("PointSpriteShader", resourceCulture); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | 122 | Resources\PointSprite.fx;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 123 | 124 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/Resources/PointSprite.fx: -------------------------------------------------------------------------------- 1 | uniform extern float4x4 WVPMatrix; 2 | uniform extern texture SpriteTexture; 3 | uniform extern bool FastFade; 4 | 5 | sampler Sampler = sampler_state { 6 | Texture = ; 7 | 8 | MinFilter = LINEAR; 9 | MagFilter = LINEAR; 10 | MipFilter = POINT; 11 | 12 | AddressU = CLAMP; 13 | AddressV = CLAMP; 14 | }; 15 | 16 | float3 HueToRgb(in float hue) { 17 | float r = abs(hue * 6 - 3) - 1; 18 | float g = 2 - abs(hue * 6 - 2); 19 | float b = 2 - abs(hue * 6 - 4); 20 | 21 | return saturate(float3(r, g, b)); 22 | } 23 | 24 | float3 HslToRgb(in float3 hsl) { 25 | float3 rgb = HueToRgb(hsl.x / 360.0f); 26 | float c = (1 - abs(2 * hsl.z - 1)) * hsl.y; 27 | 28 | return (rgb - 0.5) * c + hsl.z; 29 | } 30 | 31 | float4 vertex_main(const in float age : COLOR1, const in float2 position : POSITION0, inout float4 color : COLOR0, inout float size : PSIZE0, const inout float rotation : COLOR2) : POSITION0 { 32 | color.xyz = HslToRgb(color.xyz); 33 | 34 | if (FastFade) { 35 | color.a = 1.0f - age; 36 | } 37 | 38 | return mul(float4(position, 0, 1), WVPMatrix); 39 | } 40 | 41 | float4 pixel_main(in float4 color : COLOR0, in float rotation : COLOR2, in float2 texCoord : TEXCOORD0) : COLOR0 { 42 | float c = cos(rotation); 43 | float s = sin(rotation); 44 | 45 | float2x2 rotationMatrix = float2x2(c, -s, s, c); 46 | 47 | texCoord = mul(texCoord - 0.5f, rotationMatrix); 48 | 49 | return tex2D(Sampler, texCoord + 0.5) * color; 50 | } 51 | 52 | technique PointSprite_2_0 { 53 | pass P0 { 54 | CullMode = NONE; 55 | PointSpriteEnable = TRUE; 56 | PointScaleEnable = FALSE; 57 | PointSize_Min = 1.0f; 58 | PointSize_Max = 1024.0f; 59 | 60 | AlphaBlendEnable = TRUE; 61 | ZWriteEnable = FALSE; 62 | 63 | vertexShader = compile vs_3_0 vertex_main(); 64 | pixelShader = compile ps_3_0 pixel_main(); 65 | } 66 | } -------------------------------------------------------------------------------- /Mercury.ParticleEngine.SharpDX.Direct3D9/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Mercury.ParticleEngine.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.24720.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mercury.ParticleEngine.Core", "Mercury.ParticleEngine.Core\Mercury.ParticleEngine.Core.csproj", "{E7B59AF1-5CCF-472B-906A-62BF48E520A8}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mercury.ParticleEngine.Core.Tests", "Mercury.ParticleEngine.Core.Tests\Mercury.ParticleEngine.Core.Tests.csproj", "{E9B007E5-A803-46A3-AEF5-05DE0B56A5AF}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mercury.ParticleEngine.SharpDX.Direct3D9", "Mercury.ParticleEngine.SharpDX.Direct3D9\Mercury.ParticleEngine.SharpDX.Direct3D9.csproj", "{ACA557D3-43A9-424F-A35A-4FB9C93FE4E3}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mercury.ParticleEngine.SharpDX.Direct3D9.Sample", "Mercury.ParticleEngine.SharpDX.Direct3D9.Sample\Mercury.ParticleEngine.SharpDX.Direct3D9.Sample.csproj", "{A80CE58A-40BF-4766-89D2-3E81C5D9BC63}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D0B6581E-6B87-4F66-A49D-EB1317CD0B8A}" 15 | ProjectSection(SolutionItems) = preProject 16 | .semver = .semver 17 | AssemblyVersion.cs = AssemblyVersion.cs 18 | Gemfile = Gemfile 19 | Rakefile = Rakefile 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Global 24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 25 | Debug|Any CPU = Debug|Any CPU 26 | Release|Any CPU = Release|Any CPU 27 | EndGlobalSection 28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 29 | {E7B59AF1-5CCF-472B-906A-62BF48E520A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {E7B59AF1-5CCF-472B-906A-62BF48E520A8}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {E7B59AF1-5CCF-472B-906A-62BF48E520A8}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {E7B59AF1-5CCF-472B-906A-62BF48E520A8}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {E9B007E5-A803-46A3-AEF5-05DE0B56A5AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {E9B007E5-A803-46A3-AEF5-05DE0B56A5AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {E9B007E5-A803-46A3-AEF5-05DE0B56A5AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {E9B007E5-A803-46A3-AEF5-05DE0B56A5AF}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {ACA557D3-43A9-424F-A35A-4FB9C93FE4E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {ACA557D3-43A9-424F-A35A-4FB9C93FE4E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {ACA557D3-43A9-424F-A35A-4FB9C93FE4E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {ACA557D3-43A9-424F-A35A-4FB9C93FE4E3}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {A80CE58A-40BF-4766-89D2-3E81C5D9BC63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {A80CE58A-40BF-4766-89D2-3E81C5D9BC63}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {A80CE58A-40BF-4766-89D2-3E81C5D9BC63}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A80CE58A-40BF-4766-89D2-3E81C5D9BC63}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(SolutionProperties) = preSolution 47 | HideSolutionNode = FALSE 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mercury-particle-engine 2 | ======================= 3 | 4 | License: WTFPL (http://www.wtfpl.net/about/) 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'albacore' 4 | require 'albacore/tasks/versionizer' 5 | 6 | task :default => [:build] 7 | 8 | desc 'Clean up the working folder' 9 | build :clean do |build| 10 | build.nologo 11 | build.sln = 'Mercury.ParticleEngine.sln' 12 | build.target = [:Clean] 13 | build.prop 'configuration', build_configuration 14 | build.logging = 'detailed' 15 | end 16 | 17 | desc 'Extract version information from .semver' 18 | Albacore::Tasks::Versionizer.new :read_semver 19 | 20 | desc 'Writes out the AssemblyVersion file' 21 | asmver :version => [:read_semver] do |file| 22 | file.file_path = 'AssemblyVersion.cs' 23 | file.attributes assembly_version: ENV['FORMAL_VERSION'], 24 | assembly_file_version: ENV['BUILD_VERSION'], 25 | assembly_informational_version: ENV['NUGET_VERSION'] 26 | end 27 | 28 | desc 'Restores missing nuget packages' 29 | nugets_restore :package_restore do |nuget| 30 | nuget.out = 'packages' 31 | nuget.nuget_gem_exe 32 | end 33 | 34 | desc 'Executes msbuild/xbuild against the project file' 35 | build :build => [:clean, :version, :package_restore] do |build| 36 | build.sln = 'Mercury.ParticleEngine.sln' 37 | build.target = [:Build] 38 | build.prop 'configuration', build_configuration 39 | build.logging = 'detailed' 40 | build.add_parameter '/consoleloggerparameters:PerformanceSummary;Summary;ShowTimestamp' 41 | end 42 | 43 | desc 'Executes unit tests' 44 | task :test => [:build] do 45 | Dir.mkdir(artifacts_path) unless Dir.exist?(artifacts_path) 46 | xunit = File.join('packages', 'xunit.runner.console.2.1.0', 'tools', 'xunit.console.exe') 47 | tests = File.join(Dir.pwd, 'Mercury.ParticleEngine.Core.Tests', 'bin', build_configuration, 'Mercury.ParticleEngine.Core.Tests.dll') 48 | system("#{xunit} #{tests} -Verbose -html #{File.join(artifacts_path, 'test-results.html')} -nunit #{File.join(artifacts_path, 'test-results.xml')}") 49 | end 50 | 51 | desc 'Writes out the nuget package for the current version' 52 | nugets_pack :package => [:test] do |nuget| 53 | Dir.mkdir(artifacts_path) unless Dir.exist?(artifacts_path) 54 | nuget.configuration = build_configuration 55 | nuget.files = FileList[File.join('Mercury.ParticleEngine.Core', 'Mercury.ParticleEngine.Core.csproj')] 56 | nuget.out = 'artifacts' 57 | nuget.nuget_gem_exe 58 | nuget.gen_symbols 59 | nuget.with_metadata do |meta| 60 | meta.version = ENV['NUGET_VERSION'] 61 | meta.authors = 'Matt Davey' 62 | meta.description = 'Mercury Particle Engine core assembly' 63 | meta.release_notes = '' 64 | end 65 | end 66 | 67 | task :publish => [:package] do 68 | package = File.join(artifacts_path, "Mercury.ParticleEngine.#{ENV['NUGET_VERSION']}.nupkg") 69 | nuget = Albacore::Nugets::find_nuget_gem_exe 70 | system(nuget, "push #{package} #{ENV['NUGET_API_KEY']} -Source #{ENV['NUGET_FEED_URL']} -NonInteractive -Verbosity detailed") 71 | end 72 | 73 | def artifacts_path 74 | return File.join(Dir.pwd, 'artifacts') 75 | end 76 | 77 | def build_configuration 78 | return ENV['configuration'] || 'Debug' 79 | end 80 | --------------------------------------------------------------------------------